diff --git a/DATABASE_SETUP.md b/DATABASE_SETUP.md new file mode 100644 index 0000000..7523708 --- /dev/null +++ b/DATABASE_SETUP.md @@ -0,0 +1,213 @@ +# ๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ๊ฐ€์ด๋“œ + +## ๐Ÿ“‹ ๊ฐœ์š” +Travel Planner v2.0์€ PostgreSQL์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ€ํ‹ฐ ์‚ฌ์šฉ์ž ์—ฌํ–‰ ๊ณ„ํš์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + +## ๐Ÿš€ ๋น ๋ฅธ ์‹œ์ž‘ + +### 1. PostgreSQL ์„ค์น˜ ํ™•์ธ +```bash +# PostgreSQL ๋ฒ„์ „ ํ™•์ธ +psql --version + +# PostgreSQL ์„œ๋น„์Šค ์ƒํƒœ ํ™•์ธ (macOS) +brew services list | grep postgresql + +# PostgreSQL ์‹œ์ž‘ (macOS) +brew services start postgresql +``` + +### 2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒ์„ฑ +```bash +# PostgreSQL ์ ‘์† +psql postgres + +# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒ์„ฑ +CREATE DATABASE kumamoto_travel; + +# ์‚ฌ์šฉ์ž ์ƒ์„ฑ (์„ ํƒ์‚ฌํ•ญ) +CREATE USER kumamoto_user WITH PASSWORD 'your_password'; +GRANT ALL PRIVILEGES ON DATABASE kumamoto_travel TO kumamoto_user; + +# ์ข…๋ฃŒ +\q +``` + +### 3. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • +```bash +# ์„œ๋ฒ„ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ด๋™ +cd server + +# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํŒŒ์ผ ์ƒ์„ฑ +cp env.example .env + +# .env ํŒŒ์ผ ํŽธ์ง‘ +nano .env +``` + +### 4. .env ํŒŒ์ผ ์˜ˆ์‹œ +```env +# ๊ธฐ๋ณธ ์„ค์ • (๋กœ์ปฌ ๊ฐœ๋ฐœ์šฉ) +DATABASE_URL=postgresql://localhost:5432/kumamoto_travel + +# ์‚ฌ์šฉ์ž ๊ณ„์ •์„ ๋งŒ๋“  ๊ฒฝ์šฐ +DATABASE_URL=postgresql://kumamoto_user:your_password@localhost:5432/kumamoto_travel + +# JWT ์‹œํฌ๋ฆฟ (๋žœ๋คํ•œ ๋ฌธ์ž์—ด๋กœ ๋ณ€๊ฒฝํ•˜์„ธ์š”) +JWT_SECRET=your-super-secret-jwt-key-change-this-in-production-123456789 + +# ์„ ํƒ์‚ฌํ•ญ +GOOGLE_MAPS_API_KEY=your-google-maps-api-key +PORT=3000 +NODE_ENV=development +``` + +## ๐Ÿ”ง ์„œ๋ฒ„ ์‹œ์ž‘ + +### 1. ์˜์กด์„ฑ ์„ค์น˜ +```bash +cd server +npm install +``` + +### 2. ์„œ๋ฒ„ ์‹คํ–‰ +```bash +# ๊ฐœ๋ฐœ ๋ชจ๋“œ +npm run dev + +# ๋˜๋Š” ์ผ๋ฐ˜ ๋ชจ๋“œ +npm start +``` + +### 3. ์„œ๋ฒ„ ํ™•์ธ +```bash +# ํ—ฌ์Šค ์ฒดํฌ +curl http://localhost:3000/health + +# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ +curl http://localhost:3000/api/setup/test-db + +# ์„ค์ • ์ƒํƒœ ํ™•์ธ +curl http://localhost:3000/api/setup/status +``` + +## ๐Ÿ“Š ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ + +### ์ฃผ์š” ํ…Œ์ด๋ธ” +- **users**: ์‚ฌ์šฉ์ž ๊ณ„์ • (๊ด€๋ฆฌ์ž/์ผ๋ฐ˜ ์‚ฌ์šฉ์ž) +- **travel_plans**: ์—ฌํ–‰ ๊ณ„ํš (๋ฉ€ํ‹ฐ ๋ชฉ์ ์ง€ ์ง€์›) +- **day_schedules**: ๋‚ ์งœ๋ณ„ ์ผ์ • +- **activities**: ๊ฐœ๋ณ„ ํ™œ๋™ +- **share_links**: ๊ณต์œ  ๋งํฌ ๊ด€๋ฆฌ +- **trip_comments**: ์—ฌํ–‰ ๊ณ„ํš ๋Œ“๊ธ€ + +### ์Šคํ‚ค๋งˆ ์—…๋ฐ์ดํŠธ +```bash +# ๊ธฐ์กด ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ๋ฐฑ์—… +pg_dump kumamoto_travel > backup.sql + +# ์ƒˆ ์Šคํ‚ค๋งˆ ์ ์šฉ +psql kumamoto_travel < server/schema_v2.sql +``` + +## ๐Ÿ” ๋ณด์•ˆ ์„ค์ • + +### JWT ์‹œํฌ๋ฆฟ ์ƒ์„ฑ +```bash +# ๋žœ๋ค ์‹œํฌ๋ฆฟ ์ƒ์„ฑ (Node.js) +node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" + +# ๋˜๋Š” OpenSSL ์‚ฌ์šฉ +openssl rand -hex 64 +``` + +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ณด์•ˆ +```sql +-- ์‚ฌ์šฉ์ž๋ณ„ ๊ถŒํ•œ ์„ค์ • +REVOKE ALL ON SCHEMA public FROM PUBLIC; +GRANT USAGE ON SCHEMA public TO kumamoto_user; +GRANT ALL ON ALL TABLES IN SCHEMA public TO kumamoto_user; +GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO kumamoto_user; +``` + +## ๐Ÿ› ๋ฌธ์ œ ํ•ด๊ฒฐ + +### ์—ฐ๊ฒฐ ์˜ค๋ฅ˜ +```bash +# PostgreSQL ์‹คํ–‰ ํ™•์ธ +ps aux | grep postgres + +# ํฌํŠธ ํ™•์ธ +lsof -i :5432 + +# ๋กœ๊ทธ ํ™•์ธ +tail -f /usr/local/var/log/postgres.log +``` + +### ๊ถŒํ•œ ์˜ค๋ฅ˜ +```sql +-- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์†Œ์œ ์ž ๋ณ€๊ฒฝ +ALTER DATABASE kumamoto_travel OWNER TO kumamoto_user; + +-- ํ…Œ์ด๋ธ” ๊ถŒํ•œ ๋ถ€์—ฌ +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO kumamoto_user; +``` + +### ์Šคํ‚ค๋งˆ ์ดˆ๊ธฐํ™” +```bash +# ๋ชจ๋“  ํ…Œ์ด๋ธ” ์‚ญ์ œ ํ›„ ์žฌ์ƒ์„ฑ +psql kumamoto_travel -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" +psql kumamoto_travel < server/schema_v2.sql +``` + +## ๐Ÿ“ˆ ์„ฑ๋Šฅ ์ตœ์ ํ™” + +### ์ธ๋ฑ์Šค ํ™•์ธ +```sql +-- ์ธ๋ฑ์Šค ์‚ฌ์šฉ ํ˜„ํ™ฉ +SELECT schemaname, tablename, indexname, idx_tup_read, idx_tup_fetch +FROM pg_stat_user_indexes; + +-- ๋А๋ฆฐ ์ฟผ๋ฆฌ ํ™•์ธ +SELECT query, mean_time, calls FROM pg_stat_statements ORDER BY mean_time DESC; +``` + +### ์—ฐ๊ฒฐ ํ’€ ์„ค์ • +```javascript +// server/db.js์—์„œ ์—ฐ๊ฒฐ ํ’€ ์กฐ์ • +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + max: 20, // ์ตœ๋Œ€ ์—ฐ๊ฒฐ ์ˆ˜ + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 2000, +}); +``` + +## ๐Ÿš€ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ + +### ํ™˜๊ฒฝ ๋ณ€์ˆ˜ (ํ”„๋กœ๋•์…˜) +```env +DATABASE_URL=postgresql://user:password@host:port/database?sslmode=require +JWT_SECRET=production-secret-key-very-long-and-random +NODE_ENV=production +PORT=3000 +``` + +### SSL ์„ค์ • +```javascript +// SSL ์—ฐ๊ฒฐ (ํ”„๋กœ๋•์…˜) +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false +}); +``` + +## ๐Ÿ“ž ์ง€์› + +๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋‹ค์Œ์„ ํ™•์ธํ•˜์„ธ์š”: +1. PostgreSQL ์„œ๋น„์Šค ์‹คํ–‰ ์ƒํƒœ +2. .env ํŒŒ์ผ์˜ DATABASE_URL ์ •ํ™•์„ฑ +3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ์ž ๊ถŒํ•œ +4. ๋ฐฉํ™”๋ฒฝ ์„ค์ • (ํฌํŠธ 5432, 3000) + +์„ฑ๊ณต์ ์œผ๋กœ ์„ค์ •๋˜๋ฉด ๋ธŒ๋ผ์šฐ์ €์—์„œ `http://localhost:5173`์— ์ ‘์†ํ•˜์—ฌ ์ดˆ๊ธฐ ์„ค์ •์„ ์™„๋ฃŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. diff --git a/IMPROVEMENT_PLAN.md b/IMPROVEMENT_PLAN.md new file mode 100644 index 0000000..07d4a83 --- /dev/null +++ b/IMPROVEMENT_PLAN.md @@ -0,0 +1,125 @@ +# ๊ตฌ๋งˆ๋ชจํ†  ์—ฌํ–‰ ์•ฑ UI ๊ฐœ์„  ๊ณ„ํš + +## ๐Ÿ“‹ ๊ฐœ์„  ๋ชฉํ‘œ + +### ๋ฐ์Šคํฌํ†ฑ ๋ชจ๋“œ (๊ณ„ํš ์ˆ˜๋ฆฝ ์ค‘์‹ฌ) +**๋ชฉํ‘œ**: ํšจ์œจ์ ์ธ ์—ฌํ–‰ ๊ณ„ํš ์ˆ˜๋ฆฝ์„ ์œ„ํ•œ ์ข…ํ•ฉ ๋Œ€์‹œ๋ณด๋“œ + +### ๋ชจ๋ฐ”์ผ ๋ชจ๋“œ (์—ฌํ–‰ ์ค‘ ์‚ฌ์šฉ) +**๋ชฉํ‘œ**: ์—ฌํ–‰ ์ค‘ ํ•„์š”ํ•œ ์ •๋ณด์— ๋น ๋ฅด๊ฒŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ์‹ค์šฉ์  ์ธํ„ฐํŽ˜์ด์Šค + +--- + +## ๐Ÿ–ฅ๏ธ ๋ฐ์Šคํฌํ†ฑ ๋ชจ๋“œ ๊ฐœ์„  ๊ณ„ํš + +### 1. ๋ ˆ์ด์•„์›ƒ ์ตœ์ ํ™” +- **3์ปฌ๋Ÿผ ๋ ˆ์ด์•„์›ƒ**: ์ง€๋„(50%) | ์ผ์ •(30%) | ์ •๋ณด(20%) +- **์ƒ๋‹จ ๋Œ€์‹œ๋ณด๋“œ**: ์—ฌํ–‰ ๊ฐœ์š”, ์ง„ํ–‰๋ฅ , ์˜ˆ์‚ฐ ์š”์•ฝ +- **ํƒญ ๊ธฐ๋ฐ˜ ๋„ค๋น„๊ฒŒ์ด์…˜**: ์ผ์ • | ๊ด€๊ด‘์ง€ | ์˜ˆ์‚ฐ | ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +### 2. ์ผ์ • ํŽธ์ง‘ ๊ธฐ๋Šฅ ๊ฐ•ํ™” +- **๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ**: ์ผ์ • ์ˆœ์„œ ๋ณ€๊ฒฝ +- **์‹œ๊ฐ„ ์Šฌ๋ผ์ด๋”**: ์ง๊ด€์  ์‹œ๊ฐ„ ์กฐ์ • +- **๊ด€๊ด‘์ง€ ์—ฐ๋™**: ๊ด€๊ด‘์ง€ ๋ชฉ๋ก์—์„œ ์ผ์ •์œผ๋กœ ๋“œ๋ž˜๊ทธ +- **์ž๋™ ์‹œ๊ฐ„ ๊ณ„์‚ฐ**: ์ด๋™ ์‹œ๊ฐ„ ์ž๋™ ๊ณ„์‚ฐ + +### 3. ์ •๋ณด ํ†ตํ•ฉ ๋ฐ ์‹œ๊ฐํ™” +- **์ง„ํ–‰๋ฅ  ํ‘œ์‹œ**: ๊ณ„ํš ์™„์„ฑ๋„ ์‹œ๊ฐํ™” +- **์˜ˆ์‚ฐ ์ฐจํŠธ**: ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์˜ˆ์‚ฐ ๋ถ„๋ฐฐ +- **๋‚ ์”จ ์ •๋ณด**: ์—ฌํ–‰ ๋‚ ์งœ ๋‚ ์”จ ์˜ˆ๋ณด +- **๊ตํ†ต ์ •๋ณด**: ๊ฒฝ๋กœ ๋ฐ ์†Œ์š” ์‹œ๊ฐ„ + +### 4. ํ˜‘์—… ๊ธฐ๋Šฅ +- **๊ณต์œ  ๋งํฌ**: ๊ฐ€์กฑ๊ณผ ๊ณ„ํš ๊ณต์œ  +- **๋Œ“๊ธ€ ์‹œ์Šคํ…œ**: ์ผ์ •๋ณ„ ๋ฉ”๋ชจ/์˜๊ฒฌ +- **๋ฒ„์ „ ๊ด€๋ฆฌ**: ๊ณ„ํš ๋ณ€๊ฒฝ ์ด๋ ฅ + +--- + +## ๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ ๋ชจ๋“œ ๊ฐœ์„  ๊ณ„ํš + +### 1. ๋„ค๋น„๊ฒŒ์ด์…˜ ์ตœ์ ํ™” +- **ํ•˜๋‹จ ํƒญ๋ฐ”**: ์ง€๋„ | ์˜ค๋Š˜์ผ์ • | ์ „์ฒด์ผ์ • | ๋”๋ณด๊ธฐ +- **ํ”Œ๋กœํŒ… ์•ก์…˜ ๋ฒ„ํŠผ**: ํ˜„์žฌ์œ„์น˜, ๋‹ค์Œ์žฅ์†Œ, ๊ธด๊ธ‰์—ฐ๋ฝ +- **์Šค์™€์ดํ”„ ์ œ์Šค์ฒ˜**: ๋‚ ์งœ ๊ฐ„ ๋น ๋ฅธ ์ „ํ™˜ + +### 2. ์‹ค์‹œ๊ฐ„ ์—ฌํ–‰ ์ง€์› +- **ํ˜„์žฌ ์ง„ํ–‰ ์ƒํ™ฉ**: ์™„๋ฃŒ๋œ ์ผ์ • ์ฒดํฌ +- **๋‹ค์Œ ๋ชฉ์ ์ง€**: ํ˜„์žฌ ์œ„์น˜์—์„œ ๋‹ค์Œ ์žฅ์†Œ๊นŒ์ง€ ๊ธธ์ฐพ๊ธฐ +- **์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ**: ์ผ์ • ์‹œ๊ฐ„ ์•Œ๋ฆผ, ๊ตํ†ต ์ •๋ณด +- **์˜คํ”„๋ผ์ธ ๋ชจ๋“œ**: ํ•„์ˆ˜ ์ •๋ณด ์บ์‹ฑ + +### 3. ๋น ๋ฅธ ์•ก์„ธ์Šค ๊ธฐ๋Šฅ +- **์›ํ„ฐ์น˜ ์•ก์…˜**: ์ „ํ™”๊ฑธ๊ธฐ, ๊ธธ์ฐพ๊ธฐ, ๋ฉ”๋ชจ +- **์Œ์„ฑ ๋ฉ”๋ชจ**: ์—ฌํ–‰ ์ค‘ ๊ฐ„ํŽธ ๊ธฐ๋ก +- **์‚ฌ์ง„ ์—ฐ๋™**: ์žฅ์†Œ๋ณ„ ์‚ฌ์ง„ ์ž๋™ ๋ถ„๋ฅ˜ +- **์ฒดํฌ์ธ ๊ธฐ๋Šฅ**: ๋ฐฉ๋ฌธ ์™„๋ฃŒ ์ฒดํฌ + +### 4. ์—ฌํ–‰ ์ค‘ ํŽธ์˜ ๊ธฐ๋Šฅ +- **์–ธ์–ด ์ง€์›**: ์ผ๋ณธ์–ด ๊ธฐ๋ณธ ๋ฌธ๊ตฌ +- **ํ™˜์œจ ๊ณ„์‚ฐ๊ธฐ**: ์‹ค์‹œ๊ฐ„ ํ™˜์œจ ์ ์šฉ +- **๊ธด๊ธ‰ ์ •๋ณด**: ๋ณ‘์›, ๊ฒฝ์ฐฐ์„œ, ๋Œ€์‚ฌ๊ด€ +- **๊ตํ†ต์นด๋“œ ์ž”์•ก**: IC์นด๋“œ ์‚ฌ์šฉ ๊ธฐ๋ก + +--- + +## ๐Ÿ”„ ๊ณตํ†ต ๊ฐœ์„  ์‚ฌํ•ญ + +### 1. ์„ฑ๋Šฅ ์ตœ์ ํ™” +- **๋กœ๋”ฉ ์‹œ๊ฐ„ ๋‹จ์ถ•**: ์ด๋ฏธ์ง€ ์ตœ์ ํ™”, ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ… +- **์˜คํ”„๋ผ์ธ ์ง€์›**: PWA ์ ์šฉ, ์บ์‹ฑ ์ „๋žต +- **๋ฐฐํ„ฐ๋ฆฌ ์ตœ์ ํ™”**: ์œ„์น˜ ์„œ๋น„์Šค ํšจ์œจํ™” + +### 2. ์ ‘๊ทผ์„ฑ ๊ฐœ์„  +- **๋‹ค๊ตญ์–ด ์ง€์›**: ํ•œ๊ตญ์–ด, ์ผ๋ณธ์–ด, ์˜์–ด +- **๋‹คํฌ๋ชจ๋“œ**: ์•ผ๊ฐ„ ์‚ฌ์šฉ ํŽธ์˜์„ฑ +- **ํฐํŠธ ํฌ๊ธฐ ์กฐ์ ˆ**: ์‚ฌ์šฉ์ž ๋งž์ถค ์„ค์ • + +### 3. ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” +- **์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™”**: ๋ฐ์Šคํฌํ†ฑ-๋ชจ๋ฐ”์ผ ๊ฐ„ ์ฆ‰์‹œ ๋ฐ˜์˜ +- **์˜คํ”„๋ผ์ธ ๋™๊ธฐํ™”**: ์—ฐ๊ฒฐ ๋ณต๊ตฌ ์‹œ ์ž๋™ ๋™๊ธฐํ™” +- **๋ฐฑ์—…/๋ณต์›**: ํด๋ผ์šฐ๋“œ ๋ฐฑ์—… ๊ธฐ๋Šฅ + +--- + +## ๐Ÿ“… ๊ตฌํ˜„ ์šฐ์„ ์ˆœ์œ„ + +### Phase 1: ํ•ต์‹ฌ ๊ธฐ๋Šฅ ๊ฐœ์„  (1์ฃผ) +1. ๋ฐ์Šคํฌํ†ฑ 3์ปฌ๋Ÿผ ๋ ˆ์ด์•„์›ƒ ๊ตฌํ˜„ +2. ๋ชจ๋ฐ”์ผ ํ•˜๋‹จ ํƒญ๋ฐ” ๋„ค๋น„๊ฒŒ์ด์…˜ +3. ์ผ์ • ๋“œ๋ž˜๊ทธ์•ค๋“œ๋กญ ๊ธฐ๋Šฅ +4. ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ + +### Phase 2: ์‚ฌ์šฉ์„ฑ ํ–ฅ์ƒ (1์ฃผ) +1. ๊ด€๊ด‘์ง€-์ผ์ • ์—ฐ๋™ ๊ธฐ๋Šฅ +2. ์‹ค์‹œ๊ฐ„ ์œ„์น˜ ๊ธฐ๋ฐ˜ ๊ธฐ๋Šฅ +3. ์˜คํ”„๋ผ์ธ ๋ชจ๋“œ ๊ธฐ๋ณธ ๊ตฌํ˜„ +4. ์„ฑ๋Šฅ ์ตœ์ ํ™” + +### Phase 3: ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ (1์ฃผ) +1. ํ˜‘์—… ๋ฐ ๊ณต์œ  ๊ธฐ๋Šฅ +2. ์Œ์„ฑ/์‚ฌ์ง„ ์—ฐ๋™ +3. ๋‹ค๊ตญ์–ด ์ง€์› +4. PWA ์™„์„ฑ + +--- + +## ๐ŸŽจ ๋””์ž์ธ ์‹œ์Šคํ…œ + +### ์ƒ‰์ƒ ํŒ”๋ ˆํŠธ +- **Primary**: ๊ตฌ๋งˆ๋ชจํ†  ๋ ˆ๋“œ (#DC2626) +- **Secondary**: ๊ตฌ๋งˆ๋ชจํ†  ๊ทธ๋ฆฐ (#059669) +- **Accent**: ๊ตฌ๋งˆ๋ชจํ†  ๋ธ”๋ฃจ (#2563EB) +- **Neutral**: ํšŒ์ƒ‰ ๊ณ„์—ด (#F3F4F6 ~ #1F2937) + +### ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ +- **์ œ๋ชฉ**: Inter/Noto Sans KR Bold +- **๋ณธ๋ฌธ**: Inter/Noto Sans KR Regular +- **์บก์…˜**: Inter/Noto Sans KR Medium + +### ์ปดํฌ๋„ŒํŠธ ์ผ๊ด€์„ฑ +- **๋ฒ„ํŠผ**: ๋‘ฅ๊ทผ ๋ชจ์„œ๋ฆฌ, ๊ทธ๋ฆผ์ž ํšจ๊ณผ +- **์นด๋“œ**: ๋ฏธ๋‹ˆ๋ฉ€ ๋””์ž์ธ, ํ˜ธ๋ฒ„ ํšจ๊ณผ +- **์ž…๋ ฅ**: ๋ช…ํ™•ํ•œ ๋ผ๋ฒจ, ์—๋Ÿฌ ์ƒํƒœ +- **์•„์ด์ฝ˜**: ์ผ๊ด€๋œ ์Šคํƒ€์ผ, ์˜๋ฏธ ๋ช…ํ™• + diff --git a/README.md b/README.md index a4bdf72..c452ecb 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,195 @@ -# ๊ตฌ๋งˆ๋ชจํ†  ์—ฌํ–‰ ๊ณ„ํš ์‚ฌ์ดํŠธ +# โœˆ๏ธ Travel Planner -2025๋…„ 2์›” 17์ผ ~ 2์›” 20์ผ ๊ตฌ๋งˆ๋ชจํ†  ์—ฌํ–‰์„ ์œ„ํ•œ ๊ฐ€์กฑ ๊ณต์œ  ์—ฌํ–‰ ๊ณ„ํš ์‚ฌ์ดํŠธ์ž…๋‹ˆ๋‹ค. +**์Šค๋งˆํŠธํ•œ ์—ฌํ–‰ ๊ณ„ํš ๊ด€๋ฆฌ ์‹œ์Šคํ…œ** -## ๊ธฐ๋Šฅ +๋‹ค์ค‘ ์‚ฌ์šฉ์ž๋ฅผ ์ง€์›ํ•˜๋Š” ํ˜„๋Œ€์ ์ธ ์—ฌํ–‰ ๊ณ„ํš ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ž…๋‹ˆ๋‹ค. ์—ฌํ–‰ ์ผ์ • ๊ด€๋ฆฌ, ์ง€๋„ ํ†ตํ•ฉ, ๊ณต์œ  ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. -- ๐Ÿ“… **์—ฌํ–‰ ์ผ์ • ๊ด€๋ฆฌ**: ๋‚ ์งœ๋ณ„ ์ผ์ •์„ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค -- ๐Ÿ—พ **๊ด€๊ด‘์ง€ ์ •๋ณด**: ๊ตฌ๋งˆ๋ชจํ†  ์ฃผ์š” ๊ด€๊ด‘์ง€ ์ •๋ณด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค -- ๐Ÿ’ฐ **์˜ˆ์‚ฐ ๊ด€๋ฆฌ**: ํ•ญ๋ชฉ๋ณ„ ์˜ˆ์‚ฐ์„ ์„ค์ •ํ•˜๊ณ  ํ™˜์œจ์„ ์ ์šฉํ•ด ์›ํ™”๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค -- โœ… **์ฒดํฌ๋ฆฌ์ŠคํŠธ**: ์ค€๋น„๋ฌผ, ์‡ผํ•‘ ๋ชฉ๋ก, ๋ฐฉ๋ฌธํ•  ๊ณณ ๋“ฑ์„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค +## ๐ŸŒŸ ์ฃผ์š” ๊ธฐ๋Šฅ -## ์‹œ์ž‘ํ•˜๊ธฐ +### ๐Ÿ‘ฅ ๋ฉ€ํ‹ฐ ์‚ฌ์šฉ์ž ์‹œ์Šคํ…œ +- **์‚ฌ์šฉ์ž ์ธ์ฆ**: JWT ๊ธฐ๋ฐ˜ ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž… +- **๊ด€๋ฆฌ์ž ์‹œ์Šคํ…œ**: ์‚ฌ์šฉ์ž ๋ฐ ์‹œ์Šคํ…œ ๊ด€๋ฆฌ +- **๊ถŒํ•œ ๊ด€๋ฆฌ**: ๊ฐœ์ธ/๊ณต๊ฐœ ์—ฌํ–‰ ๊ณ„ํš ์„ค์ • -### ์„ค์น˜ +### ๐Ÿ—บ๏ธ ์—ฌํ–‰ ๊ณ„ํš ๊ด€๋ฆฌ +- **๋‹ค์ค‘ ์—ฌํ–‰ ๊ด€๋ฆฌ**: ์—ฌ๋Ÿฌ ์—ฌํ–‰ ๊ณ„ํš ๋™์‹œ ๊ด€๋ฆฌ +- **ํ…œํ”Œ๋ฆฟ ์‹œ์Šคํ…œ**: ๋„์‹œ๋ณ„ ์—ฌํ–‰ ํ…œํ”Œ๋ฆฟ ์ œ๊ณต +- **์ผ์ • ๊ด€๋ฆฌ**: ๋‚ ์งœ๋ณ„ ์ƒ์„ธ ์ผ์ • ์ž‘์„ฑ +- **์žฅ์†Œ ๊ฒ€์ƒ‰**: Google Places API ํ†ตํ•ฉ + +### ๐Ÿ”— ๊ณต์œ  ๋ฐ ํ˜‘์—… +- **์—ฌํ–‰ ๊ณต์œ **: ๋งํฌ๋ฅผ ํ†ตํ•œ ์—ฌํ–‰ ๊ณ„ํš ๊ณต์œ  +- **๊ถŒํ•œ ์„ค์ •**: ๋ณด๊ธฐ/ํŽธ์ง‘/๋Œ“๊ธ€ ๊ถŒํ•œ ์ œ์–ด +- **๋Œ“๊ธ€ ์‹œ์Šคํ…œ**: ๊ณต์œ ๋œ ์—ฌํ–‰์— ๋Œ“๊ธ€ ์ž‘์„ฑ + +### ๐Ÿ—บ๏ธ ์ง€๋„ ํ†ตํ•ฉ +- **Google Maps**: ์žฅ์†Œ ๊ฒ€์ƒ‰ ๋ฐ ๊ฒฝ๋กœ ์ตœ์ ํ™” +- **Leaflet**: ์˜คํ”„๋ผ์ธ ์ง€๋„ ์ง€์› +- **๊ฒฝ๋กœ ๊ณ„ํš**: ์—ฌํ–‰์ง€ ๊ฐ„ ์ตœ์  ๊ฒฝ๋กœ ์ œ์•ˆ + +## ๐Ÿš€ ๋น ๋ฅธ ์‹œ์ž‘ + +### Docker๋กœ ์‹คํ–‰ (๊ถŒ์žฅ) ```bash -npm install +# ์ €์žฅ์†Œ ํด๋ก  +git clone +cd travel-planner + +# Docker ํ™˜๊ฒฝ ์‹œ์ž‘ +./docker-start.sh + +# ๋˜๋Š” ์ˆ˜๋™ ์‹คํ–‰ +docker-compose up -d ``` -### ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰ +### ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ```bash +# ์˜์กด์„ฑ ์„ค์น˜ +npm install + +# ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹œ์ž‘ +npm run dev + +# API ์„œ๋ฒ„ ์‹œ์ž‘ (๋ณ„๋„ ํ„ฐ๋ฏธ๋„) +cd server +npm install npm run dev ``` -๋ธŒ๋ผ์šฐ์ €์—์„œ `http://localhost:5173`์„ ์—ด์–ด ํ™•์ธํ•˜์„ธ์š”. +## ๐Ÿ”ง ํ™˜๊ฒฝ ์„ค์ • -### ๋นŒ๋“œ +### ํ•„์ˆ˜ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ -```bash -npm run build +```env +# ํ”„๋ก ํŠธ์—”๋“œ (.env) +VITE_API_URL=http://localhost:3001 +VITE_GOOGLE_OAUTH_CLIENT_ID=your-google-oauth-client-id + +# ๋ฐฑ์—”๋“œ (server/.env) +DATABASE_URL=postgresql://user:password@localhost:5432/travel_planner +JWT_SECRET=your-jwt-secret-key +GOOGLE_MAPS_API_KEY=your-google-maps-api-key ``` -## ๊ธฐ์ˆ  ์Šคํƒ +### ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • -- React 18 -- TypeScript -- Vite -- Tailwind CSS -- date-fns +```bash +# PostgreSQL ์„ค์น˜ ๋ฐ ์‹œ์ž‘ +brew install postgresql +brew services start postgresql -## ์‚ฌ์šฉ ๋ฐฉ๋ฒ• +# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒ์„ฑ +createdb travel_planner -1. **์ผ์ • ์ถ”๊ฐ€**: ๊ฐ ๋‚ ์งœ ์˜†์˜ "+ ์ผ์ • ์ถ”๊ฐ€" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ์ผ์ •์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค -2. **์˜ˆ์‚ฐ ์„ค์ •**: ์˜ˆ์‚ฐ ๊ด€๋ฆฌ ์„น์…˜์—์„œ ๊ฐ ํ•ญ๋ชฉ์„ ํด๋ฆญํ•˜์—ฌ ์˜ˆ์‚ฐ์„ ์ž…๋ ฅํ•ฉ๋‹ˆ๋‹ค -3. **์ฒดํฌ๋ฆฌ์ŠคํŠธ**: ์ฒดํฌ๋ฆฌ์ŠคํŠธ์— ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์™„๋ฃŒ ์‹œ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ํด๋ฆญํ•ฉ๋‹ˆ๋‹ค +# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ (์ž๋™) +npm start # ์„œ๋ฒ„ ์‹œ์ž‘ ์‹œ ์ž๋™ ์‹คํ–‰ +``` -## ๊ณต์œ ํ•˜๊ธฐ +## ๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ -์ด ์‚ฌ์ดํŠธ๋Š” ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€์กฑ๊ณผ ๊ณต์œ ํ•˜๋ ค๋ฉด: +``` +travel-planner/ +โ”œโ”€โ”€ src/ # ํ”„๋ก ํŠธ์—”๋“œ ์†Œ์Šค +โ”‚ โ”œโ”€โ”€ components/ # React ์ปดํฌ๋„ŒํŠธ +โ”‚ โ”œโ”€โ”€ services/ # API ์„œ๋น„์Šค +โ”‚ โ”œโ”€โ”€ types/ # TypeScript ํƒ€์ž… +โ”‚ โ””โ”€โ”€ utils/ # ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ +โ”œโ”€โ”€ server/ # ๋ฐฑ์—”๋“œ API +โ”‚ โ”œโ”€โ”€ routes/ # API ๋ผ์šฐํŠธ +โ”‚ โ”œโ”€โ”€ migrations/ # DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ +โ”‚ โ””โ”€โ”€ uploads/ # ํŒŒ์ผ ์—…๋กœ๋“œ +โ”œโ”€โ”€ docker/ # Docker ์„ค์ • +โ””โ”€โ”€ docs/ # ๋ฌธ์„œ +``` -1. ๊ฐœ๋ฐœ ์„œ๋ฒ„๋ฅผ ์‹คํ–‰ํ•œ ํ›„ ๋„คํŠธ์›Œํฌ IP๋กœ ์ ‘๊ทผํ•˜๊ฑฐ๋‚˜ -2. ๋นŒ๋“œ ํ›„ ์ •์  ํ˜ธ์ŠคํŒ… ์„œ๋น„์Šค(Vercel, Netlify ๋“ฑ)์— ๋ฐฐํฌํ•˜์„ธ์š” +## ๐Ÿ› ๏ธ ๊ธฐ์ˆ  ์Šคํƒ +### ํ”„๋ก ํŠธ์—”๋“œ +- **React 18** + **TypeScript** +- **Vite** (๋นŒ๋“œ ๋„๊ตฌ) +- **Tailwind CSS** (์Šคํƒ€์ผ๋ง) +- **React Router** (๋ผ์šฐํŒ…) +- **Leaflet** + **Google Maps** (์ง€๋„) + +### ๋ฐฑ์—”๋“œ +- **Node.js** + **Express** +- **PostgreSQL** (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค) +- **JWT** (์ธ์ฆ) +- **Multer** (ํŒŒ์ผ ์—…๋กœ๋“œ) + +### ์ธํ”„๋ผ +- **Docker** + **Docker Compose** +- **Nginx** (ํ”„๋กœ๋•์…˜) + +## ๐Ÿ“Š API ๋ฌธ์„œ + +### ์ธ์ฆ API +``` +POST /api/auth/register # ํšŒ์›๊ฐ€์ž… +POST /api/auth/login # ๋กœ๊ทธ์ธ +GET /api/auth/verify # ํ† ํฐ ๊ฒ€์ฆ +``` + +### ์—ฌํ–‰ ๊ณ„ํš API +``` +GET /api/travel-plans # ์—ฌํ–‰ ๋ชฉ๋ก +POST /api/travel-plans # ์—ฌํ–‰ ์ƒ์„ฑ +GET /api/travel-plans/:id # ์—ฌํ–‰ ์กฐํšŒ +PUT /api/travel-plans/:id # ์—ฌํ–‰ ์ˆ˜์ • +DELETE /api/travel-plans/:id # ์—ฌํ–‰ ์‚ญ์ œ +``` + +### ๊ณต์œ  API +``` +POST /api/share/create # ๊ณต์œ  ๋งํฌ ์ƒ์„ฑ +GET /api/share/:code # ๊ณต์œ ๋œ ์—ฌํ–‰ ์กฐํšŒ +``` + +## ๐Ÿ”’ ๋ณด์•ˆ + +- **JWT ํ† ํฐ**: ์•ˆ์ „ํ•œ ์‚ฌ์šฉ์ž ์ธ์ฆ +- **๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹œ**: bcrypt ์•”ํ˜ธํ™” +- **SQL ์ธ์ ์…˜ ๋ฐฉ์ง€**: ๋งค๊ฐœ๋ณ€์ˆ˜ํ™”๋œ ์ฟผ๋ฆฌ +- **CORS ์„ค์ •**: ํ—ˆ์šฉ๋œ ๋„๋ฉ”์ธ๋งŒ ์ ‘๊ทผ + +## ๐ŸŒ ๋ฐฐํฌ + +### Docker ๋ฐฐํฌ +```bash +# ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ +docker-compose -f docker-compose.prod.yml up -d +``` + +### ์ˆ˜๋™ ๋ฐฐํฌ +```bash +# ํ”„๋ก ํŠธ์—”๋“œ ๋นŒ๋“œ +npm run build + +# ์„œ๋ฒ„ ์‹œ์ž‘ +cd server +npm start +``` + +## ๐Ÿค ๊ธฐ์—ฌํ•˜๊ธฐ + +1. Fork the repository +2. Create feature branch (`git checkout -b feature/amazing-feature`) +3. Commit changes (`git commit -m 'Add amazing feature'`) +4. Push to branch (`git push origin feature/amazing-feature`) +5. Open Pull Request + +## ๐Ÿ“ ๋ผ์ด์„ ์Šค + +MIT License - ์ž์„ธํ•œ ๋‚ด์šฉ์€ [LICENSE](LICENSE) ํŒŒ์ผ์„ ์ฐธ์กฐํ•˜์„ธ์š”. + +## ๐Ÿ“ž ์ง€์› + +- **์ด์Šˆ ๋ฆฌํฌํŠธ**: GitHub Issues +- **๋ฌธ์„œ**: [Wiki](../../wiki) +- **FAQ**: [์ž์ฃผ ๋ฌป๋Š” ์งˆ๋ฌธ](docs/FAQ.md) + +--- + +**Travel Planner** - ๋‹น์‹ ์˜ ์™„๋ฒฝํ•œ ์—ฌํ–‰์„ ๊ณ„ํšํ•˜์„ธ์š”! โœˆ๏ธ๐Ÿ—บ๏ธ \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index d1ed09f..a3097cf 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,6 +1,40 @@ -version: '3.8' - services: + # PostgreSQL Database + db: + image: postgres:16-alpine + container_name: kumamoto-db + environment: + POSTGRES_USER: kumamoto + POSTGRES_PASSWORD: kumamoto123 + POSTGRES_DB: kumamoto_travel + TZ: Asia/Seoul + PGTZ: Asia/Seoul + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + restart: unless-stopped + + # Backend API Server + api: + image: node:20-alpine + container_name: kumamoto-api + working_dir: /app + volumes: + - ./server:/app + - /app/node_modules + ports: + - "3000:3000" + environment: + DATABASE_URL: postgresql://kumamoto:kumamoto123@db:5432/kumamoto_travel + NODE_ENV: development + TZ: Asia/Seoul + command: sh -c "npm install && npm run dev" + depends_on: + - db + restart: unless-stopped + + # Frontend (Vite) web: image: node:20-alpine working_dir: /app @@ -9,31 +43,15 @@ services: - /app/node_modules ports: - "5173:5173" + environment: + VITE_API_URL: http://localhost:3000 + TZ: Asia/Seoul command: sh -c "npm install && npm run dev -- --host" container_name: kumamoto-travel-planner-dev - restart: unless-stopped depends_on: - - map-server - environment: - - VITE_MAP_TILES_URL=http://localhost:8080/tiles - - map-server: - build: - context: ./docker/map-server - dockerfile: Dockerfile - ports: - - "8080:80" - container_name: kumamoto-map-server-dev + - api restart: unless-stopped - volumes: - - map_data_dev:/var/lib/postgresql/data - - map_tiles_dev:/var/www/html/tiles - environment: - - POSTGRES_DB=kumamoto_map - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=mapserver123 volumes: - map_data_dev: - map_tiles_dev: + postgres_data: diff --git a/docker-compose.yml b/docker-compose.yml index 012c62a..fd41e7e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,26 +1,50 @@ version: '3.8' services: + # ํ”„๋ก ํŠธ์—”๋“œ web: build: context: . dockerfile: Dockerfile ports: - "3000:80" - container_name: kumamoto-travel-planner + container_name: travel-planner-web + restart: unless-stopped + depends_on: + - api-server + - map-server + environment: + - VITE_MAP_TILES_URL=http://localhost:8080/tiles + - VITE_API_URL=http://localhost:3001 + + # API ์„œ๋ฒ„ + api-server: + build: + context: ./server + dockerfile: Dockerfile + ports: + - "3001:3000" + container_name: travel-planner-api restart: unless-stopped depends_on: - map-server environment: - - VITE_MAP_TILES_URL=http://localhost:8080/tiles + - DATABASE_URL=postgresql://postgres:mapserver123@map-server:5432/kumamoto_map + - JWT_SECRET=travel-planner-jwt-secret-key-2024-docker + - NODE_ENV=production + - PORT=3000 + volumes: + - ./server/uploads:/app/uploads + - ./server/migrations:/app/migrations + # ์ง€๋„ ์„œ๋ฒ„ (๊ธฐ์กด) map-server: build: context: ./docker/map-server dockerfile: Dockerfile ports: - "8080:80" - container_name: kumamoto-map-server + container_name: travel-planner-map-server restart: unless-stopped volumes: - map_data:/var/lib/postgresql/data @@ -31,6 +55,6 @@ services: - POSTGRES_PASSWORD=mapserver123 volumes: - map_data: - map_tiles: + map_data: # ์ง€๋„ ๋ฐ์ดํ„ฐ (๊ธฐ์กด DB ํฌํ•จ) + map_tiles: # ์ง€๋„ ํƒ€์ผ diff --git a/docker-start.sh b/docker-start.sh new file mode 100644 index 0000000..cbe02a2 --- /dev/null +++ b/docker-start.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Travel Planner Docker ์‹œ์ž‘ ์Šคํฌ๋ฆฝํŠธ + +echo "๐Ÿณ Travel Planner Docker ํ™˜๊ฒฝ ์‹œ์ž‘ ์ค‘..." + +# ๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ ์ •๋ฆฌ (์„ ํƒ์‚ฌํ•ญ) +read -p "๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ •๋ฆฌํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? (y/N): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "๐Ÿงน ๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ ์ •๋ฆฌ ์ค‘..." + docker-compose down -v + docker system prune -f +fi + +# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํŒŒ์ผ ํ™•์ธ +if [ ! -f .env ]; then + echo "๐Ÿ“‹ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํŒŒ์ผ ์ƒ์„ฑ ์ค‘..." + cp env.docker .env + echo "โš ๏ธ .env ํŒŒ์ผ์„ ํ™•์ธํ•˜๊ณ  ํ•„์š”ํ•œ ์„ค์ •์„ ์ˆ˜์ •ํ•˜์„ธ์š”." +fi + +# ์„œ๋ฒ„ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํ™•์ธ +if [ ! -f server/.env ]; then + echo "๐Ÿ“‹ ์„œ๋ฒ„ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํŒŒ์ผ ์ƒ์„ฑ ์ค‘..." + cp server/env.example server/.env + echo "โœ… ์„œ๋ฒ„ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ Docker Compose์—์„œ ์ž๋™ ์„ค์ •๋ฉ๋‹ˆ๋‹ค." +fi + +# Docker ์ด๋ฏธ์ง€ ๋นŒ๋“œ ๋ฐ ์‹œ์ž‘ +echo "๐Ÿ”จ Docker ์ด๋ฏธ์ง€ ๋นŒ๋“œ ์ค‘..." +docker-compose build + +echo "๐Ÿš€ ์„œ๋น„์Šค ์‹œ์ž‘ ์ค‘..." +docker-compose up -d + +# ์„œ๋น„์Šค ์ƒํƒœ ํ™•์ธ +echo "โณ ์„œ๋น„์Šค ์‹œ์ž‘ ๋Œ€๊ธฐ ์ค‘..." +sleep 10 + +echo "๐Ÿ” ์„œ๋น„์Šค ์ƒํƒœ ํ™•์ธ ์ค‘..." +docker-compose ps + +# ํ—ฌ์Šค ์ฒดํฌ +echo "๐Ÿฅ ํ—ฌ์Šค ์ฒดํฌ ์ค‘..." +echo "- ํ”„๋ก ํŠธ์—”๋“œ: http://localhost:3000" +echo "- API ์„œ๋ฒ„: http://localhost:3001/health" +echo "- ์ง€๋„ ์„œ๋ฒ„: http://localhost:8080" +echo "- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค: localhost:5432" + +# API ์„œ๋ฒ„ ํ—ฌ์Šค ์ฒดํฌ +echo "" +echo "๐Ÿ“ก API ์„œ๋ฒ„ ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ..." +sleep 5 +curl -f http://localhost:3001/health || echo "โš ๏ธ API ์„œ๋ฒ„๊ฐ€ ์•„์ง ์‹œ์ž‘ ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”." + +echo "" +echo "๐ŸŽ‰ Travel Planner Docker ํ™˜๊ฒฝ์ด ์‹œ์ž‘๋˜์—ˆ์Šต๋‹ˆ๋‹ค!" +echo "" +echo "๐Ÿ“– ์‚ฌ์šฉ๋ฒ•:" +echo " - ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜: http://localhost:3000" +echo " - ์ดˆ๊ธฐ ์„ค์ •: http://localhost:3000/?debug=true" +echo " - API ์„œ๋ฒ„: http://localhost:3001" +echo " - ๋กœ๊ทธ ํ™•์ธ: docker-compose logs -f" +echo " - ์ค‘์ง€: docker-compose down" +echo "" diff --git a/env.docker b/env.docker new file mode 100644 index 0000000..c5bf22d --- /dev/null +++ b/env.docker @@ -0,0 +1,4 @@ +# Docker ํ™˜๊ฒฝ ๋ณ€์ˆ˜ (๊ธฐ์กด DB ์‚ฌ์šฉ) +VITE_API_URL=http://localhost:3001 +VITE_MAP_TILES_URL=http://localhost:8080/tiles +VITE_GOOGLE_OAUTH_CLIENT_ID=your-google-oauth-client-id diff --git a/index.html b/index.html index 3ab28ee..cd9499b 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - ๊ตฌ๋งˆ๋ชจํ†  ์—ฌํ–‰ ๊ณ„ํš - 2025๋…„ 2์›” + Travel Planner - ์—ฌํ–‰ ๊ณ„ํš ๊ด€๋ฆฌ ์‹œ์Šคํ…œ
diff --git a/package-lock.json b/package-lock.json index 02d3572..44af81a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "leaflet": "^1.9.4", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-leaflet": "^4.2.1" + "react-leaflet": "^4.2.1", + "react-router-dom": "^7.9.5" }, "devDependencies": { "@types/react": "^18.2.43", @@ -1565,6 +1566,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2525,6 +2535,44 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz", + "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.5.tgz", + "integrity": "sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.5" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -2665,6 +2713,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index 4867e42..9b1da41 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "kumamoto-travel-planner", + "name": "travel-planner", "private": true, - "version": "1.0.0", + "version": "2.0.0", "type": "module", "scripts": { "dev": "vite", @@ -14,7 +14,8 @@ "leaflet": "^1.9.4", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-leaflet": "^4.2.1" + "react-leaflet": "^4.2.1", + "react-router-dom": "^7.9.5" }, "devDependencies": { "@types/react": "^18.2.43", diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..8f4fb02 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,27 @@ +# API Server Dockerfile +FROM node:18-alpine + +# ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ • +WORKDIR /app + +# ํŒจํ‚ค์ง€ ํŒŒ์ผ ๋ณต์‚ฌ +COPY package*.json ./ + +# ์˜์กด์„ฑ ์„ค์น˜ +RUN npm ci --only=production + +# ์†Œ์Šค ์ฝ”๋“œ ๋ณต์‚ฌ +COPY . . + +# ์—…๋กœ๋“œ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ +RUN mkdir -p uploads + +# ํฌํŠธ ๋…ธ์ถœ +EXPOSE 3000 + +# ํ—ฌ์Šค์ฒดํฌ +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3000/health || exit 1 + +# ์„œ๋ฒ„ ์‹œ์ž‘ +CMD ["npm", "start"] diff --git a/server/db.js b/server/db.js new file mode 100644 index 0000000..c41cb77 --- /dev/null +++ b/server/db.js @@ -0,0 +1,45 @@ +const { Pool } = require('pg'); +const fs = require('fs'); +const path = require('path'); + +// PostgreSQL ์—ฐ๊ฒฐ ํ’€ ์ƒ์„ฑ +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); + +// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” (ํ…Œ์ด๋ธ” ์ƒ์„ฑ) +async function initializeDatabase() { + try { + // v2 ์Šคํ‚ค๋งˆ ์‚ฌ์šฉ (์ƒˆ๋กœ์šด ๋ฉ€ํ‹ฐ์œ ์ € ์‹œ์Šคํ…œ) + const schemaPath = fs.existsSync(path.join(__dirname, 'schema_v2.sql')) + ? 'schema_v2.sql' + : 'schema.sql'; + + const schema = fs.readFileSync(path.join(__dirname, schemaPath), 'utf8'); + await pool.query(schema); + console.log(`โœ… Database tables initialized successfully (${schemaPath})`); + } catch (error) { + console.error('โŒ Error initializing database:', error); + throw error; + } +} + +// ์ฟผ๋ฆฌ ์‹คํ–‰ ํ—ฌํผ ํ•จ์ˆ˜ +async function query(text, params) { + const start = Date.now(); + try { + const res = await pool.query(text, params); + const duration = Date.now() - start; + console.log('Executed query', { text, duration, rows: res.rowCount }); + return res; + } catch (error) { + console.error('Query error:', error); + throw error; + } +} + +module.exports = { + query, + pool, + initializeDatabase, +}; diff --git a/server/env.example b/server/env.example new file mode 100644 index 0000000..f8c9460 --- /dev/null +++ b/server/env.example @@ -0,0 +1,18 @@ +# Database Configuration +DATABASE_URL=postgresql://username:password@localhost:5432/kumamoto_travel + +# JWT Configuration +JWT_SECRET=your-super-secret-jwt-key-change-this-in-production + +# Google Maps API (Optional) +GOOGLE_MAPS_API_KEY=your-google-maps-api-key + +# Email Configuration (Optional) +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=your-email@gmail.com +SMTP_PASS=your-app-password + +# Server Configuration +PORT=3000 +NODE_ENV=development diff --git a/server/migrate.js b/server/migrate.js new file mode 100644 index 0000000..d137479 --- /dev/null +++ b/server/migrate.js @@ -0,0 +1,80 @@ +const { query } = require('./db'); +const fs = require('fs'); +const path = require('path'); + +async function runMigrations() { + try { + console.log('๐Ÿ”„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹œ์ž‘...'); + + // ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋””๋ ‰ํ† ๋ฆฌ ํ™•์ธ + const migrationsDir = path.join(__dirname, 'migrations'); + if (!fs.existsSync(migrationsDir)) { + console.log('๐Ÿ“ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค.'); + return; + } + + // ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ๋ชฉ๋ก + const migrationFiles = fs.readdirSync(migrationsDir) + .filter(file => file.endsWith('.sql')) + .sort(); + + if (migrationFiles.length === 0) { + console.log('๐Ÿ“„ ์‹คํ–‰ํ•  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ์—†์Šต๋‹ˆ๋‹ค.'); + return; + } + + // ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํ…Œ์ด๋ธ” ์ƒ์„ฑ + await query(` + CREATE TABLE IF NOT EXISTS migrations ( + id SERIAL PRIMARY KEY, + filename VARCHAR(255) UNIQUE NOT NULL, + executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + + // ๊ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ + for (const filename of migrationFiles) { + // ์ด๋ฏธ ์‹คํ–‰๋œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ธ์ง€ ํ™•์ธ + const existingResult = await query( + 'SELECT id FROM migrations WHERE filename = $1', + [filename] + ); + + if (existingResult.rows.length > 0) { + console.log(`โญ๏ธ ${filename} - ์ด๋ฏธ ์‹คํ–‰๋จ`); + continue; + } + + console.log(`๐Ÿ”ง ${filename} ์‹คํ–‰ ์ค‘...`); + + // ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ ์ฝ๊ธฐ ๋ฐ ์‹คํ–‰ + const migrationPath = path.join(migrationsDir, filename); + const migrationSQL = fs.readFileSync(migrationPath, 'utf8'); + + await query(migrationSQL); + + // ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ธฐ๋ก + await query( + 'INSERT INTO migrations (filename) VALUES ($1)', + [filename] + ); + + console.log(`โœ… ${filename} ์™„๋ฃŒ`); + } + + console.log('๐ŸŽ‰ ๋ชจ๋“  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!'); + + } catch (error) { + console.error('โŒ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํŒจ:', error); + throw error; + } +} + +// ์ง์ ‘ ์‹คํ–‰ ์‹œ +if (require.main === module) { + runMigrations() + .then(() => process.exit(0)) + .catch(() => process.exit(1)); +} + +module.exports = { runMigrations }; diff --git a/server/migrations/001_add_user_system.sql b/server/migrations/001_add_user_system.sql new file mode 100644 index 0000000..0aba88d --- /dev/null +++ b/server/migrations/001_add_user_system.sql @@ -0,0 +1,129 @@ +-- ๊ธฐ์กด DB์— ์‚ฌ์šฉ์ž ์‹œ์Šคํ…œ ์ถ”๊ฐ€ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ + +-- ์‚ฌ์šฉ์ž ํ…Œ์ด๋ธ” ์ถ”๊ฐ€ +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, + role VARCHAR(20) DEFAULT 'user' CHECK (role IN ('admin', 'user')), + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login TIMESTAMP +); + +-- ๊ธฐ์กด travel_plans ํ…Œ์ด๋ธ”์— ์ปฌ๋Ÿผ ์ถ”๊ฐ€ (์•ˆ์ „ํ•˜๊ฒŒ ํ•˜๋‚˜์”ฉ) +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='travel_plans' AND column_name='user_id') THEN + ALTER TABLE travel_plans ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE SET NULL; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='travel_plans' AND column_name='title') THEN + ALTER TABLE travel_plans ADD COLUMN title VARCHAR(255) DEFAULT 'Untitled Trip'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='travel_plans' AND column_name='description') THEN + ALTER TABLE travel_plans ADD COLUMN description TEXT; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='travel_plans' AND column_name='destination') THEN + ALTER TABLE travel_plans ADD COLUMN destination VARCHAR(255) DEFAULT 'Kumamoto'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='travel_plans' AND column_name='is_public') THEN + ALTER TABLE travel_plans ADD COLUMN is_public BOOLEAN DEFAULT false; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='travel_plans' AND column_name='is_template') THEN + ALTER TABLE travel_plans ADD COLUMN is_template BOOLEAN DEFAULT false; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='travel_plans' AND column_name='template_category') THEN + ALTER TABLE travel_plans ADD COLUMN template_category VARCHAR(20); + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='travel_plans' AND column_name='tags') THEN + ALTER TABLE travel_plans ADD COLUMN tags TEXT[] DEFAULT '{}'; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='travel_plans' AND column_name='status') THEN + ALTER TABLE travel_plans ADD COLUMN status VARCHAR(20) DEFAULT 'draft'; + END IF; +END $$; + +-- ๊ณต์œ  ๋งํฌ ํ…Œ์ด๋ธ” ์ถ”๊ฐ€ +CREATE TABLE IF NOT EXISTS share_links ( + id SERIAL PRIMARY KEY, + trip_id INTEGER NOT NULL REFERENCES travel_plans(id) ON DELETE CASCADE, + created_by INTEGER NOT NULL REFERENCES users(id), + share_code VARCHAR(8) UNIQUE NOT NULL, + expires_at TIMESTAMP, + is_active BOOLEAN DEFAULT true, + access_count INTEGER DEFAULT 0, + max_access_count INTEGER, + can_view BOOLEAN DEFAULT true, + can_edit BOOLEAN DEFAULT false, + can_comment BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_accessed TIMESTAMP +); + +-- ๋Œ“๊ธ€ ํ…Œ์ด๋ธ” ์ถ”๊ฐ€ +CREATE TABLE IF NOT EXISTS trip_comments ( + id SERIAL PRIMARY KEY, + trip_id INTEGER NOT NULL REFERENCES travel_plans(id) ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users(id), + content TEXT NOT NULL, + is_edited BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ์‹œ์Šคํ…œ ์„ค์ • ํ…Œ์ด๋ธ” ์ถ”๊ฐ€ +CREATE TABLE IF NOT EXISTS system_settings ( + key VARCHAR(100) PRIMARY KEY, + value TEXT, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ์ธ๋ฑ์Šค ์ถ”๊ฐ€ +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); +CREATE INDEX IF NOT EXISTS idx_users_role ON users(role); +CREATE INDEX IF NOT EXISTS idx_travel_plans_user ON travel_plans(user_id); +CREATE INDEX IF NOT EXISTS idx_travel_plans_status ON travel_plans(status); +CREATE INDEX IF NOT EXISTS idx_share_links_code ON share_links(share_code); +CREATE INDEX IF NOT EXISTS idx_share_links_trip ON share_links(trip_id); +CREATE INDEX IF NOT EXISTS idx_comments_trip ON trip_comments(trip_id); + +-- ์ดˆ๊ธฐ ์‹œ์Šคํ…œ ์„ค์ • +INSERT INTO system_settings (key, value, description) VALUES + ('app_version', '2.0.0', '์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฒ„์ „'), + ('setup_completed', 'false', '์ดˆ๊ธฐ ์„ค์ • ์™„๋ฃŒ ์—ฌ๋ถ€'), + ('jwt_secret_set', 'false', 'JWT ์‹œํฌ๋ฆฟ ์„ค์ • ์—ฌ๋ถ€') +ON CONFLICT (key) DO NOTHING; + +-- ์—…๋ฐ์ดํŠธ ํŠธ๋ฆฌ๊ฑฐ ํ•จ์ˆ˜ +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- ์—…๋ฐ์ดํŠธ ํŠธ๋ฆฌ๊ฑฐ ์ ์šฉ +DROP TRIGGER IF EXISTS update_users_updated_at ON users; +CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +DROP TRIGGER IF EXISTS update_travel_plans_updated_at ON travel_plans; +CREATE TRIGGER update_travel_plans_updated_at BEFORE UPDATE ON travel_plans FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +DROP TRIGGER IF EXISTS update_comments_updated_at ON trip_comments; +CREATE TRIGGER update_comments_updated_at BEFORE UPDATE ON trip_comments FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +DROP TRIGGER IF EXISTS update_settings_updated_at ON system_settings; +CREATE TRIGGER update_settings_updated_at BEFORE UPDATE ON system_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..a30ba4f --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,2154 @@ +{ + "name": "travel-planner-api", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "travel-planner-api", + "version": "2.0.0", + "dependencies": { + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "multer": "^2.0.2", + "pg": "^8.11.3" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..b6a8b9c --- /dev/null +++ b/server/package.json @@ -0,0 +1,22 @@ +{ + "name": "travel-planner-api", + "version": "2.0.0", + "description": "Backend API for Travel Planner - Multi-user travel planning system", + "main": "server.js", + "scripts": { + "dev": "nodemon server.js", + "start": "node server.js" + }, + "dependencies": { + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.2", + "multer": "^2.0.2", + "pg": "^8.11.3" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } +} diff --git a/server/routes/auth.js b/server/routes/auth.js new file mode 100644 index 0000000..3866d9e --- /dev/null +++ b/server/routes/auth.js @@ -0,0 +1,179 @@ +const express = require('express'); +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +const { query } = require('../db'); + +const router = express.Router(); + +// JWT ์‹œํฌ๋ฆฟ (ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ ๊ฐ€์ ธ์˜ค๊ฑฐ๋‚˜ ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ) +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-this-in-production'; + +// ํšŒ์›๊ฐ€์ž… +router.post('/register', async (req, res) => { + try { + const { email, password, name } = req.body; + + // ์ด๋ฉ”์ผ ์ค‘๋ณต ํ™•์ธ + const existingUser = await query('SELECT id FROM users WHERE email = $1', [email]); + if (existingUser.rows.length > 0) { + return res.status(400).json({ success: false, message: '์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค' }); + } + + // ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹œ + const saltRounds = 10; + const passwordHash = await bcrypt.hash(password, saltRounds); + + // ์‚ฌ์šฉ์ž ์ƒ์„ฑ + const result = await query( + 'INSERT INTO users (email, password_hash, name) VALUES ($1, $2, $3) RETURNING id, email, name, role, created_at', + [email, passwordHash, name] + ); + + const user = result.rows[0]; + + // JWT ํ† ํฐ ์ƒ์„ฑ + const token = jwt.sign( + { userId: user.id, email: user.email, role: user.role }, + JWT_SECRET, + { expiresIn: '7d' } + ); + + res.json({ + success: true, + user: { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + created_at: user.created_at + }, + token, + message: 'ํšŒ์›๊ฐ€์ž…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค' + }); + } catch (error) { + console.error('Registration error:', error); + res.status(500).json({ success: false, message: 'ํšŒ์›๊ฐ€์ž… ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค' }); + } +}); + +// ๋กœ๊ทธ์ธ +router.post('/login', async (req, res) => { + try { + const { email, password } = req.body; + + // ์‚ฌ์šฉ์ž ์กฐํšŒ + const result = await query( + 'SELECT id, email, password_hash, name, role, is_active FROM users WHERE email = $1', + [email] + ); + + if (result.rows.length === 0) { + return res.status(400).json({ success: false, message: '๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค' }); + } + + const user = result.rows[0]; + + if (!user.is_active) { + return res.status(400).json({ success: false, message: '๋น„ํ™œ์„ฑํ™”๋œ ๊ณ„์ •์ž…๋‹ˆ๋‹ค' }); + } + + // ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ + const isValidPassword = await bcrypt.compare(password, user.password_hash); + if (!isValidPassword) { + return res.status(400).json({ success: false, message: '๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค' }); + } + + // ๋งˆ์ง€๋ง‰ ๋กœ๊ทธ์ธ ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ + await query('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = $1', [user.id]); + + // JWT ํ† ํฐ ์ƒ์„ฑ + const token = jwt.sign( + { userId: user.id, email: user.email, role: user.role }, + JWT_SECRET, + { expiresIn: '7d' } + ); + + res.json({ + success: true, + user: { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + is_active: user.is_active + }, + token, + message: '๋กœ๊ทธ์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค' + }); + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ success: false, message: '๋กœ๊ทธ์ธ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค' }); + } +}); + +// ํ† ํฐ ๊ฒ€์ฆ +router.get('/verify', authenticateToken, async (req, res) => { + try { + const result = await query( + 'SELECT id, email, name, role, is_active, last_login FROM users WHERE id = $1', + [req.user.userId] + ); + + if (result.rows.length === 0) { + return res.status(404).json({ success: false, message: '์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค' }); + } + + const user = result.rows[0]; + + if (!user.is_active) { + return res.status(403).json({ success: false, message: '๋น„ํ™œ์„ฑํ™”๋œ ๊ณ„์ •์ž…๋‹ˆ๋‹ค' }); + } + + res.json({ + success: true, + user: { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + is_active: user.is_active, + last_login: user.last_login + } + }); + } catch (error) { + console.error('Token verification error:', error); + res.status(500).json({ success: false, message: 'ํ† ํฐ ๊ฒ€์ฆ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค' }); + } +}); + +// JWT ํ† ํฐ ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด +function authenticateToken(req, res, next) { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({ success: false, message: '์•ก์„ธ์Šค ํ† ํฐ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค' }); + } + + jwt.verify(token, JWT_SECRET, (err, user) => { + if (err) { + return res.status(403).json({ success: false, message: '์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค' }); + } + req.user = user; + next(); + }); +} + +// ๊ด€๋ฆฌ์ž ๊ถŒํ•œ ํ™•์ธ ๋ฏธ๋“ค์›จ์–ด +function requireAdmin(req, res, next) { + if (req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '๊ด€๋ฆฌ์ž ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค' }); + } + next(); +} + +module.exports = { + router, + authenticateToken, + requireAdmin +}; diff --git a/server/routes/basePoints.js b/server/routes/basePoints.js new file mode 100644 index 0000000..d8f3527 --- /dev/null +++ b/server/routes/basePoints.js @@ -0,0 +1,77 @@ +const express = require('express'); +const { query } = require('../db'); + +const router = express.Router(); + +// ๋ชจ๋“  ๊ธฐ๋ณธ ํฌ์ธํŠธ ์กฐํšŒ +router.get('/', async (req, res) => { + try { + const result = await query( + 'SELECT * FROM base_points ORDER BY created_at DESC' + ); + + const basePoints = result.rows.map(row => ({ + id: row.id.toString(), + name: row.name, + address: row.address, + type: row.type, + coordinates: { + lat: parseFloat(row.lat), + lng: parseFloat(row.lng) + }, + memo: row.memo + })); + + res.json(basePoints); + } catch (error) { + console.error('Error fetching base points:', error); + res.status(500).json({ error: error.message }); + } +}); + +// ๊ธฐ๋ณธ ํฌ์ธํŠธ ์ถ”๊ฐ€ +router.post('/', async (req, res) => { + try { + const { name, address, type, coordinates, memo } = req.body; + + const result = await query( + `INSERT INTO base_points (name, address, type, lat, lng, memo) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING *`, + [name, address || null, type, coordinates.lat, coordinates.lng, memo || null] + ); + + const basePoint = { + id: result.rows[0].id.toString(), + name: result.rows[0].name, + address: result.rows[0].address, + type: result.rows[0].type, + coordinates: { + lat: parseFloat(result.rows[0].lat), + lng: parseFloat(result.rows[0].lng) + }, + memo: result.rows[0].memo + }; + + res.json(basePoint); + } catch (error) { + console.error('Error creating base point:', error); + res.status(500).json({ error: error.message }); + } +}); + +// ๊ธฐ๋ณธ ํฌ์ธํŠธ ์‚ญ์ œ +router.delete('/:id', async (req, res) => { + try { + const { id } = req.params; + + await query('DELETE FROM base_points WHERE id = $1', [id]); + + res.json({ success: true }); + } catch (error) { + console.error('Error deleting base point:', error); + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/server/routes/setup.js b/server/routes/setup.js new file mode 100644 index 0000000..be5363d --- /dev/null +++ b/server/routes/setup.js @@ -0,0 +1,187 @@ +const express = require('express'); +const bcrypt = require('bcrypt'); +const { query } = require('../db'); + +const router = express.Router(); + +// ์„ค์ • ์ƒํƒœ ํ™•์ธ +router.get('/status', async (req, res) => { + try { + // ์‹œ์Šคํ…œ ์„ค์ • ํ™•์ธ + const settingsResult = await query('SELECT key, value FROM system_settings'); + const settings = {}; + settingsResult.rows.forEach(row => { + settings[row.key] = row.value; + }); + + // ๊ด€๋ฆฌ์ž ๊ณ„์ • ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ + const adminResult = await query('SELECT COUNT(*) as count FROM users WHERE role = $1', ['admin']); + const hasAdmin = parseInt(adminResult.rows[0].count) > 0; + + // ์ „์ฒด ์‚ฌ์šฉ์ž ์ˆ˜ + const userCountResult = await query('SELECT COUNT(*) as count FROM users'); + const totalUsers = parseInt(userCountResult.rows[0].count); + + const isSetupComplete = settings.setup_completed === 'true' && hasAdmin; + + res.json({ + isSetupComplete, + is_setup_required: !isSetupComplete, + setup_step: isSetupComplete ? 'completed' : 'initial', + has_admin: hasAdmin, + total_users: totalUsers, + version: settings.app_version || '2.0.0', + settings: { + jwt_secret_set: settings.jwt_secret_set === 'true', + google_maps_configured: settings.google_maps_configured === 'true', + email_configured: settings.email_configured === 'true' + } + }); + } catch (error) { + console.error('Setup status check error:', error); + res.status(500).json({ + success: false, + message: '์„ค์ • ์ƒํƒœ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค', + error: error.message + }); + } +}); + +// ์ดˆ๊ธฐ ๊ด€๋ฆฌ์ž ๊ณ„์ • ์ƒ์„ฑ +router.post('/admin', async (req, res) => { + try { + const { name, email, password } = req.body; + + // ์ž…๋ ฅ ๊ฒ€์ฆ + if (!name || !email || !password) { + return res.status(400).json({ + success: false, + message: '์ด๋ฆ„, ์ด๋ฉ”์ผ, ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ชจ๋‘ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”' + }); + } + + // ์ด๋ฏธ ๊ด€๋ฆฌ์ž๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ + const existingAdmin = await query('SELECT id FROM users WHERE role = $1', ['admin']); + if (existingAdmin.rows.length > 0) { + return res.status(400).json({ + success: false, + message: '์ด๋ฏธ ๊ด€๋ฆฌ์ž ๊ณ„์ •์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค' + }); + } + + // ์ด๋ฉ”์ผ ์ค‘๋ณต ํ™•์ธ + const existingUser = await query('SELECT id FROM users WHERE email = $1', [email]); + if (existingUser.rows.length > 0) { + return res.status(400).json({ + success: false, + message: '์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค' + }); + } + + // ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹œ + const saltRounds = 10; + const passwordHash = await bcrypt.hash(password, saltRounds); + + // ๊ด€๋ฆฌ์ž ๊ณ„์ • ์ƒ์„ฑ + const result = await query( + 'INSERT INTO users (email, password_hash, name, role) VALUES ($1, $2, $3, $4) RETURNING id, email, name, role, created_at', + [email, passwordHash, name, 'admin'] + ); + + const admin = result.rows[0]; + + // ์„ค์ • ์™„๋ฃŒ ํ‘œ์‹œ + await query( + 'UPDATE system_settings SET value = $1, updated_at = CURRENT_TIMESTAMP WHERE key = $2', + ['true', 'setup_completed'] + ); + + res.json({ + success: true, + message: '๊ด€๋ฆฌ์ž ๊ณ„์ •์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค', + admin: { + id: admin.id, + email: admin.email, + name: admin.name, + role: admin.role, + created_at: admin.created_at + } + }); + } catch (error) { + console.error('Admin creation error:', error); + res.status(500).json({ + success: false, + message: '๊ด€๋ฆฌ์ž ๊ณ„์ • ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค', + error: error.message + }); + } +}); + +// ํ™˜๊ฒฝ ์„ค์ • ์—…๋ฐ์ดํŠธ +router.post('/config', async (req, res) => { + try { + const { jwt_secret, google_maps_api_key, email_config } = req.body; + + const updates = []; + + // JWT ์‹œํฌ๋ฆฟ ์„ค์ • + if (jwt_secret) { + process.env.JWT_SECRET = jwt_secret; + updates.push(['jwt_secret_set', 'true']); + } + + // Google Maps API ํ‚ค ์„ค์ • + if (google_maps_api_key) { + process.env.GOOGLE_MAPS_API_KEY = google_maps_api_key; + updates.push(['google_maps_configured', 'true']); + } + + // ์ด๋ฉ”์ผ ์„ค์ • + if (email_config) { + // ์ด๋ฉ”์ผ ์„ค์ • ๋กœ์ง (SMTP ๋“ฑ) + updates.push(['email_configured', 'true']); + } + + // ์„ค์ • ์—…๋ฐ์ดํŠธ + for (const [key, value] of updates) { + await query( + 'UPDATE system_settings SET value = $1, updated_at = CURRENT_TIMESTAMP WHERE key = $2', + [value, key] + ); + } + + res.json({ + success: true, + message: 'ํ™˜๊ฒฝ ์„ค์ •์ด ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค', + updated: updates.map(([key]) => key) + }); + } catch (error) { + console.error('Config update error:', error); + res.status(500).json({ + success: false, + message: 'ํ™˜๊ฒฝ ์„ค์ • ์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค', + error: error.message + }); + } +}); + +// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ +router.get('/test-db', async (req, res) => { + try { + const result = await query('SELECT NOW() as current_time, version() as db_version'); + res.json({ + success: true, + message: '๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์„ฑ๊ณต', + data: result.rows[0] + }); + } catch (error) { + console.error('Database test error:', error); + res.status(500).json({ + success: false, + message: '๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์‹คํŒจ', + error: error.message + }); + } +}); + +module.exports = router; diff --git a/server/routes/travelPlans.js b/server/routes/travelPlans.js new file mode 100644 index 0000000..48cdc7d --- /dev/null +++ b/server/routes/travelPlans.js @@ -0,0 +1,183 @@ +const express = require('express'); +const { query } = require('../db'); + +const router = express.Router(); + +// ์—ฌํ–‰ ๊ณ„ํš ์ „์ฒด ์กฐํšŒ (์ตœ์‹  1๊ฐœ) +router.get('/', async (req, res) => { + try { + // ์ตœ์‹  ์—ฌํ–‰ ๊ณ„ํš ์กฐํšŒ + const planResult = await query( + 'SELECT * FROM travel_plans ORDER BY created_at DESC LIMIT 1' + ); + + if (planResult.rows.length === 0) { + return res.json(null); + } + + const plan = planResult.rows[0]; + + // ํ•ด๋‹น ๊ณ„ํš์˜ ๋ชจ๋“  ์ผ์ • ์กฐํšŒ + const schedules = await query( + `SELECT ds.id, ds.schedule_date, + json_agg( + json_build_object( + 'id', a.id, + 'time', a.time, + 'title', a.title, + 'description', a.description, + 'location', a.location, + 'type', a.type, + 'coordinates', CASE + WHEN a.lat IS NOT NULL AND a.lng IS NOT NULL + THEN json_build_object('lat', a.lat, 'lng', a.lng) + ELSE NULL + END, + 'images', a.images, + 'links', a.links, + 'relatedPlaces', ( + SELECT json_agg( + json_build_object( + 'id', rp.id, + 'name', rp.name, + 'description', rp.description, + 'address', rp.address, + 'coordinates', CASE + WHEN rp.lat IS NOT NULL AND rp.lng IS NOT NULL + THEN json_build_object('lat', rp.lat, 'lng', rp.lng) + ELSE NULL + END, + 'memo', rp.memo, + 'willVisit', rp.will_visit, + 'category', rp.category, + 'images', rp.images, + 'links', rp.links + ) + ) + FROM related_places rp + WHERE rp.activity_id = a.id + ) + ) ORDER BY a.time + ) FILTER (WHERE a.id IS NOT NULL) as activities + FROM day_schedules ds + LEFT JOIN activities a ON a.day_schedule_id = ds.id + WHERE ds.travel_plan_id = $1 + GROUP BY ds.id, ds.schedule_date + ORDER BY ds.schedule_date`, + [plan.id] + ); + + const travelPlan = { + id: plan.id, + startDate: plan.start_date, + endDate: plan.end_date, + schedule: schedules.rows.map(row => ({ + date: row.schedule_date, + activities: row.activities || [] + })), + budget: { + total: 0, + accommodation: 0, + food: 0, + transportation: 0, + shopping: 0, + activities: 0 + }, + checklist: [] + }; + + res.json(travelPlan); + } catch (error) { + console.error('Error fetching travel plan:', error); + res.status(500).json({ error: error.message }); + } +}); + +// ์—ฌํ–‰ ๊ณ„ํš ์ €์žฅ/์—…๋ฐ์ดํŠธ +router.post('/', async (req, res) => { + const client = await query('BEGIN'); + + try { + const { startDate, endDate, schedule } = req.body; + + // ๊ธฐ์กด ๊ณ„ํš ์‚ญ์ œ (๋‹จ์ˆœํ™”: ํ•ญ์ƒ ์ตœ์‹  1๊ฐœ๋งŒ ์œ ์ง€) + await query('DELETE FROM travel_plans'); + + // ์ƒˆ ์—ฌํ–‰ ๊ณ„ํš ์ƒ์„ฑ + const planResult = await query( + 'INSERT INTO travel_plans (start_date, end_date) VALUES ($1, $2) RETURNING id', + [startDate, endDate] + ); + + const planId = planResult.rows[0].id; + + // ์ผ์ •๋ณ„ ๋ฐ์ดํ„ฐ ์‚ฝ์ž… + for (const day of schedule) { + // day_schedule ์‚ฝ์ž… + const scheduleResult = await query( + 'INSERT INTO day_schedules (travel_plan_id, schedule_date) VALUES ($1, $2) RETURNING id', + [planId, day.date] + ); + + const scheduleId = scheduleResult.rows[0].id; + + // activities ์‚ฝ์ž… + if (day.activities && day.activities.length > 0) { + for (const activity of day.activities) { + const activityResult = await query( + `INSERT INTO activities ( + day_schedule_id, time, title, description, location, type, lat, lng, images, links + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id`, + [ + scheduleId, + activity.time, + activity.title, + activity.description || null, + activity.location || null, + activity.type, + activity.coordinates?.lat || null, + activity.coordinates?.lng || null, + activity.images || null, + activity.links || null + ] + ); + + const activityId = activityResult.rows[0].id; + + // related_places ์‚ฝ์ž… + if (activity.relatedPlaces && activity.relatedPlaces.length > 0) { + for (const place of activity.relatedPlaces) { + await query( + `INSERT INTO related_places ( + activity_id, name, description, address, lat, lng, memo, will_visit, category, images, links + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, + [ + activityId, + place.name, + place.description || null, + place.address || null, + place.coordinates?.lat || null, + place.coordinates?.lng || null, + place.memo || null, + place.willVisit || false, + place.category || 'other', + place.images || null, + place.links || null + ] + ); + } + } + } + } + } + + await query('COMMIT'); + res.json({ success: true, id: planId }); + } catch (error) { + await query('ROLLBACK'); + console.error('Error saving travel plan:', error); + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/server/routes/uploads.js b/server/routes/uploads.js new file mode 100644 index 0000000..680304b --- /dev/null +++ b/server/routes/uploads.js @@ -0,0 +1,88 @@ +const express = require('express'); +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); + +const router = express.Router(); + +// uploads ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ +const uploadsDir = path.join(__dirname, '../uploads'); +if (!fs.existsSync(uploadsDir)) { + fs.mkdirSync(uploadsDir, { recursive: true }); +} + +// Multer ์„ค์ •: ํŒŒ์ผ ์ €์žฅ ์œ„์น˜์™€ ํŒŒ์ผ๋ช… ์„ค์ • +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, uploadsDir); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + const ext = path.extname(file.originalname); + cb(null, file.fieldname + '-' + uniqueSuffix + ext); + } +}); + +// ํŒŒ์ผ ํ•„ํ„ฐ: ์ด๋ฏธ์ง€๋งŒ ํ—ˆ์šฉ +const fileFilter = (req, file, cb) => { + const allowedTypes = /jpeg|jpg|png|gif|webp/; + const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); + const mimetype = allowedTypes.test(file.mimetype); + + if (mimetype && extname) { + return cb(null, true); + } else { + cb(new Error('์ด๋ฏธ์ง€ ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค (jpeg, jpg, png, gif, webp)')); + } +}; + +const upload = multer({ + storage: storage, + limits: { + fileSize: 5 * 1024 * 1024 // 5MB ์ œํ•œ + }, + fileFilter: fileFilter +}); + +// ๋‹ค์ค‘ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ (์ตœ๋Œ€ 5๊ฐœ) +router.post('/', upload.array('images', 5), (req, res) => { + try { + if (!req.files || req.files.length === 0) { + return res.status(400).json({ error: 'ํŒŒ์ผ์ด ์—…๋กœ๋“œ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค' }); + } + + // ์—…๋กœ๋“œ๋œ ํŒŒ์ผ์˜ URL ๋ฐฐ์—ด ์ƒ์„ฑ + const fileUrls = req.files.map(file => `/uploads/${file.filename}`); + + res.json({ + success: true, + files: fileUrls, + message: `${req.files.length}๊ฐœ์˜ ํŒŒ์ผ์ด ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค` + }); + } catch (error) { + console.error('์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์˜ค๋ฅ˜:', error); + res.status(500).json({ error: error.message }); + } +}); + +// ๋‹จ์ผ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ +router.post('/single', upload.single('image'), (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ error: 'ํŒŒ์ผ์ด ์—…๋กœ๋“œ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค' }); + } + + const fileUrl = `/uploads/${req.file.filename}`; + + res.json({ + success: true, + file: fileUrl, + message: 'ํŒŒ์ผ์ด ์—…๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค' + }); + } catch (error) { + console.error('์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์˜ค๋ฅ˜:', error); + res.status(500).json({ error: error.message }); + } +}); + +module.exports = router; diff --git a/server/schema.sql b/server/schema.sql new file mode 100644 index 0000000..465607d --- /dev/null +++ b/server/schema.sql @@ -0,0 +1,66 @@ +-- ์—ฌํ–‰ ๊ณ„ํš ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS travel_plans ( + id SERIAL PRIMARY KEY, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ๋‚ ์งœ๋ณ„ ์ผ์ • ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS day_schedules ( + id SERIAL PRIMARY KEY, + travel_plan_id INTEGER REFERENCES travel_plans(id) ON DELETE CASCADE, + schedule_date DATE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ํ™œ๋™ ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS activities ( + id SERIAL PRIMARY KEY, + day_schedule_id INTEGER REFERENCES day_schedules(id) ON DELETE CASCADE, + time VARCHAR(10) NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + location VARCHAR(255), + type VARCHAR(50) NOT NULL, + lat DECIMAL(10, 7), + lng DECIMAL(10, 7), + images TEXT[], + links TEXT[], + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ๊ด€๋ จ ์žฅ์†Œ ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS related_places ( + id SERIAL PRIMARY KEY, + activity_id INTEGER REFERENCES activities(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + description TEXT, + address VARCHAR(255), + lat DECIMAL(10, 7), + lng DECIMAL(10, 7), + memo TEXT, + will_visit BOOLEAN DEFAULT false, + category VARCHAR(50) DEFAULT 'other', + images TEXT[], + links TEXT[], + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ๊ธฐ๋ณธ ํฌ์ธํŠธ ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS base_points ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + address VARCHAR(255), + type VARCHAR(50) NOT NULL, + lat DECIMAL(10, 7) NOT NULL, + lng DECIMAL(10, 7) NOT NULL, + memo TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ์ธ๋ฑ์Šค ์ƒ์„ฑ +CREATE INDEX IF NOT EXISTS idx_day_schedules_plan ON day_schedules(travel_plan_id); +CREATE INDEX IF NOT EXISTS idx_activities_schedule ON activities(day_schedule_id); +CREATE INDEX IF NOT EXISTS idx_related_places_activity ON related_places(activity_id); diff --git a/server/schema_v2.sql b/server/schema_v2.sql new file mode 100644 index 0000000..391e090 --- /dev/null +++ b/server/schema_v2.sql @@ -0,0 +1,226 @@ +-- Travel Planner v2.0 Database Schema +-- ๋ฉ€ํ‹ฐ ์‚ฌ์šฉ์ž ๋ฐ ์—ฌํ–‰ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ + +-- ์‚ฌ์šฉ์ž ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, + role VARCHAR(20) DEFAULT 'user' CHECK (role IN ('admin', 'user')), + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login TIMESTAMP, + created_by UUID REFERENCES users(id) +); + +-- ์—ฌํ–‰ ๊ณ„ํš ํ…Œ์ด๋ธ” (ํ™•์žฅ) +CREATE TABLE IF NOT EXISTS travel_plans ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + description TEXT, + + -- ๋ชฉ์ ์ง€ ์ •๋ณด + destination_country VARCHAR(100) NOT NULL, + destination_city VARCHAR(100) NOT NULL, + destination_region VARCHAR(100), + destination_lat DECIMAL(10, 7), + destination_lng DECIMAL(10, 7), + + -- ๋‚ ์งœ + start_date DATE NOT NULL, + end_date DATE NOT NULL, + + -- ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ + is_public BOOLEAN DEFAULT false, + is_template BOOLEAN DEFAULT false, + template_category VARCHAR(20) CHECK (template_category IN ('japan', 'korea', 'asia', 'europe', 'america', 'other')), + tags TEXT[] DEFAULT '{}', + thumbnail VARCHAR(500), + status VARCHAR(20) DEFAULT 'draft' CHECK (status IN ('draft', 'active', 'completed', 'cancelled')), + + -- ํƒ€์ž„์Šคํƒฌํ”„ + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ๋‚ ์งœ๋ณ„ ์ผ์ • ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS day_schedules ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + travel_plan_id UUID NOT NULL REFERENCES travel_plans(id) ON DELETE CASCADE, + schedule_date DATE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ํ™œ๋™ ํ…Œ์ด๋ธ” (ํ™•์žฅ) +CREATE TABLE IF NOT EXISTS activities ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + day_schedule_id UUID NOT NULL REFERENCES day_schedules(id) ON DELETE CASCADE, + time VARCHAR(10) NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + location VARCHAR(255), + type VARCHAR(50) NOT NULL CHECK (type IN ('attraction', 'food', 'accommodation', 'transport', 'other')), + + -- ์ขŒํ‘œ + lat DECIMAL(10, 7), + lng DECIMAL(10, 7), + + -- ๋ฏธ๋””์–ด + images TEXT[] DEFAULT '{}', + links JSONB DEFAULT '[]', + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ๊ด€๋ จ ์žฅ์†Œ ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS related_places ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + activity_id UUID NOT NULL REFERENCES activities(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + description TEXT, + address VARCHAR(255), + lat DECIMAL(10, 7), + lng DECIMAL(10, 7), + memo TEXT, + will_visit BOOLEAN DEFAULT false, + category VARCHAR(50) DEFAULT 'other' CHECK (category IN ('restaurant', 'attraction', 'shopping', 'accommodation', 'other')), + images TEXT[] DEFAULT '{}', + links JSONB DEFAULT '[]', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ๊ธฐ๋ณธ ํฌ์ธํŠธ ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS base_points ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + address VARCHAR(255), + type VARCHAR(50) NOT NULL CHECK (type IN ('accommodation', 'airport', 'station', 'parking', 'other')), + lat DECIMAL(10, 7) NOT NULL, + lng DECIMAL(10, 7) NOT NULL, + memo TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ์˜ˆ์‚ฐ ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS budgets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + travel_plan_id UUID NOT NULL REFERENCES travel_plans(id) ON DELETE CASCADE, + total_amount DECIMAL(12, 2) DEFAULT 0, + accommodation DECIMAL(12, 2) DEFAULT 0, + food DECIMAL(12, 2) DEFAULT 0, + transportation DECIMAL(12, 2) DEFAULT 0, + shopping DECIMAL(12, 2) DEFAULT 0, + activities DECIMAL(12, 2) DEFAULT 0, + currency VARCHAR(3) DEFAULT 'KRW', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS checklist_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + travel_plan_id UUID NOT NULL REFERENCES travel_plans(id) ON DELETE CASCADE, + text VARCHAR(500) NOT NULL, + checked BOOLEAN DEFAULT false, + category VARCHAR(50) DEFAULT 'other' CHECK (category IN ('preparation', 'shopping', 'visit', 'other')), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ๊ณต์œ  ๋งํฌ ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS share_links ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + trip_id UUID NOT NULL REFERENCES travel_plans(id) ON DELETE CASCADE, + created_by UUID NOT NULL REFERENCES users(id), + share_code VARCHAR(8) UNIQUE NOT NULL, + expires_at TIMESTAMP, + is_active BOOLEAN DEFAULT true, + access_count INTEGER DEFAULT 0, + max_access_count INTEGER, + + -- ๊ถŒํ•œ + can_view BOOLEAN DEFAULT true, + can_edit BOOLEAN DEFAULT false, + can_comment BOOLEAN DEFAULT false, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_accessed TIMESTAMP +); + +-- ๋Œ“๊ธ€ ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS trip_comments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + trip_id UUID NOT NULL REFERENCES travel_plans(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id), + content TEXT NOT NULL, + is_edited BOOLEAN DEFAULT false, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ์‹œ์Šคํ…œ ์„ค์ • ํ…Œ์ด๋ธ” +CREATE TABLE IF NOT EXISTS system_settings ( + key VARCHAR(100) PRIMARY KEY, + value TEXT, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ์ธ๋ฑ์Šค ์ƒ์„ฑ +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); +CREATE INDEX IF NOT EXISTS idx_users_role ON users(role); +CREATE INDEX IF NOT EXISTS idx_travel_plans_user ON travel_plans(user_id); +CREATE INDEX IF NOT EXISTS idx_travel_plans_status ON travel_plans(status); +CREATE INDEX IF NOT EXISTS idx_travel_plans_category ON travel_plans(template_category); +CREATE INDEX IF NOT EXISTS idx_travel_plans_public ON travel_plans(is_public); +CREATE INDEX IF NOT EXISTS idx_day_schedules_plan ON day_schedules(travel_plan_id); +CREATE INDEX IF NOT EXISTS idx_activities_schedule ON activities(day_schedule_id); +CREATE INDEX IF NOT EXISTS idx_related_places_activity ON related_places(activity_id); +CREATE INDEX IF NOT EXISTS idx_base_points_user ON base_points(user_id); +CREATE INDEX IF NOT EXISTS idx_budgets_plan ON budgets(travel_plan_id); +CREATE INDEX IF NOT EXISTS idx_checklist_plan ON checklist_items(travel_plan_id); +CREATE INDEX IF NOT EXISTS idx_share_links_code ON share_links(share_code); +CREATE INDEX IF NOT EXISTS idx_share_links_trip ON share_links(trip_id); +CREATE INDEX IF NOT EXISTS idx_comments_trip ON trip_comments(trip_id); + +-- ์ดˆ๊ธฐ ์‹œ์Šคํ…œ ์„ค์ • ๋ฐ์ดํ„ฐ +INSERT INTO system_settings (key, value, description) VALUES + ('app_version', '2.0.0', '์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฒ„์ „'), + ('setup_completed', 'false', '์ดˆ๊ธฐ ์„ค์ • ์™„๋ฃŒ ์—ฌ๋ถ€'), + ('jwt_secret_set', 'false', 'JWT ์‹œํฌ๋ฆฟ ์„ค์ • ์—ฌ๋ถ€'), + ('google_maps_configured', 'false', 'Google Maps API ์„ค์ • ์—ฌ๋ถ€'), + ('email_configured', 'false', '์ด๋ฉ”์ผ ์„ค์ • ์—ฌ๋ถ€') +ON CONFLICT (key) DO NOTHING; + +-- ์—…๋ฐ์ดํŠธ ํŠธ๋ฆฌ๊ฑฐ ํ•จ์ˆ˜ +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- ์—…๋ฐ์ดํŠธ ํŠธ๋ฆฌ๊ฑฐ ์ ์šฉ +DROP TRIGGER IF EXISTS update_users_updated_at ON users; +CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +DROP TRIGGER IF EXISTS update_travel_plans_updated_at ON travel_plans; +CREATE TRIGGER update_travel_plans_updated_at BEFORE UPDATE ON travel_plans FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +DROP TRIGGER IF EXISTS update_budgets_updated_at ON budgets; +CREATE TRIGGER update_budgets_updated_at BEFORE UPDATE ON budgets FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +DROP TRIGGER IF EXISTS update_checklist_updated_at ON checklist_items; +CREATE TRIGGER update_checklist_updated_at BEFORE UPDATE ON checklist_items FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +DROP TRIGGER IF EXISTS update_comments_updated_at ON trip_comments; +CREATE TRIGGER update_comments_updated_at BEFORE UPDATE ON trip_comments FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +DROP TRIGGER IF EXISTS update_settings_updated_at ON system_settings; +CREATE TRIGGER update_settings_updated_at BEFORE UPDATE ON system_settings FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..db55611 --- /dev/null +++ b/server/server.js @@ -0,0 +1,66 @@ +const express = require('express'); +const cors = require('cors'); +const path = require('path'); +const { initializeDatabase } = require('./db'); +const { runMigrations } = require('./migrate'); +const travelPlanRoutes = require('./routes/travelPlans'); +const basePointsRoutes = require('./routes/basePoints'); +const uploadsRoutes = require('./routes/uploads'); +const { router: authRoutes } = require('./routes/auth'); +const setupRoutes = require('./routes/setup'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// ๋ฏธ๋“ค์›จ์–ด +app.use(cors()); +app.use(express.json()); + +// ์ •์  ํŒŒ์ผ ์ œ๊ณต (์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€) +app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); + +// ๋กœ๊ทธ ๋ฏธ๋“ค์›จ์–ด +app.use((req, res, next) => { + console.log(`${req.method} ${req.path}`); + next(); +}); + +// ๋ผ์šฐํŠธ +app.use('/api/auth', authRoutes); +app.use('/api/setup', setupRoutes); +app.use('/api/travel-plans', travelPlanRoutes); +app.use('/api/base-points', basePointsRoutes); +app.use('/api/uploads', uploadsRoutes); + +// ํ—ฌ์Šค ์ฒดํฌ +app.get('/health', (req, res) => { + res.json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + +// ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ +app.use((err, req, res, next) => { + console.error('Error:', err); + res.status(500).json({ error: err.message }); +}); + +// ์„œ๋ฒ„ ์‹œ์ž‘ +async function startServer() { + try { + // ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดˆ๊ธฐํ™” (๊ธฐ์กด ์Šคํ‚ค๋งˆ) + await initializeDatabase(); + + // ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰ (์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ถ”๊ฐ€) + await runMigrations(); + + app.listen(PORT, '0.0.0.0', () => { + console.log(`๐Ÿš€ Server running on port ${PORT}`); + console.log(`๐Ÿ“Š API available at http://localhost:${PORT}`); + console.log(`๐Ÿ—„๏ธ Database: Using existing kumamoto_map database`); + }); + } catch (error) { + console.error('Failed to start server:', error); + process.exit(1); + } +} + +startServer(); diff --git a/server/uploads/images-1762666367693-99912431.webp b/server/uploads/images-1762666367693-99912431.webp new file mode 100644 index 0000000..fde02f1 Binary files /dev/null and b/server/uploads/images-1762666367693-99912431.webp differ diff --git a/server/uploads/images-1762666492636-867051147.jpg b/server/uploads/images-1762666492636-867051147.jpg new file mode 100644 index 0000000..52db59f Binary files /dev/null and b/server/uploads/images-1762666492636-867051147.jpg differ diff --git a/src/App.tsx b/src/App.tsx index 8db415d..4ccdb0e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,144 +1,252 @@ -import { useState } from 'react' -import Header from './components/Header' -import Timeline from './components/Timeline' -import Attractions, { attractions } from './components/Attractions' -import Map from './components/Map' -import Budget from './components/Budget' -import Checklist from './components/Checklist' -import { TravelPlan } from './types' +import { BrowserRouter, Routes, Route, Navigate, useNavigate } from 'react-router-dom' +import { useEffect, useState } from 'react' +import PlanPage from './pages/PlanPage' +import TripPage from './pages/TripPage' +import InitialSetup from './components/InitialSetup' +import AuthForm from './components/AuthForm' +import AdminDashboard from './components/AdminDashboard' +import UserDashboard from './components/UserDashboard' +import SharedTripViewer from './components/SharedTripViewer' +import DebugInfo from './components/DebugInfo' +import { tripManagerService } from './services/tripManager' +import { defaultTravelPlan } from './constants/defaultData' +import { initialSetupService, SetupUtils } from './services/initialSetup' +import { userAuthService } from './services/userAuth' -function App() { - const [travelPlan, setTravelPlan] = useState({ - startDate: new Date('2025-02-17'), - endDate: new Date('2025-02-20'), - schedule: [ - { - date: new Date('2025-02-18'), - activities: [ - { - id: '1', - time: '09:00', - title: '๋ ŒํŠธ์นด ํ”ฝ์—…', - description: '๋ ŒํŠธ์นด๋กœ ์ด๋™', - type: 'transport', - }, - { - id: '2', - time: '10:00', - title: '๊ธฐ์ฟ ์น˜ํ˜‘๊ณก', - description: '๋ ŒํŠธ์นด๋กœ ์ด๋™', - location: '๊ธฐ์ฟ ์น˜์‹œ', - type: 'attraction', - }, - { - id: '3', - time: '12:00', - title: '์ฟ ์‚ฌ์„ผ๋ฆฌ', - description: '๋ ŒํŠธ์นด๋กœ ์ด๋™', - location: '์•„์†Œ์‹œ', - type: 'attraction', - }, - { - id: '4', - time: '14:00', - title: '์•„์†Œ์‚ฐ', - description: '๋ ŒํŠธ์นด๋กœ ์ด๋™', - location: '์•„์†Œ์‹œ', - type: 'attraction', - }, - { - id: '5', - time: '16:00', - title: '์‚ฌ๋ผ์นด์™€์ˆ˜์›', - description: '๋ ŒํŠธ์นด๋กœ ์ด๋™', - location: '๊ตฌ๋งˆ๋ชจํ† ์‹œ', - type: 'attraction', - }, - ], - }, - { - date: new Date('2025-02-19'), - activities: [ - { - id: '6', - time: '09:00', - title: '๊ตฌ๋งˆ๋ชจํ† ์„ฑ', - description: '๋Œ€์ค‘๊ตํ†ต์œผ๋กœ ์ด๋™', - location: '๊ตฌ๋งˆ๋ชจํ† ์‹œ ์ฃผ์˜ค๊ตฌ', - type: 'attraction', - }, - { - id: '7', - time: '11:30', - title: '์‚ฌ์ฟ ๋ผ๋…ธ๋ฐ”๋ฐ”', - description: '๋Œ€์ค‘๊ตํ†ต์œผ๋กœ ์ด๋™', - location: '๊ตฌ๋งˆ๋ชจํ† ์‹œ', - type: 'attraction', - }, - { - id: '8', - time: '14:00', - title: '์Šค์ด์  ์ง€์กฐ์ฃผ์—”', - description: '๋Œ€์ค‘๊ตํ†ต์œผ๋กœ ์ด๋™', - location: '๊ตฌ๋งˆ๋ชจํ† ์‹œ ์ฃผ์˜ค๊ตฌ', - type: 'attraction', - }, - { - id: '9', - time: '16:00', - title: '์‹œ๋ชจํ† ๋ฆฌ์•„์ผ€์ด๋“œ', - description: '๋Œ€์ค‘๊ตํ†ต์œผ๋กœ ์ด๋™', - location: '๊ตฌ๋งˆ๋ชจํ† ์‹œ', - type: 'attraction', - }, - ], - }, - ], - budget: { - total: 0, - accommodation: 0, - food: 0, - transportation: 0, - shopping: 0, - activities: 0, - }, - checklist: [], - }) +// ๋ฉ”์ธ ๋Œ€์‹œ๋ณด๋“œ ์ปดํฌ๋„ŒํŠธ +function Home() { + return ( +
+ +
+ ) +} + +// ๋ ˆ๊ฑฐ์‹œ ๋ผ์šฐํŠธ ์ฒ˜๋ฆฌ ์ปดํฌ๋„ŒํŠธ +function LegacyTripRedirect() { + const navigate = useNavigate() + const [isCreating, setIsCreating] = useState(false) + + useEffect(() => { + const createLegacyTrip = async () => { + setIsCreating(true) + + try { + // ๊ธฐ์กด ๊ตฌ๋งˆ๋ชจํ†  ์—ฌํ–‰์ด ์žˆ๋Š”์ง€ ํ™•์ธ + const userTrips = await tripManagerService.getUserTrips({ search: '๊ตฌ๋งˆ๋ชจํ† ' }) + + let legacyTrip = userTrips.trips.find(trip => + trip.title.includes('๊ตฌ๋งˆ๋ชจํ† ') || trip.destination.city === '๊ตฌ๋งˆ๋ชจํ† ' + ) + + // ์—†์œผ๋ฉด ์ƒˆ๋กœ ์ƒ์„ฑ + if (!legacyTrip) { + const result = await tripManagerService.createTrip({ + title: defaultTravelPlan.title, + description: defaultTravelPlan.description, + destination: defaultTravelPlan.destination, + startDate: defaultTravelPlan.startDate, + endDate: defaultTravelPlan.endDate, + template_category: defaultTravelPlan.template_category, + tags: defaultTravelPlan.tags, + is_public: defaultTravelPlan.is_public + }) + + if (result.success && result.trip) { + // ๊ธฐ๋ณธ ์ผ์ •๋„ ์ถ”๊ฐ€ + await tripManagerService.updateTrip(result.trip.id, { + ...result.trip, + schedule: defaultTravelPlan.schedule, + budget: defaultTravelPlan.budget, + checklist: defaultTravelPlan.checklist + }) + legacyTrip = result.trip + } + } + + // ์—ฌํ–‰ ๋ชจ๋“œ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ + if (legacyTrip) { + const currentPath = window.location.pathname + if (currentPath.includes('/plan')) { + navigate(`/plan/${legacyTrip.id}`) + } else { + navigate(`/trip/${legacyTrip.id}`) + } + } else { + // ์ƒ์„ฑ ์‹คํŒจ์‹œ ๋ฉ”์ธ์œผ๋กœ + navigate('/') + } + } catch (error) { + console.error('Failed to create legacy trip:', error) + navigate('/') + } finally { + setIsCreating(false) + } + } + + createLegacyTrip() + }, [navigate]) return ( -
-
-
-
-
- ๊ตฌ๋งˆ๋ชจํ†  ํ’๊ฒฝ -
-
-

๊ตฌ๋งˆ๋ชจํ†  ์—ฌํ–‰ ๊ณ„ํš

-

- 2025๋…„ 2์›” 17์ผ ~ 2์›” 20์ผ (4์ผ๊ฐ„) -

+
+
+
+
+ {isCreating ? '๊ตฌ๋งˆ๋ชจํ†  ์—ฌํ–‰ ๊ณ„ํš์„ ์ค€๋น„ํ•˜๋Š” ์ค‘...' : 'ํŽ˜์ด์ง€๋ฅผ ๋กœ๋”ฉ ์ค‘...'} +
+
+ ๊ธฐ์กด ์—ฌํ–‰ ๊ณ„ํš์„ ์ƒˆ๋กœ์šด ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค +
+
+
+ ) +} + +function App() { + const [isSetupRequired, setIsSetupRequired] = useState(null) + const [isAuthenticated, setIsAuthenticated] = useState(false) + const [isLoading, setIsLoading] = useState(true) + + // ๋””๋ฒ„๊ทธ ๋ชจ๋“œ (URL์— ?debug=true๊ฐ€ ์žˆ์œผ๋ฉด ํ™œ์„ฑํ™”) + const showDebug = new URLSearchParams(window.location.search).get('debug') === 'true' + + useEffect(() => { + checkAppStatus() + }, []) + + const checkAppStatus = async () => { + try { + // 1. ์ดˆ๊ธฐ ์„ค์ • ํ•„์š” ์—ฌ๋ถ€ ํ™•์ธ + const setupRequired = await SetupUtils.isSetupRequired() + setIsSetupRequired(setupRequired) + + if (!setupRequired) { + // 2. ์‚ฌ์šฉ์ž ์ธ์ฆ ์ƒํƒœ ํ™•์ธ + const authenticated = userAuthService.isAuthenticated() + setIsAuthenticated(authenticated) + } + } catch (error) { + console.error('App status check failed:', error) + setIsSetupRequired(true) // ์˜ค๋ฅ˜ ์‹œ ์ดˆ๊ธฐ ์„ค์ •์œผ๋กœ ์•ˆ๋‚ด + } finally { + setIsLoading(false) + } + } + + const handleSetupComplete = () => { + setIsSetupRequired(false) + checkAppStatus() // ์ƒํƒœ ์žฌํ™•์ธ + } + + const handleAuthSuccess = () => { + setIsAuthenticated(true) + } + + const handleLogout = async () => { + await userAuthService.logout() + setIsAuthenticated(false) + } + + // ๋กœ๋”ฉ ์ค‘ + // ๋””๋ฒ„๊ทธ ๋ชจ๋“œ + if (showDebug) { + return + } + + if (isLoading) { + return ( +
+
+
+
์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™” ์ค‘...
+
+
+ ) + } + + // ์ดˆ๊ธฐ ์„ค์ • ํ•„์š” + if (isSetupRequired) { + return + } + + // ๋กœ๊ทธ์ธ ํ•„์š” + if (!isAuthenticated) { + return ( +
+ +
+ ) + } + + // ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž - ๋ฉ”์ธ ์•ฑ + return ( + +
+ {/* ์ƒ๋‹จ ๋„ค๋น„๊ฒŒ์ด์…˜ */} +
+ -
-
- - - -
-
- - -
-
-
-
+ {/* ๋ฉ”์ธ ์ฝ˜ํ…์ธ  */} + + } /> + } /> + } /> + } /> + + + + ) : ( + + ) + } + /> + {/* ๋ ˆ๊ฑฐ์‹œ ๋ผ์šฐํŠธ (๊ธฐ์กด ๊ตฌ๋งˆ๋ชจํ†  ์—ฌํ–‰) - ํ…Œ์ŠคํŠธ์šฉ ์ž๋™ ์ƒ์„ฑ */} + } /> + } /> + } /> + + + ) } diff --git a/src/components/ActivityEditor.tsx b/src/components/ActivityEditor.tsx new file mode 100644 index 0000000..1278590 --- /dev/null +++ b/src/components/ActivityEditor.tsx @@ -0,0 +1,208 @@ +import { useState } from 'react' +import { Activity, RelatedPlace } from '../types' +import RelatedPlacesManager from './RelatedPlacesManager' +import LocationSelector from './forms/LocationSelector' +import ImageUploadField from './forms/ImageUploadField' +import LinkManagement from './forms/LinkManagement' + +interface ActivityEditorProps { + activity: Activity + onUpdate: (activity: Activity) => void + onClose: () => void +} + +const ActivityEditor = ({ activity, onUpdate, onClose }: ActivityEditorProps) => { + const [formData, setFormData] = useState(activity) + const [showRelatedPlacesManager, setShowRelatedPlacesManager] = useState(false) + + const updateRelatedPlaces = (relatedPlaces: RelatedPlace[]) => { + setFormData({ + ...formData, + relatedPlaces + }) + } + + const handleSave = () => { + if (!formData.title || !formData.time) { + alert('์ œ๋ชฉ๊ณผ ์‹œ๊ฐ„์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.') + return + } + + // ์ขŒํ‘œ๊ฐ€ 0,0์ด๋ฉด ์ œ๊ฑฐ + const updatedActivity = { + ...formData, + coordinates: formData.coordinates && + formData.coordinates.lat !== 0 && + formData.coordinates.lng !== 0 + ? formData.coordinates + : undefined + } + + onUpdate(updatedActivity) + onClose() + } + + return ( +
+
+
+

์ผ์ • ํŽธ์ง‘

+ +
+ +
+ {/* ์‹œ๊ฐ„ */} +
+ + setFormData({ ...formData, time: e.target.value })} + className="w-full px-3 py-2 border rounded" + /> +
+ + {/* ์ œ๋ชฉ */} +
+ + setFormData({ ...formData, title: e.target.value })} + className="w-full px-3 py-2 border rounded" + placeholder="์ผ์ • ์ œ๋ชฉ" + /> +
+ + {/* ์„ค๋ช… */} +
+ +