From fd5a68e44ac484d9e80225aac2370316c314504c Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Tue, 25 Nov 2025 10:39:58 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=AF=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EB=A6=AC=EB=B8=8C=EB=9E=9C=EB=94=A9:=20Kumamoto=20?= =?UTF-8?q?=E2=86=92=20Travel=20Planner=20v2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ 주요 변경사항: - 프로젝트 이름: kumamoto-travel-planner → travel-planner - 버전 업그레이드: v1.0.0 → v2.0.0 - 멀티유저 시스템 구현 (JWT 인증) - PostgreSQL 마이그레이션 시스템 추가 - Docker 컨테이너 이름 변경 - UI 브랜딩 업데이트 (Travel Planner) - API 서버 및 인증 시스템 추가 - 여행 공유 기능 구현 - 템플릿 시스템 추가 🔧 기술 스택: - Frontend: React + TypeScript + Vite - Backend: Node.js + Express + JWT - Database: PostgreSQL + 마이그레이션 - Infrastructure: Docker + Docker Compose 🌟 새로운 기능: - 사용자 인증 및 권한 관리 - 다중 여행 계획 관리 - 여행 템플릿 시스템 - 공유 링크 및 댓글 시스템 - 관리자 대시보드 --- DATABASE_SETUP.md | 213 ++ IMPROVEMENT_PLAN.md | 125 + README.md | 199 +- docker-compose.dev.yml | 64 +- docker-compose.yml | 34 +- docker-start.sh | 66 + env.docker | 4 + index.html | 2 +- package-lock.json | 56 +- package.json | 7 +- server/Dockerfile | 27 + server/db.js | 45 + server/env.example | 18 + server/migrate.js | 80 + server/migrations/001_add_user_system.sql | 129 + server/package-lock.json | 2154 +++++++++++++++++ server/package.json | 22 + server/routes/auth.js | 179 ++ server/routes/basePoints.js | 77 + server/routes/setup.js | 187 ++ server/routes/travelPlans.js | 183 ++ server/routes/uploads.js | 88 + server/schema.sql | 66 + server/schema_v2.sql | 226 ++ server/server.js | 66 + .../images-1762666367693-99912431.webp | Bin 0 -> 31956 bytes .../images-1762666492636-867051147.jpg | Bin 0 -> 63403 bytes src/App.tsx | 378 +-- src/components/ActivityEditor.tsx | 208 ++ src/components/AdminDashboard.tsx | 510 ++++ src/components/Attractions.tsx | 95 +- src/components/AuthForm.tsx | 298 +++ src/components/DebugInfo.tsx | 164 ++ src/components/DynamicMap.tsx | 80 + src/components/GoogleAuthManager.tsx | 285 +++ src/components/GoogleMapComponent.tsx | 497 ++++ src/components/GoogleMapsApiKeyManager.tsx | 245 ++ src/components/GoogleMapsUsageIndicator.tsx | 179 ++ src/components/Header.tsx | 4 +- src/components/InitialSetup.tsx | 429 ++++ src/components/LeafletMapComponent.tsx | 561 +++++ src/components/Map.tsx | 4 +- src/components/MobileTabBar.tsx | 47 + src/components/PlacesSearch.tsx | 272 +++ src/components/RelatedPlacesManager.tsx | 472 ++++ src/components/RouteOptimizer.tsx | 239 ++ src/components/SharedTripViewer.tsx | 428 ++++ src/components/TabNavigation.tsx | 55 + src/components/TemplateBrowser.tsx | 485 ++++ src/components/Timeline.tsx | 541 +++-- src/components/TravelDashboard.tsx | 136 ++ src/components/TravelMap.tsx | 535 ++++ src/components/TripSharingManager.tsx | 436 ++++ src/components/UnifiedMap.tsx | 303 +++ src/components/UserDashboard.tsx | 660 +++++ src/components/forms/CoordinateInput.tsx | 58 + src/components/forms/ImageUploadField.tsx | 103 + src/components/forms/LinkManagement.tsx | 177 ++ src/components/forms/LocationSelector.tsx | 139 ++ src/constants/defaultData.ts | 232 ++ src/data/tripTemplates.ts | 590 +++++ src/env.d.ts | 12 + src/index.css | 11 + src/pages/PlanPage.tsx | 278 +++ src/pages/TripPage.tsx | 332 +++ src/services/api.ts | 98 + src/services/apiKeyManager.ts | 171 ++ src/services/googleAuth.ts | 319 +++ src/services/googleDirections.ts | 313 +++ src/services/googleMapsUsage.ts | 257 ++ src/services/googlePlaces.ts | 267 ++ src/services/initialSetup.ts | 304 +++ src/services/offlineSupport.ts | 381 +++ src/services/tripManager.ts | 517 ++++ src/services/tripSharing.ts | 395 +++ src/services/userAuth.ts | 673 +++++ src/types.ts | 123 +- src/utils/categoryMapping.ts | 129 + src/utils/env.ts | 34 + start-server.sh | 40 + tailwind.config.js | 3 + 81 files changed, 18420 insertions(+), 399 deletions(-) create mode 100644 DATABASE_SETUP.md create mode 100644 IMPROVEMENT_PLAN.md create mode 100644 docker-start.sh create mode 100644 env.docker create mode 100644 server/Dockerfile create mode 100644 server/db.js create mode 100644 server/env.example create mode 100644 server/migrate.js create mode 100644 server/migrations/001_add_user_system.sql create mode 100644 server/package-lock.json create mode 100644 server/package.json create mode 100644 server/routes/auth.js create mode 100644 server/routes/basePoints.js create mode 100644 server/routes/setup.js create mode 100644 server/routes/travelPlans.js create mode 100644 server/routes/uploads.js create mode 100644 server/schema.sql create mode 100644 server/schema_v2.sql create mode 100644 server/server.js create mode 100644 server/uploads/images-1762666367693-99912431.webp create mode 100644 server/uploads/images-1762666492636-867051147.jpg create mode 100644 src/components/ActivityEditor.tsx create mode 100644 src/components/AdminDashboard.tsx create mode 100644 src/components/AuthForm.tsx create mode 100644 src/components/DebugInfo.tsx create mode 100644 src/components/DynamicMap.tsx create mode 100644 src/components/GoogleAuthManager.tsx create mode 100644 src/components/GoogleMapComponent.tsx create mode 100644 src/components/GoogleMapsApiKeyManager.tsx create mode 100644 src/components/GoogleMapsUsageIndicator.tsx create mode 100644 src/components/InitialSetup.tsx create mode 100644 src/components/LeafletMapComponent.tsx create mode 100644 src/components/MobileTabBar.tsx create mode 100644 src/components/PlacesSearch.tsx create mode 100644 src/components/RelatedPlacesManager.tsx create mode 100644 src/components/RouteOptimizer.tsx create mode 100644 src/components/SharedTripViewer.tsx create mode 100644 src/components/TabNavigation.tsx create mode 100644 src/components/TemplateBrowser.tsx create mode 100644 src/components/TravelDashboard.tsx create mode 100644 src/components/TravelMap.tsx create mode 100644 src/components/TripSharingManager.tsx create mode 100644 src/components/UnifiedMap.tsx create mode 100644 src/components/UserDashboard.tsx create mode 100644 src/components/forms/CoordinateInput.tsx create mode 100644 src/components/forms/ImageUploadField.tsx create mode 100644 src/components/forms/LinkManagement.tsx create mode 100644 src/components/forms/LocationSelector.tsx create mode 100644 src/constants/defaultData.ts create mode 100644 src/data/tripTemplates.ts create mode 100644 src/env.d.ts create mode 100644 src/pages/PlanPage.tsx create mode 100644 src/pages/TripPage.tsx create mode 100644 src/services/api.ts create mode 100644 src/services/apiKeyManager.ts create mode 100644 src/services/googleAuth.ts create mode 100644 src/services/googleDirections.ts create mode 100644 src/services/googleMapsUsage.ts create mode 100644 src/services/googlePlaces.ts create mode 100644 src/services/initialSetup.ts create mode 100644 src/services/offlineSupport.ts create mode 100644 src/services/tripManager.ts create mode 100644 src/services/tripSharing.ts create mode 100644 src/services/userAuth.ts create mode 100644 src/utils/categoryMapping.ts create mode 100644 src/utils/env.ts create mode 100644 start-server.sh 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 0000000000000000000000000000000000000000..fde02f1231c789110c557c89385409433345cbd0 GIT binary patch literal 31956 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 g|g+q(+o7#*!fn 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 K;)*D%5&zl$I(H@j literal 0 HcmV?d00001 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="일정 제목" + /> +
+ + {/* 설명 */} +
+ +