From c80b1196f72f30caccd0174c012bec05f6b53286 Mon Sep 17 00:00:00 2001 From: higashiaka Date: Wed, 25 Mar 2026 17:41:34 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=201=EC=A3=BC=EC=B0=A8=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=A0=9C=EC=B6=9C=20-=20Chapter=202.=20=EC=8B=A4?= =?UTF-8?q?=EC=A0=84=20SQL=20-=20=EC=96=B4=EB=96=A4=20Query=EB=A5=BC=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=ED=95=B4=EC=95=BC=20=ED=95=A0=EA=B9=8C=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../week2/mission_queries.sql" | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 "\353\217\204\354\226\217/week2/mission_queries.sql" diff --git "a/\353\217\204\354\226\217/week2/mission_queries.sql" "b/\353\217\204\354\226\217/week2/mission_queries.sql" new file mode 100644 index 0000000..5d1588a --- /dev/null +++ "b/\353\217\204\354\226\217/week2/mission_queries.sql" @@ -0,0 +1,117 @@ +-- ============================================= +-- UMC 2주차 미션 쿼리 (제공해주신 로직 + 1주차 ERD 스키마 반영) +-- ============================================= + +-- ============================================= +-- 1. 내가 진행중/진행 완료한 미션 목록 조회 (페이징 포함) +-- ============================================= +SELECT + m.c_mis_id AS mission_id, + s.c_sto_name AS store_name, + m.c_mis_title AS mission_title, + m.c_mis_reward AS point, + mm.c_mm_status AS mission_status, + mm.c_mm_created AS started_at, + mm.c_mm_updated AS completed_at +FROM t_member_mission mm +JOIN t_mission m ON mm.c_mm_mission_id = m.c_mis_id +JOIN t_store s ON m.c_mis_store_id = s.c_sto_id +WHERE mm.c_mm_member_id = 1 -- 로그인한 유저 id + AND mm.c_mm_status = 'CHALLENGING' -- 진행중: 'CHALLENGING' / 진행완료: 'COMPLETE' +ORDER BY mm.c_mm_created DESC +LIMIT 10 OFFSET 0; -- 1페이지: OFFSET 0, 2페이지: OFFSET 10 + +-- 진행완료 탭으로 바꿀 때는 status 조건만 변경 +-- AND mm.c_mm_status = 'COMPLETE' + + +-- ============================================= +-- 2. 리뷰 작성 쿼리 (사진 제외) +-- ============================================= +INSERT INTO t_review ( + c_rev_member_id, + c_rev_store_id, + c_rev_content, + c_rev_score, + c_rev_created, + c_rev_updated +) +SELECT + mm.c_mm_member_id, + m.c_mis_store_id, + '너무 맛있어요! 포인트도 받고 좋았습니다.', -- 실제로는 유저가 입력한 내용 + 4.5, -- 실제로는 유저가 선택한 별점 + NOW(), + NOW() +FROM t_member_mission mm +JOIN t_mission m ON mm.c_mm_mission_id = m.c_mis_id +WHERE mm.c_mm_member_id = 1 -- 로그인한 유저 id (본인 미션만 작성 가능) + AND mm.c_mm_mission_id = 1 -- 리뷰 작성할 미션 id + AND mm.c_mm_status = 'COMPLETE'; -- 완료된 미션만 리뷰 작성 가능 + +-- 리뷰 작성 후 해당 가게의 평균 별점 업데이트 +-- (참고: 현재 ERD의 t_store에는 별점 컬럼이 없으나, 'c_sto_score' 컬럼이 있다고 가정) +/* +UPDATE t_store s +SET c_sto_score = ( + SELECT AVG(r.c_rev_score) + FROM t_review r + WHERE r.c_rev_store_id = s.c_sto_id +) +WHERE s.c_sto_id = ( + SELECT m.c_mis_store_id + FROM t_mission m + WHERE m.c_mis_id = 1 +); +*/ + + +-- ============================================= +-- 3. 홈 화면 - 현재 선택된 지역에서 도전 가능한 미션 목록 (페이징 포함) +-- ============================================= + +-- 3-1. 현재 지역 완료 미션 수 (상단 7/10 표시용) +SELECT COUNT(*) AS complete_count +FROM t_member_mission mm +JOIN t_mission m ON mm.c_mm_mission_id = m.c_mis_id +JOIN t_store s ON m.c_mis_store_id = s.c_sto_id +WHERE mm.c_mm_member_id = 1 -- 로그인한 유저 id + AND s.c_sto_region_id = 1 -- 현재 선택된 지역 id + AND mm.c_mm_status = 'COMPLETE'; + +-- 3-2. 현재 지역에서 도전 가능한 미션 목록 (아직 도전 안 한 미션) +SELECT + m.c_mis_id AS mission_id, + s.c_sto_name AS store_name, + fc.c_fc_name AS category, + m.c_mis_title AS mission_title, + m.c_mis_reward AS point, + m.c_mis_deadline AS deadline +FROM t_mission m +JOIN t_store s ON m.c_mis_store_id = s.c_sto_id +JOIN t_food_category fc ON s.c_sto_fc_id = fc.c_fc_id +WHERE s.c_sto_region_id = 1 -- 현재 선택된 지역 id + AND s.c_sto_status = 'OPEN' -- 상점 오픈 상태 가정 + AND m.c_mis_id NOT IN ( -- 내가 이미 도전중이거나 완료한 미션 제외 + SELECT c_mm_mission_id + FROM t_member_mission + WHERE c_mm_member_id = 1 -- 로그인한 유저 id + ) +ORDER BY m.c_mis_created DESC +LIMIT 10 OFFSET 0; -- 1페이지: OFFSET 0, 2페이지: OFFSET 10 + + +-- ============================================= +-- 4. 마이페이지 화면 쿼리 +-- ============================================= +SELECT + u.c_mem_nickname AS nickname, + u.c_mem_social_id AS email, -- 이메일 전용 컬럼 부재로 소셜 계정 ID 매핑 + u.c_mem_phone_num AS phone, + u.c_mem_profile_image_url AS profile_img, + u.c_mem_point AS point, + COUNT(r.c_rev_id) AS review_count -- 작성한 리뷰 수 +FROM t_member u +LEFT JOIN t_review r ON r.c_rev_member_id = u.c_mem_id +WHERE u.c_mem_id = 1 -- 로그인한 유저 id +GROUP BY u.c_mem_id; From 4d398da6494e4f67783241c5a05a1d14d7f0a0b0 Mon Sep 17 00:00:00 2001 From: higashiaka Date: Mon, 30 Mar 2026 10:11:13 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=203=EC=A3=BC=EC=B0=A8=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=A0=9C=EC=B6=9C=20-=20Chapter=203.=20API=20URL?= =?UTF-8?q?=EC=9D=98=20=EC=84=A4=EA=B3=84=20&=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "\353\217\204\354\226\217/week3/WEEK3_API.md" | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 "\353\217\204\354\226\217/week3/WEEK3_API.md" diff --git "a/\353\217\204\354\226\217/week3/WEEK3_API.md" "b/\353\217\204\354\226\217/week3/WEEK3_API.md" new file mode 100644 index 0000000..2ebf111 --- /dev/null +++ "b/\353\217\204\354\226\217/week3/WEEK3_API.md" @@ -0,0 +1,107 @@ + +--- + +# 미션 서비스 API 명세서 + +## 1. 회원 관련 API + +### **[POST] 회원가입** +* **Endpoint**: `/users/signup` +* **Request Header**: `Content-Type: application/json` +* **Request Body**: + ```json + { + "email": "string", + "password": "string", + "name": "string", + "gender": "integer", + "birth": "string", + "address": "string" + } + ``` +* **설명**: 새로운 사용자 계정을 생성합니다. + +### **[POST] 선호 조사 내역 저장** +* **Endpoint**: `/users/preferences` +* **Request Header**: `Authorization: Bearer {token}`, `Content-Type: application/json` +* **Request Body**: + ```json + { + "category_ids": [1, 2, 3] + } + ``` +* **설명**: 회원가입 후 사용자의 관심 카테고리 정보를 저장합니다. + +--- + +## 2. 홈 및 미션 관리 API + +### **[GET] 홈 화면: 내가 받은 미션 조회** +* **Endpoint**: `/members/me/missions/active` +* **Request Header**: `Authorization: Bearer {token}` +* **Query String**: `page=0&size=10` +* **설명**: 홈 화면에서 현재 사용자가 할당받거나 진행 중인 미션 목록을 조회합니다. + +### **[GET] 미션 목록 조회 (수행 중 / 완료)** +* **Endpoint**: `/members/me/missions` +* **Request Header**: `Authorization: Bearer {token}` +* **Query String**: + * `status`: `CHALLENGING` (수행 중) 또는 `COMPLETE` (완료) + * `page`: 페이지 번호 +* **설명**: 사용자의 미션 수행 기록을 상태별로 필터링하여 조회합니다. + +### **[POST] 미션 도전하기 (성공 누르기)** +* **Endpoint**: `/members/me/missions/{missionId}` +* **Path Variable**: `missionId` (도전할 미션의 ID) +* **Request Header**: `Authorization: Bearer {token}` +* **설명**: 특정 미션을 수행하기 시작하거나 완료를 요청합니다. + +--- + +## 3. 지도 및 가게 관련 API + +### **[GET] 지역별 가게 리스트 조회** +* **Endpoint**: `/regions/{regionId}/stores` +* **Path Variable**: `regionId` (지역 ID) +* **Query String**: `last_store_id=10&size=10` +* **설명**: 특정 지역에 등록된 가게들의 목록을 조회합니다. + +### **[GET] 가게 정보 및 미션 조회** +* **Endpoint**: `/stores/{storeId}` +* **Path Variable**: `storeId` (가게 ID) +* **설명**: 특정 가게의 상세 정보와 해당 가게에서 진행 가능한 미션 목록을 조회합니다. + +--- + +## 4. 마이페이지 및 리뷰 API + +### **[POST] 리뷰 작성하기** +* **Endpoint**: `/members/me/missions/{memberMissionId}/reviews` +* **Path Variable**: `memberMissionId` (완료된 미션 수행 기록 ID) +* **Request Header**: `Authorization: Bearer {token}` +* **Request Body**: + ```json + { + "content": "string", + "score": "float", + "image_url": "string" + } + ``` +* **설명**: 완료된 미션에 대해 가게 리뷰와 별점을 작성합니다. + +### **[GET] 내 포인트 조회** +* **Endpoint**: `/members/me/points` +* **Request Header**: `Authorization: Bearer {token}` +* **설명**: 사용자가 현재 보유한 총 포인트와 적립 내역을 확인합니다. + +--- + +## 핵심 비즈니스 로직 + +### **지역 보너스 포인트 자동 지급** +* **적용 대상 API**: `POST /members/me/missions/{missionId}` (미션 완료 처리 시) +* **로직 상세**: + 1. 사용자가 미션을 완료할 때마다 해당 가게의 `region_id`를 확인합니다. + 2. 서버 내부에서 해당 사용자가 동일 지역에서 완료한 미션의 총개수를 카운트합니다. + 3. **모든 지역마다 누적 완료 미션이 10개가 될 때마다 1000 point를 즉시 지급**합니다. + 4. 보너스 지급 시 사용자에게 알림을 발송합니다. \ No newline at end of file From 6d923d49e6871b124a1a5825de452de64093d53e Mon Sep 17 00:00:00 2001 From: higashiaka Date: Wed, 8 Apr 2026 15:03:38 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=204=EC=A3=BC=EC=B0=A8=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=A0=9C=EC=B6=9C=20-=20Chapter=204.=20Node.js?= =?UTF-8?q?=EC=99=80=20ES6=20=EA=B8=B0=EB=B0=98=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=20=EA=B8=B0=EC=B4=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.json | 7 + "\353\217\204\354\226\217/week4/.gitignore" | 3 + .../week4/package-lock.json" | 1983 +++++++++++++++++ "\353\217\204\354\226\217/week4/package.json" | 28 + "\353\217\204\354\226\217/week4/schema.sql" | 223 ++ .../src/controllers/member.controller.ts" | 41 + .../src/controllers/mission.controller.ts" | 14 + .../src/controllers/store.controller.ts" | 14 + .../week4/src/db/index.ts" | 32 + "\353\217\204\354\226\217/week4/src/index.ts" | 24 + .../week4/src/middleware/auth.middleware.ts" | 23 + .../week4/src/middleware/error.middleware.ts" | 16 + .../src/repositories/member.repository.ts" | 43 + .../src/repositories/mission.repository.ts" | 20 + .../src/repositories/store.repository.ts" | 17 + .../week4/src/routes/member.route.ts" | 10 + .../week4/src/routes/mission.route.ts" | 7 + .../week4/src/routes/store.route.ts" | 7 + .../week4/src/services/member.service.ts" | 76 + .../week4/src/services/mission.service.ts" | 24 + .../week4/src/services/store.service.ts" | 29 + .../week4/src/types/index.ts" | 57 + .../week4/tsconfig.json" | 14 + 23 files changed, 2712 insertions(+) create mode 100644 .claude/settings.json create mode 100644 "\353\217\204\354\226\217/week4/.gitignore" create mode 100644 "\353\217\204\354\226\217/week4/package-lock.json" create mode 100644 "\353\217\204\354\226\217/week4/package.json" create mode 100644 "\353\217\204\354\226\217/week4/schema.sql" create mode 100644 "\353\217\204\354\226\217/week4/src/controllers/member.controller.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/controllers/mission.controller.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/controllers/store.controller.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/db/index.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/index.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/middleware/auth.middleware.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/middleware/error.middleware.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/repositories/member.repository.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/repositories/mission.repository.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/repositories/store.repository.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/routes/member.route.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/routes/mission.route.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/routes/store.route.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/services/member.service.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/services/mission.service.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/services/store.service.ts" create mode 100644 "\353\217\204\354\226\217/week4/src/types/index.ts" create mode 100644 "\353\217\204\354\226\217/week4/tsconfig.json" diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..c87baa1 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(timeout 5 npx tsx src/index.ts)" + ] + } +} diff --git "a/\353\217\204\354\226\217/week4/.gitignore" "b/\353\217\204\354\226\217/week4/.gitignore" new file mode 100644 index 0000000..deed335 --- /dev/null +++ "b/\353\217\204\354\226\217/week4/.gitignore" @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.env diff --git "a/\353\217\204\354\226\217/week4/package-lock.json" "b/\353\217\204\354\226\217/week4/package-lock.json" new file mode 100644 index 0000000..96c9186 --- /dev/null +++ "b/\353\217\204\354\226\217/week4/package-lock.json" @@ -0,0 +1,1983 @@ +{ + "name": "umc-week4", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "umc-week4", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "bcryptjs": "^2.4.3", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.2" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/express": "^5.0.1", + "@types/jsonwebtoken": "^9.0.9", + "@types/node": "^22.14.0", + "nodemon": "^3.1.14", + "tsx": "^4.19.3", + "typescript": "^5.8.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "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": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/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/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "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/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/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/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/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "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": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "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": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/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/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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/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.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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-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/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "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/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.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/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": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "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/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/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-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "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": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.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/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/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "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/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/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" + } + } +} diff --git "a/\353\217\204\354\226\217/week4/package.json" "b/\353\217\204\354\226\217/week4/package.json" new file mode 100644 index 0000000..5010ac4 --- /dev/null +++ "b/\353\217\204\354\226\217/week4/package.json" @@ -0,0 +1,28 @@ +{ + "name": "umc-week4", + "version": "1.0.0", + "description": "UMC 4주차 - Node.js TypeScript 기반 서버 개발", + "main": "src/index.ts", + "scripts": { + "start": "tsx src/index.ts", + "dev": "nodemon --exec tsx src/index.ts", + "build": "tsc", + "start:prod": "node dist/index.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "bcryptjs": "^2.4.3", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.2" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/express": "^5.0.1", + "@types/jsonwebtoken": "^9.0.9", + "@types/node": "^22.14.0", + "nodemon": "^3.1.14", + "tsx": "^4.19.3", + "typescript": "^5.8.3" + } +} diff --git "a/\353\217\204\354\226\217/week4/schema.sql" "b/\353\217\204\354\226\217/week4/schema.sql" new file mode 100644 index 0000000..743615e --- /dev/null +++ "b/\353\217\204\354\226\217/week4/schema.sql" @@ -0,0 +1,223 @@ +-- ============================================================ +-- UMC 미션 서비스 - MySQL DDL +-- 기반: week1 ERD + week3 API 명세 +-- ============================================================ + +CREATE DATABASE IF NOT EXISTS umc_mission DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE umc_mission; + +-- ============================================================ +-- 1. region (지역) +-- ============================================================ +CREATE TABLE region ( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL COMMENT '지역명', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +-- ============================================================ +-- 2. food_category (음식 카테고리) +-- ============================================================ +CREATE TABLE food_category ( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL COMMENT '카테고리명 (한식, 중식, 일식 등)', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +-- ============================================================ +-- 3. terms (약관) +-- ============================================================ +CREATE TABLE terms ( + id BIGINT NOT NULL AUTO_INCREMENT, + title VARCHAR(100) NOT NULL COMMENT '약관 제목', + content TEXT NOT NULL COMMENT '약관 내용', + optional BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'TRUE: 선택 동의, FALSE: 필수 동의', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +-- ============================================================ +-- 4. member (회원) +-- - social_type / social_id: 소셜 로그인 (kakao, naver, google 등) +-- - email / password: 일반 이메일 로그인 (week3 API 명세 반영) +-- - status: ACTIVE | INACTIVE | BANNED +-- - gender: MALE | FEMALE | OTHER +-- ============================================================ +CREATE TABLE member ( + id BIGINT NOT NULL AUTO_INCREMENT, + social_type VARCHAR(20) NULL COMMENT '소셜 로그인 타입 (kakao, naver, google)', + social_id VARCHAR(100) NULL COMMENT '소셜 고유 ID', + email VARCHAR(100) NULL COMMENT '이메일 (일반 로그인)', + password VARCHAR(255) NULL COMMENT '비밀번호 해시 (일반 로그인)', + name VARCHAR(50) NOT NULL COMMENT '실명', + nickname VARCHAR(50) NOT NULL COMMENT '닉네임', + profile_image_url VARCHAR(500) NULL COMMENT '프로필 이미지 URL', + phone_num VARCHAR(20) NULL COMMENT '전화번호', + phone_verified BOOLEAN NOT NULL DEFAULT FALSE COMMENT '전화번호 인증 여부', + birth DATE NULL COMMENT '생년월일', + gender VARCHAR(10) NULL COMMENT 'MALE | FEMALE | OTHER', + address VARCHAR(200) NULL COMMENT '기본 주소', + spec_address VARCHAR(200) NULL COMMENT '상세 주소', + point INT NOT NULL DEFAULT 0 COMMENT '보유 포인트', + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE | INACTIVE | BANNED', + inactive_date DATETIME NULL COMMENT '비활성화 일시', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_member_email (email), + UNIQUE KEY uq_member_social (social_type, social_id) +); + +-- ============================================================ +-- 5. member_agree (회원 약관 동의) +-- ============================================================ +CREATE TABLE member_agree ( + member_id BIGINT NOT NULL, + terms_id BIGINT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (member_id, terms_id), + CONSTRAINT fk_member_agree_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_member_agree_terms FOREIGN KEY (terms_id) REFERENCES terms (id) +); + +-- ============================================================ +-- 6. member_prefer (회원 음식 카테고리 선호) +-- ============================================================ +CREATE TABLE member_prefer ( + member_id BIGINT NOT NULL, + food_id BIGINT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (member_id, food_id), + CONSTRAINT fk_member_prefer_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_member_prefer_food FOREIGN KEY (food_id) REFERENCES food_category (id) +); + +-- ============================================================ +-- 7. store (가게) +-- - status: OPEN | CLOSED | PENDING +-- ============================================================ +CREATE TABLE store ( + id BIGINT NOT NULL AUTO_INCREMENT, + region_id BIGINT NOT NULL, + food_category_id BIGINT NOT NULL, + name VARCHAR(100) NOT NULL COMMENT '가게명', + description TEXT NULL COMMENT '가게 설명', + lat DECIMAL(10,7) NULL COMMENT '위도', + lng DECIMAL(10,7) NULL COMMENT '경도', + address VARCHAR(200) NOT NULL COMMENT '주소', + status VARCHAR(20) NOT NULL DEFAULT 'OPEN' COMMENT 'OPEN | CLOSED | PENDING', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_store_region FOREIGN KEY (region_id) REFERENCES region (id), + CONSTRAINT fk_store_category FOREIGN KEY (food_category_id) REFERENCES food_category (id) +); + +-- ============================================================ +-- 8. store_image (가게 이미지) +-- ============================================================ +CREATE TABLE store_image ( + id BIGINT NOT NULL AUTO_INCREMENT, + store_id BIGINT NOT NULL, + image_url VARCHAR(500) NOT NULL COMMENT '이미지 URL', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_store_image_store FOREIGN KEY (store_id) REFERENCES store (id) +); + +-- ============================================================ +-- 9. store_hours (가게 영업 시간) +-- - day_of_week: MON | TUE | WED | THU | FRI | SAT | SUN +-- ============================================================ +CREATE TABLE store_hours ( + id BIGINT NOT NULL AUTO_INCREMENT, + store_id BIGINT NOT NULL, + day_of_week VARCHAR(3) NOT NULL COMMENT 'MON | TUE | WED | THU | FRI | SAT | SUN', + open_time TIME NOT NULL COMMENT '영업 시작 시간', + close_time TIME NOT NULL COMMENT '영업 종료 시간', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_store_hours_day (store_id, day_of_week), + CONSTRAINT fk_store_hours_store FOREIGN KEY (store_id) REFERENCES store (id) +); + +-- ============================================================ +-- 10. mission (미션) +-- ============================================================ +CREATE TABLE mission ( + id BIGINT NOT NULL AUTO_INCREMENT, + store_id BIGINT NOT NULL, + title VARCHAR(200) NOT NULL COMMENT '미션 제목', + reward INT NOT NULL DEFAULT 0 COMMENT '완료 시 지급 포인트', + spec VARCHAR(500) NULL COMMENT '미션 상세 조건', + dead_line DATE NULL COMMENT '미션 마감일', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_mission_store FOREIGN KEY (store_id) REFERENCES store (id) +); + +-- ============================================================ +-- 11. member_mission (회원 미션 수행 기록) +-- - status: CHALLENGING | COMPLETE +-- - surrogate PK(id) 사용: review에서 FK 참조 가능하도록 +-- (week3 API: /members/me/missions/{memberMissionId}/reviews) +-- ============================================================ +CREATE TABLE member_mission ( + id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + mission_id BIGINT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'CHALLENGING' COMMENT 'CHALLENGING | COMPLETE', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_member_mission (member_id, mission_id), + CONSTRAINT fk_member_mission_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_member_mission_mission FOREIGN KEY (mission_id) REFERENCES mission (id) +); + +-- ============================================================ +-- 12. review (리뷰) +-- - member_mission_id: 완료된 미션 수행 기록과 연결 +-- (week3 API: POST /members/me/missions/{memberMissionId}/reviews) +-- - score: 1.0 ~ 5.0 (소수점 첫째 자리) +-- ============================================================ +CREATE TABLE review ( + id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + store_id BIGINT NOT NULL, + member_mission_id BIGINT NULL COMMENT '연결된 미션 수행 기록 (선택)', + content TEXT NOT NULL COMMENT '리뷰 내용', + score DECIMAL(2,1) NOT NULL COMMENT '별점 (1.0 ~ 5.0)', + owner_reply VARCHAR(500) NULL COMMENT '사장님 답글', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_review_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_review_store FOREIGN KEY (store_id) REFERENCES store (id), + CONSTRAINT fk_review_member_mission FOREIGN KEY (member_mission_id) REFERENCES member_mission (id), + CONSTRAINT chk_review_score CHECK (score BETWEEN 1.0 AND 5.0) +); + +-- ============================================================ +-- 13. review_image (리뷰 이미지) +-- - week3 API의 image_url 필드 저장 (다중 이미지 지원) +-- ============================================================ +CREATE TABLE review_image ( + id BIGINT NOT NULL AUTO_INCREMENT, + review_id BIGINT NOT NULL, + image_url VARCHAR(500) NOT NULL COMMENT '이미지 URL', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_review_image_review FOREIGN KEY (review_id) REFERENCES review (id) +); diff --git "a/\353\217\204\354\226\217/week4/src/controllers/member.controller.ts" "b/\353\217\204\354\226\217/week4/src/controllers/member.controller.ts" new file mode 100644 index 0000000..fce6fd2 --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/controllers/member.controller.ts" @@ -0,0 +1,41 @@ +import { Request, Response, NextFunction } from 'express' +import { memberService } from '../services/member.service.js' + +export const memberController = { + signUp: async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const result = await memberService.signUp(req.body) + res.status(201).json({ success: true, code: 'S201', message: '회원가입이 완료되었습니다.', data: result }) + } catch (e) { + next(e) + } + }, + + login: async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const result = await memberService.login(req.body) + res.status(200).json({ success: true, code: 'S200', message: '로그인에 성공했습니다.', data: result }) + } catch (e) { + next(e) + } + }, + + getMyPage: (req: Request, res: Response, next: NextFunction): void => { + try { + const result = memberService.getMyPage(req.memberId!) + res.status(200).json({ success: true, code: 'S200', message: '마이페이지 조회에 성공했습니다.', data: result }) + } catch (e) { + next(e) + } + }, + + getMyMissions: (req: Request, res: Response, next: NextFunction): void => { + try { + const { status, page, size } = req.query as Record + const result = memberService.getMyMissions(req.memberId!, status, page, size) + res.status(200).json({ success: true, code: 'S200', message: '미션 목록 조회에 성공했습니다.', data: result }) + } catch (e) { + next(e) + } + }, +} diff --git "a/\353\217\204\354\226\217/week4/src/controllers/mission.controller.ts" "b/\353\217\204\354\226\217/week4/src/controllers/mission.controller.ts" new file mode 100644 index 0000000..12719e2 --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/controllers/mission.controller.ts" @@ -0,0 +1,14 @@ +import { Request, Response, NextFunction } from 'express' +import { missionService } from '../services/mission.service.js' + +export const missionController = { + challengeMission: (req: Request, res: Response, next: NextFunction): void => { + try { + const missionId = parseInt(String(req.params.missionId), 10) + const result = missionService.challengeMission(missionId, req.memberId!) + res.status(201).json({ success: true, code: 'S201', message: '미션 도전이 시작되었습니다.', data: result }) + } catch (e) { + next(e) + } + }, +} diff --git "a/\353\217\204\354\226\217/week4/src/controllers/store.controller.ts" "b/\353\217\204\354\226\217/week4/src/controllers/store.controller.ts" new file mode 100644 index 0000000..df9ec46 --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/controllers/store.controller.ts" @@ -0,0 +1,14 @@ +import { Request, Response, NextFunction } from 'express' +import { storeService } from '../services/store.service.js' + +export const storeController = { + createReview: (req: Request, res: Response, next: NextFunction): void => { + try { + const storeId = parseInt(String(req.params.storeId), 10) + const result = storeService.createReview(storeId, req.memberId!, req.body) + res.status(201).json({ success: true, code: 'S201', message: '리뷰가 등록되었습니다.', data: result }) + } catch (e) { + next(e) + } + }, +} diff --git "a/\353\217\204\354\226\217/week4/src/db/index.ts" "b/\353\217\204\354\226\217/week4/src/db/index.ts" new file mode 100644 index 0000000..0fba219 --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/db/index.ts" @@ -0,0 +1,32 @@ +import type { Member, Store, Review, Mission, MemberMission } from '../types/index.js' + +// 인메모리 DB (week5에서 실제 DB로 교체 예정) +let memberIdSeq = 1 +let reviewIdSeq = 1 +let missionIdSeq = 3 +let memberMissionIdSeq = 1 +let storeIdSeq = 3 + +export const db = { + members: [] as Member[], + + stores: [ + { id: 1, name: '맛있는 식당', address: '서울시 강남구' }, + { id: 2, name: '카페 UMC', address: '서울시 마포구' }, + ] as Store[], + + reviews: [] as Review[], + + missions: [ + { id: 1, storeId: 1, title: '첫 방문 미션', reward: 500, deadline: '2026-12-31', missionSpec: '음식 주문 후 리뷰 남기기' }, + { id: 2, storeId: 2, title: '카페 방문 미션', reward: 300, deadline: '2026-12-31', missionSpec: '음료 주문하기' }, + ] as Mission[], + + memberMissions: [] as MemberMission[], + + nextMemberId: () => memberIdSeq++, + nextReviewId: () => reviewIdSeq++, + nextMissionId: () => missionIdSeq++, + nextMemberMissionId: () => memberMissionIdSeq++, + nextStoreId: () => storeIdSeq++, +} diff --git "a/\353\217\204\354\226\217/week4/src/index.ts" "b/\353\217\204\354\226\217/week4/src/index.ts" new file mode 100644 index 0000000..092d041 --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/index.ts" @@ -0,0 +1,24 @@ +import express from 'express' +import { memberRouter } from './routes/member.route.js' +import { storeRouter } from './routes/store.route.js' +import { missionRouter } from './routes/mission.route.js' +import { errorMiddleware } from './middleware/error.middleware.js' + +const app = express() +const port = 3000 + +app.use(express.json()) + +app.use('/api/v1/members', memberRouter) +app.use('/api/v1/stores', storeRouter) +app.use('/api/v1/missions', missionRouter) + +app.get('/', (_req, res) => { + res.send('UMC 4주차 서버 실행 중!') +}) + +app.use(errorMiddleware) + +app.listen(port, () => { + console.log(`Server is running on port ${port}`) +}) diff --git "a/\353\217\204\354\226\217/week4/src/middleware/auth.middleware.ts" "b/\353\217\204\354\226\217/week4/src/middleware/auth.middleware.ts" new file mode 100644 index 0000000..83b898e --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/middleware/auth.middleware.ts" @@ -0,0 +1,23 @@ +import { Request, Response, NextFunction } from 'express' +import jwt from 'jsonwebtoken' + +const JWT_SECRET = process.env.JWT_SECRET ?? 'umc-week4-secret' + +export const authMiddleware = (req: Request, res: Response, next: NextFunction): void => { + const authHeader = req.headers.authorization + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + res.status(401).json({ success: false, code: 'E401', message: '인증 토큰이 필요합니다.' }) + return + } + + const token = authHeader.split(' ')[1] + + try { + const decoded = jwt.verify(token, JWT_SECRET) as { memberId: number } + req.memberId = decoded.memberId + next() + } catch { + res.status(401).json({ success: false, code: 'E401', message: '유효하지 않은 토큰입니다.' }) + } +} diff --git "a/\353\217\204\354\226\217/week4/src/middleware/error.middleware.ts" "b/\353\217\204\354\226\217/week4/src/middleware/error.middleware.ts" new file mode 100644 index 0000000..6a8897b --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/middleware/error.middleware.ts" @@ -0,0 +1,16 @@ +import { Request, Response, NextFunction } from 'express' + +interface AppError extends Error { + status?: number +} + +export const errorMiddleware = (err: AppError, req: Request, res: Response, _next: NextFunction): void => { + console.error(`[Error] ${err.message}`) + + const status = err.status ?? 500 + res.status(status).json({ + success: false, + code: `E${status}`, + message: err.message || '서버 오류가 발생했습니다.', + }) +} diff --git "a/\353\217\204\354\226\217/week4/src/repositories/member.repository.ts" "b/\353\217\204\354\226\217/week4/src/repositories/member.repository.ts" new file mode 100644 index 0000000..1eaba8f --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/repositories/member.repository.ts" @@ -0,0 +1,43 @@ +import { db } from '../db/index.js' +import type { Member, MemberMission, PageResult } from '../types/index.js' + +export const memberRepository = { + findByEmail: (email: string): Member | undefined => + db.members.find((m) => m.email === email), + + findById: (id: number): Member | undefined => + db.members.find((m) => m.id === id), + + save: (member: Omit): Member => { + const newMember: Member = { + ...member, + id: db.nextMemberId(), + point: 0, + createdAt: new Date().toISOString(), + } + db.members.push(newMember) + return newMember + }, + + findMissionsByMemberId: ( + memberId: number, + status: string | undefined, + page: string | undefined, + size: string | undefined, + ): PageResult => { + let missions = db.memberMissions.filter((mm) => mm.memberId === memberId) + + if (status) { + missions = missions.filter((mm) => mm.status === status) + } + + const pageNum = parseInt(page ?? '1', 10) + const sizeNum = parseInt(size ?? '10', 10) + const totalCount = missions.length + const totalPages = Math.max(1, Math.ceil(totalCount / sizeNum)) + const start = (pageNum - 1) * sizeNum + const paged = missions.slice(start, start + sizeNum) + + return { missions: paged, totalPages, currentPage: pageNum, isLast: pageNum >= totalPages } + }, +} diff --git "a/\353\217\204\354\226\217/week4/src/repositories/mission.repository.ts" "b/\353\217\204\354\226\217/week4/src/repositories/mission.repository.ts" new file mode 100644 index 0000000..4594c37 --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/repositories/mission.repository.ts" @@ -0,0 +1,20 @@ +import { db } from '../db/index.js' +import type { Mission, MemberMission } from '../types/index.js' + +export const missionRepository = { + findById: (id: number): Mission | undefined => + db.missions.find((m) => m.id === id), + + findMemberMission: (memberId: number, missionId: number): MemberMission | undefined => + db.memberMissions.find((mm) => mm.memberId === memberId && mm.missionId === missionId), + + saveMemberMission: (memberMission: Omit): MemberMission => { + const newMM: MemberMission = { + ...memberMission, + id: db.nextMemberMissionId(), + createdAt: new Date().toISOString(), + } + db.memberMissions.push(newMM) + return newMM + }, +} diff --git "a/\353\217\204\354\226\217/week4/src/repositories/store.repository.ts" "b/\353\217\204\354\226\217/week4/src/repositories/store.repository.ts" new file mode 100644 index 0000000..cfb2e41 --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/repositories/store.repository.ts" @@ -0,0 +1,17 @@ +import { db } from '../db/index.js' +import type { Store, Review } from '../types/index.js' + +export const storeRepository = { + findById: (id: number): Store | undefined => + db.stores.find((s) => s.id === id), + + saveReview: (review: Omit): Review => { + const newReview: Review = { + ...review, + id: db.nextReviewId(), + createdAt: new Date().toISOString(), + } + db.reviews.push(newReview) + return newReview + }, +} diff --git "a/\353\217\204\354\226\217/week4/src/routes/member.route.ts" "b/\353\217\204\354\226\217/week4/src/routes/member.route.ts" new file mode 100644 index 0000000..047749e --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/routes/member.route.ts" @@ -0,0 +1,10 @@ +import express from 'express' +import { memberController } from '../controllers/member.controller.js' +import { authMiddleware } from '../middleware/auth.middleware.js' + +export const memberRouter = express.Router() + +memberRouter.post('/', memberController.signUp) +memberRouter.post('/login', memberController.login) +memberRouter.get('/me', authMiddleware, memberController.getMyPage) +memberRouter.get('/me/missions', authMiddleware, memberController.getMyMissions) diff --git "a/\353\217\204\354\226\217/week4/src/routes/mission.route.ts" "b/\353\217\204\354\226\217/week4/src/routes/mission.route.ts" new file mode 100644 index 0000000..e05c73e --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/routes/mission.route.ts" @@ -0,0 +1,7 @@ +import express from 'express' +import { missionController } from '../controllers/mission.controller.js' +import { authMiddleware } from '../middleware/auth.middleware.js' + +export const missionRouter = express.Router() + +missionRouter.post('/:missionId/challenges', authMiddleware, missionController.challengeMission) diff --git "a/\353\217\204\354\226\217/week4/src/routes/store.route.ts" "b/\353\217\204\354\226\217/week4/src/routes/store.route.ts" new file mode 100644 index 0000000..a239a4e --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/routes/store.route.ts" @@ -0,0 +1,7 @@ +import express from 'express' +import { storeController } from '../controllers/store.controller.js' +import { authMiddleware } from '../middleware/auth.middleware.js' + +export const storeRouter = express.Router() + +storeRouter.post('/:storeId/reviews', authMiddleware, storeController.createReview) diff --git "a/\353\217\204\354\226\217/week4/src/services/member.service.ts" "b/\353\217\204\354\226\217/week4/src/services/member.service.ts" new file mode 100644 index 0000000..8b81eca --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/services/member.service.ts" @@ -0,0 +1,76 @@ +import bcrypt from 'bcryptjs' +import jwt from 'jsonwebtoken' +import { memberRepository } from '../repositories/member.repository.js' +import type { MemberMission, PageResult } from '../types/index.js' + +const JWT_SECRET = process.env.JWT_SECRET ?? 'umc-week4-secret' + +const makeError = (message: string, status: number): Error & { status: number } => { + const err = new Error(message) as Error & { status: number } + err.status = status + return err +} + +export const memberService = { + signUp: async (body: Record) => { + const { name, nickname, email, password } = body as { + name?: string + nickname?: string + email?: string + password?: string + } + + if (!name || !nickname || !email || !password) { + throw makeError('필수 항목(name, nickname, email, password)을 모두 입력해 주세요.', 400) + } + + if (memberRepository.findByEmail(email)) { + throw makeError('이미 사용 중인 이메일입니다.', 409) + } + + const hashedPassword = await bcrypt.hash(password, 10) + const member = memberRepository.save({ name, nickname, email, password: hashedPassword }) + const token = jwt.sign({ memberId: member.id }, JWT_SECRET, { expiresIn: '7d' }) + + return { memberId: member.id, name: member.name, nickname: member.nickname, token } + }, + + login: async (body: Record) => { + const { email, password } = body as { email?: string; password?: string } + + if (!email || !password) { + throw makeError('이메일과 비밀번호를 입력해 주세요.', 400) + } + + const member = memberRepository.findByEmail(email) + if (!member) { + throw makeError('이메일 또는 비밀번호가 올바르지 않습니다.', 401) + } + + const isValid = await bcrypt.compare(password, member.password) + if (!isValid) { + throw makeError('이메일 또는 비밀번호가 올바르지 않습니다.', 401) + } + + const token = jwt.sign({ memberId: member.id }, JWT_SECRET, { expiresIn: '7d' }) + return { memberId: member.id, name: member.name, nickname: member.nickname, token } + }, + + getMyPage: (memberId: number) => { + const member = memberRepository.findById(memberId) + if (!member) { + throw makeError('회원을 찾을 수 없습니다.', 404) + } + const { password: _, ...safeData } = member + return safeData + }, + + getMyMissions: ( + memberId: number, + status: string | undefined, + page: string | undefined, + size: string | undefined, + ): PageResult => { + return memberRepository.findMissionsByMemberId(memberId, status, page, size) + }, +} diff --git "a/\353\217\204\354\226\217/week4/src/services/mission.service.ts" "b/\353\217\204\354\226\217/week4/src/services/mission.service.ts" new file mode 100644 index 0000000..d9f0068 --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/services/mission.service.ts" @@ -0,0 +1,24 @@ +import { missionRepository } from '../repositories/mission.repository.js' +import type { MemberMission } from '../types/index.js' + +const makeError = (message: string, status: number): Error & { status: number } => { + const err = new Error(message) as Error & { status: number } + err.status = status + return err +} + +export const missionService = { + challengeMission: (missionId: number, memberId: number): MemberMission => { + const mission = missionRepository.findById(missionId) + if (!mission) { + throw makeError('존재하지 않는 미션입니다.', 404) + } + + const existing = missionRepository.findMemberMission(memberId, missionId) + if (existing) { + throw makeError('이미 도전 중이거나 완료한 미션입니다.', 409) + } + + return missionRepository.saveMemberMission({ memberId, missionId, status: 'CHALLENGING' }) + }, +} diff --git "a/\353\217\204\354\226\217/week4/src/services/store.service.ts" "b/\353\217\204\354\226\217/week4/src/services/store.service.ts" new file mode 100644 index 0000000..94a740b --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/services/store.service.ts" @@ -0,0 +1,29 @@ +import { storeRepository } from '../repositories/store.repository.js' +import type { Review } from '../types/index.js' + +const makeError = (message: string, status: number): Error & { status: number } => { + const err = new Error(message) as Error & { status: number } + err.status = status + return err +} + +export const storeService = { + createReview: (storeId: number, memberId: number, body: Record): Review => { + const { content, score } = body as { content?: string; score?: number } + + if (!content || score === undefined) { + throw makeError('내용(content)과 별점(score)을 입력해 주세요.', 400) + } + + if (score < 1 || score > 5) { + throw makeError('별점은 1~5 사이여야 합니다.', 400) + } + + const store = storeRepository.findById(storeId) + if (!store) { + throw makeError('존재하지 않는 가게입니다.', 404) + } + + return storeRepository.saveReview({ storeId, memberId, content, score }) + }, +} diff --git "a/\353\217\204\354\226\217/week4/src/types/index.ts" "b/\353\217\204\354\226\217/week4/src/types/index.ts" new file mode 100644 index 0000000..f128546 --- /dev/null +++ "b/\353\217\204\354\226\217/week4/src/types/index.ts" @@ -0,0 +1,57 @@ +export interface Member { + id: number + name: string + nickname: string + email: string + password: string + point: number + createdAt: string +} + +export interface Store { + id: number + name: string + address: string +} + +export interface Review { + id: number + storeId: number + memberId: number + content: string + score: number + createdAt: string +} + +export interface Mission { + id: number + storeId: number + title: string + reward: number + deadline: string + missionSpec: string +} + +export interface MemberMission { + id: number + memberId: number + missionId: number + status: 'CHALLENGING' | 'COMPLETE' + createdAt: string +} + +export interface PageResult { + missions: T[] + totalPages: number + currentPage: number + isLast: boolean +} + +// Express Request에 memberId 주입을 위한 타입 확장 +declare global { + namespace Express { + interface Request { + memberId?: number + } + } +} diff --git "a/\353\217\204\354\226\217/week4/tsconfig.json" "b/\353\217\204\354\226\217/week4/tsconfig.json" new file mode 100644 index 0000000..1725aa8 --- /dev/null +++ "b/\353\217\204\354\226\217/week4/tsconfig.json" @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} From 35c8b56964aa4a46b1e1464915071bb3730fd2d4 Mon Sep 17 00:00:00 2001 From: higashiaka Date: Sat, 18 Apr 2026 11:32:53 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=EB=8F=84=EC=96=8F=205=EC=A3=BC?= =?UTF-8?q?=EC=B0=A8=20=EB=AF=B8=EC=85=98=20=EC=A0=9C=EC=B6=9C=20-=20Chapt?= =?UTF-8?q?er=205.=20API=20=EB=B0=8F=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=84=A4=EC=A0=95=20=EA=B8=B0=EC=B4=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "\353\217\204\354\226\217/week5/.gitignore" | 19 + .../week5/POSTMAN_GUIDE.md" | 327 +++ .../week5/package-lock.json" | 2077 +++++++++++++++++ "\353\217\204\354\226\217/week5/package.json" | 31 + "\353\217\204\354\226\217/week5/reset_db.sql" | 215 ++ .../week5/src/db.config.ts" | 15 + "\353\217\204\354\226\217/week5/src/index.ts" | 47 + .../week5/src/middleware/error.middleware.ts" | 21 + .../members/controllers/member.controller.ts" | 18 + .../src/modules/members/dtos/member.dto.ts" | 45 + .../repositories/member.repository.ts" | 72 + .../members/services/member.service.ts" | 50 + .../controllers/mission.controller.ts" | 34 + .../src/modules/missions/dtos/mission.dto.ts" | 56 + .../repositories/mission.repository.ts" | 99 + .../missions/services/mission.service.ts" | 79 + .../reviews/controllers/review.controller.ts" | 19 + .../src/modules/reviews/dtos/review.dto.ts" | 34 + .../repositories/review.repository.ts" | 40 + .../reviews/services/review.service.ts" | 39 + .../stores/controllers/store.controller.ts" | 18 + .../src/modules/stores/dtos/store.dto.ts" | 38 + .../stores/repositories/store.repository.ts" | 67 + .../modules/stores/services/store.service.ts" | 25 + .../week5/todolist.json" | 353 +++ .../week5/tsconfig.json" | 18 + 26 files changed, 3856 insertions(+) create mode 100644 "\353\217\204\354\226\217/week5/.gitignore" create mode 100644 "\353\217\204\354\226\217/week5/POSTMAN_GUIDE.md" create mode 100644 "\353\217\204\354\226\217/week5/package-lock.json" create mode 100644 "\353\217\204\354\226\217/week5/package.json" create mode 100644 "\353\217\204\354\226\217/week5/reset_db.sql" create mode 100644 "\353\217\204\354\226\217/week5/src/db.config.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/index.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/middleware/error.middleware.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/members/controllers/member.controller.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/members/dtos/member.dto.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/members/repositories/member.repository.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/members/services/member.service.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/missions/controllers/mission.controller.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/missions/dtos/mission.dto.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/missions/repositories/mission.repository.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/missions/services/mission.service.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/reviews/controllers/review.controller.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/reviews/dtos/review.dto.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/reviews/repositories/review.repository.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/reviews/services/review.service.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/stores/controllers/store.controller.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/stores/dtos/store.dto.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/stores/repositories/store.repository.ts" create mode 100644 "\353\217\204\354\226\217/week5/src/modules/stores/services/store.service.ts" create mode 100644 "\353\217\204\354\226\217/week5/todolist.json" create mode 100644 "\353\217\204\354\226\217/week5/tsconfig.json" diff --git "a/\353\217\204\354\226\217/week5/.gitignore" "b/\353\217\204\354\226\217/week5/.gitignore" new file mode 100644 index 0000000..81e6f7c --- /dev/null +++ "b/\353\217\204\354\226\217/week5/.gitignore" @@ -0,0 +1,19 @@ +# dependency directories +node_modules/ + +# build output +dist/ + +# dotenv environment variable files +.env +.env.local +.env.development +.env.production +.env.* + +# macOS +.DS_Store + +# logs +*.log +npm-debug.log* diff --git "a/\353\217\204\354\226\217/week5/POSTMAN_GUIDE.md" "b/\353\217\204\354\226\217/week5/POSTMAN_GUIDE.md" new file mode 100644 index 0000000..c77064e --- /dev/null +++ "b/\353\217\204\354\226\217/week5/POSTMAN_GUIDE.md" @@ -0,0 +1,327 @@ +# Postman API 테스트 가이드 — Week 5 UMC Mission Service + +## 목차 +1. [Workspace & Collection 생성](#1-workspace--collection-생성) +2. [환경 변수 설정](#2-환경-변수-설정) +3. [API 요청 목록](#3-api-요청-목록) + - [서버 상태 확인](#31-서버-상태-확인) + - [회원가입](#32-회원가입) + - [가게 등록](#33-가게-등록) + - [리뷰 작성](#34-리뷰-작성) + - [미션 생성](#35-미션-생성) + - [미션 도전](#36-미션-도전) +4. [에러 케이스 테스트](#4-에러-케이스-테스트) +5. [응답 형식 정리](#5-응답-형식-정리) + +--- + +## 1. Workspace & Collection 생성 + +1. Postman 왼쪽 상단 **[Workspaces]** → 내 작업 공간으로 이동 +2. 왼쪽 메뉴 **[Collections]** 옆 `+` 버튼 클릭 +3. 이름을 `UMC-Week5-Mission-Service`로 변경 +4. 하위에 폴더를 만들어 도메인별로 요청을 분류하면 편리합니다: + - `Members` — 회원 관련 + - `Stores` — 가게 관련 + - `Reviews` — 리뷰 관련 + - `Missions` — 미션 관련 + +--- + +## 2. 환경 변수 설정 + +서버 주소를 매번 입력하지 않도록 환경 변수를 등록합니다. + +1. 왼쪽 메뉴 **[Environments]** → `+` 버튼 클릭 +2. 환경 이름: `Local` +3. 아래 변수 등록 후 **Save** + +| Variable | Initial Value | 설명 | +|---|---|---| +| `host` | `http://localhost:3000` | 서버 주소 (`.env`의 `PORT=3000` 기준) | + +4. 우측 상단 드롭다운에서 **Local** 선택 + +> 요청 URL에서 `{{host}}`로 참조합니다. 예: `{{host}}/api/v1/members/signup` + +--- + +## 3. API 요청 목록 + +### 3.1 서버 상태 확인 + +| 항목 | 내용 | +|---|---| +| **Method** | `GET` | +| **URL** | `{{host}}/` | +| **Body** | 없음 | +| **기대 응답** | `200 OK` | + +**응답 예시** +```json +"Hello World!" +``` + +--- + +### 3.2 회원가입 + +| 항목 | 내용 | +|---|---| +| **Method** | `POST` | +| **URL** | `{{host}}/api/v1/members/signup` | +| **Headers** | `Content-Type: application/json` | +| **기대 응답** | `201 Created` | + +**Request Body (raw → JSON)** +```json +{ + "name": "언년", + "nickname": "unyeon", + "email": "unyeon@umc.com", + "password": "password123!", + "phoneNum": "010-1234-5678", + "birth": "2000-01-01", + "gender": "FEMALE", + "address": "서울특별시 강남구", + "specAddress": "101동 202호" +} +``` + +> `name`, `nickname`은 필수값입니다. 나머지는 선택 사항입니다. +> `gender` 허용값: `"MALE"` | `"FEMALE"` | `"OTHER"` +> `birth`, `deadLine` 날짜 형식: `"YYYY-MM-DD"` + +**성공 응답 예시** +```json +{ + "success": true, + "data": { + "memberId": 1, + "name": "언년", + "nickname": "unyeon", + "email": "unyeon@umc.com", + "phoneNum": "010-1234-5678", + "status": "ACTIVE" + } +} +``` + +--- + +### 3.3 가게 등록 + +| 항목 | 내용 | +|---|---| +| **Method** | `POST` | +| **URL** | `{{host}}/api/v1/stores` | +| **Headers** | `Content-Type: application/json` | +| **기대 응답** | `201 Created` | + +**Request Body (raw → JSON)** +```json +{ + "regionId": 1, + "foodCategoryId": 1, + "name": "맛있는 치킨집", + "description": "바삭하고 맛있는 치킨을 판매합니다.", + "address": "서울특별시 마포구 홍대입구역 1번 출구", + "lat": 37.5563, + "lng": 126.9239 +} +``` + +> `regionId`, `foodCategoryId`, `name`, `address`는 필수값입니다. +> `regionId`와 `foodCategoryId`는 DB에 미리 존재하는 값을 사용해야 합니다. + +**성공 응답 예시** +```json +{ + "success": true, + "data": { + "storeId": 1, + "name": "맛있는 치킨집", + "address": "서울특별시 마포구 홍대입구역 1번 출구", + "regionId": 1 + } +} +``` + +--- + +### 3.4 리뷰 작성 + +| 항목 | 내용 | +|---|---| +| **Method** | `POST` | +| **URL** | `{{host}}/api/v1/stores/:storeId/reviews` | +| **URL 예시** | `{{host}}/api/v1/stores/1/reviews` | +| **Headers** | `Content-Type: application/json` | +| **기대 응답** | `201 Created` | + +**Request Body (raw → JSON)** +```json +{ + "memberId": 1, + "content": "치킨이 정말 바삭하고 맛있었어요! 또 방문할 것 같아요.", + "score": 5 +} +``` + +> `memberId`, `content`, `score`는 모두 필수값입니다. +> `score` 허용 범위: `1` ~ `5` (정수) + +**성공 응답 예시** +```json +{ + "success": true, + "data": { + "reviewId": 1, + "memberId": 1, + "storeId": 1, + "content": "치킨이 정말 바삭하고 맛있었어요! 또 방문할 것 같아요.", + "score": 5, + "createdAt": "2026-04-18T12:00:00.000Z" + } +} +``` + +--- + +### 3.5 미션 생성 + +| 항목 | 내용 | +|---|---| +| **Method** | `POST` | +| **URL** | `{{host}}/api/v1/stores/:storeId/missions` | +| **URL 예시** | `{{host}}/api/v1/stores/1/missions` | +| **Headers** | `Content-Type: application/json` | +| **기대 응답** | `201 Created` | + +**Request Body (raw → JSON)** +```json +{ + "title": "치킨 3번 주문하기", + "reward": 500, + "spec": "한 달 내에 치킨을 3번 주문하면 500포인트 적립!", + "deadLine": "2026-05-31" +} +``` + +> `title`, `reward`는 필수값입니다. +> `reward`는 지급할 포인트 수량입니다. + +**성공 응답 예시** +```json +{ + "success": true, + "data": { + "missionId": 1, + "storeId": 1, + "title": "치킨 3번 주문하기", + "reward": 500, + "spec": "한 달 내에 치킨을 3번 주문하면 500포인트 적립!", + "deadLine": "2026-05-31T00:00:00.000Z" + } +} +``` + +--- + +### 3.6 미션 도전 + +| 항목 | 내용 | +|---|---| +| **Method** | `POST` | +| **URL** | `{{host}}/api/v1/missions/:missionId/challenge` | +| **URL 예시** | `{{host}}/api/v1/missions/1/challenge` | +| **Headers** | `Content-Type: application/json` | +| **기대 응답** | `201 Created` | + +**Request Body (raw → JSON)** +```json +{ + "memberId": 1 +} +``` + +**성공 응답 예시** +```json +{ + "success": true, + "data": { + "memberMissionId": 1, + "memberId": 1, + "missionId": 1, + "status": "CHALLENGING" + } +} +``` + +--- + +## 4. 에러 케이스 테스트 + +각 요청을 **복제(Duplicate)** 해서 에러 케이스 전용 요청으로 저장해 두면 좋습니다. + +### 회원가입 에러 + +| 케이스 | 방법 | 기대 응답 | +|---|---|---| +| 이메일 중복 | 동일한 `email`로 두 번 요청 | `409 Conflict` | +| 필수값 누락 | `name` 또는 `nickname` 제거 | `400 Bad Request` 또는 DB 에러 | + +### 리뷰 작성 에러 + +| 케이스 | 방법 | 기대 응답 | +|---|---|---| +| 존재하지 않는 가게 | URL의 `storeId`를 `99999`로 변경 | `404 Not Found` | +| 점수 범위 초과 | `"score": 6` 또는 `"score": 0` | `400 Bad Request` | + +### 미션 도전 에러 + +| 케이스 | 방법 | 기대 응답 | +|---|---|---| +| 존재하지 않는 미션 | URL의 `missionId`를 `99999`로 변경 | `404 Not Found` | +| 중복 도전 | 동일한 `memberId`로 같은 미션에 두 번 요청 | `409 Conflict` | + +### 미션/리뷰 공통 에러 + +| 케이스 | 방법 | 기대 응답 | +|---|---|---| +| 존재하지 않는 가게에 미션 생성 | URL의 `storeId`를 `99999`로 변경 | `404 Not Found` | + +--- + +## 5. 응답 형식 정리 + +모든 API는 아래 두 가지 형식 중 하나로 응답합니다. + +**성공 시** +```json +{ + "success": true, + "data": { ... } +} +``` + +**실패 시** +```json +{ + "success": false, + "code": "E404", + "message": "에러 메시지" +} +``` + +| 상태 코드 | 코드 형식 | 상황 | +|---|---|---| +| `201 Created` | — | 리소스 생성 성공 | +| `400 Bad Request` | `E400` | 잘못된 입력값 (예: score 범위 초과) | +| `404 Not Found` | `E404` | 존재하지 않는 리소스 | +| `409 Conflict` | `E409` | 중복 데이터 (이메일, 미션 중복 도전) | +| `500 Internal Server Error` | `E500` | 서버/DB 오류 | + +--- + +> **응답 저장 팁:** 성공 응답이 왔을 때 응답창 우측 **[Save Response]** → **[Save as example]** 을 클릭하면 Postman Documentation 탭에 예시 응답이 자동으로 기록됩니다. 에러 케이스도 함께 저장해두면 팀원과 공유하기 편리합니다. diff --git "a/\353\217\204\354\226\217/week5/package-lock.json" "b/\353\217\204\354\226\217/week5/package-lock.json" new file mode 100644 index 0000000..08b27e8 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/package-lock.json" @@ -0,0 +1,2077 @@ +{ + "name": "umc-week5", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "umc-week5", + "version": "1.0.0", + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "http-status-codes": "^2.3.0", + "mysql2": "^3.9.7" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/dotenv": "^8.2.0", + "@types/express": "^4.17.21", + "@types/node": "^22.0.0", + "nodemon": "^3.1.0", + "tsx": "^4.19.3", + "typescript": "^5.8.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/dotenv": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.3.tgz", + "integrity": "sha512-g2FXjlDX/cYuc5CiQvyU/6kkbP1JtmGzh0obW50zD7OKeILVL0NSpPWLXVfqoAGQjom2/SLLx9zHq0KXvD6mbw==", + "deprecated": "This is a stub types definition. dotenv provides its own type definitions, so you do not need this installed.", + "dev": true, + "license": "MIT", + "dependencies": { + "dotenv": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "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/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/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/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "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.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "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.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "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": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/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/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.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "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/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/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/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/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "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.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "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.14.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.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "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.2", + "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/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/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "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/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "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/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-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/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "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": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/mysql2": { + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.1.tgz", + "integrity": "sha512-48+9UXehKyxxiP2pqCxUq+MSFvX+v41jwsSpFDQO/jAoFuAELutBGJUhWJnDbe82/OBlIhSBMC82WeonmznT/Q==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.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/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "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/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/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/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-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "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.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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/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/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.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/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/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "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/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "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/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" + } + } + } +} diff --git "a/\353\217\204\354\226\217/week5/package.json" "b/\353\217\204\354\226\217/week5/package.json" new file mode 100644 index 0000000..69d2ef2 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/package.json" @@ -0,0 +1,31 @@ +{ + "name": "umc-week5", + "version": "1.0.0", + "description": "UMC 5주차 - Express + TypeScript + MySQL API 서버", + "main": "src/index.ts", + "type": "module", + "scripts": { + "start": "tsx src/index.ts", + "dev": "nodemon --exec tsx src/index.ts", + "build": "tsc", + "start:prod": "node dist/index.js" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "http-status-codes": "^2.3.0", + "mysql2": "^3.9.7" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/dotenv": "^8.2.0", + "@types/express": "^4.17.21", + "@types/node": "^22.0.0", + "nodemon": "^3.1.0", + "tsx": "^4.19.3", + "typescript": "^5.8.3" + } +} diff --git "a/\353\217\204\354\226\217/week5/reset_db.sql" "b/\353\217\204\354\226\217/week5/reset_db.sql" new file mode 100644 index 0000000..ff192db --- /dev/null +++ "b/\353\217\204\354\226\217/week5/reset_db.sql" @@ -0,0 +1,215 @@ +-- ============================================================ +-- DB 초기화 및 재생성 스크립트 +-- ============================================================ + +CREATE DATABASE IF NOT EXISTS umc_mission DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE umc_mission; + +-- FK 체크 비활성화 후 전체 DROP +SET FOREIGN_KEY_CHECKS = 0; + +DROP TABLE IF EXISTS review_image; +DROP TABLE IF EXISTS review; +DROP TABLE IF EXISTS member_mission; +DROP TABLE IF EXISTS mission; +DROP TABLE IF EXISTS store_hours; +DROP TABLE IF EXISTS store_image; +DROP TABLE IF EXISTS store; +DROP TABLE IF EXISTS member_prefer; +DROP TABLE IF EXISTS member_agree; +DROP TABLE IF EXISTS member; +DROP TABLE IF EXISTS terms; +DROP TABLE IF EXISTS food_category; +DROP TABLE IF EXISTS region; + +SET FOREIGN_KEY_CHECKS = 1; + +-- ============================================================ +-- 테이블 재생성 +-- ============================================================ + +CREATE TABLE region ( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL COMMENT '지역명', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE food_category ( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL COMMENT '카테고리명 (한식, 중식, 일식 등)', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE terms ( + id BIGINT NOT NULL AUTO_INCREMENT, + title VARCHAR(100) NOT NULL COMMENT '약관 제목', + content TEXT NOT NULL COMMENT '약관 내용', + optional BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'TRUE: 선택 동의, FALSE: 필수 동의', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE member ( + id BIGINT NOT NULL AUTO_INCREMENT, + social_type VARCHAR(20) NULL, + social_id VARCHAR(100) NULL, + email VARCHAR(100) NULL, + password VARCHAR(255) NULL, + name VARCHAR(50) NOT NULL, + nickname VARCHAR(50) NOT NULL, + profile_image_url VARCHAR(500) NULL, + phone_num VARCHAR(20) NULL, + phone_verified BOOLEAN NOT NULL DEFAULT FALSE, + birth DATE NULL, + gender VARCHAR(10) NULL, + address VARCHAR(200) NULL, + spec_address VARCHAR(200) NULL, + point INT NOT NULL DEFAULT 0, + status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + inactive_date DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_member_email (email), + UNIQUE KEY uq_member_social (social_type, social_id) +); + +CREATE TABLE member_agree ( + member_id BIGINT NOT NULL, + terms_id BIGINT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (member_id, terms_id), + CONSTRAINT fk_member_agree_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_member_agree_terms FOREIGN KEY (terms_id) REFERENCES terms (id) +); + +CREATE TABLE member_prefer ( + member_id BIGINT NOT NULL, + food_id BIGINT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (member_id, food_id), + CONSTRAINT fk_member_prefer_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_member_prefer_food FOREIGN KEY (food_id) REFERENCES food_category (id) +); + +CREATE TABLE store ( + id BIGINT NOT NULL AUTO_INCREMENT, + region_id BIGINT NOT NULL, + food_category_id BIGINT NOT NULL, + name VARCHAR(100) NOT NULL, + description TEXT NULL, + lat DECIMAL(10,7) NULL, + lng DECIMAL(10,7) NULL, + address VARCHAR(200) NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'OPEN', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_store_region FOREIGN KEY (region_id) REFERENCES region (id), + CONSTRAINT fk_store_category FOREIGN KEY (food_category_id) REFERENCES food_category (id) +); + +CREATE TABLE store_image ( + id BIGINT NOT NULL AUTO_INCREMENT, + store_id BIGINT NOT NULL, + image_url VARCHAR(500) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_store_image_store FOREIGN KEY (store_id) REFERENCES store (id) +); + +CREATE TABLE store_hours ( + id BIGINT NOT NULL AUTO_INCREMENT, + store_id BIGINT NOT NULL, + day_of_week VARCHAR(3) NOT NULL, + open_time TIME NOT NULL, + close_time TIME NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_store_hours_day (store_id, day_of_week), + CONSTRAINT fk_store_hours_store FOREIGN KEY (store_id) REFERENCES store (id) +); + +CREATE TABLE mission ( + id BIGINT NOT NULL AUTO_INCREMENT, + store_id BIGINT NOT NULL, + title VARCHAR(200) NOT NULL, + reward INT NOT NULL DEFAULT 0, + spec VARCHAR(500) NULL, + dead_line DATE NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_mission_store FOREIGN KEY (store_id) REFERENCES store (id) +); + +CREATE TABLE member_mission ( + id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + mission_id BIGINT NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'CHALLENGING', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_member_mission (member_id, mission_id), + CONSTRAINT fk_member_mission_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_member_mission_mission FOREIGN KEY (mission_id) REFERENCES mission (id) +); + +CREATE TABLE review ( + id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + store_id BIGINT NOT NULL, + member_mission_id BIGINT NULL, + content TEXT NOT NULL, + score DECIMAL(2,1) NOT NULL, + owner_reply VARCHAR(500) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_review_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_review_store FOREIGN KEY (store_id) REFERENCES store (id), + CONSTRAINT fk_review_member_mission FOREIGN KEY (member_mission_id) REFERENCES member_mission (id), + CONSTRAINT chk_review_score CHECK (score BETWEEN 1.0 AND 5.0) +); + +CREATE TABLE review_image ( + id BIGINT NOT NULL AUTO_INCREMENT, + review_id BIGINT NOT NULL, + image_url VARCHAR(500) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_review_image_review FOREIGN KEY (review_id) REFERENCES review (id) +); + +-- ============================================================ +-- 시드 데이터 (API 테스트용 초기 데이터) +-- ============================================================ + +INSERT INTO region (name) VALUES + ('서울'), + ('경기'), + ('인천'), + ('부산'), + ('대구'); + +INSERT INTO food_category (name) VALUES + ('한식'), + ('중식'), + ('일식'), + ('양식'), + ('분식'), + ('카페/디저트'), + ('치킨'), + ('피자'), + ('패스트푸드'); diff --git "a/\353\217\204\354\226\217/week5/src/db.config.ts" "b/\353\217\204\354\226\217/week5/src/db.config.ts" new file mode 100644 index 0000000..a9c5115 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/db.config.ts" @@ -0,0 +1,15 @@ +import mysql from 'mysql2/promise' +import dotenv from 'dotenv' + +dotenv.config() + +export const pool = mysql.createPool({ + host: process.env.DB_HOST ?? 'localhost', + user: process.env.DB_USER ?? 'root', + port: parseInt(process.env.DB_PORT ?? '3306'), + database: process.env.DB_NAME ?? 'umc_mission', + password: process.env.DB_PASSWORD ?? '', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, +}) diff --git "a/\353\217\204\354\226\217/week5/src/index.ts" "b/\353\217\204\354\226\217/week5/src/index.ts" new file mode 100644 index 0000000..4cc7f93 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/index.ts" @@ -0,0 +1,47 @@ +import dotenv from 'dotenv' +import express, { Express, Request, Response } from 'express' +import cors from 'cors' + +// 컨트롤러 import +import { handleCreateStore } from './modules/stores/controllers/store.controller.js' +import { handleCreateReview } from './modules/reviews/controllers/review.controller.js' +import { handleCreateMission, handleChallengeMission } from './modules/missions/controllers/mission.controller.js' +import { handleSignUp } from './modules/members/controllers/member.controller.js' + +// 에러 미들웨어 import +import { errorMiddleware } from './middleware/error.middleware.js' + +// 1. 환경 변수 설정 (가장 먼저 호출) +dotenv.config() + +const app: Express = express() +const port = process.env.PORT ?? 3000 + +// 2. 미들웨어 설정 +app.use(cors()) +app.use(express.json()) +app.use(express.urlencoded({ extended: false })) + +// 3. 라우터 등록 +app.get('/', (_req: Request, res: Response) => { + res.send('UMC 5주차 서버 실행 중!') +}) + +// 회원 +app.post('/api/v1/members/signup', handleSignUp) + +// 가게 +app.post('/api/v1/stores', handleCreateStore) +app.post('/api/v1/stores/:storeId/reviews', handleCreateReview) +app.post('/api/v1/stores/:storeId/missions', handleCreateMission) + +// 미션 +app.post('/api/v1/missions/:missionId/challenge', handleChallengeMission) + +// 4. 전역 에러 핸들러 (반드시 라우터 등록 이후에 위치) +app.use(errorMiddleware) + +// 5. 서버 시작 +app.listen(port, () => { + console.log(`[server]: Server is running at http://localhost:${port}`) +}) diff --git "a/\353\217\204\354\226\217/week5/src/middleware/error.middleware.ts" "b/\353\217\204\354\226\217/week5/src/middleware/error.middleware.ts" new file mode 100644 index 0000000..ec8849d --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/middleware/error.middleware.ts" @@ -0,0 +1,21 @@ +import { Request, Response, NextFunction } from 'express' + +interface AppError extends Error { + status?: number +} + +export const errorMiddleware = ( + err: AppError, + _req: Request, + res: Response, + _next: NextFunction, +): void => { + console.error(`[Error] ${err.message}`) + + const status = err.status ?? 500 + res.status(status).json({ + success: false, + code: `E${status}`, + message: err.message ?? '서버 오류가 발생했습니다.', + }) +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/members/controllers/member.controller.ts" "b/\353\217\204\354\226\217/week5/src/modules/members/controllers/member.controller.ts" new file mode 100644 index 0000000..3494ab7 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/members/controllers/member.controller.ts" @@ -0,0 +1,18 @@ +import { Request, Response, NextFunction } from 'express' +import { StatusCodes } from 'http-status-codes' +import { MemberSignUpRequest } from '../dtos/member.dto.js' +import { signUp } from '../services/member.service.js' + +// POST /api/v1/members/signup +export const handleSignUp = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const result = await signUp(req.body as MemberSignUpRequest) + res.status(StatusCodes.CREATED).json({ success: true, data: result }) + } catch (err) { + next(err) + } +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/members/dtos/member.dto.ts" "b/\353\217\204\354\226\217/week5/src/modules/members/dtos/member.dto.ts" new file mode 100644 index 0000000..b0cbf3f --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/members/dtos/member.dto.ts" @@ -0,0 +1,45 @@ +// 회원가입 요청 인터페이스 +export interface MemberSignUpRequest { + name: string + nickname: string + email?: string + password?: string + phoneNum?: string + birth?: string // "YYYY-MM-DD" + gender?: string // "MALE" | "FEMALE" | "OTHER" + address?: string + specAddress?: string +} + +// req.body → 내부 데이터로 변환 +export const bodyToMember = (body: MemberSignUpRequest) => { + return { + name: body.name, + nickname: body.nickname, + email: body.email ?? null, + phoneNum: body.phoneNum ?? null, + birth: body.birth ? new Date(body.birth) : null, + gender: body.gender ?? null, + address: body.address ?? null, + specAddress: body.specAddress ?? null, + } +} + +// DB 결과 → 응답 형태로 변환 +export const responseFromMember = (member: { + id: number + name: string + nickname: string + email: string | null + phone_num: string | null + status: string +}) => { + return { + memberId: member.id, + name: member.name, + nickname: member.nickname, + email: member.email, + phoneNum: member.phone_num, + status: member.status, + } +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/members/repositories/member.repository.ts" "b/\353\217\204\354\226\217/week5/src/modules/members/repositories/member.repository.ts" new file mode 100644 index 0000000..a6c73b1 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/members/repositories/member.repository.ts" @@ -0,0 +1,72 @@ +import { ResultSetHeader, RowDataPacket } from 'mysql2' +import { pool } from '../../../db.config.js' + +// 이메일 중복 확인 +export const findMemberByEmail = async (email: string): Promise => { + const conn = await pool.getConnection() + try { + const [rows] = await conn.query( + 'SELECT id FROM member WHERE email = ?', + [email], + ) + return rows[0] ?? null + } catch (err) { + throw new Error(`DB 오류: ${err}`) + } finally { + conn.release() + } +} + +// 회원 추가 +export const addMember = async (data: { + name: string + nickname: string + email: string | null + hashedPassword: string | null + phoneNum: string | null + birth: Date | null + gender: string | null + address: string | null + specAddress: string | null +}): Promise => { + const conn = await pool.getConnection() + try { + const [result] = await conn.query( + `INSERT INTO member + (name, nickname, email, password, phone_num, birth, gender, address, spec_address) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + data.name, + data.nickname, + data.email, + data.hashedPassword, + data.phoneNum, + data.birth, + data.gender, + data.address, + data.specAddress, + ], + ) + return result.insertId + } catch (err) { + throw new Error(`DB 오류: ${err}`) + } finally { + conn.release() + } +} + +// 회원 조회 +export const getMemberById = async (memberId: number): Promise => { + const conn = await pool.getConnection() + try { + const [rows] = await conn.query( + 'SELECT * FROM member WHERE id = ?', + [memberId], + ) + return rows[0] ?? null + } catch (err) { + throw new Error(`DB 오류: ${err}`) + } finally { + conn.release() + } +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/members/services/member.service.ts" "b/\353\217\204\354\226\217/week5/src/modules/members/services/member.service.ts" new file mode 100644 index 0000000..ed1b3ad --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/members/services/member.service.ts" @@ -0,0 +1,50 @@ +import bcrypt from 'bcryptjs' +import { MemberSignUpRequest, bodyToMember, responseFromMember } from '../dtos/member.dto.js' +import { + findMemberByEmail, + addMember, + getMemberById, +} from '../repositories/member.repository.js' + +const makeError = (message: string, status: number): Error & { status: number } => { + const err = new Error(message) as Error & { status: number } + err.status = status + return err +} + +export const signUp = async (data: MemberSignUpRequest) => { + // 필수값 검증 + if (!data.name || !data.nickname) { + throw makeError('name과 nickname은 필수 입력값입니다.', 400) + } + + // 이메일 중복 검증 + if (data.email) { + const existing = await findMemberByEmail(data.email) + if (existing) { + throw makeError('이미 존재하는 이메일입니다.', 409) + } + } + + // 비밀번호 해싱 (password가 있는 경우에만) + const hashedPassword = data.password + ? await bcrypt.hash(data.password, 10) + : null + + const memberData = bodyToMember(data) + const memberId = await addMember({ ...memberData, hashedPassword }) + + const member = await getMemberById(memberId) + if (!member) { + throw makeError('회원 생성 후 조회에 실패했습니다.', 500) + } + + return responseFromMember(member as { + id: number + name: string + nickname: string + email: string | null + phone_num: string | null + status: string + }) +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/missions/controllers/mission.controller.ts" "b/\353\217\204\354\226\217/week5/src/modules/missions/controllers/mission.controller.ts" new file mode 100644 index 0000000..aa93baa --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/missions/controllers/mission.controller.ts" @@ -0,0 +1,34 @@ +import { Request, Response, NextFunction } from 'express' +import { StatusCodes } from 'http-status-codes' +import { MissionCreateRequest, MissionChallengeRequest } from '../dtos/mission.dto.js' +import { createMission, challengeMission } from '../services/mission.service.js' + +// POST /api/v1/stores/:storeId/missions +export const handleCreateMission = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const storeId = parseInt(req.params['storeId'] ?? '0', 10) + const result = await createMission(storeId, req.body as MissionCreateRequest) + res.status(StatusCodes.CREATED).json({ success: true, data: result }) + } catch (err) { + next(err) + } +} + +// POST /api/v1/missions/:missionId/challenge +export const handleChallengeMission = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const missionId = parseInt(req.params['missionId'] ?? '0', 10) + const result = await challengeMission(missionId, req.body as MissionChallengeRequest) + res.status(StatusCodes.CREATED).json({ success: true, data: result }) + } catch (err) { + next(err) + } +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/missions/dtos/mission.dto.ts" "b/\353\217\204\354\226\217/week5/src/modules/missions/dtos/mission.dto.ts" new file mode 100644 index 0000000..284fa98 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/missions/dtos/mission.dto.ts" @@ -0,0 +1,56 @@ +// 미션 추가 요청 인터페이스 +export interface MissionCreateRequest { + title: string + reward: number + spec?: string + deadLine?: string // "YYYY-MM-DD" +} + +// 미션 도전 요청 인터페이스 +export interface MissionChallengeRequest { + memberId: number +} + +// req.body → 내부 데이터로 변환 (미션 추가) +export const bodyToMission = (body: MissionCreateRequest) => { + return { + title: body.title, + reward: body.reward, + spec: body.spec ?? null, + deadLine: body.deadLine ? new Date(body.deadLine) : null, + } +} + +// DB 결과 → 응답 형태로 변환 (미션) +export const responseFromMission = (mission: { + id: number + store_id: number + title: string + reward: number + spec: string | null + dead_line: Date | null +}) => { + return { + missionId: mission.id, + storeId: mission.store_id, + title: mission.title, + reward: mission.reward, + spec: mission.spec, + deadLine: mission.dead_line, + } +} + +// DB 결과 → 응답 형태로 변환 (미션 도전) +export const responseFromMemberMission = (mm: { + id: number + member_id: number + mission_id: number + status: string +}) => { + return { + memberMissionId: mm.id, + memberId: mm.member_id, + missionId: mm.mission_id, + status: mm.status, + } +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/missions/repositories/mission.repository.ts" "b/\353\217\204\354\226\217/week5/src/modules/missions/repositories/mission.repository.ts" new file mode 100644 index 0000000..470cb5a --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/missions/repositories/mission.repository.ts" @@ -0,0 +1,99 @@ +import { ResultSetHeader, RowDataPacket } from 'mysql2' +import { pool } from '../../../db.config.js' + +// 미션 추가 +export const addMission = async (data: { + storeId: number + title: string + reward: number + spec: string | null + deadLine: Date | null +}): Promise => { + const conn = await pool.getConnection() + try { + const [result] = await conn.query( + `INSERT INTO mission (store_id, title, reward, spec, dead_line) + VALUES (?, ?, ?, ?, ?)`, + [data.storeId, data.title, data.reward, data.spec, data.deadLine], + ) + return result.insertId + } catch (err) { + throw new Error(`DB 오류: ${err}`) + } finally { + conn.release() + } +} + +// 미션 조회 +export const getMissionById = async (missionId: number): Promise => { + const conn = await pool.getConnection() + try { + const [rows] = await conn.query( + 'SELECT * FROM mission WHERE id = ?', + [missionId], + ) + return rows[0] ?? null + } catch (err) { + throw new Error(`DB 오류: ${err}`) + } finally { + conn.release() + } +} + +// 이미 도전 중인 미션인지 확인 +export const findMemberMission = async ( + memberId: number, + missionId: number, +): Promise => { + const conn = await pool.getConnection() + try { + const [rows] = await conn.query( + `SELECT * FROM member_mission + WHERE member_id = ? AND mission_id = ?`, + [memberId, missionId], + ) + return rows[0] ?? null + } catch (err) { + throw new Error(`DB 오류: ${err}`) + } finally { + conn.release() + } +} + +// 미션 도전 추가 +export const addMemberMission = async ( + memberId: number, + missionId: number, +): Promise => { + const conn = await pool.getConnection() + try { + const [result] = await conn.query( + `INSERT INTO member_mission (member_id, mission_id, status) + VALUES (?, ?, 'CHALLENGING')`, + [memberId, missionId], + ) + return result.insertId + } catch (err) { + throw new Error(`DB 오류: ${err}`) + } finally { + conn.release() + } +} + +// 방금 추가한 미션 도전 기록 조회 +export const getMemberMissionById = async ( + memberMissionId: number, +): Promise => { + const conn = await pool.getConnection() + try { + const [rows] = await conn.query( + 'SELECT * FROM member_mission WHERE id = ?', + [memberMissionId], + ) + return rows[0] ?? null + } catch (err) { + throw new Error(`DB 오류: ${err}`) + } finally { + conn.release() + } +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/missions/services/mission.service.ts" "b/\353\217\204\354\226\217/week5/src/modules/missions/services/mission.service.ts" new file mode 100644 index 0000000..d8be396 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/missions/services/mission.service.ts" @@ -0,0 +1,79 @@ +import { + MissionCreateRequest, + MissionChallengeRequest, + bodyToMission, + responseFromMission, + responseFromMemberMission, +} from '../dtos/mission.dto.js' +import { + addMission, + getMissionById, + findMemberMission, + addMemberMission, + getMemberMissionById, +} from '../repositories/mission.repository.js' +import { findStoreById } from '../../stores/repositories/store.repository.js' + +const makeError = (message: string, status: number): Error & { status: number } => { + const err = new Error(message) as Error & { status: number } + err.status = status + return err +} + +// 미션 추가 +export const createMission = async (storeId: number, data: MissionCreateRequest) => { + // 가게 존재 여부 검증 + const store = await findStoreById(storeId) + if (!store) { + throw makeError('존재하지 않는 가게입니다.', 404) + } + + const missionData = bodyToMission(data) + const missionId = await addMission({ ...missionData, storeId }) + + const mission = await getMissionById(missionId) + if (!mission) { + throw makeError('미션 생성 후 조회에 실패했습니다.', 500) + } + + return responseFromMission(mission as { + id: number + store_id: number + title: string + reward: number + spec: string | null + dead_line: Date | null + }) +} + +// 미션 도전하기 +export const challengeMission = async ( + missionId: number, + data: MissionChallengeRequest, +) => { + // 미션 존재 여부 검증 + const mission = await getMissionById(missionId) + if (!mission) { + throw makeError('존재하지 않는 미션입니다.', 404) + } + + // 이미 도전 중인지 검증 + const existing = await findMemberMission(data.memberId, missionId) + if (existing) { + throw makeError('이미 도전 중인 미션입니다.', 409) + } + + const memberMissionId = await addMemberMission(data.memberId, missionId) + + const memberMission = await getMemberMissionById(memberMissionId) + if (!memberMission) { + throw makeError('미션 도전 후 조회에 실패했습니다.', 500) + } + + return responseFromMemberMission(memberMission as { + id: number + member_id: number + mission_id: number + status: string + }) +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/reviews/controllers/review.controller.ts" "b/\353\217\204\354\226\217/week5/src/modules/reviews/controllers/review.controller.ts" new file mode 100644 index 0000000..8828e3c --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/reviews/controllers/review.controller.ts" @@ -0,0 +1,19 @@ +import { Request, Response, NextFunction } from 'express' +import { StatusCodes } from 'http-status-codes' +import { ReviewCreateRequest } from '../dtos/review.dto.js' +import { createReview } from '../services/review.service.js' + +// POST /api/v1/stores/:storeId/reviews +export const handleCreateReview = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const storeId = parseInt(req.params['storeId'] ?? '0', 10) + const result = await createReview(storeId, req.body as ReviewCreateRequest) + res.status(StatusCodes.CREATED).json({ success: true, data: result }) + } catch (err) { + next(err) + } +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/reviews/dtos/review.dto.ts" "b/\353\217\204\354\226\217/week5/src/modules/reviews/dtos/review.dto.ts" new file mode 100644 index 0000000..0009736 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/reviews/dtos/review.dto.ts" @@ -0,0 +1,34 @@ +// 리뷰 추가 요청 인터페이스 +export interface ReviewCreateRequest { + memberId: number + content: string + score: number +} + +// req.body → 내부 데이터로 변환 +export const bodyToReview = (body: ReviewCreateRequest) => { + return { + memberId: body.memberId, + content: body.content, + score: body.score, + } +} + +// DB 결과 → 응답 형태로 변환 +export const responseFromReview = (review: { + id: number + member_id: number + store_id: number + content: string + score: number + created_at: Date +}) => { + return { + reviewId: review.id, + memberId: review.member_id, + storeId: review.store_id, + content: review.content, + score: review.score, + createdAt: review.created_at, + } +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/reviews/repositories/review.repository.ts" "b/\353\217\204\354\226\217/week5/src/modules/reviews/repositories/review.repository.ts" new file mode 100644 index 0000000..618fb02 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/reviews/repositories/review.repository.ts" @@ -0,0 +1,40 @@ +import { ResultSetHeader, RowDataPacket } from 'mysql2' +import { pool } from '../../../db.config.js' + +// 리뷰 추가 +export const addReview = async (data: { + memberId: number + storeId: number + content: string + score: number +}): Promise => { + const conn = await pool.getConnection() + try { + const [result] = await conn.query( + `INSERT INTO review (member_id, store_id, content, score) + VALUES (?, ?, ?, ?)`, + [data.memberId, data.storeId, data.content, data.score], + ) + return result.insertId + } catch (err) { + throw new Error(`DB 오류: ${err}`) + } finally { + conn.release() + } +} + +// 방금 추가한 리뷰 조회 +export const getReviewById = async (reviewId: number): Promise => { + const conn = await pool.getConnection() + try { + const [rows] = await conn.query( + 'SELECT * FROM review WHERE id = ?', + [reviewId], + ) + return rows[0] ?? null + } catch (err) { + throw new Error(`DB 오류: ${err}`) + } finally { + conn.release() + } +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/reviews/services/review.service.ts" "b/\353\217\204\354\226\217/week5/src/modules/reviews/services/review.service.ts" new file mode 100644 index 0000000..45a67a5 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/reviews/services/review.service.ts" @@ -0,0 +1,39 @@ +import { ReviewCreateRequest, bodyToReview, responseFromReview } from '../dtos/review.dto.js' +import { addReview, getReviewById } from '../repositories/review.repository.js' +import { findStoreById } from '../../stores/repositories/store.repository.js' + +const makeError = (message: string, status: number): Error & { status: number } => { + const err = new Error(message) as Error & { status: number } + err.status = status + return err +} + +export const createReview = async (storeId: number, data: ReviewCreateRequest) => { + // 가게 존재 여부 검증 + const store = await findStoreById(storeId) + if (!store) { + throw makeError('존재하지 않는 가게입니다.', 404) + } + + // 별점 유효성 검사 + if (data.score < 1 || data.score > 5) { + throw makeError('별점은 1~5 사이여야 합니다.', 400) + } + + const reviewData = bodyToReview(data) + const reviewId = await addReview({ ...reviewData, storeId }) + + const review = await getReviewById(reviewId) + if (!review) { + throw makeError('리뷰 생성 후 조회에 실패했습니다.', 500) + } + + return responseFromReview(review as { + id: number + member_id: number + store_id: number + content: string + score: number + created_at: Date + }) +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/stores/controllers/store.controller.ts" "b/\353\217\204\354\226\217/week5/src/modules/stores/controllers/store.controller.ts" new file mode 100644 index 0000000..b767414 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/stores/controllers/store.controller.ts" @@ -0,0 +1,18 @@ +import { Request, Response, NextFunction } from 'express' +import { StatusCodes } from 'http-status-codes' +import { StoreCreateRequest } from '../dtos/store.dto.js' +import { createStore } from '../services/store.service.js' + +// POST /api/v1/stores +export const handleCreateStore = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const result = await createStore(req.body as StoreCreateRequest) + res.status(StatusCodes.CREATED).json({ success: true, data: result }) + } catch (err) { + next(err) + } +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/stores/dtos/store.dto.ts" "b/\353\217\204\354\226\217/week5/src/modules/stores/dtos/store.dto.ts" new file mode 100644 index 0000000..fe0d839 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/stores/dtos/store.dto.ts" @@ -0,0 +1,38 @@ +// 가게 추가 요청 인터페이스 +export interface StoreCreateRequest { + regionId: number + foodCategoryId: number + name: string + description?: string + address: string + lat?: number + lng?: number +} + +// req.body → 내부 데이터로 변환 +export const bodyToStore = (body: StoreCreateRequest) => { + return { + regionId: body.regionId, + foodCategoryId: body.foodCategoryId, + name: body.name, + description: body.description ?? null, + address: body.address, + lat: body.lat ?? null, + lng: body.lng ?? null, + } +} + +// DB 조회 결과 → 응답 형태로 변환 +export const responseFromStore = (store: { + id: number + name: string + address: string + region_id: number +}) => { + return { + storeId: store.id, + name: store.name, + address: store.address, + regionId: store.region_id, + } +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/stores/repositories/store.repository.ts" "b/\353\217\204\354\226\217/week5/src/modules/stores/repositories/store.repository.ts" new file mode 100644 index 0000000..5869ef5 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/stores/repositories/store.repository.ts" @@ -0,0 +1,67 @@ +import { ResultSetHeader, RowDataPacket } from 'mysql2' +import { pool } from '../../../db.config.js' + +// 가게 존재 여부 확인 +export const findStoreById = async (storeId: number): Promise => { + const conn = await pool.getConnection() + try { + const [rows] = await conn.query( + 'SELECT * FROM store WHERE id = ?', + [storeId], + ) + return rows[0] ?? null + } catch (err) { + throw new Error(`DB 오류: ${err}`) + } finally { + conn.release() + } +} + +// 가게 추가 +export const addStore = async (data: { + regionId: number + foodCategoryId: number + name: string + description: string | null + address: string + lat: number | null + lng: number | null +}): Promise => { + const conn = await pool.getConnection() + try { + const [result] = await conn.query( + `INSERT INTO store (region_id, food_category_id, name, description, address, lat, lng) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [ + data.regionId, + data.foodCategoryId, + data.name, + data.description, + data.address, + data.lat, + data.lng, + ], + ) + return result.insertId + } catch (err) { + throw new Error(`DB 오류: ${err}`) + } finally { + conn.release() + } +} + +// 방금 추가한 가게 정보 조회 +export const getStoreById = async (storeId: number): Promise => { + const conn = await pool.getConnection() + try { + const [rows] = await conn.query( + 'SELECT * FROM store WHERE id = ?', + [storeId], + ) + return rows[0] ?? null + } catch (err) { + throw new Error(`DB 오류: ${err}`) + } finally { + conn.release() + } +} diff --git "a/\353\217\204\354\226\217/week5/src/modules/stores/services/store.service.ts" "b/\353\217\204\354\226\217/week5/src/modules/stores/services/store.service.ts" new file mode 100644 index 0000000..3dcf178 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/stores/services/store.service.ts" @@ -0,0 +1,25 @@ +import { StoreCreateRequest, bodyToStore, responseFromStore } from '../dtos/store.dto.js' +import { addStore, getStoreById } from '../repositories/store.repository.js' + +const makeError = (message: string, status: number): Error & { status: number } => { + const err = new Error(message) as Error & { status: number } + err.status = status + return err +} + +export const createStore = async (data: StoreCreateRequest) => { + const storeData = bodyToStore(data) + const storeId = await addStore(storeData) + + const store = await getStoreById(storeId) + if (!store) { + throw makeError('가게 생성 후 조회에 실패했습니다.', 500) + } + + return responseFromStore(store as { + id: number + name: string + address: string + region_id: number + }) +} diff --git "a/\353\217\204\354\226\217/week5/todolist.json" "b/\353\217\204\354\226\217/week5/todolist.json" new file mode 100644 index 0000000..830b572 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/todolist.json" @@ -0,0 +1,353 @@ +{ + "chapter": "Chapter 5. API 및 프로젝트 설정 기초", + "branch": "feature/chapter-05", + "week4_reference": "../week4", + "keywords": [ + { + "term": "환경 변수 (Environment Variables)", + "summary": "DB 비밀번호·API Key 등 민감한 값을 코드에 하드코딩하지 않고 .env 파일로 관리. dotenv 라이브러리로 로드하며 .gitignore에 반드시 추가해야 함.", + "example": "process.env.DB_PASSWORD" + }, + { + "term": "CORS (Cross-Origin Resource Sharing)", + "summary": "브라우저가 다른 Origin(도메인·포트)의 서버에 요청할 때 발생하는 보안 정책. Express에서는 cors 미들웨어로 허용 처리.", + "example": "app.use(cors())" + }, + { + "term": "DB Connection Pool", + "summary": "매 요청마다 DB 커넥션을 새로 생성·해제하지 않고, 미리 만들어 둔 커넥션 풀에서 빌려 쓰는 방식. mysql2의 createPool()로 구현. finally 블록에서 conn.release()로 반환해야 함.", + "example": "const pool = mysql.createPool({ connectionLimit: 10 })" + }, + { + "term": "비동기 (async / await)", + "summary": "DB 쿼리·외부 API 호출처럼 응답 대기가 필요한 작업을 Promise 기반으로 처리. async 함수 안에서 await로 결과를 기다리며, 동기 코드처럼 읽기 쉽게 작성 가능.", + "example": "const [rows] = await pool.query('SELECT ...')" + }, + { + "term": "try / catch / finally", + "summary": "비동기 작업에서 발생할 수 있는 에러를 구조적으로 처리. try: 정상 로직, catch: 에러 처리, finally: 커넥션 반환(conn.release()) 등 항상 실행되어야 하는 정리 코드.", + "example": "try { ... } catch(err) { throw new Error(...) } finally { conn.release() }" + }, + { + "term": "Interface (인터페이스)", + "summary": "TypeScript에서 객체의 형태(속성·타입)를 정의하는 설계도. DTO·Repository 반환 타입 등에 활용. ?를 붙이면 선택적 프로퍼티.", + "example": "export interface StoreCreateRequest { regionId: number; name: string; address: string; }" + }, + { + "term": "Type Assertion (as 키워드)", + "summary": "TypeScript 컴파일러에게 '이 값은 이 타입이야'라고 강제로 알려주는 문법. req.body는 기본적으로 any이므로, as 로 정의한 인터페이스 타입으로 변환해 사용.", + "example": "req.body as StoreCreateRequest" + } + ], + "project_structure": { + "root": "week5/", + "note": "week4의 in-memory DB → MySQL 실제 DB 연결로 전환. 모듈형 모노리스 구조 채택.", + "files": [ + "src/index.ts - Express 앱 진입점, 미들웨어·라우터 등록", + "src/db.config.ts - MySQL Connection Pool 설정", + "src/modules/stores/controllers/store.controller.ts", + "src/modules/stores/services/store.service.ts", + "src/modules/stores/repositories/store.repository.ts", + "src/modules/stores/dtos/store.dto.ts", + "src/modules/reviews/controllers/review.controller.ts", + "src/modules/reviews/services/review.service.ts", + "src/modules/reviews/repositories/review.repository.ts", + "src/modules/reviews/dtos/review.dto.ts", + "src/modules/missions/controllers/mission.controller.ts", + "src/modules/missions/services/mission.service.ts", + "src/modules/missions/repositories/mission.repository.ts", + "src/modules/missions/dtos/mission.dto.ts", + ".env - DB 접속 정보·PORT (gitignore 필수)", + ".gitignore", + "package.json", + "tsconfig.json", + "schema.sql - week4 schema.sql 재사용 (테이블 이미 정의됨)" + ] + }, + "todos": [ + { + "id": 1, + "phase": "사전 준비", + "title": "GitHub 이슈 생성 및 브랜치 분기", + "status": "todo", + "details": [ + "GitHub 저장소 Issues 탭에서 라벨 정리: bug, docs, feature, refactor", + "이슈 제목: '[feat] Chapter 5 - API 구현 (가게 추가 / 리뷰 / 미션)'", + "Assignee: 본인, Label: feature 로 이슈 생성", + "이슈에서 'Create a branch' 클릭 → feature/chapter-05 브랜치 생성", + "로컬에서: git fetch origin && git checkout feature/chapter-05" + ] + }, + { + "id": 2, + "phase": "사전 준비", + "title": "Postman 설치 및 기본 사용법 확인", + "status": "todo", + "details": [ + "Postman 설치 (https://www.postman.com/downloads/)", + "Params / Authorization / Headers / Body 탭 역할 이해", + "Body > raw > JSON 선택 방법 숙지", + "나중에 API 테스트 시 스크린샷 저장할 준비" + ] + }, + { + "id": 3, + "phase": "프로젝트 세팅", + "title": "week5 폴더 초기화 및 의존성 설치", + "status": "todo", + "details": [ + "cd week5 && npm init -y", + "npm install express cors dotenv http-status-codes mysql2", + "npm install -D typescript @types/node @types/express @types/cors @types/dotenv nodemon tsx", + "npx tsc --init 후 tsconfig.json 수정 (rootDir: ./src, outDir: ./dist, module: NodeNext, strict: true 등)", + "week4/tsconfig.json을 참고해 module/moduleResolution 설정 일치시키기", + "package.json scripts 추가: start / dev (nodemon --exec tsx src/index.ts)" + ] + }, + { + "id": 4, + "phase": "프로젝트 세팅", + "title": ".env 파일 및 .gitignore 작성", + "status": "todo", + "details": [ + ".gitignore에 node_modules/ / .env / .env.* 추가", + ".env에 PORT=3000, DB_HOST=localhost, DB_PORT=3306, DB_USER=root, DB_PASSWORD=비밀번호, DB_NAME=umc_mission 작성", + "DB_NAME은 week4/schema.sql 기준 umc_mission 사용 (이미 테이블 있음)" + ] + }, + { + "id": 5, + "phase": "프로젝트 세팅", + "title": "src/db.config.ts 작성 - MySQL Connection Pool", + "status": "todo", + "details": [ + "mysql2/promise의 createPool 사용", + "환경 변수(DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME)로 설정", + "connectionLimit: 10, waitForConnections: true", + "week4 schema.sql의 DB(umc_mission)와 연결" + ], + "reference_week4": "week4/src/db/index.ts 구조 참고 (단, in-memory→MySQL로 변경)" + }, + { + "id": 6, + "phase": "프로젝트 세팅", + "title": "src/index.ts 작성 - Express 앱 진입점", + "status": "todo", + "details": [ + "dotenv.config() 최상단 호출", + "cors(), express.json(), express.urlencoded() 미들웨어 등록", + "각 모듈 라우터 등록: /api/v1/stores, /api/v1/reviews, /api/v1/missions", + "전역 에러 핸들러 미들웨어 등록 (시니어 미션: JSON 에러 응답)", + "app.listen(process.env.PORT || 3000)" + ], + "reference_week4": "week4/src/index.ts 구조 그대로 활용, cors·dotenv 추가" + }, + { + "id": 7, + "phase": "DB 준비", + "title": "MySQL에 week5용 테이블 확인 및 더미데이터 삽입", + "status": "todo", + "details": [ + "week4/schema.sql로 테이블 생성 (이미 되어있으면 생략)", + "food_category 더미데이터 삽입: INSERT INTO food_category(name) VALUES ('한식'),('중식'),('일식'),('양식'),('치킨'),('분식'),('고기/구이'),('도시락'),('아식'),('패스트푸드'),('다저트'),('아시안푸드')", + "region 더미데이터 삽입: INSERT INTO region(name) VALUES ('서울'),('경기'),('부산')...", + "member 더미데이터 1건 삽입 (API 테스트용 첫 번째 사용자)" + ] + }, + { + "id": 8, + "phase": "API 구현 - 1-1", + "title": "[필수] 특정 지역에 가게 추가하기 API", + "status": "todo", + "priority": "optional", + "endpoint": "POST /api/v1/stores", + "request_body": { + "regionId": "number", + "foodCategoryId": "number", + "name": "string", + "description": "string (선택)", + "address": "string", + "lat": "number (선택)", + "lng": "number (선택)" + }, + "details": [ + "store.dto.ts - StoreCreateRequest 인터페이스 정의", + "store.dto.ts - bodyToStore() 변환 함수 작성", + "store.repository.ts - addStore(): INSERT INTO store(...) VALUES(?)", + "store.service.ts - createStore(data): addStore 호출 후 결과 반환", + "store.controller.ts - handleCreateStore: bodyToStore(req.body as ...) → service 호출 → 201 응답", + "index.ts에 라우터 등록: app.post('/api/v1/stores', handleCreateStore)" + ], + "reference_week4": "week4/src/controllers/store.controller.ts 패턴 참고" + }, + { + "id": 9, + "phase": "API 구현 - 1-2", + "title": "[필수★] 가게에 리뷰 추가하기 API", + "status": "todo", + "priority": "required", + "endpoint": "POST /api/v1/stores/:storeId/reviews", + "request_body": { + "memberId": "number (특정 사용자로 가정, DB 첫 번째 사용자)", + "content": "string", + "score": "number (1.0~5.0)" + }, + "details": [ + "review.dto.ts - ReviewCreateRequest 인터페이스 (content, score, memberId)", + "review.dto.ts - bodyToReview() 변환 함수", + "review.repository.ts - addReview(): INSERT INTO review(...)", + "store.repository.ts (또는 review.repository.ts) - findStoreById(): SELECT * FROM store WHERE id=?", + "review.service.ts - createReview(storeId, data): 가게 존재 검증 → addReview 호출", + " 검증 실패 시 throw new Error('존재하지 않는 가게입니다.')", + "review.controller.ts - handleCreateReview: storeId = parseInt(req.params.storeId) → service 호출", + "index.ts에 라우터 등록" + ], + "validation": "리뷰를 추가하려는 가게가 존재하는지 검증 필요", + "reference_week4": "week4/src/services/store.service.ts의 createReview 패턴, week4 스키마의 review 테이블" + }, + { + "id": 10, + "phase": "API 구현 - 1-3", + "title": "가게에 미션 추가하기 API", + "status": "todo", + "priority": "optional", + "endpoint": "POST /api/v1/stores/:storeId/missions", + "request_body": { + "title": "string", + "reward": "number", + "spec": "string (선택)", + "deadLine": "string (YYYY-MM-DD, 선택)" + }, + "details": [ + "mission.dto.ts - MissionCreateRequest 인터페이스", + "mission.dto.ts - bodyToMission() 변환 함수 (deadLine → Date 변환)", + "mission.repository.ts - addMission(): INSERT INTO mission(store_id, title, reward, spec, dead_line)", + "mission.service.ts - createMission(storeId, data): 가게 존재 검증 → addMission", + "mission.controller.ts - handleCreateMission", + "index.ts에 라우터 등록" + ], + "reference_week4": "week4/src/repositories/mission.repository.ts 패턴, week4 schema.sql의 mission 테이블" + }, + { + "id": 11, + "phase": "API 구현 - 1-4", + "title": "[필수★] 가게의 미션을 도전 중인 미션에 추가(미션 도전하기) API", + "status": "todo", + "priority": "required", + "endpoint": "POST /api/v1/missions/:missionId/challenge", + "request_body": { + "memberId": "number (특정 사용자로 가정, DB 첫 번째 사용자)" + }, + "details": [ + "mission.dto.ts - MissionChallengeRequest 인터페이스 ({ memberId: number })", + "mission.repository.ts - findMemberMission(): SELECT * FROM member_mission WHERE member_id=? AND mission_id=?", + "mission.repository.ts - addMemberMission(): INSERT INTO member_mission(member_id, mission_id, status) VALUES(?,?,'CHALLENGING')", + "mission.service.ts - challengeMission(missionId, data): 중복 도전 검증 → addMemberMission", + " 검증 실패 시 throw new Error('이미 도전 중인 미션입니다.')", + "mission.controller.ts - handleChallengeMission: missionId = parseInt(req.params.missionId) → service", + "index.ts에 라우터 등록" + ], + "validation": "도전하려는 미션이 이미 도전 중이지는 않은지 검증 필요", + "reference_week4": "week4/src/repositories/mission.repository.ts, week4 schema.sql의 member_mission 테이블" + }, + { + "id": 12, + "phase": "추가 미션", + "title": "[공통 미션 2번] Controller → Service → Repository → DB 요청 흐름 정리", + "status": "todo", + "details": [ + "예: POST /api/v1/stores/:storeId/reviews 요청 흐름을 순서대로 작성", + "1. 사용자가 POST /api/v1/stores/1/reviews 요청 전송", + "2. index.ts의 라우터가 handleCreateReview 컨트롤러 호출", + "3. Controller: req.body를 ReviewCreateRequest 타입으로 변환(bodyToReview), storeId 파싱", + "4. Service: 가게 존재 여부 검증(findStoreById) → 없으면 Error throw", + "5. Repository: INSERT INTO review 쿼리 실행 → insertId 반환", + "6. Service: 결과를 DTO로 변환해 Controller에 반환", + "7. Controller: 201 JSON 응답 전송", + "워크북의 요약 정리 섹션에 이 내용 포함" + ] + }, + { + "id": 13, + "phase": "추가 미션", + "title": "[공통 미션 3번] 회원가입 API에 bcrypt 비밀번호 해싱 추가", + "status": "todo", + "details": [ + "npm install bcryptjs && npm install -D @types/bcryptjs (week4에 이미 설치됨)", + "member.dto.ts - MemberSignUpRequest 인터페이스에 password 필드 추가", + "member.repository.ts - addMember(): INSERT INTO member(..., password) VALUES(...)", + "member.service.ts - signUp(): const hashedPw = await bcrypt.hash(data.password, 10) → addMember에 전달", + "member.controller.ts - handleSignUp 작성", + "POST /api/v1/members/signup 라우터 등록" + ], + "reference_week4": "week4에서 bcryptjs 이미 사용 중 - week4/package.json 참고" + }, + { + "id": 14, + "phase": "시니어 미션", + "title": "[시니어] 전역 에러 핸들러 - JSON 형태 에러 응답", + "status": "todo", + "details": [ + "src/middleware/error.middleware.ts 생성", + "ErrorRequestHandler 타입 사용: (err, req, res, next) => void", + "res.status(err.status || 500).json({ success: false, message: err.message || '서버 에러' })", + "index.ts 맨 마지막에 app.use(errorMiddleware) 등록", + "Controller에서 try-catch 제거하거나 next(err) 사용으로 변경", + "기존 HTML 에러 응답 → JSON 에러 응답으로 개선" + ], + "reference_week4": "week4/src/middleware/error.middleware.ts 그대로 활용 가능" + }, + { + "id": 15, + "phase": "테스트", + "title": "Postman / curl로 각 API 호출 및 스크린샷 저장", + "status": "todo", + "details": [ + "npm run dev 로 서버 실행", + "API 1-1: POST /api/v1/stores - 가게 추가 성공 스크린샷", + "API 1-2: POST /api/v1/stores/:storeId/reviews - 리뷰 추가 성공 스크린샷", + "API 1-2: 존재하지 않는 storeId로 요청 → 에러 응답 스크린샷", + "API 1-3: POST /api/v1/stores/:storeId/missions - 미션 추가 성공 스크린샷", + "API 1-4: POST /api/v1/missions/:missionId/challenge - 도전 성공 스크린샷", + "API 1-4: 동일 미션 재도전 → '이미 도전 중' 에러 스크린샷", + "DB에서 SELECT로 데이터 삽입 확인 스크린샷" + ] + }, + { + "id": 16, + "phase": "마무리", + "title": "feature/chapter-05 브랜치에 push 및 PR 생성", + "status": "todo", + "details": [ + "git add . && git commit -m 'feat: 5주차 미션 - API 구현 (가게/리뷰/미션)'", + "git push origin feature/chapter-05", + "GitHub에서 PR 생성 (main 브랜치에 merge하지 말 것!)", + "워크북의 미션 기록란에 GitHub 링크 제출", + "GitHub 이슈 Close" + ] + }, + { + "id": 17, + "phase": "마무리", + "title": "핵심 키워드 및 요약 정리 작성", + "status": "todo", + "details": [ + "이 파일 상단의 keywords 섹션을 참고해 워크북에 기입", + "요약 정리: Controller→Service→Repository→DB 흐름 설명", + "위클리 스크럼 질문 답변: DTO 없이 사용할 때의 문제점 / Service Layer 필요성" + ] + } + ], + "required_apis_summary": { + "must_implement": ["1-2 (가게에 리뷰 추가하기)", "1-4 (미션 도전하기)"], + "minimum_count": "필수 2개 포함 총 3개 이상", + "senior_mission": "4개 전부 + JSON 에러 응답 개선" + }, + "key_differences_from_week4": { + "database": "in-memory(week4) → MySQL Connection Pool(week5)", + "modules": "flat 구조(week4) → 모듈형 모노리스(week5: src/modules/{도메인}/)", + "env": ".env 파일 추가 (dotenv 사용)", + "cors": "cors 미들웨어 추가", + "error_response": "HTML 에러(기본) → JSON 에러(시니어 미션 개선)" + } +} diff --git "a/\353\217\204\354\226\217/week5/tsconfig.json" "b/\353\217\204\354\226\217/week5/tsconfig.json" new file mode 100644 index 0000000..1143499 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/tsconfig.json" @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ESNext"], + "strict": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} From 91fab916f5bb6767f0b80d9b979d84b1d1f67d19 Mon Sep 17 00:00:00 2001 From: higashiaka Date: Sat, 18 Apr 2026 11:37:28 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=ED=8C=8C=ED=8A=B8=EC=9E=A5=20?= =?UTF-8?q?=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "\353\217\204\354\226\217/week5/reset_db.sql" | 8 ++++---- .../week5/src/modules/missions/dtos/mission.dto.ts" | 1 + .../modules/missions/repositories/mission.repository.ts" | 5 +++-- .../src/modules/missions/services/mission.service.ts" | 7 ++++++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git "a/\353\217\204\354\226\217/week5/reset_db.sql" "b/\353\217\204\354\226\217/week5/reset_db.sql" index ff192db..454f20b 100644 --- "a/\353\217\204\354\226\217/week5/reset_db.sql" +++ "b/\353\217\204\354\226\217/week5/reset_db.sql" @@ -66,11 +66,11 @@ CREATE TABLE member ( phone_num VARCHAR(20) NULL, phone_verified BOOLEAN NOT NULL DEFAULT FALSE, birth DATE NULL, - gender VARCHAR(10) NULL, + gender ENUM('MALE', 'FEMALE', 'OTHER') NULL, address VARCHAR(200) NULL, spec_address VARCHAR(200) NULL, point INT NOT NULL DEFAULT 0, - status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', + status ENUM('ACTIVE', 'INACTIVE', 'BANNED') NOT NULL DEFAULT 'ACTIVE', inactive_date DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, @@ -108,7 +108,7 @@ CREATE TABLE store ( lat DECIMAL(10,7) NULL, lng DECIMAL(10,7) NULL, address VARCHAR(200) NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'OPEN', + status ENUM('OPEN', 'CLOSED', 'PENDING') NOT NULL DEFAULT 'OPEN', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), @@ -156,7 +156,7 @@ CREATE TABLE member_mission ( id BIGINT NOT NULL AUTO_INCREMENT, member_id BIGINT NOT NULL, mission_id BIGINT NOT NULL, - status VARCHAR(20) NOT NULL DEFAULT 'CHALLENGING', + status ENUM('CHALLENGING', 'COMPLETE') NOT NULL DEFAULT 'CHALLENGING', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), diff --git "a/\353\217\204\354\226\217/week5/src/modules/missions/dtos/mission.dto.ts" "b/\353\217\204\354\226\217/week5/src/modules/missions/dtos/mission.dto.ts" index 284fa98..8786670 100644 --- "a/\353\217\204\354\226\217/week5/src/modules/missions/dtos/mission.dto.ts" +++ "b/\353\217\204\354\226\217/week5/src/modules/missions/dtos/mission.dto.ts" @@ -9,6 +9,7 @@ export interface MissionCreateRequest { // 미션 도전 요청 인터페이스 export interface MissionChallengeRequest { memberId: number + status: 'CHALLENGING' | 'COMPLETE' } // req.body → 내부 데이터로 변환 (미션 추가) diff --git "a/\353\217\204\354\226\217/week5/src/modules/missions/repositories/mission.repository.ts" "b/\353\217\204\354\226\217/week5/src/modules/missions/repositories/mission.repository.ts" index 470cb5a..65a6e13 100644 --- "a/\353\217\204\354\226\217/week5/src/modules/missions/repositories/mission.repository.ts" +++ "b/\353\217\204\354\226\217/week5/src/modules/missions/repositories/mission.repository.ts" @@ -64,13 +64,14 @@ export const findMemberMission = async ( export const addMemberMission = async ( memberId: number, missionId: number, + status: string, ): Promise => { const conn = await pool.getConnection() try { const [result] = await conn.query( `INSERT INTO member_mission (member_id, mission_id, status) - VALUES (?, ?, 'CHALLENGING')`, - [memberId, missionId], + VALUES (?, ?, ?)`, + [memberId, missionId, status], ) return result.insertId } catch (err) { diff --git "a/\353\217\204\354\226\217/week5/src/modules/missions/services/mission.service.ts" "b/\353\217\204\354\226\217/week5/src/modules/missions/services/mission.service.ts" index d8be396..65869d4 100644 --- "a/\353\217\204\354\226\217/week5/src/modules/missions/services/mission.service.ts" +++ "b/\353\217\204\354\226\217/week5/src/modules/missions/services/mission.service.ts" @@ -57,13 +57,18 @@ export const challengeMission = async ( throw makeError('존재하지 않는 미션입니다.', 404) } + // 필수값 검증 + if (!data.status) { + throw makeError('status는 필수 입력값입니다. (CHALLENGING 또는 COMPLETE)', 400) + } + // 이미 도전 중인지 검증 const existing = await findMemberMission(data.memberId, missionId) if (existing) { throw makeError('이미 도전 중인 미션입니다.', 409) } - const memberMissionId = await addMemberMission(data.memberId, missionId) + const memberMissionId = await addMemberMission(data.memberId, missionId, data.status) const memberMission = await getMemberMissionById(memberMissionId) if (!memberMission) { From 29d2ea2920fb6d6f959607b0d392873b2f0ca968 Mon Sep 17 00:00:00 2001 From: higashiaka Date: Wed, 29 Apr 2026 11:04:31 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=EB=8F=84=EC=96=8F=206=EC=A3=BC?= =?UTF-8?q?=EC=B0=A8=20=EB=AF=B8=EC=85=98=20=EC=A0=9C=EC=B6=9C=20-=20Chapt?= =?UTF-8?q?er=206.=20ORM=20=EC=82=AC=EC=9A=A9=ED=97=A4=EB=B3=B4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "\353\217\204\354\226\217/week6/.gitignore" | 21 + .../week6/package-lock.json" | 3122 +++++++++++++++++ "\353\217\204\354\226\217/week6/package.json" | 23 + .../week6/prisma.config.ts" | 12 + .../migration.sql" | 39 + .../migration.sql" | 25 + .../migration.sql" | 33 + .../prisma/migrations/migration_lock.toml" | 3 + .../week6/prisma/schema.prisma" | 100 + "\353\217\204\354\226\217/week6/reset_db.sql" | 215 ++ .../week6/src/db.config.ts" | 17 + "\353\217\204\354\226\217/week6/src/index.ts" | 55 + .../week6/src/middleware/error.middleware.ts" | 21 + .../members/controllers/member.controller.ts" | 18 + .../src/modules/members/dtos/member.dto.ts" | 45 + .../repositories/member.repository.ts" | 26 + .../members/services/member.service.ts" | 42 + .../controllers/mission.controller.ts" | 67 + .../src/modules/missions/dtos/mission.dto.ts" | 57 + .../repositories/mission.repository.ts" | 72 + .../missions/services/mission.service.ts" | 116 + .../reviews/controllers/review.controller.ts" | 30 + .../src/modules/reviews/dtos/review.dto.ts" | 43 + .../repositories/review.repository.ts" | 30 + .../reviews/services/review.service.ts" | 44 + .../stores/controllers/store.controller.ts" | 29 + .../src/modules/stores/dtos/store.dto.ts" | 49 + .../stores/repositories/store.repository.ts" | 28 + .../modules/stores/services/store.service.ts" | 30 + .../week6/todolist.json" | 353 ++ .../week6/tsconfig.json" | 19 + 31 files changed, 4784 insertions(+) create mode 100644 "\353\217\204\354\226\217/week6/.gitignore" create mode 100644 "\353\217\204\354\226\217/week6/package-lock.json" create mode 100644 "\353\217\204\354\226\217/week6/package.json" create mode 100644 "\353\217\204\354\226\217/week6/prisma.config.ts" create mode 100644 "\353\217\204\354\226\217/week6/prisma/migrations/20260428071132_init_database/migration.sql" create mode 100644 "\353\217\204\354\226\217/week6/prisma/migrations/20260428071239_add_store_and_review_tables/migration.sql" create mode 100644 "\353\217\204\354\226\217/week6/prisma/migrations/20260428072732_npx_prisma_migrate_dev/migration.sql" create mode 100644 "\353\217\204\354\226\217/week6/prisma/migrations/migration_lock.toml" create mode 100644 "\353\217\204\354\226\217/week6/prisma/schema.prisma" create mode 100644 "\353\217\204\354\226\217/week6/reset_db.sql" create mode 100644 "\353\217\204\354\226\217/week6/src/db.config.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/index.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/middleware/error.middleware.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/members/controllers/member.controller.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/members/dtos/member.dto.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/members/repositories/member.repository.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/members/services/member.service.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/missions/controllers/mission.controller.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/missions/dtos/mission.dto.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/missions/repositories/mission.repository.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/missions/services/mission.service.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/reviews/controllers/review.controller.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/reviews/dtos/review.dto.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/reviews/repositories/review.repository.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/reviews/services/review.service.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/stores/controllers/store.controller.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/stores/dtos/store.dto.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/stores/repositories/store.repository.ts" create mode 100644 "\353\217\204\354\226\217/week6/src/modules/stores/services/store.service.ts" create mode 100644 "\353\217\204\354\226\217/week6/todolist.json" create mode 100644 "\353\217\204\354\226\217/week6/tsconfig.json" diff --git "a/\353\217\204\354\226\217/week6/.gitignore" "b/\353\217\204\354\226\217/week6/.gitignore" new file mode 100644 index 0000000..928abd6 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/.gitignore" @@ -0,0 +1,21 @@ +# dependency directories +node_modules/ + +# build output +dist/ + +# dotenv environment variable files +.env +.env.local +.env.development +.env.production +.env.* + +# macOS +.DS_Store + +# logs +*.log +npm-debug.log* + +/src/generated/prisma diff --git "a/\353\217\204\354\226\217/week6/package-lock.json" "b/\353\217\204\354\226\217/week6/package-lock.json" new file mode 100644 index 0000000..f9315aa --- /dev/null +++ "b/\353\217\204\354\226\217/week6/package-lock.json" @@ -0,0 +1,3122 @@ +{ + "name": "week6", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@prisma/adapter-mariadb": "^7.8.0", + "@prisma/client": "^7.8.0", + "bcryptjs": "^3.0.3", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "http-status-codes": "^2.3.0" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/node": "^25.6.0", + "nodemon": "^3.1.14", + "prisma": "^7.8.0", + "tsx": "^4.21.0" + } + }, + "node_modules/@electric-sql/pglite": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", + "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz", + "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz", + "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@prisma/adapter-mariadb": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-mariadb/-/adapter-mariadb-7.8.0.tgz", + "integrity": "sha512-mWsgcfbUjxB3qSzRlLs8E03vsKrqXzYK2zpx3e8u6wIgeHJM/sE46cuOGcYvHiZGmeQLCd3xL6YSSGM9QOLI6w==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.8.0", + "mariadb": "3.4.5" + } + }, + "node_modules/@prisma/client": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", + "integrity": "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.8.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.8.0.tgz", + "integrity": "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", + "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.3.4", + "deepmerge-ts": "7.1.5", + "effect": "3.20.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz", + "integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz", + "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.4.1", + "@electric-sql/pglite-socket": "0.1.1", + "@electric-sql/pglite-tools": "0.3.1", + "@hono/node-server": "1.19.11", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "@prisma/streams-local": "0.1.2", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "^4.12.8", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.2.0", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz", + "integrity": "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/engines": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz", + "integrity": "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/fetch-engine": "7.8.0", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a.tgz", + "integrity": "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.8.0.tgz", + "integrity": "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/streams-local": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz", + "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "better-result": "^2.7.0", + "env-paths": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "engines": { + "bun": ">=1.3.6", + "node": ">=22.0.0" + } + }, + "node_modules/@prisma/studio-core": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz", + "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-toggle": "1.1.10", + "chart.js": "4.5.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0", + "pnpm": "8" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/better-result": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.0.tgz", + "integrity": "sha512-NHwGDGVbRlWDOce3CwcfGIrcNR9zY37ut3SVwQVfv57DZdVhxjhA4mfaHN1n8QwWnRAR4iErpW1X/eaiaUaFYg==", + "devOptional": true, + "license": "MIT" + }, + "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": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/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/c12": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "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/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT", + "peer": true + }, + "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/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "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/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "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/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/effect": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", + "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "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/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "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": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "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-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "devOptional": true, + "license": "MIT" + }, + "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/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz", + "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", + "devOptional": true, + "license": "MIT", + "bin": { + "giget": "dist/cli.mjs" + } + }, + "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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "devOptional": true, + "license": "MIT" + }, + "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/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.15", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz", + "integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "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/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-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/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/mariadb": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.4.5.tgz", + "integrity": "sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ==", + "license": "LGPL-2.1-or-later", + "dependencies": { + "@types/geojson": "^7946.0.16", + "@types/node": "^24.0.13", + "denque": "^2.1.0", + "iconv-lite": "^0.6.3", + "lru-cache": "^10.4.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/mariadb/node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/mariadb/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "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": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "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/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/nodemon/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/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/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/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/prisma": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz", + "integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.8.0", + "@prisma/dev": "0.24.3", + "@prisma/engines": "7.8.0", + "@prisma/studio-core": "0.27.3", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/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==", + "devOptional": true, + "license": "ISC" + }, + "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/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "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": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/rc9": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", + "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.6", + "destr": "^2.0.5" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "devOptional": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "devOptional": true, + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "devOptional": true, + "license": "MIT" + }, + "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/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/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "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/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + } + } +} diff --git "a/\353\217\204\354\226\217/week6/package.json" "b/\353\217\204\354\226\217/week6/package.json" new file mode 100644 index 0000000..4771fb6 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/package.json" @@ -0,0 +1,23 @@ +{ + "scripts": { + "dev": "nodemon --ext ts,prisma --ignore src/generated --exec \"npx prisma generate && tsx src/index.ts\"" + }, + "dependencies": { + "@prisma/adapter-mariadb": "^7.8.0", + "@prisma/client": "^7.8.0", + "bcryptjs": "^3.0.3", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "http-status-codes": "^2.3.0" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/node": "^25.6.0", + "nodemon": "^3.1.14", + "prisma": "^7.8.0", + "tsx": "^4.21.0" + } +} diff --git "a/\353\217\204\354\226\217/week6/prisma.config.ts" "b/\353\217\204\354\226\217/week6/prisma.config.ts" new file mode 100644 index 0000000..5170cc4 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/prisma.config.ts" @@ -0,0 +1,12 @@ +/// +import "dotenv/config"; +import { defineConfig } from "prisma/config"; + +const { DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME } = process.env; + +export default defineConfig({ + schema: "prisma/schema.prisma", + datasource: { + url: `mysql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT ?? 3306}/${DB_NAME}`, + }, +}); diff --git "a/\353\217\204\354\226\217/week6/prisma/migrations/20260428071132_init_database/migration.sql" "b/\353\217\204\354\226\217/week6/prisma/migrations/20260428071132_init_database/migration.sql" new file mode 100644 index 0000000..31b12a1 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/prisma/migrations/20260428071132_init_database/migration.sql" @@ -0,0 +1,39 @@ +-- CreateTable +CREATE TABLE `user` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `email` VARCHAR(255) NOT NULL, + `name` VARCHAR(100) NOT NULL, + `gender` VARCHAR(15) NOT NULL, + `birth` DATE NOT NULL, + `address` VARCHAR(255) NOT NULL, + `detail_address` VARCHAR(255) NULL, + `phone_number` VARCHAR(15) NOT NULL, + + UNIQUE INDEX `email`(`email`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `food_category` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `user_favor_category` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `food_category_id` INTEGER NOT NULL, + + INDEX `f_category_id`(`food_category_id`), + INDEX `user_id`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `user_favor_category` ADD CONSTRAINT `user_favor_category_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_favor_category` ADD CONSTRAINT `user_favor_category_food_category_id_fkey` FOREIGN KEY (`food_category_id`) REFERENCES `food_category`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git "a/\353\217\204\354\226\217/week6/prisma/migrations/20260428071239_add_store_and_review_tables/migration.sql" "b/\353\217\204\354\226\217/week6/prisma/migrations/20260428071239_add_store_and_review_tables/migration.sql" new file mode 100644 index 0000000..99445d4 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/prisma/migrations/20260428071239_add_store_and_review_tables/migration.sql" @@ -0,0 +1,25 @@ +-- CreateTable +CREATE TABLE `store` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `user_store_review` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `store_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `content` TEXT NOT NULL, + + INDEX `store_id`(`store_id`), + INDEX `user_id`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `user_store_review` ADD CONSTRAINT `user_store_review_store_id_fkey` FOREIGN KEY (`store_id`) REFERENCES `store`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_store_review` ADD CONSTRAINT `user_store_review_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git "a/\353\217\204\354\226\217/week6/prisma/migrations/20260428072732_npx_prisma_migrate_dev/migration.sql" "b/\353\217\204\354\226\217/week6/prisma/migrations/20260428072732_npx_prisma_migrate_dev/migration.sql" new file mode 100644 index 0000000..a6dd8b0 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/prisma/migrations/20260428072732_npx_prisma_migrate_dev/migration.sql" @@ -0,0 +1,33 @@ +-- CreateTable +CREATE TABLE `mission` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `store_id` INTEGER NOT NULL, + `title` VARCHAR(200) NOT NULL, + `reward` INTEGER NOT NULL, + `spec` TEXT NULL, + `dead_line` DATETIME(3) NULL, + + INDEX `store_id`(`store_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `member_mission` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `member_id` INTEGER NOT NULL, + `mission_id` INTEGER NOT NULL, + `status` VARCHAR(15) NOT NULL, + + INDEX `member_id`(`member_id`), + INDEX `mission_id`(`mission_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `mission` ADD CONSTRAINT `mission_store_id_fkey` FOREIGN KEY (`store_id`) REFERENCES `store`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `member_mission` ADD CONSTRAINT `member_mission_member_id_fkey` FOREIGN KEY (`member_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `member_mission` ADD CONSTRAINT `member_mission_mission_id_fkey` FOREIGN KEY (`mission_id`) REFERENCES `mission`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git "a/\353\217\204\354\226\217/week6/prisma/migrations/migration_lock.toml" "b/\353\217\204\354\226\217/week6/prisma/migrations/migration_lock.toml" new file mode 100644 index 0000000..592fc0b --- /dev/null +++ "b/\353\217\204\354\226\217/week6/prisma/migrations/migration_lock.toml" @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "mysql" diff --git "a/\353\217\204\354\226\217/week6/prisma/schema.prisma" "b/\353\217\204\354\226\217/week6/prisma/schema.prisma" new file mode 100644 index 0000000..a5758e0 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/prisma/schema.prisma" @@ -0,0 +1,100 @@ +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +datasource db { + provider = "mysql" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique(map: "email") @db.VarChar(255) + name String @db.VarChar(100) + gender String @db.VarChar(15) + birth DateTime @db.Date + address String @db.VarChar(255) + detailAddress String? @map("detail_address") @db.VarChar(255) + phoneNumber String @map("phone_number") @db.VarChar(15) + + userFavorCategories UserFavorCategory[] + reviews UserStoreReview[] + memberMissions MemberMission[] + + @@map("user") +} + +model FoodCategory { + id Int @id @default(autoincrement()) + name String @db.VarChar(100) + + userFavorCategories UserFavorCategory[] + + @@map("food_category") +} + +model UserFavorCategory { + id Int @id @default(autoincrement()) + userId Int @map("user_id") + foodCategoryId Int @map("food_category_id") + user User @relation(fields: [userId], references: [id]) + foodCategory FoodCategory @relation(fields: [foodCategoryId], references: [id]) + + @@index([foodCategoryId], map: "f_category_id") + @@index([userId], map: "user_id") + @@map("user_favor_category") +} + +model Store { + id Int @id @default(autoincrement()) + name String @db.VarChar(100) + + reviews UserStoreReview[] + missions Mission[] + + @@map("store") +} + +model UserStoreReview { + id Int @id @default(autoincrement()) + storeId Int @map("store_id") + userId Int @map("user_id") + content String @db.Text + + store Store @relation(fields: [storeId], references: [id]) + user User @relation(fields: [userId], references: [id]) + + @@index([storeId], map: "store_id") + @@index([userId], map: "user_id") + @@map("user_store_review") +} + +model Mission { + id Int @id @default(autoincrement()) + storeId Int @map("store_id") + title String @db.VarChar(200) + reward Int + spec String? @db.Text + deadLine DateTime? @map("dead_line") + + store Store @relation(fields: [storeId], references: [id]) + memberMissions MemberMission[] + + @@index([storeId], map: "store_id") + @@map("mission") +} + +model MemberMission { + id Int @id @default(autoincrement()) + userId Int @map("member_id") + missionId Int @map("mission_id") + status String @db.VarChar(15) + + user User @relation(fields: [userId], references: [id]) + mission Mission @relation(fields: [missionId], references: [id]) + + @@index([userId], map: "member_id") + @@index([missionId], map: "mission_id") + @@map("member_mission") +} + diff --git "a/\353\217\204\354\226\217/week6/reset_db.sql" "b/\353\217\204\354\226\217/week6/reset_db.sql" new file mode 100644 index 0000000..454f20b --- /dev/null +++ "b/\353\217\204\354\226\217/week6/reset_db.sql" @@ -0,0 +1,215 @@ +-- ============================================================ +-- DB 초기화 및 재생성 스크립트 +-- ============================================================ + +CREATE DATABASE IF NOT EXISTS umc_mission DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE umc_mission; + +-- FK 체크 비활성화 후 전체 DROP +SET FOREIGN_KEY_CHECKS = 0; + +DROP TABLE IF EXISTS review_image; +DROP TABLE IF EXISTS review; +DROP TABLE IF EXISTS member_mission; +DROP TABLE IF EXISTS mission; +DROP TABLE IF EXISTS store_hours; +DROP TABLE IF EXISTS store_image; +DROP TABLE IF EXISTS store; +DROP TABLE IF EXISTS member_prefer; +DROP TABLE IF EXISTS member_agree; +DROP TABLE IF EXISTS member; +DROP TABLE IF EXISTS terms; +DROP TABLE IF EXISTS food_category; +DROP TABLE IF EXISTS region; + +SET FOREIGN_KEY_CHECKS = 1; + +-- ============================================================ +-- 테이블 재생성 +-- ============================================================ + +CREATE TABLE region ( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL COMMENT '지역명', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE food_category ( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL COMMENT '카테고리명 (한식, 중식, 일식 등)', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE terms ( + id BIGINT NOT NULL AUTO_INCREMENT, + title VARCHAR(100) NOT NULL COMMENT '약관 제목', + content TEXT NOT NULL COMMENT '약관 내용', + optional BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'TRUE: 선택 동의, FALSE: 필수 동의', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE member ( + id BIGINT NOT NULL AUTO_INCREMENT, + social_type VARCHAR(20) NULL, + social_id VARCHAR(100) NULL, + email VARCHAR(100) NULL, + password VARCHAR(255) NULL, + name VARCHAR(50) NOT NULL, + nickname VARCHAR(50) NOT NULL, + profile_image_url VARCHAR(500) NULL, + phone_num VARCHAR(20) NULL, + phone_verified BOOLEAN NOT NULL DEFAULT FALSE, + birth DATE NULL, + gender ENUM('MALE', 'FEMALE', 'OTHER') NULL, + address VARCHAR(200) NULL, + spec_address VARCHAR(200) NULL, + point INT NOT NULL DEFAULT 0, + status ENUM('ACTIVE', 'INACTIVE', 'BANNED') NOT NULL DEFAULT 'ACTIVE', + inactive_date DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_member_email (email), + UNIQUE KEY uq_member_social (social_type, social_id) +); + +CREATE TABLE member_agree ( + member_id BIGINT NOT NULL, + terms_id BIGINT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (member_id, terms_id), + CONSTRAINT fk_member_agree_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_member_agree_terms FOREIGN KEY (terms_id) REFERENCES terms (id) +); + +CREATE TABLE member_prefer ( + member_id BIGINT NOT NULL, + food_id BIGINT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (member_id, food_id), + CONSTRAINT fk_member_prefer_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_member_prefer_food FOREIGN KEY (food_id) REFERENCES food_category (id) +); + +CREATE TABLE store ( + id BIGINT NOT NULL AUTO_INCREMENT, + region_id BIGINT NOT NULL, + food_category_id BIGINT NOT NULL, + name VARCHAR(100) NOT NULL, + description TEXT NULL, + lat DECIMAL(10,7) NULL, + lng DECIMAL(10,7) NULL, + address VARCHAR(200) NOT NULL, + status ENUM('OPEN', 'CLOSED', 'PENDING') NOT NULL DEFAULT 'OPEN', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_store_region FOREIGN KEY (region_id) REFERENCES region (id), + CONSTRAINT fk_store_category FOREIGN KEY (food_category_id) REFERENCES food_category (id) +); + +CREATE TABLE store_image ( + id BIGINT NOT NULL AUTO_INCREMENT, + store_id BIGINT NOT NULL, + image_url VARCHAR(500) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_store_image_store FOREIGN KEY (store_id) REFERENCES store (id) +); + +CREATE TABLE store_hours ( + id BIGINT NOT NULL AUTO_INCREMENT, + store_id BIGINT NOT NULL, + day_of_week VARCHAR(3) NOT NULL, + open_time TIME NOT NULL, + close_time TIME NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_store_hours_day (store_id, day_of_week), + CONSTRAINT fk_store_hours_store FOREIGN KEY (store_id) REFERENCES store (id) +); + +CREATE TABLE mission ( + id BIGINT NOT NULL AUTO_INCREMENT, + store_id BIGINT NOT NULL, + title VARCHAR(200) NOT NULL, + reward INT NOT NULL DEFAULT 0, + spec VARCHAR(500) NULL, + dead_line DATE NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_mission_store FOREIGN KEY (store_id) REFERENCES store (id) +); + +CREATE TABLE member_mission ( + id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + mission_id BIGINT NOT NULL, + status ENUM('CHALLENGING', 'COMPLETE') NOT NULL DEFAULT 'CHALLENGING', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_member_mission (member_id, mission_id), + CONSTRAINT fk_member_mission_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_member_mission_mission FOREIGN KEY (mission_id) REFERENCES mission (id) +); + +CREATE TABLE review ( + id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + store_id BIGINT NOT NULL, + member_mission_id BIGINT NULL, + content TEXT NOT NULL, + score DECIMAL(2,1) NOT NULL, + owner_reply VARCHAR(500) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_review_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_review_store FOREIGN KEY (store_id) REFERENCES store (id), + CONSTRAINT fk_review_member_mission FOREIGN KEY (member_mission_id) REFERENCES member_mission (id), + CONSTRAINT chk_review_score CHECK (score BETWEEN 1.0 AND 5.0) +); + +CREATE TABLE review_image ( + id BIGINT NOT NULL AUTO_INCREMENT, + review_id BIGINT NOT NULL, + image_url VARCHAR(500) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_review_image_review FOREIGN KEY (review_id) REFERENCES review (id) +); + +-- ============================================================ +-- 시드 데이터 (API 테스트용 초기 데이터) +-- ============================================================ + +INSERT INTO region (name) VALUES + ('서울'), + ('경기'), + ('인천'), + ('부산'), + ('대구'); + +INSERT INTO food_category (name) VALUES + ('한식'), + ('중식'), + ('일식'), + ('양식'), + ('분식'), + ('카페/디저트'), + ('치킨'), + ('피자'), + ('패스트푸드'); diff --git "a/\353\217\204\354\226\217/week6/src/db.config.ts" "b/\353\217\204\354\226\217/week6/src/db.config.ts" new file mode 100644 index 0000000..7b88907 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/db.config.ts" @@ -0,0 +1,17 @@ +import "dotenv/config"; +import { PrismaClient } from "./generated/prisma/client.js"; +import { PrismaMariaDb } from "@prisma/adapter-mariadb"; + +const adapter = new PrismaMariaDb({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + port: process.env.DB_PORT ? parseInt(process.env.DB_PORT, 10) : 3306, + connectionLimit: 10, +}); + +export const prisma = new PrismaClient({ + adapter, + log: ["query", "info", "error", "warn"], +}); diff --git "a/\353\217\204\354\226\217/week6/src/index.ts" "b/\353\217\204\354\226\217/week6/src/index.ts" new file mode 100644 index 0000000..1e525f9 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/index.ts" @@ -0,0 +1,55 @@ +import dotenv from 'dotenv' +import express, { Express, Request, Response } from 'express' +import cors from 'cors' + +// 컨트롤러 import +import { handleCreateStore, handleListStoreReviews } from './modules/stores/controllers/store.controller.js' +import { handleListUserReviews } from './modules/reviews/controllers/review.controller.js' +import { handleCreateReview } from './modules/reviews/controllers/review.controller.js' +import { handleCreateMission, handleChallengeMission, handleListStoreMissions, handleListOngoingMissions, handleCompleteMission } from './modules/missions/controllers/mission.controller.js' +import { handleSignUp } from './modules/members/controllers/member.controller.js' + +// 에러 미들웨어 import +import { errorMiddleware } from './middleware/error.middleware.js' + +// 1. 환경 변수 설정 (가장 먼저 호출) +dotenv.config() + +const app: Express = express() +const port = process.env.PORT ?? 3000 + +// 2. 미들웨어 설정 +app.use(cors()) +app.use(express.json()) +app.use(express.urlencoded({ extended: false })) + +// 3. 라우터 등록 +app.get('/', (_req: Request, res: Response) => { + res.send('UMC 6주차 서버 실행 중!') +}) + +// 회원 +app.post('/api/v1/members/signup', handleSignUp) +app.get('/api/v1/users/:userId/reviews', handleListUserReviews) +app.get('/api/v1/users/:userId/missions', handleListOngoingMissions) + +// 가게 +app.post('/api/v1/stores', handleCreateStore) +app.post('/api/v1/stores/:storeId/reviews', handleCreateReview) +app.get('/api/v1/stores/:storeId/reviews', handleListStoreReviews) +app.get('/api/v1/stores/:storeId/missions', handleListStoreMissions) +app.post('/api/v1/stores/:storeId/missions', handleCreateMission) + +// 미션 +app.post('/api/v1/missions/:missionId/challenge', handleChallengeMission) +app.patch('/api/v1/users/:userId/missions/:missionId', handleCompleteMission) + + + +// 4. 전역 에러 핸들러 (반드시 라우터 등록 이후에 위치) +app.use(errorMiddleware) + +// 5. 서버 시작 +app.listen(port, () => { + console.log(`[server]: Server is running at http://localhost:${port}`) +}) diff --git "a/\353\217\204\354\226\217/week6/src/middleware/error.middleware.ts" "b/\353\217\204\354\226\217/week6/src/middleware/error.middleware.ts" new file mode 100644 index 0000000..ec8849d --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/middleware/error.middleware.ts" @@ -0,0 +1,21 @@ +import { Request, Response, NextFunction } from 'express' + +interface AppError extends Error { + status?: number +} + +export const errorMiddleware = ( + err: AppError, + _req: Request, + res: Response, + _next: NextFunction, +): void => { + console.error(`[Error] ${err.message}`) + + const status = err.status ?? 500 + res.status(status).json({ + success: false, + code: `E${status}`, + message: err.message ?? '서버 오류가 발생했습니다.', + }) +} diff --git "a/\353\217\204\354\226\217/week6/src/modules/members/controllers/member.controller.ts" "b/\353\217\204\354\226\217/week6/src/modules/members/controllers/member.controller.ts" new file mode 100644 index 0000000..3494ab7 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/members/controllers/member.controller.ts" @@ -0,0 +1,18 @@ +import { Request, Response, NextFunction } from 'express' +import { StatusCodes } from 'http-status-codes' +import { MemberSignUpRequest } from '../dtos/member.dto.js' +import { signUp } from '../services/member.service.js' + +// POST /api/v1/members/signup +export const handleSignUp = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const result = await signUp(req.body as MemberSignUpRequest) + res.status(StatusCodes.CREATED).json({ success: true, data: result }) + } catch (err) { + next(err) + } +} diff --git "a/\353\217\204\354\226\217/week6/src/modules/members/dtos/member.dto.ts" "b/\353\217\204\354\226\217/week6/src/modules/members/dtos/member.dto.ts" new file mode 100644 index 0000000..b0cbf3f --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/members/dtos/member.dto.ts" @@ -0,0 +1,45 @@ +// 회원가입 요청 인터페이스 +export interface MemberSignUpRequest { + name: string + nickname: string + email?: string + password?: string + phoneNum?: string + birth?: string // "YYYY-MM-DD" + gender?: string // "MALE" | "FEMALE" | "OTHER" + address?: string + specAddress?: string +} + +// req.body → 내부 데이터로 변환 +export const bodyToMember = (body: MemberSignUpRequest) => { + return { + name: body.name, + nickname: body.nickname, + email: body.email ?? null, + phoneNum: body.phoneNum ?? null, + birth: body.birth ? new Date(body.birth) : null, + gender: body.gender ?? null, + address: body.address ?? null, + specAddress: body.specAddress ?? null, + } +} + +// DB 결과 → 응답 형태로 변환 +export const responseFromMember = (member: { + id: number + name: string + nickname: string + email: string | null + phone_num: string | null + status: string +}) => { + return { + memberId: member.id, + name: member.name, + nickname: member.nickname, + email: member.email, + phoneNum: member.phone_num, + status: member.status, + } +} diff --git "a/\353\217\204\354\226\217/week6/src/modules/members/repositories/member.repository.ts" "b/\353\217\204\354\226\217/week6/src/modules/members/repositories/member.repository.ts" new file mode 100644 index 0000000..549030e --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/members/repositories/member.repository.ts" @@ -0,0 +1,26 @@ +import { prisma } from '../../../db.config.js' + +// 유저 생성 +export const addUser = async (data: any) => { + const exists = await prisma.user.findFirst({ where: { email: data.email } }) + if (exists) return null + + const created = await prisma.user.create({ data }) + return created.id +} + +// 유저 조회 (없으면 예외 throw) +export const getUser = async (userId: number) => + prisma.user.findFirstOrThrow({ where: { id: userId } }) + +// 선호 음식 카테고리 등록 +export const setPreference = async (userId: number, foodCategoryId: number) => + prisma.userFavorCategory.create({ data: { userId, foodCategoryId } }) + +// 선호 카테고리 목록 조회 (JOIN 포함) +export const getUserPreferencesByUserId = async (userId: number) => + prisma.userFavorCategory.findMany({ + where: { userId }, + include: { foodCategory: true }, + orderBy: { foodCategoryId: 'asc' }, + }) diff --git "a/\353\217\204\354\226\217/week6/src/modules/members/services/member.service.ts" "b/\353\217\204\354\226\217/week6/src/modules/members/services/member.service.ts" new file mode 100644 index 0000000..f5a0e0e --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/members/services/member.service.ts" @@ -0,0 +1,42 @@ +import bcrypt from 'bcryptjs' +import { MemberSignUpRequest, bodyToMember, responseFromMember } from '../dtos/member.dto.js' +import { + addUser, + getUser, +} from '../repositories/member.repository.js' + +const makeError = (message: string, status: number): Error & { status: number } => { + const err = new Error(message) as Error & { status: number } + err.status = status + return err +} + +export const signUp = async (data: MemberSignUpRequest) => { + // 필수값 검증 + if (!data.name || !data.nickname) { + throw makeError('name과 nickname은 필수 입력값입니다.', 400) + } + + // 비밀번호 해싱 (password가 있는 경우에만) + const hashedPassword = data.password + ? await bcrypt.hash(data.password, 10) + : null + + const memberData = bodyToMember(data) + const memberId = await addUser({ ...memberData, hashedPassword }) + + if (memberId === null) { + throw makeError('이미 존재하는 이메일입니다.', 409) + } + + const member = await getUser(memberId) + + return responseFromMember(member as { + id: number + name: string + nickname: string + email: string | null + phone_num: string | null + status: string + }) +} diff --git "a/\353\217\204\354\226\217/week6/src/modules/missions/controllers/mission.controller.ts" "b/\353\217\204\354\226\217/week6/src/modules/missions/controllers/mission.controller.ts" new file mode 100644 index 0000000..52d7cc8 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/missions/controllers/mission.controller.ts" @@ -0,0 +1,67 @@ +import { Request, Response, NextFunction } from 'express' +import { StatusCodes } from 'http-status-codes' +import { MissionCreateRequest, MissionChallengeRequest } from '../dtos/mission.dto.js' +import { createMission, challengeMission, listStoreMissions, listOngoingMissions, finishMission } from '../services/mission.service.js' + +// GET /api/v1/users/:userId/missions +export const handleListOngoingMissions = async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = parseInt(String(req.params['userId'] ?? '0'), 10) + const cursor = typeof req.query['cursor'] === 'string' ? parseInt(req.query['cursor'], 10) : 0 + res.status(200).json(await listOngoingMissions(userId, cursor)) + } catch (err) { + next(err) + } +} + +// GET /api/v1/stores/:storeId/missions +export const handleListStoreMissions = async (req: Request, res: Response, next: NextFunction) => { + try { + const storeId = parseInt(String(req.params['storeId'] ?? '0'), 10) + const cursor = typeof req.query['cursor'] === 'string' ? parseInt(req.query['cursor'], 10) : 0 + res.status(200).json(await listStoreMissions(storeId, cursor)) + } catch (err) { + next(err) + } +} + +// POST /api/v1/stores/:storeId/missions +export const handleCreateMission = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const storeId = parseInt(String(req.params['storeId'] ?? '0'), 10) + const result = await createMission(storeId, req.body as MissionCreateRequest) + res.status(StatusCodes.CREATED).json({ success: true, data: result }) + } catch (err) { + next(err) + } +} + +// POST /api/v1/missions/:missionId/challenge +export const handleChallengeMission = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const missionId = parseInt(String(req.params['missionId'] ?? '0'), 10) + const result = await challengeMission(missionId, req.body as MissionChallengeRequest) + res.status(StatusCodes.CREATED).json({ success: true, data: result }) + } catch (err) { + next(err) + } +} + +// PATCH /api/v1/users/:userId/missions/:missionId +export const handleCompleteMission = async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = parseInt(String(req.params['userId'] ?? '0'), 10) + const missionId = parseInt(String(req.params['missionId'] ?? '0'), 10) + res.status(200).json(await finishMission(userId, missionId)) + } catch (err) { + next(err) + } +} diff --git "a/\353\217\204\354\226\217/week6/src/modules/missions/dtos/mission.dto.ts" "b/\353\217\204\354\226\217/week6/src/modules/missions/dtos/mission.dto.ts" new file mode 100644 index 0000000..8786670 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/missions/dtos/mission.dto.ts" @@ -0,0 +1,57 @@ +// 미션 추가 요청 인터페이스 +export interface MissionCreateRequest { + title: string + reward: number + spec?: string + deadLine?: string // "YYYY-MM-DD" +} + +// 미션 도전 요청 인터페이스 +export interface MissionChallengeRequest { + memberId: number + status: 'CHALLENGING' | 'COMPLETE' +} + +// req.body → 내부 데이터로 변환 (미션 추가) +export const bodyToMission = (body: MissionCreateRequest) => { + return { + title: body.title, + reward: body.reward, + spec: body.spec ?? null, + deadLine: body.deadLine ? new Date(body.deadLine) : null, + } +} + +// DB 결과 → 응답 형태로 변환 (미션) +export const responseFromMission = (mission: { + id: number + store_id: number + title: string + reward: number + spec: string | null + dead_line: Date | null +}) => { + return { + missionId: mission.id, + storeId: mission.store_id, + title: mission.title, + reward: mission.reward, + spec: mission.spec, + deadLine: mission.dead_line, + } +} + +// DB 결과 → 응답 형태로 변환 (미션 도전) +export const responseFromMemberMission = (mm: { + id: number + member_id: number + mission_id: number + status: string +}) => { + return { + memberMissionId: mm.id, + memberId: mm.member_id, + missionId: mm.mission_id, + status: mm.status, + } +} diff --git "a/\353\217\204\354\226\217/week6/src/modules/missions/repositories/mission.repository.ts" "b/\353\217\204\354\226\217/week6/src/modules/missions/repositories/mission.repository.ts" new file mode 100644 index 0000000..bee3e62 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/missions/repositories/mission.repository.ts" @@ -0,0 +1,72 @@ +import { prisma } from '../../../db.config.js' + +// 미션 추가 +export const addMission = async (data: { + storeId: number + title: string + reward: number + spec: string | null + deadLine: Date | null +}): Promise => { + const created = await prisma.mission.create({ + data: { + storeId: data.storeId, + title: data.title, + reward: data.reward, + spec: data.spec, + deadLine: data.deadLine, + }, + }) + return created.id +} + +// 미션 조회 +export const getMissionById = async (missionId: number) => + prisma.mission.findFirst({ where: { id: missionId } }) + +// 이미 도전 중인 미션인지 확인 +export const findMemberMission = async (memberId: number, missionId: number) => + prisma.memberMission.findFirst({ + where: { userId: memberId, missionId }, + }) + +// 미션 도전 추가 +export const addMemberMission = async ( + memberId: number, + missionId: number, + status: string, +): Promise => { + const created = await prisma.memberMission.create({ + data: { userId: memberId, missionId, status }, + }) + return created.id +} + +// 미션 도전 기록 조회 +export const getMemberMissionById = async (memberMissionId: number) => + prisma.memberMission.findFirst({ where: { id: memberMissionId } }) + +// 특정 가게의 미션 목록 조회 (커서 기반 페이지네이션) +export const getStoreMissions = async (storeId: number, cursor: number) => + prisma.mission.findMany({ + where: { storeId, id: { gt: cursor } }, + orderBy: { id: 'asc' }, + take: 5, + }) + + +// 유저가 진행 중인 미션 목록 조회 +export const getOngoingMissions = async (userId: number, cursor: number) => + prisma.memberMission.findMany({ + where: { userId, status: '진행중', id: { gt: cursor } }, + include: { mission: { include: { store: true } } }, + orderBy: { id: 'asc' }, + take: 5, + }) + +// 진행 중인 미션을 완료로 변경 +export const completeMission = async (userId: number, missionId: number) => + prisma.memberMission.updateMany({ + where: { userId, missionId, status: '진행중' }, + data: { status: '완료' }, + }) \ No newline at end of file diff --git "a/\353\217\204\354\226\217/week6/src/modules/missions/services/mission.service.ts" "b/\353\217\204\354\226\217/week6/src/modules/missions/services/mission.service.ts" new file mode 100644 index 0000000..b706590 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/missions/services/mission.service.ts" @@ -0,0 +1,116 @@ +import { + MissionCreateRequest, + MissionChallengeRequest, + bodyToMission, + responseFromMission, + responseFromMemberMission, +} from '../dtos/mission.dto.js' +import { + addMission, + getMissionById, + findMemberMission, + addMemberMission, + getMemberMissionById, + getStoreMissions, + getOngoingMissions, + completeMission, +} from '../repositories/mission.repository.js' +import { getStoreById } from '../../stores/repositories/store.repository.js' + +const makeError = (message: string, status: number): Error & { status: number } => { + const err = new Error(message) as Error & { status: number } + err.status = status + return err +} + +// 미션 추가 +export const createMission = async (storeId: number, data: MissionCreateRequest) => { + // 가게 존재 여부 검증 + const store = await getStoreById(storeId) + if (!store) { + throw makeError('존재하지 않는 가게입니다.', 404) + } + + const missionData = bodyToMission(data) + const missionId = await addMission({ ...missionData, storeId }) + + const mission = await getMissionById(missionId) + if (!mission) { + throw makeError('미션 생성 후 조회에 실패했습니다.', 500) + } + + return responseFromMission(mission as unknown as { + id: number + store_id: number + title: string + reward: number + spec: string | null + dead_line: Date | null + }) +} + +// 특정 가게의 미션 목록 +export const listStoreMissions = async (storeId: number, cursor: number) => { + const missions = await getStoreMissions(storeId, cursor) + const last = missions[missions.length - 1] + return { + data: missions, + pagination: { cursor: last ? last.id : null }, + } +} + +// 내가 진행 중인 미션 목록 +export const listOngoingMissions = async (userId: number, cursor: number) => { + const missions = await getOngoingMissions(userId, cursor) + const last = missions[missions.length - 1] + return { + data: missions, + pagination: { cursor: last ? last.id : null }, + } +} + +// 진행 중인 미션을 완료로 변경 +export const finishMission = async (userId: number, missionId: number) => { + const result = await completeMission(userId, missionId) + if (result.count === 0) { + throw makeError('진행 중인 미션이 없습니다.', 404) + } + return { message: '미션이 완료 처리됐습니다.' } +} + +// 미션 도전하기 +export const challengeMission = async ( + missionId: number, + data: MissionChallengeRequest, +) => { + // 미션 존재 여부 검증 + const mission = await getMissionById(missionId) + if (!mission) { + throw makeError('존재하지 않는 미션입니다.', 404) + } + + // 필수값 검증 + if (!data.status) { + throw makeError('status는 필수 입력값입니다. (CHALLENGING 또는 COMPLETE)', 400) + } + + // 이미 도전 중인지 검증 + const existing = await findMemberMission(data.memberId, missionId) + if (existing) { + throw makeError('이미 도전 중인 미션입니다.', 409) + } + + const memberMissionId = await addMemberMission(data.memberId, missionId, data.status) + + const memberMission = await getMemberMissionById(memberMissionId) + if (!memberMission) { + throw makeError('미션 도전 후 조회에 실패했습니다.', 500) + } + + return responseFromMemberMission(memberMission as unknown as { + id: number + member_id: number + mission_id: number + status: string + }) +} diff --git "a/\353\217\204\354\226\217/week6/src/modules/reviews/controllers/review.controller.ts" "b/\353\217\204\354\226\217/week6/src/modules/reviews/controllers/review.controller.ts" new file mode 100644 index 0000000..9cef30f --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/reviews/controllers/review.controller.ts" @@ -0,0 +1,30 @@ +import { Request, Response, NextFunction } from 'express' +import { StatusCodes } from 'http-status-codes' +import { ReviewCreateRequest } from '../dtos/review.dto.js' +import { createReview, listUserReviews } from '../services/review.service.js' + +// GET /api/v1/users/:userId/reviews +export const handleListUserReviews = async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = parseInt(req.params['userId'] ?? '0', 10) + const cursor = typeof req.query['cursor'] === 'string' ? parseInt(req.query['cursor'], 10) : 0 + res.status(200).json(await listUserReviews(userId, cursor)) + } catch (err) { + next(err) + } +} + +// POST /api/v1/stores/:storeId/reviews +export const handleCreateReview = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const storeId = parseInt(req.params['storeId'] ?? '0', 10) + const result = await createReview(storeId, req.body as ReviewCreateRequest) + res.status(StatusCodes.CREATED).json({ success: true, data: result }) + } catch (err) { + next(err) + } +} diff --git "a/\353\217\204\354\226\217/week6/src/modules/reviews/dtos/review.dto.ts" "b/\353\217\204\354\226\217/week6/src/modules/reviews/dtos/review.dto.ts" new file mode 100644 index 0000000..0738b1c --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/reviews/dtos/review.dto.ts" @@ -0,0 +1,43 @@ +// 리뷰 추가 요청 인터페이스 +export interface ReviewCreateRequest { + memberId: number + content: string + score: number +} + +// req.body → 내부 데이터로 변환 +export const bodyToReview = (body: ReviewCreateRequest) => { + return { + memberId: body.memberId, + content: body.content, + score: body.score, + } +} + +// 리뷰 목록 → 응답 형태로 변환 (커서 기반 페이지네이션) +export const responseFromUserReviews = (reviews: any[]) => { + const last = reviews[reviews.length - 1] + return { + data: reviews, + pagination: { cursor: last ? last.id : null }, + } +} + +// DB 결과 → 응답 형태로 변환 +export const responseFromReview = (review: { + id: number + member_id: number + store_id: number + content: string + score: number + created_at: Date +}) => { + return { + reviewId: review.id, + memberId: review.member_id, + storeId: review.store_id, + content: review.content, + score: review.score, + createdAt: review.created_at, + } +} diff --git "a/\353\217\204\354\226\217/week6/src/modules/reviews/repositories/review.repository.ts" "b/\353\217\204\354\226\217/week6/src/modules/reviews/repositories/review.repository.ts" new file mode 100644 index 0000000..adca307 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/reviews/repositories/review.repository.ts" @@ -0,0 +1,30 @@ +import { prisma } from '../../../db.config.js' + +// 리뷰 추가 +export const addReview = async (data: { + memberId: number + storeId: number + content: string +}): Promise => { + const created = await prisma.userStoreReview.create({ + data: { + userId: data.memberId, + storeId: data.storeId, + content: data.content, + }, + }) + return created.id +} + +// 리뷰 조회 +export const getReviewById = async (reviewId: number) => + prisma.userStoreReview.findFirst({ where: { id: reviewId } }) + +// 내가 작성한 리뷰 목록 조회 (커서 기반 페이지네이션) +export const getUserReviews = async (userId: number, cursor: number) => + prisma.userStoreReview.findMany({ + where: { userId, id: { gt: cursor } }, + include: { store: true }, + orderBy: { id: 'asc' }, + take: 5, + }) diff --git "a/\353\217\204\354\226\217/week6/src/modules/reviews/services/review.service.ts" "b/\353\217\204\354\226\217/week6/src/modules/reviews/services/review.service.ts" new file mode 100644 index 0000000..bdd952f --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/reviews/services/review.service.ts" @@ -0,0 +1,44 @@ +import { ReviewCreateRequest, bodyToReview, responseFromReview, responseFromUserReviews } from '../dtos/review.dto.js' +import { addReview, getReviewById, getUserReviews } from '../repositories/review.repository.js' +import { getStoreById } from '../../stores/repositories/store.repository.js' + +const makeError = (message: string, status: number): Error & { status: number } => { + const err = new Error(message) as Error & { status: number } + err.status = status + return err +} + +export const listUserReviews = async (userId: number, cursor: number) => { + const reviews = await getUserReviews(userId, cursor) + return responseFromUserReviews(reviews) +} + +export const createReview = async (storeId: number, data: ReviewCreateRequest) => { + // 가게 존재 여부 검증 + const store = await getStoreById(storeId) + if (!store) { + throw makeError('존재하지 않는 가게입니다.', 404) + } + + // 별점 유효성 검사 + if (data.score < 1 || data.score > 5) { + throw makeError('별점은 1~5 사이여야 합니다.', 400) + } + + const reviewData = bodyToReview(data) + const reviewId = await addReview({ ...reviewData, storeId }) + + const review = await getReviewById(reviewId) + if (!review) { + throw makeError('리뷰 생성 후 조회에 실패했습니다.', 500) + } + + return responseFromReview(review as { + id: number + member_id: number + store_id: number + content: string + score: number + created_at: Date + }) +} diff --git "a/\353\217\204\354\226\217/week6/src/modules/stores/controllers/store.controller.ts" "b/\353\217\204\354\226\217/week6/src/modules/stores/controllers/store.controller.ts" new file mode 100644 index 0000000..f626991 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/stores/controllers/store.controller.ts" @@ -0,0 +1,29 @@ +import { Request, Response, NextFunction } from 'express' +import { StatusCodes } from 'http-status-codes' +import { StoreCreateRequest } from '../dtos/store.dto.js' +import { createStore, listStoreReviews } from '../services/store.service.js' + +// POST /api/v1/stores +export const handleCreateStore = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const result = await createStore(req.body as StoreCreateRequest) + res.status(StatusCodes.CREATED).json({ success: true, data: result }) + } catch (err) { + next(err) + } +} + +// GET /api/v1/stores/:storeId/reviews +export const handleListStoreReviews = async (req: Request, res: Response, next: NextFunction) => { + try { + const storeId = parseInt(req.params.storeId, 10) + const cursor = typeof req.query.cursor === 'string' ? parseInt(req.query.cursor, 10) : 0 + res.status(200).json(await listStoreReviews(storeId, cursor)) + } catch (err) { + next(err) + } +} diff --git "a/\353\217\204\354\226\217/week6/src/modules/stores/dtos/store.dto.ts" "b/\353\217\204\354\226\217/week6/src/modules/stores/dtos/store.dto.ts" new file mode 100644 index 0000000..6922309 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/stores/dtos/store.dto.ts" @@ -0,0 +1,49 @@ +// 가게 추가 요청 인터페이스 +export interface StoreCreateRequest { + regionId: number + foodCategoryId: number + name: string + description?: string + address: string + lat?: number + lng?: number +} + +// req.body → 내부 데이터로 변환 +export const bodyToStore = (body: StoreCreateRequest) => { + return { + regionId: body.regionId, + foodCategoryId: body.foodCategoryId, + name: body.name, + description: body.description ?? null, + address: body.address, + lat: body.lat ?? null, + lng: body.lng ?? null, + } +} + +// 리뷰 목록 → 응답 형태로 변환 (커서 기반 페이지네이션) +export const responseFromReviews = (reviews: any[]) => { + const last = reviews[reviews.length - 1] + return { + data: reviews, + pagination: { + cursor: last ? last.id : null, + }, + } +} + +// DB 조회 결과 → 응답 형태로 변환 +export const responseFromStore = (store: { + id: number + name: string + address: string + region_id: number +}) => { + return { + storeId: store.id, + name: store.name, + address: store.address, + regionId: store.region_id, + } +} diff --git "a/\353\217\204\354\226\217/week6/src/modules/stores/repositories/store.repository.ts" "b/\353\217\204\354\226\217/week6/src/modules/stores/repositories/store.repository.ts" new file mode 100644 index 0000000..9a9ac1e --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/stores/repositories/store.repository.ts" @@ -0,0 +1,28 @@ +import { prisma } from '../../../db.config.js' + +// 가게 추가 +export const addStore = async (data: { name: string }): Promise => { + const created = await prisma.store.create({ data }) + return created.id +} + +// 가게 조회 +export const getStoreById = async (storeId: number) => + prisma.store.findFirst({ where: { id: storeId } }) + +// 가게 리뷰 목록 조회 (커서 기반 페이지네이션) +export const getAllStoreReviews = async (storeId: number, cursor: number) => + prisma.userStoreReview.findMany({ + select: { + id: true, + content: true, + store: true, + user: true, + }, + where: { + storeId, + id: { gt: cursor }, + }, + orderBy: { id: 'asc' }, + take: 5, + }) diff --git "a/\353\217\204\354\226\217/week6/src/modules/stores/services/store.service.ts" "b/\353\217\204\354\226\217/week6/src/modules/stores/services/store.service.ts" new file mode 100644 index 0000000..94e88f8 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/src/modules/stores/services/store.service.ts" @@ -0,0 +1,30 @@ +import { StoreCreateRequest, bodyToStore, responseFromStore, responseFromReviews } from '../dtos/store.dto.js' +import { addStore, getStoreById, getAllStoreReviews } from '../repositories/store.repository.js' + +const makeError = (message: string, status: number): Error & { status: number } => { + const err = new Error(message) as Error & { status: number } + err.status = status + return err +} + +export const listStoreReviews = async (storeId: number, cursor: number) => { + const reviews = await getAllStoreReviews(storeId, cursor) + return responseFromReviews(reviews) +} + +export const createStore = async (data: StoreCreateRequest) => { + const storeData = bodyToStore(data) + const storeId = await addStore(storeData) + + const store = await getStoreById(storeId) + if (!store) { + throw makeError('가게 생성 후 조회에 실패했습니다.', 500) + } + + return responseFromStore(store as { + id: number + name: string + address: string + region_id: number + }) +} diff --git "a/\353\217\204\354\226\217/week6/todolist.json" "b/\353\217\204\354\226\217/week6/todolist.json" new file mode 100644 index 0000000..830b572 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/todolist.json" @@ -0,0 +1,353 @@ +{ + "chapter": "Chapter 5. API 및 프로젝트 설정 기초", + "branch": "feature/chapter-05", + "week4_reference": "../week4", + "keywords": [ + { + "term": "환경 변수 (Environment Variables)", + "summary": "DB 비밀번호·API Key 등 민감한 값을 코드에 하드코딩하지 않고 .env 파일로 관리. dotenv 라이브러리로 로드하며 .gitignore에 반드시 추가해야 함.", + "example": "process.env.DB_PASSWORD" + }, + { + "term": "CORS (Cross-Origin Resource Sharing)", + "summary": "브라우저가 다른 Origin(도메인·포트)의 서버에 요청할 때 발생하는 보안 정책. Express에서는 cors 미들웨어로 허용 처리.", + "example": "app.use(cors())" + }, + { + "term": "DB Connection Pool", + "summary": "매 요청마다 DB 커넥션을 새로 생성·해제하지 않고, 미리 만들어 둔 커넥션 풀에서 빌려 쓰는 방식. mysql2의 createPool()로 구현. finally 블록에서 conn.release()로 반환해야 함.", + "example": "const pool = mysql.createPool({ connectionLimit: 10 })" + }, + { + "term": "비동기 (async / await)", + "summary": "DB 쿼리·외부 API 호출처럼 응답 대기가 필요한 작업을 Promise 기반으로 처리. async 함수 안에서 await로 결과를 기다리며, 동기 코드처럼 읽기 쉽게 작성 가능.", + "example": "const [rows] = await pool.query('SELECT ...')" + }, + { + "term": "try / catch / finally", + "summary": "비동기 작업에서 발생할 수 있는 에러를 구조적으로 처리. try: 정상 로직, catch: 에러 처리, finally: 커넥션 반환(conn.release()) 등 항상 실행되어야 하는 정리 코드.", + "example": "try { ... } catch(err) { throw new Error(...) } finally { conn.release() }" + }, + { + "term": "Interface (인터페이스)", + "summary": "TypeScript에서 객체의 형태(속성·타입)를 정의하는 설계도. DTO·Repository 반환 타입 등에 활용. ?를 붙이면 선택적 프로퍼티.", + "example": "export interface StoreCreateRequest { regionId: number; name: string; address: string; }" + }, + { + "term": "Type Assertion (as 키워드)", + "summary": "TypeScript 컴파일러에게 '이 값은 이 타입이야'라고 강제로 알려주는 문법. req.body는 기본적으로 any이므로, as 로 정의한 인터페이스 타입으로 변환해 사용.", + "example": "req.body as StoreCreateRequest" + } + ], + "project_structure": { + "root": "week5/", + "note": "week4의 in-memory DB → MySQL 실제 DB 연결로 전환. 모듈형 모노리스 구조 채택.", + "files": [ + "src/index.ts - Express 앱 진입점, 미들웨어·라우터 등록", + "src/db.config.ts - MySQL Connection Pool 설정", + "src/modules/stores/controllers/store.controller.ts", + "src/modules/stores/services/store.service.ts", + "src/modules/stores/repositories/store.repository.ts", + "src/modules/stores/dtos/store.dto.ts", + "src/modules/reviews/controllers/review.controller.ts", + "src/modules/reviews/services/review.service.ts", + "src/modules/reviews/repositories/review.repository.ts", + "src/modules/reviews/dtos/review.dto.ts", + "src/modules/missions/controllers/mission.controller.ts", + "src/modules/missions/services/mission.service.ts", + "src/modules/missions/repositories/mission.repository.ts", + "src/modules/missions/dtos/mission.dto.ts", + ".env - DB 접속 정보·PORT (gitignore 필수)", + ".gitignore", + "package.json", + "tsconfig.json", + "schema.sql - week4 schema.sql 재사용 (테이블 이미 정의됨)" + ] + }, + "todos": [ + { + "id": 1, + "phase": "사전 준비", + "title": "GitHub 이슈 생성 및 브랜치 분기", + "status": "todo", + "details": [ + "GitHub 저장소 Issues 탭에서 라벨 정리: bug, docs, feature, refactor", + "이슈 제목: '[feat] Chapter 5 - API 구현 (가게 추가 / 리뷰 / 미션)'", + "Assignee: 본인, Label: feature 로 이슈 생성", + "이슈에서 'Create a branch' 클릭 → feature/chapter-05 브랜치 생성", + "로컬에서: git fetch origin && git checkout feature/chapter-05" + ] + }, + { + "id": 2, + "phase": "사전 준비", + "title": "Postman 설치 및 기본 사용법 확인", + "status": "todo", + "details": [ + "Postman 설치 (https://www.postman.com/downloads/)", + "Params / Authorization / Headers / Body 탭 역할 이해", + "Body > raw > JSON 선택 방법 숙지", + "나중에 API 테스트 시 스크린샷 저장할 준비" + ] + }, + { + "id": 3, + "phase": "프로젝트 세팅", + "title": "week5 폴더 초기화 및 의존성 설치", + "status": "todo", + "details": [ + "cd week5 && npm init -y", + "npm install express cors dotenv http-status-codes mysql2", + "npm install -D typescript @types/node @types/express @types/cors @types/dotenv nodemon tsx", + "npx tsc --init 후 tsconfig.json 수정 (rootDir: ./src, outDir: ./dist, module: NodeNext, strict: true 등)", + "week4/tsconfig.json을 참고해 module/moduleResolution 설정 일치시키기", + "package.json scripts 추가: start / dev (nodemon --exec tsx src/index.ts)" + ] + }, + { + "id": 4, + "phase": "프로젝트 세팅", + "title": ".env 파일 및 .gitignore 작성", + "status": "todo", + "details": [ + ".gitignore에 node_modules/ / .env / .env.* 추가", + ".env에 PORT=3000, DB_HOST=localhost, DB_PORT=3306, DB_USER=root, DB_PASSWORD=비밀번호, DB_NAME=umc_mission 작성", + "DB_NAME은 week4/schema.sql 기준 umc_mission 사용 (이미 테이블 있음)" + ] + }, + { + "id": 5, + "phase": "프로젝트 세팅", + "title": "src/db.config.ts 작성 - MySQL Connection Pool", + "status": "todo", + "details": [ + "mysql2/promise의 createPool 사용", + "환경 변수(DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME)로 설정", + "connectionLimit: 10, waitForConnections: true", + "week4 schema.sql의 DB(umc_mission)와 연결" + ], + "reference_week4": "week4/src/db/index.ts 구조 참고 (단, in-memory→MySQL로 변경)" + }, + { + "id": 6, + "phase": "프로젝트 세팅", + "title": "src/index.ts 작성 - Express 앱 진입점", + "status": "todo", + "details": [ + "dotenv.config() 최상단 호출", + "cors(), express.json(), express.urlencoded() 미들웨어 등록", + "각 모듈 라우터 등록: /api/v1/stores, /api/v1/reviews, /api/v1/missions", + "전역 에러 핸들러 미들웨어 등록 (시니어 미션: JSON 에러 응답)", + "app.listen(process.env.PORT || 3000)" + ], + "reference_week4": "week4/src/index.ts 구조 그대로 활용, cors·dotenv 추가" + }, + { + "id": 7, + "phase": "DB 준비", + "title": "MySQL에 week5용 테이블 확인 및 더미데이터 삽입", + "status": "todo", + "details": [ + "week4/schema.sql로 테이블 생성 (이미 되어있으면 생략)", + "food_category 더미데이터 삽입: INSERT INTO food_category(name) VALUES ('한식'),('중식'),('일식'),('양식'),('치킨'),('분식'),('고기/구이'),('도시락'),('아식'),('패스트푸드'),('다저트'),('아시안푸드')", + "region 더미데이터 삽입: INSERT INTO region(name) VALUES ('서울'),('경기'),('부산')...", + "member 더미데이터 1건 삽입 (API 테스트용 첫 번째 사용자)" + ] + }, + { + "id": 8, + "phase": "API 구현 - 1-1", + "title": "[필수] 특정 지역에 가게 추가하기 API", + "status": "todo", + "priority": "optional", + "endpoint": "POST /api/v1/stores", + "request_body": { + "regionId": "number", + "foodCategoryId": "number", + "name": "string", + "description": "string (선택)", + "address": "string", + "lat": "number (선택)", + "lng": "number (선택)" + }, + "details": [ + "store.dto.ts - StoreCreateRequest 인터페이스 정의", + "store.dto.ts - bodyToStore() 변환 함수 작성", + "store.repository.ts - addStore(): INSERT INTO store(...) VALUES(?)", + "store.service.ts - createStore(data): addStore 호출 후 결과 반환", + "store.controller.ts - handleCreateStore: bodyToStore(req.body as ...) → service 호출 → 201 응답", + "index.ts에 라우터 등록: app.post('/api/v1/stores', handleCreateStore)" + ], + "reference_week4": "week4/src/controllers/store.controller.ts 패턴 참고" + }, + { + "id": 9, + "phase": "API 구현 - 1-2", + "title": "[필수★] 가게에 리뷰 추가하기 API", + "status": "todo", + "priority": "required", + "endpoint": "POST /api/v1/stores/:storeId/reviews", + "request_body": { + "memberId": "number (특정 사용자로 가정, DB 첫 번째 사용자)", + "content": "string", + "score": "number (1.0~5.0)" + }, + "details": [ + "review.dto.ts - ReviewCreateRequest 인터페이스 (content, score, memberId)", + "review.dto.ts - bodyToReview() 변환 함수", + "review.repository.ts - addReview(): INSERT INTO review(...)", + "store.repository.ts (또는 review.repository.ts) - findStoreById(): SELECT * FROM store WHERE id=?", + "review.service.ts - createReview(storeId, data): 가게 존재 검증 → addReview 호출", + " 검증 실패 시 throw new Error('존재하지 않는 가게입니다.')", + "review.controller.ts - handleCreateReview: storeId = parseInt(req.params.storeId) → service 호출", + "index.ts에 라우터 등록" + ], + "validation": "리뷰를 추가하려는 가게가 존재하는지 검증 필요", + "reference_week4": "week4/src/services/store.service.ts의 createReview 패턴, week4 스키마의 review 테이블" + }, + { + "id": 10, + "phase": "API 구현 - 1-3", + "title": "가게에 미션 추가하기 API", + "status": "todo", + "priority": "optional", + "endpoint": "POST /api/v1/stores/:storeId/missions", + "request_body": { + "title": "string", + "reward": "number", + "spec": "string (선택)", + "deadLine": "string (YYYY-MM-DD, 선택)" + }, + "details": [ + "mission.dto.ts - MissionCreateRequest 인터페이스", + "mission.dto.ts - bodyToMission() 변환 함수 (deadLine → Date 변환)", + "mission.repository.ts - addMission(): INSERT INTO mission(store_id, title, reward, spec, dead_line)", + "mission.service.ts - createMission(storeId, data): 가게 존재 검증 → addMission", + "mission.controller.ts - handleCreateMission", + "index.ts에 라우터 등록" + ], + "reference_week4": "week4/src/repositories/mission.repository.ts 패턴, week4 schema.sql의 mission 테이블" + }, + { + "id": 11, + "phase": "API 구현 - 1-4", + "title": "[필수★] 가게의 미션을 도전 중인 미션에 추가(미션 도전하기) API", + "status": "todo", + "priority": "required", + "endpoint": "POST /api/v1/missions/:missionId/challenge", + "request_body": { + "memberId": "number (특정 사용자로 가정, DB 첫 번째 사용자)" + }, + "details": [ + "mission.dto.ts - MissionChallengeRequest 인터페이스 ({ memberId: number })", + "mission.repository.ts - findMemberMission(): SELECT * FROM member_mission WHERE member_id=? AND mission_id=?", + "mission.repository.ts - addMemberMission(): INSERT INTO member_mission(member_id, mission_id, status) VALUES(?,?,'CHALLENGING')", + "mission.service.ts - challengeMission(missionId, data): 중복 도전 검증 → addMemberMission", + " 검증 실패 시 throw new Error('이미 도전 중인 미션입니다.')", + "mission.controller.ts - handleChallengeMission: missionId = parseInt(req.params.missionId) → service", + "index.ts에 라우터 등록" + ], + "validation": "도전하려는 미션이 이미 도전 중이지는 않은지 검증 필요", + "reference_week4": "week4/src/repositories/mission.repository.ts, week4 schema.sql의 member_mission 테이블" + }, + { + "id": 12, + "phase": "추가 미션", + "title": "[공통 미션 2번] Controller → Service → Repository → DB 요청 흐름 정리", + "status": "todo", + "details": [ + "예: POST /api/v1/stores/:storeId/reviews 요청 흐름을 순서대로 작성", + "1. 사용자가 POST /api/v1/stores/1/reviews 요청 전송", + "2. index.ts의 라우터가 handleCreateReview 컨트롤러 호출", + "3. Controller: req.body를 ReviewCreateRequest 타입으로 변환(bodyToReview), storeId 파싱", + "4. Service: 가게 존재 여부 검증(findStoreById) → 없으면 Error throw", + "5. Repository: INSERT INTO review 쿼리 실행 → insertId 반환", + "6. Service: 결과를 DTO로 변환해 Controller에 반환", + "7. Controller: 201 JSON 응답 전송", + "워크북의 요약 정리 섹션에 이 내용 포함" + ] + }, + { + "id": 13, + "phase": "추가 미션", + "title": "[공통 미션 3번] 회원가입 API에 bcrypt 비밀번호 해싱 추가", + "status": "todo", + "details": [ + "npm install bcryptjs && npm install -D @types/bcryptjs (week4에 이미 설치됨)", + "member.dto.ts - MemberSignUpRequest 인터페이스에 password 필드 추가", + "member.repository.ts - addMember(): INSERT INTO member(..., password) VALUES(...)", + "member.service.ts - signUp(): const hashedPw = await bcrypt.hash(data.password, 10) → addMember에 전달", + "member.controller.ts - handleSignUp 작성", + "POST /api/v1/members/signup 라우터 등록" + ], + "reference_week4": "week4에서 bcryptjs 이미 사용 중 - week4/package.json 참고" + }, + { + "id": 14, + "phase": "시니어 미션", + "title": "[시니어] 전역 에러 핸들러 - JSON 형태 에러 응답", + "status": "todo", + "details": [ + "src/middleware/error.middleware.ts 생성", + "ErrorRequestHandler 타입 사용: (err, req, res, next) => void", + "res.status(err.status || 500).json({ success: false, message: err.message || '서버 에러' })", + "index.ts 맨 마지막에 app.use(errorMiddleware) 등록", + "Controller에서 try-catch 제거하거나 next(err) 사용으로 변경", + "기존 HTML 에러 응답 → JSON 에러 응답으로 개선" + ], + "reference_week4": "week4/src/middleware/error.middleware.ts 그대로 활용 가능" + }, + { + "id": 15, + "phase": "테스트", + "title": "Postman / curl로 각 API 호출 및 스크린샷 저장", + "status": "todo", + "details": [ + "npm run dev 로 서버 실행", + "API 1-1: POST /api/v1/stores - 가게 추가 성공 스크린샷", + "API 1-2: POST /api/v1/stores/:storeId/reviews - 리뷰 추가 성공 스크린샷", + "API 1-2: 존재하지 않는 storeId로 요청 → 에러 응답 스크린샷", + "API 1-3: POST /api/v1/stores/:storeId/missions - 미션 추가 성공 스크린샷", + "API 1-4: POST /api/v1/missions/:missionId/challenge - 도전 성공 스크린샷", + "API 1-4: 동일 미션 재도전 → '이미 도전 중' 에러 스크린샷", + "DB에서 SELECT로 데이터 삽입 확인 스크린샷" + ] + }, + { + "id": 16, + "phase": "마무리", + "title": "feature/chapter-05 브랜치에 push 및 PR 생성", + "status": "todo", + "details": [ + "git add . && git commit -m 'feat: 5주차 미션 - API 구현 (가게/리뷰/미션)'", + "git push origin feature/chapter-05", + "GitHub에서 PR 생성 (main 브랜치에 merge하지 말 것!)", + "워크북의 미션 기록란에 GitHub 링크 제출", + "GitHub 이슈 Close" + ] + }, + { + "id": 17, + "phase": "마무리", + "title": "핵심 키워드 및 요약 정리 작성", + "status": "todo", + "details": [ + "이 파일 상단의 keywords 섹션을 참고해 워크북에 기입", + "요약 정리: Controller→Service→Repository→DB 흐름 설명", + "위클리 스크럼 질문 답변: DTO 없이 사용할 때의 문제점 / Service Layer 필요성" + ] + } + ], + "required_apis_summary": { + "must_implement": ["1-2 (가게에 리뷰 추가하기)", "1-4 (미션 도전하기)"], + "minimum_count": "필수 2개 포함 총 3개 이상", + "senior_mission": "4개 전부 + JSON 에러 응답 개선" + }, + "key_differences_from_week4": { + "database": "in-memory(week4) → MySQL Connection Pool(week5)", + "modules": "flat 구조(week4) → 모듈형 모노리스(week5: src/modules/{도메인}/)", + "env": ".env 파일 추가 (dotenv 사용)", + "cors": "cors 미들웨어 추가", + "error_response": "HTML 에러(기본) → JSON 에러(시니어 미션 개선)" + } +} diff --git "a/\353\217\204\354\226\217/week6/tsconfig.json" "b/\353\217\204\354\226\217/week6/tsconfig.json" new file mode 100644 index 0000000..d25f8a3 --- /dev/null +++ "b/\353\217\204\354\226\217/week6/tsconfig.json" @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ESNext"], + "types": ["node"], + "strict": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} From 70ed5aee8b0aab6a5709626e724f1be903b4c648 Mon Sep 17 00:00:00 2001 From: higashiaka Date: Wed, 20 May 2026 11:14:33 +0900 Subject: [PATCH 7/9] =?UTF-8?q?feat:=EB=8F=84=EC=96=8F=207=EC=A3=BC?= =?UTF-8?q?=EC=B0=A8=20=EB=AF=B8=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "\353\217\204\354\226\217/week7/.gitignore" | 21 + "\353\217\204\354\226\217/week7/README.md" | 1 + .../week7/package-lock.json" | 3250 +++++++++++++++++ "\353\217\204\354\226\217/week7/package.json" | 28 + .../week7/prisma.config.ts" | 12 + .../migration.sql" | 39 + .../migration.sql" | 25 + .../migration.sql" | 33 + .../prisma/migrations/migration_lock.toml" | 3 + .../week7/prisma/schema.prisma" | 100 + "\353\217\204\354\226\217/week7/reset_db.sql" | 215 ++ .../week7/src/db.config.ts" | 17 + "\353\217\204\354\226\217/week7/src/index.ts" | 59 + .../week7/src/middleware/error.middleware.ts" | 38 + .../members/controllers/member.controller.ts" | 19 + .../src/modules/members/dtos/member.dto.ts" | 45 + .../repositories/member.repository.ts" | 26 + .../members/services/member.service.ts" | 39 + .../controllers/mission.controller.ts" | 71 + .../src/modules/missions/dtos/mission.dto.ts" | 57 + .../repositories/mission.repository.ts" | 72 + .../missions/services/mission.service.ts" | 128 + .../reviews/controllers/review.controller.ts" | 32 + .../src/modules/reviews/dtos/review.dto.ts" | 43 + .../repositories/review.repository.ts" | 30 + .../reviews/services/review.service.ts" | 50 + .../stores/controllers/store.controller.ts" | 31 + .../src/modules/stores/dtos/store.dto.ts" | 49 + .../stores/repositories/store.repository.ts" | 28 + .../modules/stores/services/store.service.ts" | 30 + .../week7/src/utils/errorCode.ts" | 26 + .../week7/src/utils/errors.ts" | 11 + .../week7/src/utils/response.ts" | 8 + .../week7/todolist.json" | 353 ++ .../week7/tsconfig.json" | 19 + .../week7/week7-commands.md" | 116 + .../week7/week7-workbook.md" | 282 ++ 37 files changed, 5406 insertions(+) create mode 100644 "\353\217\204\354\226\217/week7/.gitignore" create mode 100644 "\353\217\204\354\226\217/week7/README.md" create mode 100644 "\353\217\204\354\226\217/week7/package-lock.json" create mode 100644 "\353\217\204\354\226\217/week7/package.json" create mode 100644 "\353\217\204\354\226\217/week7/prisma.config.ts" create mode 100644 "\353\217\204\354\226\217/week7/prisma/migrations/20260428071132_init_database/migration.sql" create mode 100644 "\353\217\204\354\226\217/week7/prisma/migrations/20260428071239_add_store_and_review_tables/migration.sql" create mode 100644 "\353\217\204\354\226\217/week7/prisma/migrations/20260428072732_npx_prisma_migrate_dev/migration.sql" create mode 100644 "\353\217\204\354\226\217/week7/prisma/migrations/migration_lock.toml" create mode 100644 "\353\217\204\354\226\217/week7/prisma/schema.prisma" create mode 100644 "\353\217\204\354\226\217/week7/reset_db.sql" create mode 100644 "\353\217\204\354\226\217/week7/src/db.config.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/index.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/middleware/error.middleware.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/members/controllers/member.controller.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/members/dtos/member.dto.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/members/repositories/member.repository.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/members/services/member.service.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/missions/controllers/mission.controller.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/missions/dtos/mission.dto.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/missions/repositories/mission.repository.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/missions/services/mission.service.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/reviews/controllers/review.controller.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/reviews/dtos/review.dto.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/reviews/repositories/review.repository.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/reviews/services/review.service.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/stores/controllers/store.controller.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/stores/dtos/store.dto.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/stores/repositories/store.repository.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/modules/stores/services/store.service.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/utils/errorCode.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/utils/errors.ts" create mode 100644 "\353\217\204\354\226\217/week7/src/utils/response.ts" create mode 100644 "\353\217\204\354\226\217/week7/todolist.json" create mode 100644 "\353\217\204\354\226\217/week7/tsconfig.json" create mode 100644 "\353\217\204\354\226\217/week7/week7-commands.md" create mode 100644 "\353\217\204\354\226\217/week7/week7-workbook.md" diff --git "a/\353\217\204\354\226\217/week7/.gitignore" "b/\353\217\204\354\226\217/week7/.gitignore" new file mode 100644 index 0000000..928abd6 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/.gitignore" @@ -0,0 +1,21 @@ +# dependency directories +node_modules/ + +# build output +dist/ + +# dotenv environment variable files +.env +.env.local +.env.development +.env.production +.env.* + +# macOS +.DS_Store + +# logs +*.log +npm-debug.log* + +/src/generated/prisma diff --git "a/\353\217\204\354\226\217/week7/README.md" "b/\353\217\204\354\226\217/week7/README.md" new file mode 100644 index 0000000..bb278f1 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/README.md" @@ -0,0 +1 @@ +# umc-node-study \ No newline at end of file diff --git "a/\353\217\204\354\226\217/week7/package-lock.json" "b/\353\217\204\354\226\217/week7/package-lock.json" new file mode 100644 index 0000000..df28c13 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/package-lock.json" @@ -0,0 +1,3250 @@ +{ + "name": "umc-node-study", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@prisma/adapter-mariadb": "^7.8.0", + "@prisma/client": "^7.8.0", + "bcryptjs": "^3.0.3", + "cookie-parser": "^1.4.7", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "http-status-codes": "^2.3.0", + "morgan": "^1.10.1" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/morgan": "^1.9.10", + "@types/node": "^25.6.0", + "nodemon": "^3.1.14", + "prisma": "^7.8.0", + "tsx": "^4.21.0", + "typescript": "^6.0.3" + } + }, + "node_modules/@electric-sql/pglite": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", + "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz", + "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz", + "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@prisma/adapter-mariadb": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-mariadb/-/adapter-mariadb-7.8.0.tgz", + "integrity": "sha512-mWsgcfbUjxB3qSzRlLs8E03vsKrqXzYK2zpx3e8u6wIgeHJM/sE46cuOGcYvHiZGmeQLCd3xL6YSSGM9QOLI6w==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.8.0", + "mariadb": "3.4.5" + } + }, + "node_modules/@prisma/client": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", + "integrity": "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.8.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.8.0.tgz", + "integrity": "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", + "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.3.4", + "deepmerge-ts": "7.1.5", + "effect": "3.20.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz", + "integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz", + "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.4.1", + "@electric-sql/pglite-socket": "0.1.1", + "@electric-sql/pglite-tools": "0.3.1", + "@hono/node-server": "1.19.11", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "@prisma/streams-local": "0.1.2", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "^4.12.8", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.2.0", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz", + "integrity": "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/engines": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz", + "integrity": "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/fetch-engine": "7.8.0", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a.tgz", + "integrity": "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.8.0.tgz", + "integrity": "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/streams-local": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz", + "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "better-result": "^2.7.0", + "env-paths": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "engines": { + "bun": ">=1.3.6", + "node": ">=22.0.0" + } + }, + "node_modules/@prisma/studio-core": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz", + "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-toggle": "1.1.10", + "chart.js": "4.5.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0", + "pnpm": "8" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/better-result": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.0.tgz", + "integrity": "sha512-NHwGDGVbRlWDOce3CwcfGIrcNR9zY37ut3SVwQVfv57DZdVhxjhA4mfaHN1n8QwWnRAR4iErpW1X/eaiaUaFYg==", + "devOptional": true, + "license": "MIT" + }, + "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": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/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/c12": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "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/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/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/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT", + "peer": true + }, + "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/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "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/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "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/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/effect": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", + "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "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/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "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": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "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-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "devOptional": true, + "license": "MIT" + }, + "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/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz", + "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", + "devOptional": true, + "license": "MIT", + "bin": { + "giget": "dist/cli.mjs" + } + }, + "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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "devOptional": true, + "license": "MIT" + }, + "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/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.15", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz", + "integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "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/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-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/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/mariadb": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.4.5.tgz", + "integrity": "sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ==", + "license": "LGPL-2.1-or-later", + "dependencies": { + "@types/geojson": "^7946.0.16", + "@types/node": "^24.0.13", + "denque": "^2.1.0", + "iconv-lite": "^0.6.3", + "lru-cache": "^10.4.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/mariadb/node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/mariadb/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "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": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/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/morgan/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/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "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/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/nodemon/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/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/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/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "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/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/prisma": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz", + "integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.8.0", + "@prisma/dev": "0.24.3", + "@prisma/engines": "7.8.0", + "@prisma/studio-core": "0.27.3", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/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==", + "devOptional": true, + "license": "ISC" + }, + "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/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "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": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/rc9": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", + "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.6", + "destr": "^2.0.5" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "devOptional": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "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/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "devOptional": true, + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "devOptional": true, + "license": "MIT" + }, + "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/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/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "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/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + } + } +} diff --git "a/\353\217\204\354\226\217/week7/package.json" "b/\353\217\204\354\226\217/week7/package.json" new file mode 100644 index 0000000..8502f59 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/package.json" @@ -0,0 +1,28 @@ +{ + "scripts": { + "dev": "nodemon --ext ts,prisma --ignore src/generated --exec \"npx prisma generate && tsx src/index.ts\"" + }, + "dependencies": { + "@prisma/adapter-mariadb": "^7.8.0", + "@prisma/client": "^7.8.0", + "bcryptjs": "^3.0.3", + "cookie-parser": "^1.4.7", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "http-status-codes": "^2.3.0", + "morgan": "^1.10.1" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/morgan": "^1.9.10", + "@types/node": "^25.6.0", + "nodemon": "^3.1.14", + "prisma": "^7.8.0", + "tsx": "^4.21.0", + "typescript": "^6.0.3" + } +} diff --git "a/\353\217\204\354\226\217/week7/prisma.config.ts" "b/\353\217\204\354\226\217/week7/prisma.config.ts" new file mode 100644 index 0000000..5170cc4 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/prisma.config.ts" @@ -0,0 +1,12 @@ +/// +import "dotenv/config"; +import { defineConfig } from "prisma/config"; + +const { DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME } = process.env; + +export default defineConfig({ + schema: "prisma/schema.prisma", + datasource: { + url: `mysql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT ?? 3306}/${DB_NAME}`, + }, +}); diff --git "a/\353\217\204\354\226\217/week7/prisma/migrations/20260428071132_init_database/migration.sql" "b/\353\217\204\354\226\217/week7/prisma/migrations/20260428071132_init_database/migration.sql" new file mode 100644 index 0000000..31b12a1 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/prisma/migrations/20260428071132_init_database/migration.sql" @@ -0,0 +1,39 @@ +-- CreateTable +CREATE TABLE `user` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `email` VARCHAR(255) NOT NULL, + `name` VARCHAR(100) NOT NULL, + `gender` VARCHAR(15) NOT NULL, + `birth` DATE NOT NULL, + `address` VARCHAR(255) NOT NULL, + `detail_address` VARCHAR(255) NULL, + `phone_number` VARCHAR(15) NOT NULL, + + UNIQUE INDEX `email`(`email`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `food_category` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `user_favor_category` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `food_category_id` INTEGER NOT NULL, + + INDEX `f_category_id`(`food_category_id`), + INDEX `user_id`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `user_favor_category` ADD CONSTRAINT `user_favor_category_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_favor_category` ADD CONSTRAINT `user_favor_category_food_category_id_fkey` FOREIGN KEY (`food_category_id`) REFERENCES `food_category`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git "a/\353\217\204\354\226\217/week7/prisma/migrations/20260428071239_add_store_and_review_tables/migration.sql" "b/\353\217\204\354\226\217/week7/prisma/migrations/20260428071239_add_store_and_review_tables/migration.sql" new file mode 100644 index 0000000..99445d4 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/prisma/migrations/20260428071239_add_store_and_review_tables/migration.sql" @@ -0,0 +1,25 @@ +-- CreateTable +CREATE TABLE `store` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `user_store_review` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `store_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `content` TEXT NOT NULL, + + INDEX `store_id`(`store_id`), + INDEX `user_id`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `user_store_review` ADD CONSTRAINT `user_store_review_store_id_fkey` FOREIGN KEY (`store_id`) REFERENCES `store`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_store_review` ADD CONSTRAINT `user_store_review_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git "a/\353\217\204\354\226\217/week7/prisma/migrations/20260428072732_npx_prisma_migrate_dev/migration.sql" "b/\353\217\204\354\226\217/week7/prisma/migrations/20260428072732_npx_prisma_migrate_dev/migration.sql" new file mode 100644 index 0000000..a6dd8b0 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/prisma/migrations/20260428072732_npx_prisma_migrate_dev/migration.sql" @@ -0,0 +1,33 @@ +-- CreateTable +CREATE TABLE `mission` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `store_id` INTEGER NOT NULL, + `title` VARCHAR(200) NOT NULL, + `reward` INTEGER NOT NULL, + `spec` TEXT NULL, + `dead_line` DATETIME(3) NULL, + + INDEX `store_id`(`store_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `member_mission` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `member_id` INTEGER NOT NULL, + `mission_id` INTEGER NOT NULL, + `status` VARCHAR(15) NOT NULL, + + INDEX `member_id`(`member_id`), + INDEX `mission_id`(`mission_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `mission` ADD CONSTRAINT `mission_store_id_fkey` FOREIGN KEY (`store_id`) REFERENCES `store`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `member_mission` ADD CONSTRAINT `member_mission_member_id_fkey` FOREIGN KEY (`member_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `member_mission` ADD CONSTRAINT `member_mission_mission_id_fkey` FOREIGN KEY (`mission_id`) REFERENCES `mission`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git "a/\353\217\204\354\226\217/week7/prisma/migrations/migration_lock.toml" "b/\353\217\204\354\226\217/week7/prisma/migrations/migration_lock.toml" new file mode 100644 index 0000000..592fc0b --- /dev/null +++ "b/\353\217\204\354\226\217/week7/prisma/migrations/migration_lock.toml" @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "mysql" diff --git "a/\353\217\204\354\226\217/week7/prisma/schema.prisma" "b/\353\217\204\354\226\217/week7/prisma/schema.prisma" new file mode 100644 index 0000000..a5758e0 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/prisma/schema.prisma" @@ -0,0 +1,100 @@ +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +datasource db { + provider = "mysql" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique(map: "email") @db.VarChar(255) + name String @db.VarChar(100) + gender String @db.VarChar(15) + birth DateTime @db.Date + address String @db.VarChar(255) + detailAddress String? @map("detail_address") @db.VarChar(255) + phoneNumber String @map("phone_number") @db.VarChar(15) + + userFavorCategories UserFavorCategory[] + reviews UserStoreReview[] + memberMissions MemberMission[] + + @@map("user") +} + +model FoodCategory { + id Int @id @default(autoincrement()) + name String @db.VarChar(100) + + userFavorCategories UserFavorCategory[] + + @@map("food_category") +} + +model UserFavorCategory { + id Int @id @default(autoincrement()) + userId Int @map("user_id") + foodCategoryId Int @map("food_category_id") + user User @relation(fields: [userId], references: [id]) + foodCategory FoodCategory @relation(fields: [foodCategoryId], references: [id]) + + @@index([foodCategoryId], map: "f_category_id") + @@index([userId], map: "user_id") + @@map("user_favor_category") +} + +model Store { + id Int @id @default(autoincrement()) + name String @db.VarChar(100) + + reviews UserStoreReview[] + missions Mission[] + + @@map("store") +} + +model UserStoreReview { + id Int @id @default(autoincrement()) + storeId Int @map("store_id") + userId Int @map("user_id") + content String @db.Text + + store Store @relation(fields: [storeId], references: [id]) + user User @relation(fields: [userId], references: [id]) + + @@index([storeId], map: "store_id") + @@index([userId], map: "user_id") + @@map("user_store_review") +} + +model Mission { + id Int @id @default(autoincrement()) + storeId Int @map("store_id") + title String @db.VarChar(200) + reward Int + spec String? @db.Text + deadLine DateTime? @map("dead_line") + + store Store @relation(fields: [storeId], references: [id]) + memberMissions MemberMission[] + + @@index([storeId], map: "store_id") + @@map("mission") +} + +model MemberMission { + id Int @id @default(autoincrement()) + userId Int @map("member_id") + missionId Int @map("mission_id") + status String @db.VarChar(15) + + user User @relation(fields: [userId], references: [id]) + mission Mission @relation(fields: [missionId], references: [id]) + + @@index([userId], map: "member_id") + @@index([missionId], map: "mission_id") + @@map("member_mission") +} + diff --git "a/\353\217\204\354\226\217/week7/reset_db.sql" "b/\353\217\204\354\226\217/week7/reset_db.sql" new file mode 100644 index 0000000..454f20b --- /dev/null +++ "b/\353\217\204\354\226\217/week7/reset_db.sql" @@ -0,0 +1,215 @@ +-- ============================================================ +-- DB 초기화 및 재생성 스크립트 +-- ============================================================ + +CREATE DATABASE IF NOT EXISTS umc_mission DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE umc_mission; + +-- FK 체크 비활성화 후 전체 DROP +SET FOREIGN_KEY_CHECKS = 0; + +DROP TABLE IF EXISTS review_image; +DROP TABLE IF EXISTS review; +DROP TABLE IF EXISTS member_mission; +DROP TABLE IF EXISTS mission; +DROP TABLE IF EXISTS store_hours; +DROP TABLE IF EXISTS store_image; +DROP TABLE IF EXISTS store; +DROP TABLE IF EXISTS member_prefer; +DROP TABLE IF EXISTS member_agree; +DROP TABLE IF EXISTS member; +DROP TABLE IF EXISTS terms; +DROP TABLE IF EXISTS food_category; +DROP TABLE IF EXISTS region; + +SET FOREIGN_KEY_CHECKS = 1; + +-- ============================================================ +-- 테이블 재생성 +-- ============================================================ + +CREATE TABLE region ( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL COMMENT '지역명', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE food_category ( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL COMMENT '카테고리명 (한식, 중식, 일식 등)', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE terms ( + id BIGINT NOT NULL AUTO_INCREMENT, + title VARCHAR(100) NOT NULL COMMENT '약관 제목', + content TEXT NOT NULL COMMENT '약관 내용', + optional BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'TRUE: 선택 동의, FALSE: 필수 동의', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE member ( + id BIGINT NOT NULL AUTO_INCREMENT, + social_type VARCHAR(20) NULL, + social_id VARCHAR(100) NULL, + email VARCHAR(100) NULL, + password VARCHAR(255) NULL, + name VARCHAR(50) NOT NULL, + nickname VARCHAR(50) NOT NULL, + profile_image_url VARCHAR(500) NULL, + phone_num VARCHAR(20) NULL, + phone_verified BOOLEAN NOT NULL DEFAULT FALSE, + birth DATE NULL, + gender ENUM('MALE', 'FEMALE', 'OTHER') NULL, + address VARCHAR(200) NULL, + spec_address VARCHAR(200) NULL, + point INT NOT NULL DEFAULT 0, + status ENUM('ACTIVE', 'INACTIVE', 'BANNED') NOT NULL DEFAULT 'ACTIVE', + inactive_date DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_member_email (email), + UNIQUE KEY uq_member_social (social_type, social_id) +); + +CREATE TABLE member_agree ( + member_id BIGINT NOT NULL, + terms_id BIGINT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (member_id, terms_id), + CONSTRAINT fk_member_agree_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_member_agree_terms FOREIGN KEY (terms_id) REFERENCES terms (id) +); + +CREATE TABLE member_prefer ( + member_id BIGINT NOT NULL, + food_id BIGINT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (member_id, food_id), + CONSTRAINT fk_member_prefer_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_member_prefer_food FOREIGN KEY (food_id) REFERENCES food_category (id) +); + +CREATE TABLE store ( + id BIGINT NOT NULL AUTO_INCREMENT, + region_id BIGINT NOT NULL, + food_category_id BIGINT NOT NULL, + name VARCHAR(100) NOT NULL, + description TEXT NULL, + lat DECIMAL(10,7) NULL, + lng DECIMAL(10,7) NULL, + address VARCHAR(200) NOT NULL, + status ENUM('OPEN', 'CLOSED', 'PENDING') NOT NULL DEFAULT 'OPEN', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_store_region FOREIGN KEY (region_id) REFERENCES region (id), + CONSTRAINT fk_store_category FOREIGN KEY (food_category_id) REFERENCES food_category (id) +); + +CREATE TABLE store_image ( + id BIGINT NOT NULL AUTO_INCREMENT, + store_id BIGINT NOT NULL, + image_url VARCHAR(500) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_store_image_store FOREIGN KEY (store_id) REFERENCES store (id) +); + +CREATE TABLE store_hours ( + id BIGINT NOT NULL AUTO_INCREMENT, + store_id BIGINT NOT NULL, + day_of_week VARCHAR(3) NOT NULL, + open_time TIME NOT NULL, + close_time TIME NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_store_hours_day (store_id, day_of_week), + CONSTRAINT fk_store_hours_store FOREIGN KEY (store_id) REFERENCES store (id) +); + +CREATE TABLE mission ( + id BIGINT NOT NULL AUTO_INCREMENT, + store_id BIGINT NOT NULL, + title VARCHAR(200) NOT NULL, + reward INT NOT NULL DEFAULT 0, + spec VARCHAR(500) NULL, + dead_line DATE NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_mission_store FOREIGN KEY (store_id) REFERENCES store (id) +); + +CREATE TABLE member_mission ( + id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + mission_id BIGINT NOT NULL, + status ENUM('CHALLENGING', 'COMPLETE') NOT NULL DEFAULT 'CHALLENGING', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_member_mission (member_id, mission_id), + CONSTRAINT fk_member_mission_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_member_mission_mission FOREIGN KEY (mission_id) REFERENCES mission (id) +); + +CREATE TABLE review ( + id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + store_id BIGINT NOT NULL, + member_mission_id BIGINT NULL, + content TEXT NOT NULL, + score DECIMAL(2,1) NOT NULL, + owner_reply VARCHAR(500) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_review_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_review_store FOREIGN KEY (store_id) REFERENCES store (id), + CONSTRAINT fk_review_member_mission FOREIGN KEY (member_mission_id) REFERENCES member_mission (id), + CONSTRAINT chk_review_score CHECK (score BETWEEN 1.0 AND 5.0) +); + +CREATE TABLE review_image ( + id BIGINT NOT NULL AUTO_INCREMENT, + review_id BIGINT NOT NULL, + image_url VARCHAR(500) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_review_image_review FOREIGN KEY (review_id) REFERENCES review (id) +); + +-- ============================================================ +-- 시드 데이터 (API 테스트용 초기 데이터) +-- ============================================================ + +INSERT INTO region (name) VALUES + ('서울'), + ('경기'), + ('인천'), + ('부산'), + ('대구'); + +INSERT INTO food_category (name) VALUES + ('한식'), + ('중식'), + ('일식'), + ('양식'), + ('분식'), + ('카페/디저트'), + ('치킨'), + ('피자'), + ('패스트푸드'); diff --git "a/\353\217\204\354\226\217/week7/src/db.config.ts" "b/\353\217\204\354\226\217/week7/src/db.config.ts" new file mode 100644 index 0000000..7b88907 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/db.config.ts" @@ -0,0 +1,17 @@ +import "dotenv/config"; +import { PrismaClient } from "./generated/prisma/client.js"; +import { PrismaMariaDb } from "@prisma/adapter-mariadb"; + +const adapter = new PrismaMariaDb({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + port: process.env.DB_PORT ? parseInt(process.env.DB_PORT, 10) : 3306, + connectionLimit: 10, +}); + +export const prisma = new PrismaClient({ + adapter, + log: ["query", "info", "error", "warn"], +}); diff --git "a/\353\217\204\354\226\217/week7/src/index.ts" "b/\353\217\204\354\226\217/week7/src/index.ts" new file mode 100644 index 0000000..142e52c --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/index.ts" @@ -0,0 +1,59 @@ +import dotenv from 'dotenv' +import express, { Express, Request, Response } from 'express' +import cors from 'cors' +import morgan from 'morgan' +import cookieParser from 'cookie-parser' + +// 컨트롤러 import +import { handleCreateStore, handleListStoreReviews } from './modules/stores/controllers/store.controller.js' +import { handleListUserReviews } from './modules/reviews/controllers/review.controller.js' +import { handleCreateReview } from './modules/reviews/controllers/review.controller.js' +import { handleCreateMission, handleChallengeMission, handleListStoreMissions, handleListOngoingMissions, handleCompleteMission } from './modules/missions/controllers/mission.controller.js' +import { handleSignUp } from './modules/members/controllers/member.controller.js' + +// 에러 미들웨어 import +import { errorMiddleware } from './middleware/error.middleware.js' + +// 1. 환경 변수 설정 (가장 먼저 호출) +dotenv.config() + +const app: Express = express() +const port = process.env.PORT ?? 3000 + +// 2. 미들웨어 설정 +app.use(morgan('dev')) +app.use(cookieParser()) +app.use(cors()) +app.use(express.json()) +app.use(express.urlencoded({ extended: false })) + +// 3. 라우터 등록 +app.get('/', (_req: Request, res: Response) => { + res.send('UMC 7주차 서버 실행 중!') +}) + +// 회원 +app.post('/api/v1/members/signup', handleSignUp) +app.get('/api/v1/users/:userId/reviews', handleListUserReviews) +app.get('/api/v1/users/:userId/missions', handleListOngoingMissions) + +// 가게 +app.post('/api/v1/stores', handleCreateStore) +app.post('/api/v1/stores/:storeId/reviews', handleCreateReview) +app.get('/api/v1/stores/:storeId/reviews', handleListStoreReviews) +app.get('/api/v1/stores/:storeId/missions', handleListStoreMissions) +app.post('/api/v1/stores/:storeId/missions', handleCreateMission) + +// 미션 +app.post('/api/v1/missions/:missionId/challenge', handleChallengeMission) +app.patch('/api/v1/users/:userId/missions/:missionId', handleCompleteMission) + + + +// 4. 전역 에러 핸들러 (반드시 라우터 등록 이후에 위치) +app.use(errorMiddleware) + +// 5. 서버 시작 +app.listen(port, () => { + console.log(`[server]: Server is running at http://localhost:${port}`) +}) diff --git "a/\353\217\204\354\226\217/week7/src/middleware/error.middleware.ts" "b/\353\217\204\354\226\217/week7/src/middleware/error.middleware.ts" new file mode 100644 index 0000000..abd0e3a --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/middleware/error.middleware.ts" @@ -0,0 +1,38 @@ +import { Request, Response, NextFunction } from 'express' +import { BaseError } from '../utils/errors.js' + +export const errorMiddleware = ( + err: unknown, + _req: Request, + res: Response, + _next: NextFunction, +): void => { + if (err instanceof BaseError) { + res.status(err.status).json({ + isSuccess: false, + code: err.code, + message: err.message, + result: null, + }) + return + } + + // Prisma unique constraint 에러 + if (typeof err === 'object' && err !== null && (err as { code?: string }).code === 'P2002') { + res.status(409).json({ + isSuccess: false, + code: 'USER4002', + message: '이미 존재하는 데이터입니다.', + result: null, + }) + return + } + + console.error(err) + res.status(500).json({ + isSuccess: false, + code: 'COMMON500', + message: '서버 내부 오류입니다.', + result: null, + }) +} diff --git "a/\353\217\204\354\226\217/week7/src/modules/members/controllers/member.controller.ts" "b/\353\217\204\354\226\217/week7/src/modules/members/controllers/member.controller.ts" new file mode 100644 index 0000000..39bd45c --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/members/controllers/member.controller.ts" @@ -0,0 +1,19 @@ +import { Request, Response, NextFunction } from 'express' +import { StatusCodes } from 'http-status-codes' +import { MemberSignUpRequest } from '../dtos/member.dto.js' +import { signUp } from '../services/member.service.js' +import { BaseResponse } from '../../../utils/response.js' + +// POST /api/v1/members/signup +export const handleSignUp = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const result = await signUp(req.body as MemberSignUpRequest) + res.status(StatusCodes.CREATED).json(BaseResponse(result)) + } catch (err) { + next(err) + } +} diff --git "a/\353\217\204\354\226\217/week7/src/modules/members/dtos/member.dto.ts" "b/\353\217\204\354\226\217/week7/src/modules/members/dtos/member.dto.ts" new file mode 100644 index 0000000..b0cbf3f --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/members/dtos/member.dto.ts" @@ -0,0 +1,45 @@ +// 회원가입 요청 인터페이스 +export interface MemberSignUpRequest { + name: string + nickname: string + email?: string + password?: string + phoneNum?: string + birth?: string // "YYYY-MM-DD" + gender?: string // "MALE" | "FEMALE" | "OTHER" + address?: string + specAddress?: string +} + +// req.body → 내부 데이터로 변환 +export const bodyToMember = (body: MemberSignUpRequest) => { + return { + name: body.name, + nickname: body.nickname, + email: body.email ?? null, + phoneNum: body.phoneNum ?? null, + birth: body.birth ? new Date(body.birth) : null, + gender: body.gender ?? null, + address: body.address ?? null, + specAddress: body.specAddress ?? null, + } +} + +// DB 결과 → 응답 형태로 변환 +export const responseFromMember = (member: { + id: number + name: string + nickname: string + email: string | null + phone_num: string | null + status: string +}) => { + return { + memberId: member.id, + name: member.name, + nickname: member.nickname, + email: member.email, + phoneNum: member.phone_num, + status: member.status, + } +} diff --git "a/\353\217\204\354\226\217/week7/src/modules/members/repositories/member.repository.ts" "b/\353\217\204\354\226\217/week7/src/modules/members/repositories/member.repository.ts" new file mode 100644 index 0000000..549030e --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/members/repositories/member.repository.ts" @@ -0,0 +1,26 @@ +import { prisma } from '../../../db.config.js' + +// 유저 생성 +export const addUser = async (data: any) => { + const exists = await prisma.user.findFirst({ where: { email: data.email } }) + if (exists) return null + + const created = await prisma.user.create({ data }) + return created.id +} + +// 유저 조회 (없으면 예외 throw) +export const getUser = async (userId: number) => + prisma.user.findFirstOrThrow({ where: { id: userId } }) + +// 선호 음식 카테고리 등록 +export const setPreference = async (userId: number, foodCategoryId: number) => + prisma.userFavorCategory.create({ data: { userId, foodCategoryId } }) + +// 선호 카테고리 목록 조회 (JOIN 포함) +export const getUserPreferencesByUserId = async (userId: number) => + prisma.userFavorCategory.findMany({ + where: { userId }, + include: { foodCategory: true }, + orderBy: { foodCategoryId: 'asc' }, + }) diff --git "a/\353\217\204\354\226\217/week7/src/modules/members/services/member.service.ts" "b/\353\217\204\354\226\217/week7/src/modules/members/services/member.service.ts" new file mode 100644 index 0000000..2cf928d --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/members/services/member.service.ts" @@ -0,0 +1,39 @@ +import bcrypt from 'bcryptjs' +import { MemberSignUpRequest, bodyToMember, responseFromMember } from '../dtos/member.dto.js' +import { addUser, getUser } from '../repositories/member.repository.js' +import { BaseError } from '../../../utils/errors.js' +import { ErrorCode } from '../../../utils/errorCode.js' + +export const signUp = async (data: MemberSignUpRequest) => { + if (!data.name || !data.nickname) { + throw new BaseError( + ErrorCode.MEMBER_REQUIRED_FIELD.message, + ErrorCode.MEMBER_REQUIRED_FIELD.status, + ErrorCode.MEMBER_REQUIRED_FIELD.code, + ) + } + + const hashedPassword = data.password ? await bcrypt.hash(data.password, 10) : null + + const memberData = bodyToMember(data) + const memberId = await addUser({ ...memberData, hashedPassword }) + + if (memberId === null) { + throw new BaseError( + ErrorCode.DUPLICATE_EMAIL.message, + ErrorCode.DUPLICATE_EMAIL.status, + ErrorCode.DUPLICATE_EMAIL.code, + ) + } + + const member = await getUser(memberId) + + return responseFromMember(member as { + id: number + name: string + nickname: string + email: string | null + phone_num: string | null + status: string + }) +} diff --git "a/\353\217\204\354\226\217/week7/src/modules/missions/controllers/mission.controller.ts" "b/\353\217\204\354\226\217/week7/src/modules/missions/controllers/mission.controller.ts" new file mode 100644 index 0000000..e3057f1 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/missions/controllers/mission.controller.ts" @@ -0,0 +1,71 @@ +import { Request, Response, NextFunction } from 'express' +import { StatusCodes } from 'http-status-codes' +import { MissionCreateRequest, MissionChallengeRequest } from '../dtos/mission.dto.js' +import { createMission, challengeMission, listStoreMissions, listOngoingMissions, finishMission } from '../services/mission.service.js' +import { BaseResponse } from '../../../utils/response.js' + +// GET /api/v1/users/:userId/missions +export const handleListOngoingMissions = async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = parseInt(String(req.params['userId'] ?? '0'), 10) + const cursor = typeof req.query['cursor'] === 'string' ? parseInt(req.query['cursor'], 10) : 0 + const result = await listOngoingMissions(userId, cursor) + res.status(StatusCodes.OK).json(BaseResponse(result)) + } catch (err) { + next(err) + } +} + +// GET /api/v1/stores/:storeId/missions +export const handleListStoreMissions = async (req: Request, res: Response, next: NextFunction) => { + try { + const storeId = parseInt(String(req.params['storeId'] ?? '0'), 10) + const cursor = typeof req.query['cursor'] === 'string' ? parseInt(req.query['cursor'], 10) : 0 + const result = await listStoreMissions(storeId, cursor) + res.status(StatusCodes.OK).json(BaseResponse(result)) + } catch (err) { + next(err) + } +} + +// POST /api/v1/stores/:storeId/missions +export const handleCreateMission = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const storeId = parseInt(String(req.params['storeId'] ?? '0'), 10) + const result = await createMission(storeId, req.body as MissionCreateRequest) + res.status(StatusCodes.CREATED).json(BaseResponse(result)) + } catch (err) { + next(err) + } +} + +// POST /api/v1/missions/:missionId/challenge +export const handleChallengeMission = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const missionId = parseInt(String(req.params['missionId'] ?? '0'), 10) + const result = await challengeMission(missionId, req.body as MissionChallengeRequest) + res.status(StatusCodes.CREATED).json(BaseResponse(result)) + } catch (err) { + next(err) + } +} + +// PATCH /api/v1/users/:userId/missions/:missionId +export const handleCompleteMission = async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = parseInt(String(req.params['userId'] ?? '0'), 10) + const missionId = parseInt(String(req.params['missionId'] ?? '0'), 10) + const result = await finishMission(userId, missionId) + res.status(StatusCodes.OK).json(BaseResponse(result)) + } catch (err) { + next(err) + } +} diff --git "a/\353\217\204\354\226\217/week7/src/modules/missions/dtos/mission.dto.ts" "b/\353\217\204\354\226\217/week7/src/modules/missions/dtos/mission.dto.ts" new file mode 100644 index 0000000..8786670 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/missions/dtos/mission.dto.ts" @@ -0,0 +1,57 @@ +// 미션 추가 요청 인터페이스 +export interface MissionCreateRequest { + title: string + reward: number + spec?: string + deadLine?: string // "YYYY-MM-DD" +} + +// 미션 도전 요청 인터페이스 +export interface MissionChallengeRequest { + memberId: number + status: 'CHALLENGING' | 'COMPLETE' +} + +// req.body → 내부 데이터로 변환 (미션 추가) +export const bodyToMission = (body: MissionCreateRequest) => { + return { + title: body.title, + reward: body.reward, + spec: body.spec ?? null, + deadLine: body.deadLine ? new Date(body.deadLine) : null, + } +} + +// DB 결과 → 응답 형태로 변환 (미션) +export const responseFromMission = (mission: { + id: number + store_id: number + title: string + reward: number + spec: string | null + dead_line: Date | null +}) => { + return { + missionId: mission.id, + storeId: mission.store_id, + title: mission.title, + reward: mission.reward, + spec: mission.spec, + deadLine: mission.dead_line, + } +} + +// DB 결과 → 응답 형태로 변환 (미션 도전) +export const responseFromMemberMission = (mm: { + id: number + member_id: number + mission_id: number + status: string +}) => { + return { + memberMissionId: mm.id, + memberId: mm.member_id, + missionId: mm.mission_id, + status: mm.status, + } +} diff --git "a/\353\217\204\354\226\217/week7/src/modules/missions/repositories/mission.repository.ts" "b/\353\217\204\354\226\217/week7/src/modules/missions/repositories/mission.repository.ts" new file mode 100644 index 0000000..bee3e62 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/missions/repositories/mission.repository.ts" @@ -0,0 +1,72 @@ +import { prisma } from '../../../db.config.js' + +// 미션 추가 +export const addMission = async (data: { + storeId: number + title: string + reward: number + spec: string | null + deadLine: Date | null +}): Promise => { + const created = await prisma.mission.create({ + data: { + storeId: data.storeId, + title: data.title, + reward: data.reward, + spec: data.spec, + deadLine: data.deadLine, + }, + }) + return created.id +} + +// 미션 조회 +export const getMissionById = async (missionId: number) => + prisma.mission.findFirst({ where: { id: missionId } }) + +// 이미 도전 중인 미션인지 확인 +export const findMemberMission = async (memberId: number, missionId: number) => + prisma.memberMission.findFirst({ + where: { userId: memberId, missionId }, + }) + +// 미션 도전 추가 +export const addMemberMission = async ( + memberId: number, + missionId: number, + status: string, +): Promise => { + const created = await prisma.memberMission.create({ + data: { userId: memberId, missionId, status }, + }) + return created.id +} + +// 미션 도전 기록 조회 +export const getMemberMissionById = async (memberMissionId: number) => + prisma.memberMission.findFirst({ where: { id: memberMissionId } }) + +// 특정 가게의 미션 목록 조회 (커서 기반 페이지네이션) +export const getStoreMissions = async (storeId: number, cursor: number) => + prisma.mission.findMany({ + where: { storeId, id: { gt: cursor } }, + orderBy: { id: 'asc' }, + take: 5, + }) + + +// 유저가 진행 중인 미션 목록 조회 +export const getOngoingMissions = async (userId: number, cursor: number) => + prisma.memberMission.findMany({ + where: { userId, status: '진행중', id: { gt: cursor } }, + include: { mission: { include: { store: true } } }, + orderBy: { id: 'asc' }, + take: 5, + }) + +// 진행 중인 미션을 완료로 변경 +export const completeMission = async (userId: number, missionId: number) => + prisma.memberMission.updateMany({ + where: { userId, missionId, status: '진행중' }, + data: { status: '완료' }, + }) \ No newline at end of file diff --git "a/\353\217\204\354\226\217/week7/src/modules/missions/services/mission.service.ts" "b/\353\217\204\354\226\217/week7/src/modules/missions/services/mission.service.ts" new file mode 100644 index 0000000..92018a3 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/missions/services/mission.service.ts" @@ -0,0 +1,128 @@ +import { + MissionCreateRequest, + MissionChallengeRequest, + bodyToMission, + responseFromMission, + responseFromMemberMission, +} from '../dtos/mission.dto.js' +import { + addMission, + getMissionById, + findMemberMission, + addMemberMission, + getMemberMissionById, + getStoreMissions, + getOngoingMissions, + completeMission, +} from '../repositories/mission.repository.js' +import { getStoreById } from '../../stores/repositories/store.repository.js' +import { BaseError } from '../../../utils/errors.js' +import { ErrorCode } from '../../../utils/errorCode.js' + +export const createMission = async (storeId: number, data: MissionCreateRequest) => { + const store = await getStoreById(storeId) + if (!store) { + throw new BaseError( + ErrorCode.STORE_NOT_FOUND.message, + ErrorCode.STORE_NOT_FOUND.status, + ErrorCode.STORE_NOT_FOUND.code, + ) + } + + const missionData = bodyToMission(data) + const missionId = await addMission({ ...missionData, storeId }) + + const mission = await getMissionById(missionId) + if (!mission) { + throw new BaseError( + ErrorCode.MISSION_CREATE_FAILED.message, + ErrorCode.MISSION_CREATE_FAILED.status, + ErrorCode.MISSION_CREATE_FAILED.code, + ) + } + + return responseFromMission(mission as unknown as { + id: number + store_id: number + title: string + reward: number + spec: string | null + dead_line: Date | null + }) +} + +export const listStoreMissions = async (storeId: number, cursor: number) => { + const missions = await getStoreMissions(storeId, cursor) + const last = missions[missions.length - 1] + return { + data: missions, + pagination: { cursor: last ? last.id : null }, + } +} + +export const listOngoingMissions = async (userId: number, cursor: number) => { + const missions = await getOngoingMissions(userId, cursor) + const last = missions[missions.length - 1] + return { + data: missions, + pagination: { cursor: last ? last.id : null }, + } +} + +export const finishMission = async (userId: number, missionId: number) => { + const result = await completeMission(userId, missionId) + if (result.count === 0) { + throw new BaseError( + ErrorCode.ONGOING_MISSION_NOT_FOUND.message, + ErrorCode.ONGOING_MISSION_NOT_FOUND.status, + ErrorCode.ONGOING_MISSION_NOT_FOUND.code, + ) + } + return { message: '미션이 완료 처리됐습니다.' } +} + +export const challengeMission = async (missionId: number, data: MissionChallengeRequest) => { + const mission = await getMissionById(missionId) + if (!mission) { + throw new BaseError( + ErrorCode.MISSION_NOT_FOUND.message, + ErrorCode.MISSION_NOT_FOUND.status, + ErrorCode.MISSION_NOT_FOUND.code, + ) + } + + if (!data.status) { + throw new BaseError( + ErrorCode.MISSION_STATUS_REQUIRED.message, + ErrorCode.MISSION_STATUS_REQUIRED.status, + ErrorCode.MISSION_STATUS_REQUIRED.code, + ) + } + + const existing = await findMemberMission(data.memberId, missionId) + if (existing) { + throw new BaseError( + ErrorCode.MISSION_ALREADY_CHALLENGING.message, + ErrorCode.MISSION_ALREADY_CHALLENGING.status, + ErrorCode.MISSION_ALREADY_CHALLENGING.code, + ) + } + + const memberMissionId = await addMemberMission(data.memberId, missionId, data.status) + + const memberMission = await getMemberMissionById(memberMissionId) + if (!memberMission) { + throw new BaseError( + ErrorCode.MISSION_CHALLENGE_FAILED.message, + ErrorCode.MISSION_CHALLENGE_FAILED.status, + ErrorCode.MISSION_CHALLENGE_FAILED.code, + ) + } + + return responseFromMemberMission(memberMission as unknown as { + id: number + member_id: number + mission_id: number + status: string + }) +} diff --git "a/\353\217\204\354\226\217/week7/src/modules/reviews/controllers/review.controller.ts" "b/\353\217\204\354\226\217/week7/src/modules/reviews/controllers/review.controller.ts" new file mode 100644 index 0000000..9d73016 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/reviews/controllers/review.controller.ts" @@ -0,0 +1,32 @@ +import { Request, Response, NextFunction } from 'express' +import { StatusCodes } from 'http-status-codes' +import { ReviewCreateRequest } from '../dtos/review.dto.js' +import { createReview, listUserReviews } from '../services/review.service.js' +import { BaseResponse } from '../../../utils/response.js' + +// GET /api/v1/users/:userId/reviews +export const handleListUserReviews = async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = parseInt(String(req.params['userId'] ?? '0'), 10) + const cursor = typeof req.query['cursor'] === 'string' ? parseInt(req.query['cursor'], 10) : 0 + const result = await listUserReviews(userId, cursor) + res.status(StatusCodes.OK).json(BaseResponse(result)) + } catch (err) { + next(err) + } +} + +// POST /api/v1/stores/:storeId/reviews +export const handleCreateReview = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const storeId = parseInt(String(req.params['storeId'] ?? '0'), 10) + const result = await createReview(storeId, req.body as ReviewCreateRequest) + res.status(StatusCodes.CREATED).json(BaseResponse(result)) + } catch (err) { + next(err) + } +} diff --git "a/\353\217\204\354\226\217/week7/src/modules/reviews/dtos/review.dto.ts" "b/\353\217\204\354\226\217/week7/src/modules/reviews/dtos/review.dto.ts" new file mode 100644 index 0000000..0738b1c --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/reviews/dtos/review.dto.ts" @@ -0,0 +1,43 @@ +// 리뷰 추가 요청 인터페이스 +export interface ReviewCreateRequest { + memberId: number + content: string + score: number +} + +// req.body → 내부 데이터로 변환 +export const bodyToReview = (body: ReviewCreateRequest) => { + return { + memberId: body.memberId, + content: body.content, + score: body.score, + } +} + +// 리뷰 목록 → 응답 형태로 변환 (커서 기반 페이지네이션) +export const responseFromUserReviews = (reviews: any[]) => { + const last = reviews[reviews.length - 1] + return { + data: reviews, + pagination: { cursor: last ? last.id : null }, + } +} + +// DB 결과 → 응답 형태로 변환 +export const responseFromReview = (review: { + id: number + member_id: number + store_id: number + content: string + score: number + created_at: Date +}) => { + return { + reviewId: review.id, + memberId: review.member_id, + storeId: review.store_id, + content: review.content, + score: review.score, + createdAt: review.created_at, + } +} diff --git "a/\353\217\204\354\226\217/week7/src/modules/reviews/repositories/review.repository.ts" "b/\353\217\204\354\226\217/week7/src/modules/reviews/repositories/review.repository.ts" new file mode 100644 index 0000000..adca307 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/reviews/repositories/review.repository.ts" @@ -0,0 +1,30 @@ +import { prisma } from '../../../db.config.js' + +// 리뷰 추가 +export const addReview = async (data: { + memberId: number + storeId: number + content: string +}): Promise => { + const created = await prisma.userStoreReview.create({ + data: { + userId: data.memberId, + storeId: data.storeId, + content: data.content, + }, + }) + return created.id +} + +// 리뷰 조회 +export const getReviewById = async (reviewId: number) => + prisma.userStoreReview.findFirst({ where: { id: reviewId } }) + +// 내가 작성한 리뷰 목록 조회 (커서 기반 페이지네이션) +export const getUserReviews = async (userId: number, cursor: number) => + prisma.userStoreReview.findMany({ + where: { userId, id: { gt: cursor } }, + include: { store: true }, + orderBy: { id: 'asc' }, + take: 5, + }) diff --git "a/\353\217\204\354\226\217/week7/src/modules/reviews/services/review.service.ts" "b/\353\217\204\354\226\217/week7/src/modules/reviews/services/review.service.ts" new file mode 100644 index 0000000..13b2281 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/reviews/services/review.service.ts" @@ -0,0 +1,50 @@ +import { ReviewCreateRequest, bodyToReview, responseFromReview, responseFromUserReviews } from '../dtos/review.dto.js' +import { addReview, getReviewById, getUserReviews } from '../repositories/review.repository.js' +import { getStoreById } from '../../stores/repositories/store.repository.js' +import { BaseError } from '../../../utils/errors.js' +import { ErrorCode } from '../../../utils/errorCode.js' + +export const listUserReviews = async (userId: number, cursor: number) => { + const reviews = await getUserReviews(userId, cursor) + return responseFromUserReviews(reviews) +} + +export const createReview = async (storeId: number, data: ReviewCreateRequest) => { + const store = await getStoreById(storeId) + if (!store) { + throw new BaseError( + ErrorCode.STORE_NOT_FOUND.message, + ErrorCode.STORE_NOT_FOUND.status, + ErrorCode.STORE_NOT_FOUND.code, + ) + } + + if (data.score < 1 || data.score > 5) { + throw new BaseError( + ErrorCode.INVALID_SCORE.message, + ErrorCode.INVALID_SCORE.status, + ErrorCode.INVALID_SCORE.code, + ) + } + + const reviewData = bodyToReview(data) + const reviewId = await addReview({ ...reviewData, storeId }) + + const review = await getReviewById(reviewId) + if (!review) { + throw new BaseError( + ErrorCode.REVIEW_CREATE_FAILED.message, + ErrorCode.REVIEW_CREATE_FAILED.status, + ErrorCode.REVIEW_CREATE_FAILED.code, + ) + } + + return responseFromReview(review as { + id: number + member_id: number + store_id: number + content: string + score: number + created_at: Date + }) +} diff --git "a/\353\217\204\354\226\217/week7/src/modules/stores/controllers/store.controller.ts" "b/\353\217\204\354\226\217/week7/src/modules/stores/controllers/store.controller.ts" new file mode 100644 index 0000000..ff13fac --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/stores/controllers/store.controller.ts" @@ -0,0 +1,31 @@ +import { Request, Response, NextFunction } from 'express' +import { StatusCodes } from 'http-status-codes' +import { StoreCreateRequest } from '../dtos/store.dto.js' +import { createStore, listStoreReviews } from '../services/store.service.js' +import { BaseResponse } from '../../../utils/response.js' + +// POST /api/v1/stores +export const handleCreateStore = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + try { + const result = await createStore(req.body as StoreCreateRequest) + res.status(StatusCodes.CREATED).json(BaseResponse(result)) + } catch (err) { + next(err) + } +} + +// GET /api/v1/stores/:storeId/reviews +export const handleListStoreReviews = async (req: Request, res: Response, next: NextFunction) => { + try { + const storeId = parseInt(String(req.params['storeId'] ?? '0'), 10) + const cursor = typeof req.query.cursor === 'string' ? parseInt(req.query.cursor, 10) : 0 + const result = await listStoreReviews(storeId, cursor) + res.status(StatusCodes.OK).json(BaseResponse(result)) + } catch (err) { + next(err) + } +} diff --git "a/\353\217\204\354\226\217/week7/src/modules/stores/dtos/store.dto.ts" "b/\353\217\204\354\226\217/week7/src/modules/stores/dtos/store.dto.ts" new file mode 100644 index 0000000..6922309 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/stores/dtos/store.dto.ts" @@ -0,0 +1,49 @@ +// 가게 추가 요청 인터페이스 +export interface StoreCreateRequest { + regionId: number + foodCategoryId: number + name: string + description?: string + address: string + lat?: number + lng?: number +} + +// req.body → 내부 데이터로 변환 +export const bodyToStore = (body: StoreCreateRequest) => { + return { + regionId: body.regionId, + foodCategoryId: body.foodCategoryId, + name: body.name, + description: body.description ?? null, + address: body.address, + lat: body.lat ?? null, + lng: body.lng ?? null, + } +} + +// 리뷰 목록 → 응답 형태로 변환 (커서 기반 페이지네이션) +export const responseFromReviews = (reviews: any[]) => { + const last = reviews[reviews.length - 1] + return { + data: reviews, + pagination: { + cursor: last ? last.id : null, + }, + } +} + +// DB 조회 결과 → 응답 형태로 변환 +export const responseFromStore = (store: { + id: number + name: string + address: string + region_id: number +}) => { + return { + storeId: store.id, + name: store.name, + address: store.address, + regionId: store.region_id, + } +} diff --git "a/\353\217\204\354\226\217/week7/src/modules/stores/repositories/store.repository.ts" "b/\353\217\204\354\226\217/week7/src/modules/stores/repositories/store.repository.ts" new file mode 100644 index 0000000..9a9ac1e --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/stores/repositories/store.repository.ts" @@ -0,0 +1,28 @@ +import { prisma } from '../../../db.config.js' + +// 가게 추가 +export const addStore = async (data: { name: string }): Promise => { + const created = await prisma.store.create({ data }) + return created.id +} + +// 가게 조회 +export const getStoreById = async (storeId: number) => + prisma.store.findFirst({ where: { id: storeId } }) + +// 가게 리뷰 목록 조회 (커서 기반 페이지네이션) +export const getAllStoreReviews = async (storeId: number, cursor: number) => + prisma.userStoreReview.findMany({ + select: { + id: true, + content: true, + store: true, + user: true, + }, + where: { + storeId, + id: { gt: cursor }, + }, + orderBy: { id: 'asc' }, + take: 5, + }) diff --git "a/\353\217\204\354\226\217/week7/src/modules/stores/services/store.service.ts" "b/\353\217\204\354\226\217/week7/src/modules/stores/services/store.service.ts" new file mode 100644 index 0000000..91f2c48 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/modules/stores/services/store.service.ts" @@ -0,0 +1,30 @@ +import { StoreCreateRequest, bodyToStore, responseFromStore, responseFromReviews } from '../dtos/store.dto.js' +import { addStore, getStoreById, getAllStoreReviews } from '../repositories/store.repository.js' +import { BaseError } from '../../../utils/errors.js' +import { ErrorCode } from '../../../utils/errorCode.js' + +export const listStoreReviews = async (storeId: number, cursor: number) => { + const reviews = await getAllStoreReviews(storeId, cursor) + return responseFromReviews(reviews) +} + +export const createStore = async (data: StoreCreateRequest) => { + const storeData = bodyToStore(data) + const storeId = await addStore(storeData) + + const store = await getStoreById(storeId) + if (!store) { + throw new BaseError( + ErrorCode.STORE_CREATE_FAILED.message, + ErrorCode.STORE_CREATE_FAILED.status, + ErrorCode.STORE_CREATE_FAILED.code, + ) + } + + return responseFromStore(store as { + id: number + name: string + address: string + region_id: number + }) +} diff --git "a/\353\217\204\354\226\217/week7/src/utils/errorCode.ts" "b/\353\217\204\354\226\217/week7/src/utils/errorCode.ts" new file mode 100644 index 0000000..1a271dd --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/utils/errorCode.ts" @@ -0,0 +1,26 @@ +export const ErrorCode = { + // 공통 + INTERNAL_ERROR: { status: 500, code: 'COMMON500', message: '서버 내부 오류입니다.' }, + INVALID_INPUT: { status: 400, code: 'COMMON400', message: '잘못된 입력값입니다.' }, + + // 회원 + USER_NOT_FOUND: { status: 404, code: 'USER4001', message: '사용자를 찾을 수 없습니다.' }, + DUPLICATE_EMAIL: { status: 409, code: 'USER4002', message: '이미 사용 중인 이메일입니다.' }, + MEMBER_REQUIRED_FIELD: { status: 400, code: 'USER4003', message: 'name과 nickname은 필수 입력값입니다.' }, + + // 가게 + STORE_NOT_FOUND: { status: 404, code: 'STORE4001', message: '존재하지 않는 가게입니다.' }, + STORE_CREATE_FAILED: { status: 500, code: 'STORE5001', message: '가게 생성 후 조회에 실패했습니다.' }, + + // 리뷰 + INVALID_SCORE: { status: 400, code: 'REVIEW4001', message: '별점은 1~5 사이여야 합니다.' }, + REVIEW_CREATE_FAILED: { status: 500, code: 'REVIEW5001', message: '리뷰 생성 후 조회에 실패했습니다.' }, + + // 미션 + MISSION_NOT_FOUND: { status: 404, code: 'MISSION4001', message: '존재하지 않는 미션입니다.' }, + MISSION_ALREADY_CHALLENGING: { status: 409, code: 'MISSION4002', message: '이미 도전 중인 미션입니다.' }, + ONGOING_MISSION_NOT_FOUND: { status: 404, code: 'MISSION4003', message: '진행 중인 미션이 없습니다.' }, + MISSION_STATUS_REQUIRED: { status: 400, code: 'MISSION4004', message: 'status는 필수 입력값입니다. (CHALLENGING 또는 COMPLETE)' }, + MISSION_CREATE_FAILED: { status: 500, code: 'MISSION5001', message: '미션 생성 후 조회에 실패했습니다.' }, + MISSION_CHALLENGE_FAILED: { status: 500, code: 'MISSION5002', message: '미션 도전 후 조회에 실패했습니다.' }, +} as const diff --git "a/\353\217\204\354\226\217/week7/src/utils/errors.ts" "b/\353\217\204\354\226\217/week7/src/utils/errors.ts" new file mode 100644 index 0000000..c237d36 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/utils/errors.ts" @@ -0,0 +1,11 @@ +export class BaseError extends Error { + status: number + code: string + + constructor(message: string, statusCode = 500, code = 'INTERNAL_ERROR') { + super(message) + this.name = 'BaseError' + this.status = statusCode + this.code = code + } +} diff --git "a/\353\217\204\354\226\217/week7/src/utils/response.ts" "b/\353\217\204\354\226\217/week7/src/utils/response.ts" new file mode 100644 index 0000000..6adf287 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/src/utils/response.ts" @@ -0,0 +1,8 @@ +export const BaseResponse = (result: T, message = '성공입니다.', code = 'COMMON200') => { + return { + isSuccess: true, + code, + message, + result, + } +} diff --git "a/\353\217\204\354\226\217/week7/todolist.json" "b/\353\217\204\354\226\217/week7/todolist.json" new file mode 100644 index 0000000..830b572 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/todolist.json" @@ -0,0 +1,353 @@ +{ + "chapter": "Chapter 5. API 및 프로젝트 설정 기초", + "branch": "feature/chapter-05", + "week4_reference": "../week4", + "keywords": [ + { + "term": "환경 변수 (Environment Variables)", + "summary": "DB 비밀번호·API Key 등 민감한 값을 코드에 하드코딩하지 않고 .env 파일로 관리. dotenv 라이브러리로 로드하며 .gitignore에 반드시 추가해야 함.", + "example": "process.env.DB_PASSWORD" + }, + { + "term": "CORS (Cross-Origin Resource Sharing)", + "summary": "브라우저가 다른 Origin(도메인·포트)의 서버에 요청할 때 발생하는 보안 정책. Express에서는 cors 미들웨어로 허용 처리.", + "example": "app.use(cors())" + }, + { + "term": "DB Connection Pool", + "summary": "매 요청마다 DB 커넥션을 새로 생성·해제하지 않고, 미리 만들어 둔 커넥션 풀에서 빌려 쓰는 방식. mysql2의 createPool()로 구현. finally 블록에서 conn.release()로 반환해야 함.", + "example": "const pool = mysql.createPool({ connectionLimit: 10 })" + }, + { + "term": "비동기 (async / await)", + "summary": "DB 쿼리·외부 API 호출처럼 응답 대기가 필요한 작업을 Promise 기반으로 처리. async 함수 안에서 await로 결과를 기다리며, 동기 코드처럼 읽기 쉽게 작성 가능.", + "example": "const [rows] = await pool.query('SELECT ...')" + }, + { + "term": "try / catch / finally", + "summary": "비동기 작업에서 발생할 수 있는 에러를 구조적으로 처리. try: 정상 로직, catch: 에러 처리, finally: 커넥션 반환(conn.release()) 등 항상 실행되어야 하는 정리 코드.", + "example": "try { ... } catch(err) { throw new Error(...) } finally { conn.release() }" + }, + { + "term": "Interface (인터페이스)", + "summary": "TypeScript에서 객체의 형태(속성·타입)를 정의하는 설계도. DTO·Repository 반환 타입 등에 활용. ?를 붙이면 선택적 프로퍼티.", + "example": "export interface StoreCreateRequest { regionId: number; name: string; address: string; }" + }, + { + "term": "Type Assertion (as 키워드)", + "summary": "TypeScript 컴파일러에게 '이 값은 이 타입이야'라고 강제로 알려주는 문법. req.body는 기본적으로 any이므로, as 로 정의한 인터페이스 타입으로 변환해 사용.", + "example": "req.body as StoreCreateRequest" + } + ], + "project_structure": { + "root": "week5/", + "note": "week4의 in-memory DB → MySQL 실제 DB 연결로 전환. 모듈형 모노리스 구조 채택.", + "files": [ + "src/index.ts - Express 앱 진입점, 미들웨어·라우터 등록", + "src/db.config.ts - MySQL Connection Pool 설정", + "src/modules/stores/controllers/store.controller.ts", + "src/modules/stores/services/store.service.ts", + "src/modules/stores/repositories/store.repository.ts", + "src/modules/stores/dtos/store.dto.ts", + "src/modules/reviews/controllers/review.controller.ts", + "src/modules/reviews/services/review.service.ts", + "src/modules/reviews/repositories/review.repository.ts", + "src/modules/reviews/dtos/review.dto.ts", + "src/modules/missions/controllers/mission.controller.ts", + "src/modules/missions/services/mission.service.ts", + "src/modules/missions/repositories/mission.repository.ts", + "src/modules/missions/dtos/mission.dto.ts", + ".env - DB 접속 정보·PORT (gitignore 필수)", + ".gitignore", + "package.json", + "tsconfig.json", + "schema.sql - week4 schema.sql 재사용 (테이블 이미 정의됨)" + ] + }, + "todos": [ + { + "id": 1, + "phase": "사전 준비", + "title": "GitHub 이슈 생성 및 브랜치 분기", + "status": "todo", + "details": [ + "GitHub 저장소 Issues 탭에서 라벨 정리: bug, docs, feature, refactor", + "이슈 제목: '[feat] Chapter 5 - API 구현 (가게 추가 / 리뷰 / 미션)'", + "Assignee: 본인, Label: feature 로 이슈 생성", + "이슈에서 'Create a branch' 클릭 → feature/chapter-05 브랜치 생성", + "로컬에서: git fetch origin && git checkout feature/chapter-05" + ] + }, + { + "id": 2, + "phase": "사전 준비", + "title": "Postman 설치 및 기본 사용법 확인", + "status": "todo", + "details": [ + "Postman 설치 (https://www.postman.com/downloads/)", + "Params / Authorization / Headers / Body 탭 역할 이해", + "Body > raw > JSON 선택 방법 숙지", + "나중에 API 테스트 시 스크린샷 저장할 준비" + ] + }, + { + "id": 3, + "phase": "프로젝트 세팅", + "title": "week5 폴더 초기화 및 의존성 설치", + "status": "todo", + "details": [ + "cd week5 && npm init -y", + "npm install express cors dotenv http-status-codes mysql2", + "npm install -D typescript @types/node @types/express @types/cors @types/dotenv nodemon tsx", + "npx tsc --init 후 tsconfig.json 수정 (rootDir: ./src, outDir: ./dist, module: NodeNext, strict: true 등)", + "week4/tsconfig.json을 참고해 module/moduleResolution 설정 일치시키기", + "package.json scripts 추가: start / dev (nodemon --exec tsx src/index.ts)" + ] + }, + { + "id": 4, + "phase": "프로젝트 세팅", + "title": ".env 파일 및 .gitignore 작성", + "status": "todo", + "details": [ + ".gitignore에 node_modules/ / .env / .env.* 추가", + ".env에 PORT=3000, DB_HOST=localhost, DB_PORT=3306, DB_USER=root, DB_PASSWORD=비밀번호, DB_NAME=umc_mission 작성", + "DB_NAME은 week4/schema.sql 기준 umc_mission 사용 (이미 테이블 있음)" + ] + }, + { + "id": 5, + "phase": "프로젝트 세팅", + "title": "src/db.config.ts 작성 - MySQL Connection Pool", + "status": "todo", + "details": [ + "mysql2/promise의 createPool 사용", + "환경 변수(DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME)로 설정", + "connectionLimit: 10, waitForConnections: true", + "week4 schema.sql의 DB(umc_mission)와 연결" + ], + "reference_week4": "week4/src/db/index.ts 구조 참고 (단, in-memory→MySQL로 변경)" + }, + { + "id": 6, + "phase": "프로젝트 세팅", + "title": "src/index.ts 작성 - Express 앱 진입점", + "status": "todo", + "details": [ + "dotenv.config() 최상단 호출", + "cors(), express.json(), express.urlencoded() 미들웨어 등록", + "각 모듈 라우터 등록: /api/v1/stores, /api/v1/reviews, /api/v1/missions", + "전역 에러 핸들러 미들웨어 등록 (시니어 미션: JSON 에러 응답)", + "app.listen(process.env.PORT || 3000)" + ], + "reference_week4": "week4/src/index.ts 구조 그대로 활용, cors·dotenv 추가" + }, + { + "id": 7, + "phase": "DB 준비", + "title": "MySQL에 week5용 테이블 확인 및 더미데이터 삽입", + "status": "todo", + "details": [ + "week4/schema.sql로 테이블 생성 (이미 되어있으면 생략)", + "food_category 더미데이터 삽입: INSERT INTO food_category(name) VALUES ('한식'),('중식'),('일식'),('양식'),('치킨'),('분식'),('고기/구이'),('도시락'),('아식'),('패스트푸드'),('다저트'),('아시안푸드')", + "region 더미데이터 삽입: INSERT INTO region(name) VALUES ('서울'),('경기'),('부산')...", + "member 더미데이터 1건 삽입 (API 테스트용 첫 번째 사용자)" + ] + }, + { + "id": 8, + "phase": "API 구현 - 1-1", + "title": "[필수] 특정 지역에 가게 추가하기 API", + "status": "todo", + "priority": "optional", + "endpoint": "POST /api/v1/stores", + "request_body": { + "regionId": "number", + "foodCategoryId": "number", + "name": "string", + "description": "string (선택)", + "address": "string", + "lat": "number (선택)", + "lng": "number (선택)" + }, + "details": [ + "store.dto.ts - StoreCreateRequest 인터페이스 정의", + "store.dto.ts - bodyToStore() 변환 함수 작성", + "store.repository.ts - addStore(): INSERT INTO store(...) VALUES(?)", + "store.service.ts - createStore(data): addStore 호출 후 결과 반환", + "store.controller.ts - handleCreateStore: bodyToStore(req.body as ...) → service 호출 → 201 응답", + "index.ts에 라우터 등록: app.post('/api/v1/stores', handleCreateStore)" + ], + "reference_week4": "week4/src/controllers/store.controller.ts 패턴 참고" + }, + { + "id": 9, + "phase": "API 구현 - 1-2", + "title": "[필수★] 가게에 리뷰 추가하기 API", + "status": "todo", + "priority": "required", + "endpoint": "POST /api/v1/stores/:storeId/reviews", + "request_body": { + "memberId": "number (특정 사용자로 가정, DB 첫 번째 사용자)", + "content": "string", + "score": "number (1.0~5.0)" + }, + "details": [ + "review.dto.ts - ReviewCreateRequest 인터페이스 (content, score, memberId)", + "review.dto.ts - bodyToReview() 변환 함수", + "review.repository.ts - addReview(): INSERT INTO review(...)", + "store.repository.ts (또는 review.repository.ts) - findStoreById(): SELECT * FROM store WHERE id=?", + "review.service.ts - createReview(storeId, data): 가게 존재 검증 → addReview 호출", + " 검증 실패 시 throw new Error('존재하지 않는 가게입니다.')", + "review.controller.ts - handleCreateReview: storeId = parseInt(req.params.storeId) → service 호출", + "index.ts에 라우터 등록" + ], + "validation": "리뷰를 추가하려는 가게가 존재하는지 검증 필요", + "reference_week4": "week4/src/services/store.service.ts의 createReview 패턴, week4 스키마의 review 테이블" + }, + { + "id": 10, + "phase": "API 구현 - 1-3", + "title": "가게에 미션 추가하기 API", + "status": "todo", + "priority": "optional", + "endpoint": "POST /api/v1/stores/:storeId/missions", + "request_body": { + "title": "string", + "reward": "number", + "spec": "string (선택)", + "deadLine": "string (YYYY-MM-DD, 선택)" + }, + "details": [ + "mission.dto.ts - MissionCreateRequest 인터페이스", + "mission.dto.ts - bodyToMission() 변환 함수 (deadLine → Date 변환)", + "mission.repository.ts - addMission(): INSERT INTO mission(store_id, title, reward, spec, dead_line)", + "mission.service.ts - createMission(storeId, data): 가게 존재 검증 → addMission", + "mission.controller.ts - handleCreateMission", + "index.ts에 라우터 등록" + ], + "reference_week4": "week4/src/repositories/mission.repository.ts 패턴, week4 schema.sql의 mission 테이블" + }, + { + "id": 11, + "phase": "API 구현 - 1-4", + "title": "[필수★] 가게의 미션을 도전 중인 미션에 추가(미션 도전하기) API", + "status": "todo", + "priority": "required", + "endpoint": "POST /api/v1/missions/:missionId/challenge", + "request_body": { + "memberId": "number (특정 사용자로 가정, DB 첫 번째 사용자)" + }, + "details": [ + "mission.dto.ts - MissionChallengeRequest 인터페이스 ({ memberId: number })", + "mission.repository.ts - findMemberMission(): SELECT * FROM member_mission WHERE member_id=? AND mission_id=?", + "mission.repository.ts - addMemberMission(): INSERT INTO member_mission(member_id, mission_id, status) VALUES(?,?,'CHALLENGING')", + "mission.service.ts - challengeMission(missionId, data): 중복 도전 검증 → addMemberMission", + " 검증 실패 시 throw new Error('이미 도전 중인 미션입니다.')", + "mission.controller.ts - handleChallengeMission: missionId = parseInt(req.params.missionId) → service", + "index.ts에 라우터 등록" + ], + "validation": "도전하려는 미션이 이미 도전 중이지는 않은지 검증 필요", + "reference_week4": "week4/src/repositories/mission.repository.ts, week4 schema.sql의 member_mission 테이블" + }, + { + "id": 12, + "phase": "추가 미션", + "title": "[공통 미션 2번] Controller → Service → Repository → DB 요청 흐름 정리", + "status": "todo", + "details": [ + "예: POST /api/v1/stores/:storeId/reviews 요청 흐름을 순서대로 작성", + "1. 사용자가 POST /api/v1/stores/1/reviews 요청 전송", + "2. index.ts의 라우터가 handleCreateReview 컨트롤러 호출", + "3. Controller: req.body를 ReviewCreateRequest 타입으로 변환(bodyToReview), storeId 파싱", + "4. Service: 가게 존재 여부 검증(findStoreById) → 없으면 Error throw", + "5. Repository: INSERT INTO review 쿼리 실행 → insertId 반환", + "6. Service: 결과를 DTO로 변환해 Controller에 반환", + "7. Controller: 201 JSON 응답 전송", + "워크북의 요약 정리 섹션에 이 내용 포함" + ] + }, + { + "id": 13, + "phase": "추가 미션", + "title": "[공통 미션 3번] 회원가입 API에 bcrypt 비밀번호 해싱 추가", + "status": "todo", + "details": [ + "npm install bcryptjs && npm install -D @types/bcryptjs (week4에 이미 설치됨)", + "member.dto.ts - MemberSignUpRequest 인터페이스에 password 필드 추가", + "member.repository.ts - addMember(): INSERT INTO member(..., password) VALUES(...)", + "member.service.ts - signUp(): const hashedPw = await bcrypt.hash(data.password, 10) → addMember에 전달", + "member.controller.ts - handleSignUp 작성", + "POST /api/v1/members/signup 라우터 등록" + ], + "reference_week4": "week4에서 bcryptjs 이미 사용 중 - week4/package.json 참고" + }, + { + "id": 14, + "phase": "시니어 미션", + "title": "[시니어] 전역 에러 핸들러 - JSON 형태 에러 응답", + "status": "todo", + "details": [ + "src/middleware/error.middleware.ts 생성", + "ErrorRequestHandler 타입 사용: (err, req, res, next) => void", + "res.status(err.status || 500).json({ success: false, message: err.message || '서버 에러' })", + "index.ts 맨 마지막에 app.use(errorMiddleware) 등록", + "Controller에서 try-catch 제거하거나 next(err) 사용으로 변경", + "기존 HTML 에러 응답 → JSON 에러 응답으로 개선" + ], + "reference_week4": "week4/src/middleware/error.middleware.ts 그대로 활용 가능" + }, + { + "id": 15, + "phase": "테스트", + "title": "Postman / curl로 각 API 호출 및 스크린샷 저장", + "status": "todo", + "details": [ + "npm run dev 로 서버 실행", + "API 1-1: POST /api/v1/stores - 가게 추가 성공 스크린샷", + "API 1-2: POST /api/v1/stores/:storeId/reviews - 리뷰 추가 성공 스크린샷", + "API 1-2: 존재하지 않는 storeId로 요청 → 에러 응답 스크린샷", + "API 1-3: POST /api/v1/stores/:storeId/missions - 미션 추가 성공 스크린샷", + "API 1-4: POST /api/v1/missions/:missionId/challenge - 도전 성공 스크린샷", + "API 1-4: 동일 미션 재도전 → '이미 도전 중' 에러 스크린샷", + "DB에서 SELECT로 데이터 삽입 확인 스크린샷" + ] + }, + { + "id": 16, + "phase": "마무리", + "title": "feature/chapter-05 브랜치에 push 및 PR 생성", + "status": "todo", + "details": [ + "git add . && git commit -m 'feat: 5주차 미션 - API 구현 (가게/리뷰/미션)'", + "git push origin feature/chapter-05", + "GitHub에서 PR 생성 (main 브랜치에 merge하지 말 것!)", + "워크북의 미션 기록란에 GitHub 링크 제출", + "GitHub 이슈 Close" + ] + }, + { + "id": 17, + "phase": "마무리", + "title": "핵심 키워드 및 요약 정리 작성", + "status": "todo", + "details": [ + "이 파일 상단의 keywords 섹션을 참고해 워크북에 기입", + "요약 정리: Controller→Service→Repository→DB 흐름 설명", + "위클리 스크럼 질문 답변: DTO 없이 사용할 때의 문제점 / Service Layer 필요성" + ] + } + ], + "required_apis_summary": { + "must_implement": ["1-2 (가게에 리뷰 추가하기)", "1-4 (미션 도전하기)"], + "minimum_count": "필수 2개 포함 총 3개 이상", + "senior_mission": "4개 전부 + JSON 에러 응답 개선" + }, + "key_differences_from_week4": { + "database": "in-memory(week4) → MySQL Connection Pool(week5)", + "modules": "flat 구조(week4) → 모듈형 모노리스(week5: src/modules/{도메인}/)", + "env": ".env 파일 추가 (dotenv 사용)", + "cors": "cors 미들웨어 추가", + "error_response": "HTML 에러(기본) → JSON 에러(시니어 미션 개선)" + } +} diff --git "a/\353\217\204\354\226\217/week7/tsconfig.json" "b/\353\217\204\354\226\217/week7/tsconfig.json" new file mode 100644 index 0000000..d25f8a3 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/tsconfig.json" @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ESNext"], + "types": ["node"], + "strict": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git "a/\353\217\204\354\226\217/week7/week7-commands.md" "b/\353\217\204\354\226\217/week7/week7-commands.md" new file mode 100644 index 0000000..37beced --- /dev/null +++ "b/\353\217\204\354\226\217/week7/week7-commands.md" @@ -0,0 +1,116 @@ +# Week 7 실행 명령어 순서 정리 + +> 아래 순서대로 실행하세요. 각 단계 완료 후 다음 단계로 진행합니다. + +--- + +## 1단계. 프로젝트 디렉토리 이동 + +```bash +cd +``` + +> week6 코드가 있는 프로젝트 루트 폴더로 이동합니다. + +--- + +## 2단계. 필수 미들웨어 패키지 설치 + +```bash +npm install morgan cookie-parser +``` + +--- + +## 3단계. 설치 확인 + +```bash +cat package.json +``` + +> `dependencies`에 `morgan`과 `cookie-parser`가 추가되었는지 확인합니다. + +--- + +## 4단계. Git 초기화 및 원격 저장소 연결 + +> GitHub에서 새 레포지토리를 먼저 만든 후 실행합니다. + +```bash +git init +git remote add origin https://github.com//.git +``` + +--- + +## 5단계. 작업 브랜치 생성 및 이동 + +```bash +git checkout -b feature/chapter-07 +``` + +> **주의:** `main` 브랜치에는 절대 push하지 않습니다. (Week 11 CI/CD 전용) + +--- + +## 6단계. 코드 작성 후 스테이징 + +> `index.js`, `BaseResponse`, `BaseError`, `errorCode.js` 등 수정/생성 후 실행 + +```bash +git add . +``` + +--- + +## 7단계. 커밋 + +```bash +git commit -m "feat: add middleware, standard response, and error handling" +``` + +--- + +## 8단계. feature 브랜치에 push + +```bash +git push origin feature/chapter-07 +``` + +--- + +## (선택) Prisma 관련 명령어 + +### 스키마 변경 후 마이그레이션 실행 + +```bash +npx prisma migrate dev --name +``` + +### DB 상태와 스키마 동기화 확인 + +```bash +npx prisma db pull +``` + +### Prisma Studio (GUI로 DB 확인) + +```bash +npx prisma studio +``` + +--- + +## 전체 순서 요약 + +| 순서 | 명령어 | 설명 | +|---|---|---| +| 1 | `cd ` | 프로젝트 폴더 이동 | +| 2 | `npm install morgan cookie-parser` | 미들웨어 설치 | +| 3 | `cat package.json` | 설치 확인 | +| 4 | `git init` | Git 초기화 | +| 5 | `git remote add origin ` | 원격 저장소 연결 | +| 6 | `git checkout -b feature/chapter-07` | 작업 브랜치 생성 | +| 7 | `git add .` | 변경 파일 스테이징 | +| 8 | `git commit -m "..."` | 커밋 | +| 9 | `git push origin feature/chapter-07` | 브랜치에 push | diff --git "a/\353\217\204\354\226\217/week7/week7-workbook.md" "b/\353\217\204\354\226\217/week7/week7-workbook.md" new file mode 100644 index 0000000..c235612 --- /dev/null +++ "b/\353\217\204\354\226\217/week7/week7-workbook.md" @@ -0,0 +1,282 @@ +# Week 7 워크북 - 미들웨어, 표준 응답/에러 처리, Git 전략 + +> **프로젝트 참조:** week6_directory_contents 기반으로 week7 리팩토링 + +--- + +## Step 1. 필수 미들웨어 설치 및 설정 + +### 설치 패키지 + +```bash +npm install morgan cookie-parser +``` + +### `index.js` 적용 예시 + +```js +const express = require('express'); +const morgan = require('morgan'); +const cookieParser = require('cookie-parser'); + +const app = express(); + +// 요청 로깅 미들웨어 +app.use(morgan('dev')); + +// 쿠키 파싱 미들웨어 +app.use(cookieParser()); + +// JSON 및 URL-encoded body 파싱 +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +``` + +### 미들웨어 역할 설명 + +| 미들웨어 | 역할 | +|---|---| +| `morgan('dev')` | HTTP 요청/응답 로그를 터미널에 출력 (개발 환경용) | +| `cookieParser()` | `req.cookies` 객체로 쿠키 값 파싱 | +| `express.json()` | `Content-Type: application/json` 요청 바디 파싱 | +| `express.urlencoded()` | HTML form 데이터 파싱 | + +--- + +## Step 2. 표준 API 성공 응답 형식 통일 + +### 응답 형식 정의 + +모든 API의 성공 응답은 아래 구조를 따릅니다: + +```json +{ + "isSuccess": true, + "code": "COMMON200", + "message": "성공입니다.", + "result": { } +} +``` + +| 필드 | 타입 | 설명 | +|---|---|---| +| `isSuccess` | boolean | 요청 성공 여부 | +| `code` | string | 응답 코드 (예: `COMMON200`, `USER404`) | +| `message` | string | 사람이 읽을 수 있는 메시지 | +| `result` | object \| array | 실제 데이터 | + +### BaseResponse 유틸리티 구현 예시 + +```js +// src/utils/response.js + +const BaseResponse = (result, message = '성공입니다.', code = 'COMMON200') => { + return { + isSuccess: true, + code, + message, + result, + }; +}; + +module.exports = { BaseResponse }; +``` + +### 컨트롤러에서 사용 + +```js +const { BaseResponse } = require('../utils/response'); + +// Before (기존 방식) +res.status(200).json(user); + +// After (표준 응답 적용) +res.status(200).json(BaseResponse(user)); +``` + +--- + +## Step 3. 중앙 집중식 커스텀 에러 처리 + +### BaseError 클래스 정의 + +```js +// src/utils/errors.js + +class BaseError extends Error { + constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') { + super(message); + this.name = 'BaseError'; + this.status = statusCode; + this.code = code; + } +} + +module.exports = { BaseError }; +``` + +### 에러 코드 상수 예시 + +```js +// src/utils/errorCode.js + +const ErrorCode = { + USER_NOT_FOUND: { status: 404, code: 'USER4001', message: '사용자를 찾을 수 없습니다.' }, + DUPLICATE_EMAIL: { status: 409, code: 'USER4002', message: '이미 사용 중인 이메일입니다.' }, + INTERNAL_ERROR: { status: 500, code: 'COMMON500', message: '서버 내부 오류입니다.' }, +}; + +module.exports = { ErrorCode }; +``` + +### 글로벌 에러 핸들러 미들웨어 (`index.js` 하단에 추가) + +```js +// 글로벌 에러 핸들러 - 반드시 다른 app.use() 아래에 위치해야 함 +app.use((err, req, res, next) => { + if (err instanceof BaseError) { + return res.status(err.status).json({ + isSuccess: false, + code: err.code, + message: err.message, + result: null, + }); + } + + // Prisma unique constraint 에러 처리 + if (err.code === 'P2002') { + return res.status(409).json({ + isSuccess: false, + code: 'USER4002', + message: '이미 존재하는 데이터입니다.', + result: null, + }); + } + + console.error(err); + return res.status(500).json({ + isSuccess: false, + code: 'COMMON500', + message: '서버 내부 오류입니다.', + result: null, + }); +}); +``` + +### 컨트롤러에서 에러 던지기 + +```js +const { BaseError } = require('../utils/errors'); +const { ErrorCode } = require('../utils/errorCode'); + +// Before (기존 방식) +res.status(404).send('User Not Found'); + +// After (next()로 에러 위임) +const user = await prisma.user.findUnique({ where: { id } }); +if (!user) { + return next(new BaseError( + ErrorCode.USER_NOT_FOUND.message, + ErrorCode.USER_NOT_FOUND.status, + ErrorCode.USER_NOT_FOUND.code + )); +} +``` + +--- + +## Step 4. GitHub 브랜치 전략 + +### 브랜치 규칙 + +| 브랜치 | 용도 | +|---|---| +| `main` | Week 11 CI/CD 파이프라인 전용 — **직접 push 금지** | +| `feature/chapter-07` | Week 7 작업 브랜치 | + +### 주의사항 + +> `main` 브랜치는 Week 11 CI/CD 설정을 위해 보호되어 있습니다. +> 모든 코드는 반드시 `feature/chapter-07` 브랜치에 push 하세요. + +--- + +## Step 5. Prisma ORM vs Raw SQL (mysql2) 비교 분석 + +### 비교표 + +| 항목 | Prisma ORM | Raw SQL (mysql2) | +|---|---|---| +| **문법** | TypeScript 기반 타입 안전 API | 직접 SQL 문자열 작성 | +| **가독성** | 높음 (직관적 메서드 체이닝) | 낮음 (SQL 숙련도 필요) | +| **타입 안전성** | 자동 타입 추론 지원 | 없음 (직접 캐스팅 필요) | +| **성능 튜닝** | 제한적 | 완전한 SQL 제어 가능 | +| **마이그레이션** | `prisma migrate dev` 자동화 | 수동 관리 | +| **학습 비용** | Prisma 문법 학습 필요 | SQL 지식만 있으면 됨 | +| **복잡 쿼리** | 한계 있음 (rawQuery 혼용) | 무제한 | + +--- + +### `prisma migrate dev` 장단점 분석 + +#### 장점 + +- **자동 마이그레이션 파일 생성:** `schema.prisma` 변경 사항을 감지해 SQL 파일 자동 생성 +- **히스토리 관리:** `prisma/migrations/` 폴더에 변경 이력 누적 보관 +- **개발 편의성:** 스키마 → DB 동기화를 명령어 한 줄로 처리 +- **Seed 연동:** `prisma db seed`와 연계하여 초기 데이터 삽입 가능 + +#### 단점 + +- **팀 협업 시 충돌 위험:** 여러 사람이 `schema.prisma`를 동시에 수정하면 migration 충돌 발생 +- **프로덕션 부적합:** 운영 환경에서는 `prisma migrate deploy` 사용 권장 (`dev`는 개발 전용) +- **자동화의 불투명성:** 생성된 SQL을 검토하지 않으면 의도치 않은 데이터 손실 위험 + +--- + +### 협업 시 Migration 충돌 방지 전략 + +1. **스키마 담당자 단일화:** `schema.prisma` 수정은 한 명이 담당하거나 PR 리뷰를 필수로 설정 +2. **브랜치 전략 준수:** 기능 브랜치에서 스키마 수정 후 PR 머지 순서 지키기 +3. **migration 파일 커밋 필수:** `prisma/migrations/` 폴더를 `.gitignore`에 추가하지 말고 항상 커밋 +4. **머지 전 `prisma migrate dev` 재실행:** 머지 후 로컬에서 반드시 재실행하여 동기화 확인 +5. **팀 내 DB 상태 공유:** 마이그레이션 실행 여부를 팀 채널(슬랙 등)에 공유 + +--- + +### 개인 선호도: Prisma vs mysql2 + +**선택: Prisma ORM** + +**이유:** + +- TypeScript 환경에서 타입 자동 완성과 컴파일 타임 에러 검출이 가능해 런타임 버그 감소 +- 복잡한 JOIN보다 단순 CRUD 위주인 현재 프로젝트 규모에 충분 +- 마이그레이션 자동화로 팀 전체의 DB 상태를 일관되게 유지 가능 +- `prisma studio`를 통한 GUI 데이터 확인이 개발 생산성을 높임 + +**mysql2가 더 적합한 경우:** + +- 복잡한 집계 쿼리나 서브쿼리가 많은 서비스 +- 극도의 성능 최적화가 필요한 대규모 트래픽 환경 + +--- + +## 디렉토리 구조 제안 + +``` +project/ +├── index.js +├── prisma/ +│ ├── schema.prisma +│ └── migrations/ +├── src/ +│ ├── controllers/ +│ ├── services/ +│ ├── repositories/ +│ └── utils/ +│ ├── response.js ← BaseResponse +│ ├── errors.js ← BaseError +│ └── errorCode.js ← 에러 코드 상수 +└── package.json +``` From 1d48207aa646386adde66549325a8a2db40e31ec Mon Sep 17 00:00:00 2001 From: higashiaka Date: Sat, 23 May 2026 15:53:20 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat:=EB=8F=84=EC=96=8F=208=EC=A3=BC?= =?UTF-8?q?=EC=B0=A8=20=EB=AF=B8=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "\353\217\204\354\226\217/week7/.gitignore" | 1 + "\353\217\204\354\226\217/week8/.gitignore" | 22 + "\353\217\204\354\226\217/week8/README.md" | 1 + .../week8/package-lock.json" | 4842 +++++++++++++++++ "\353\217\204\354\226\217/week8/package.json" | 33 + .../week8/prisma.config.ts" | 12 + .../migration.sql" | 39 + .../migration.sql" | 25 + .../migration.sql" | 33 + .../prisma/migrations/migration_lock.toml" | 3 + .../week8/prisma/schema.prisma" | 100 + "\353\217\204\354\226\217/week8/reset_db.sql" | 215 + .../week8/src/db.config.ts" | 17 + .../week8/src/generated/routes.ts" | 622 +++ "\353\217\204\354\226\217/week8/src/index.ts" | 44 + .../week8/src/middleware/error.middleware.ts" | 38 + .../members/controllers/member.controller.ts" | 24 + .../members/controllers/user.controller.ts" | 71 + .../src/modules/members/dtos/member.dto.ts" | 85 + .../repositories/member.repository.ts" | 26 + .../members/services/member.service.ts" | 39 + .../controllers/mission.controller.ts" | 28 + .../src/modules/missions/dtos/mission.dto.ts" | 120 + .../repositories/mission.repository.ts" | 72 + .../missions/services/mission.service.ts" | 128 + .../reviews/controllers/review.controller.ts" | 7 + .../src/modules/reviews/dtos/review.dto.ts" | 80 + .../repositories/review.repository.ts" | 30 + .../reviews/services/review.service.ts" | 50 + .../stores/controllers/store.controller.ts" | 106 + .../src/modules/stores/dtos/store.dto.ts" | 108 + .../stores/repositories/store.repository.ts" | 28 + .../modules/stores/services/store.service.ts" | 30 + .../week8/src/utils/errorCode.ts" | 26 + .../week8/src/utils/errors.ts" | 11 + .../week8/src/utils/response.ts" | 25 + .../week8/todolist.json" | 353 ++ .../week8/tsconfig.json" | 20 + "\353\217\204\354\226\217/week8/tsoa.json" | 13 + .../week8/week7-commands.md" | 116 + .../week8/week7-workbook.md" | 282 + 41 files changed, 7925 insertions(+) create mode 100644 "\353\217\204\354\226\217/week8/.gitignore" create mode 100644 "\353\217\204\354\226\217/week8/README.md" create mode 100644 "\353\217\204\354\226\217/week8/package-lock.json" create mode 100644 "\353\217\204\354\226\217/week8/package.json" create mode 100644 "\353\217\204\354\226\217/week8/prisma.config.ts" create mode 100644 "\353\217\204\354\226\217/week8/prisma/migrations/20260428071132_init_database/migration.sql" create mode 100644 "\353\217\204\354\226\217/week8/prisma/migrations/20260428071239_add_store_and_review_tables/migration.sql" create mode 100644 "\353\217\204\354\226\217/week8/prisma/migrations/20260428072732_npx_prisma_migrate_dev/migration.sql" create mode 100644 "\353\217\204\354\226\217/week8/prisma/migrations/migration_lock.toml" create mode 100644 "\353\217\204\354\226\217/week8/prisma/schema.prisma" create mode 100644 "\353\217\204\354\226\217/week8/reset_db.sql" create mode 100644 "\353\217\204\354\226\217/week8/src/db.config.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/generated/routes.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/index.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/middleware/error.middleware.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/members/controllers/member.controller.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/members/controllers/user.controller.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/members/dtos/member.dto.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/members/repositories/member.repository.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/members/services/member.service.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/missions/controllers/mission.controller.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/missions/dtos/mission.dto.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/missions/repositories/mission.repository.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/missions/services/mission.service.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/reviews/controllers/review.controller.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/reviews/dtos/review.dto.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/reviews/repositories/review.repository.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/reviews/services/review.service.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/stores/controllers/store.controller.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/stores/dtos/store.dto.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/stores/repositories/store.repository.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/stores/services/store.service.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/utils/errorCode.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/utils/errors.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/utils/response.ts" create mode 100644 "\353\217\204\354\226\217/week8/todolist.json" create mode 100644 "\353\217\204\354\226\217/week8/tsconfig.json" create mode 100644 "\353\217\204\354\226\217/week8/tsoa.json" create mode 100644 "\353\217\204\354\226\217/week8/week7-commands.md" create mode 100644 "\353\217\204\354\226\217/week8/week7-workbook.md" diff --git "a/\353\217\204\354\226\217/week7/.gitignore" "b/\353\217\204\354\226\217/week7/.gitignore" index 928abd6..7206753 100644 --- "a/\353\217\204\354\226\217/week7/.gitignore" +++ "b/\353\217\204\354\226\217/week7/.gitignore" @@ -10,6 +10,7 @@ dist/ .env.development .env.production .env.* +.claude* # macOS .DS_Store diff --git "a/\353\217\204\354\226\217/week8/.gitignore" "b/\353\217\204\354\226\217/week8/.gitignore" new file mode 100644 index 0000000..7206753 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/.gitignore" @@ -0,0 +1,22 @@ +# dependency directories +node_modules/ + +# build output +dist/ + +# dotenv environment variable files +.env +.env.local +.env.development +.env.production +.env.* +.claude* + +# macOS +.DS_Store + +# logs +*.log +npm-debug.log* + +/src/generated/prisma diff --git "a/\353\217\204\354\226\217/week8/README.md" "b/\353\217\204\354\226\217/week8/README.md" new file mode 100644 index 0000000..bb278f1 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/README.md" @@ -0,0 +1 @@ +# umc-node-study \ No newline at end of file diff --git "a/\353\217\204\354\226\217/week8/package-lock.json" "b/\353\217\204\354\226\217/week8/package-lock.json" new file mode 100644 index 0000000..181b1c6 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/package-lock.json" @@ -0,0 +1,4842 @@ +{ + "name": "week8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@prisma/adapter-mariadb": "^7.8.0", + "@prisma/client": "^7.8.0", + "bcryptjs": "^3.0.3", + "cookie-parser": "^1.4.7", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "http-status-codes": "^2.3.0", + "morgan": "^1.10.1", + "swagger-ui-express": "^5.0.1", + "tsoa": "^6.6.0" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/morgan": "^1.9.10", + "@types/node": "^25.6.0", + "@types/swagger-ui-express": "^4.1.8", + "nodemon": "^3.1.14", + "prisma": "^7.8.0", + "tsx": "^4.21.0", + "typescript": "^6.0.3" + } + }, + "node_modules/@electric-sql/pglite": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.4.1.tgz", + "integrity": "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@electric-sql/pglite-socket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.1.1.tgz", + "integrity": "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "pglite-server": "dist/scripts/server.js" + }, + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@electric-sql/pglite-tools": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.3.1.tgz", + "integrity": "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==", + "devOptional": true, + "license": "Apache-2.0", + "peerDependencies": { + "@electric-sql/pglite": "0.4.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hapi/accept": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-6.0.3.tgz", + "integrity": "sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/ammo": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/ammo/-/ammo-6.0.1.tgz", + "integrity": "sha512-pmL+nPod4g58kXrMcsGLp05O2jF4P2Q3GiL8qYV7nKYEh3cGf+rV4P5Jyi2Uq0agGhVU63GtaSAfBEZOlrJn9w==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/b64": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/b64/-/b64-6.0.1.tgz", + "integrity": "sha512-ZvjX4JQReUmBheeCq+S9YavcnMMHWqx3S0jHNXWIM1kQDxB9cyfSycpVvjfrKcIS8Mh5N3hmu/YKo4Iag9g2Kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/boom": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-10.0.1.tgz", + "integrity": "sha512-ERcCZaEjdH3OgSJlyjVk8pHIFeus91CjKP3v+MpgBNp5IvGzP2l/bRiD78nqYcKPaZdbKkK5vDBVPd2ohHBlsA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/bounce": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/bounce/-/bounce-3.0.2.tgz", + "integrity": "sha512-d0XmlTi3H9HFDHhQLjg4F4auL1EY3Wqj7j7/hGDhFFe6xAbnm3qiGrXeT93zZnPH8gH+SKAFYiRzu26xkXcH3g==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/bourne": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/call": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@hapi/call/-/call-9.0.1.tgz", + "integrity": "sha512-uPojQRqEL1GRZR4xXPqcLMujQGaEpyVPRyBlD8Pp5rqgIwLhtveF9PkixiKru2THXvuN8mUrLeet5fqxKAAMGg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/catbox": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@hapi/catbox/-/catbox-12.1.1.tgz", + "integrity": "sha512-hDqYB1J+R0HtZg4iPH3LEnldoaBsar6bYp0EonBmNQ9t5CO+1CqgCul2ZtFveW1ReA5SQuze9GPSU7/aecERhw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/podium": "^5.0.0", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/catbox-memory": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/catbox-memory/-/catbox-memory-6.0.2.tgz", + "integrity": "sha512-H1l4ugoFW/ZRkqeFrIo8p1rWN0PA4MDTfu4JmcoNDvnY975o29mqoZblqFTotxNHlEkMPpIiIBJTV+Mbi+aF0g==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/content": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/content/-/content-6.0.2.tgz", + "integrity": "sha512-OKyCOTjNR1hftwSjk9ueyAQTw8AwapvzBrPIWMGn39vhR5PmqLdYFmLc35bsSBye7gSMnlkXfc679bUdMIcRyQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.0" + } + }, + "node_modules/@hapi/cryptiles": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@hapi/cryptiles/-/cryptiles-6.0.3.tgz", + "integrity": "sha512-r6VKalpbMHz4ci3gFjFysBmhwCg70RpYZy6OkjEpdXzAYnYFX5XsW7n4YMJvuIYpnMwLxGUjK/cBhA7X3JDvXw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/file": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/file/-/file-3.0.0.tgz", + "integrity": "sha512-w+lKW+yRrLhJu620jT3y+5g2mHqnKfepreykvdOcl9/6up8GrQQn+l3FRTsjHTKbkbfQFkuksHpdv2EcpKcJ4Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hapi": { + "version": "21.4.9", + "resolved": "https://registry.npmjs.org/@hapi/hapi/-/hapi-21.4.9.tgz", + "integrity": "sha512-YnecZOVx2AD08VvPl0ZaFS0MjEHqg+InGRmBRli731ct+VwI++dpu3BIYA1Z4SMr6HUAnpyvbQ1aq5woe3fBWg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/accept": "^6.0.3", + "@hapi/ammo": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/bounce": "^3.0.2", + "@hapi/call": "^9.0.1", + "@hapi/catbox": "^12.1.1", + "@hapi/catbox-memory": "^6.0.2", + "@hapi/heavy": "^8.0.1", + "@hapi/hoek": "^11.0.7", + "@hapi/mimos": "^7.0.1", + "@hapi/podium": "^5.0.2", + "@hapi/shot": "^6.0.2", + "@hapi/somever": "^4.1.1", + "@hapi/statehood": "^8.2.1", + "@hapi/subtext": "^8.1.3", + "@hapi/teamwork": "^6.0.1", + "@hapi/topo": "^6.0.2", + "@hapi/validate": "^2.0.1" + }, + "engines": { + "node": ">=14.15.0" + } + }, + "node_modules/@hapi/heavy": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@hapi/heavy/-/heavy-8.0.1.tgz", + "integrity": "sha512-gBD/NANosNCOp6RsYTsjo2vhr5eYA3BEuogk6cxY0QdhllkkTaJFYtTXv46xd6qhBVMbMMqcSdtqey+UQU3//w==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/iron": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@hapi/iron/-/iron-7.0.1.tgz", + "integrity": "sha512-tEZnrOujKpS6jLKliyWBl3A9PaE+ppuL/+gkbyPPDb/l2KSKQyH4lhMkVb+sBhwN+qaxxlig01JRqB8dk/mPxQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/b64": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/cryptiles": "^6.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/mimos": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@hapi/mimos/-/mimos-7.0.1.tgz", + "integrity": "sha512-b79V+BrG0gJ9zcRx1VGcCI6r6GEzzZUgiGEJVoq5gwzuB2Ig9Cax8dUuBauQCFKvl2YWSWyOc8mZ8HDaJOtkew==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "mime-db": "^1.52.0" + } + }, + "node_modules/@hapi/nigel": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hapi/nigel/-/nigel-5.0.1.tgz", + "integrity": "sha512-uv3dtYuB4IsNaha+tigWmN8mQw/O9Qzl5U26Gm4ZcJVtDdB1AVJOwX3X5wOX+A07qzpEZnOMBAm8jjSqGsU6Nw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/vise": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/pez": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@hapi/pez/-/pez-6.1.1.tgz", + "integrity": "sha512-yg2OS1tC0S1sHXvhUtWsfRn6lrKl9jKtRhZ+EI0woOW/gqX5vM2PZ1459ypCvCYDRLJ9nIyueeEH5MJV1ZDqIg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/b64": "^6.0.1", + "@hapi/boom": "^10.0.1", + "@hapi/content": "^6.0.1", + "@hapi/hoek": "^11.0.7", + "@hapi/nigel": "^5.0.1" + } + }, + "node_modules/@hapi/podium": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@hapi/podium/-/podium-5.0.2.tgz", + "integrity": "sha512-T7gf2JYHQQfEfewTQFbsaXoZxSvuXO/QBIGljucUQ/lmPnTTNAepoIKOakWNVWvo2fMEDjycu77r8k6dhreqHA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/teamwork": "^6.0.0", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/shot": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/shot/-/shot-6.0.2.tgz", + "integrity": "sha512-WKK1ShfJTrL1oXC0skoIZQYzvLsyMDEF8lfcWuQBjpjCN29qivr9U36ld1z0nt6edvzv28etNMOqUF4klnHryw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/somever": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@hapi/somever/-/somever-4.1.1.tgz", + "integrity": "sha512-lt3QQiDDOVRatS0ionFDNrDIv4eXz58IibQaZQDOg4DqqdNme8oa0iPWcE0+hkq/KTeBCPtEOjDOBKBKwDumVg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/bounce": "^3.0.1", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/statehood": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@hapi/statehood/-/statehood-8.2.1.tgz", + "integrity": "sha512-xf72TG/QINW26jUu+uL5H+crE1o8GplIgfPWwPZhnAGJzetIVAQEQYvzq+C0aEVHg5/lMMtQ+L9UryuSa5Yjkg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bounce": "^3.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/cryptiles": "^6.0.1", + "@hapi/hoek": "^11.0.2", + "@hapi/iron": "^7.0.1", + "@hapi/validate": "^2.0.1" + } + }, + "node_modules/@hapi/subtext": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@hapi/subtext/-/subtext-8.1.3.tgz", + "integrity": "sha512-WTpEZQjBP3UJ3gGunNl3w5Ao1EOJsuu2vttZ2KEcG+csSLxc0dI6VIkl2md2jDlHiQ2ARAoqdSUScy05A/NHtA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/content": "^6.0.2", + "@hapi/file": "^3.0.0", + "@hapi/hoek": "^11.0.7", + "@hapi/pez": "^6.1.1", + "@hapi/wreck": "^18.1.1" + } + }, + "node_modules/@hapi/teamwork": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@hapi/teamwork/-/teamwork-6.0.1.tgz", + "integrity": "sha512-52OXRslUfYwXAOG8k58f2h2ngXYQGP0x5RPOo+eWA/FtyLgHjGMrE3+e9LSXP/0q2YfHAK5wj9aA9DTy1K+kyQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/validate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/validate/-/validate-2.0.1.tgz", + "integrity": "sha512-NZmXRnrSLK8MQ9y/CMqE9WSspgB9xA41/LlYR0k967aSZebWr4yNrpxIbov12ICwKy4APSlWXZga9jN5p6puPA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2", + "@hapi/topo": "^6.0.1" + } + }, + "node_modules/@hapi/vise": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hapi/vise/-/vise-5.0.1.tgz", + "integrity": "sha512-XZYWzzRtINQLedPYlIkSkUr7m5Ddwlu99V9elh8CSygXstfv3UnWIXT0QD+wmR0VAG34d2Vx3olqcEhRRoTu9A==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hapi/wreck": { + "version": "18.1.2", + "resolved": "https://registry.npmjs.org/@hapi/wreck/-/wreck-18.1.2.tgz", + "integrity": "sha512-3dMnV2pfhQiyEqu8DL3VBmxkdLiRDiiUDuG79Dp+UK1gL9ZxAfDOUhB6k3D5MLqcgJJ1IARyGFhwoc1NITr/pg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/bourne": "^3.0.0", + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@prisma/adapter-mariadb": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-mariadb/-/adapter-mariadb-7.8.0.tgz", + "integrity": "sha512-mWsgcfbUjxB3qSzRlLs8E03vsKrqXzYK2zpx3e8u6wIgeHJM/sE46cuOGcYvHiZGmeQLCd3xL6YSSGM9QOLI6w==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.8.0", + "mariadb": "3.4.5" + } + }, + "node_modules/@prisma/client": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", + "integrity": "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/client-runtime-utils": "7.8.0" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/client-runtime-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.8.0.tgz", + "integrity": "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/config": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz", + "integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.3.4", + "deepmerge-ts": "7.1.5", + "effect": "3.20.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz", + "integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/dev": { + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.24.3.tgz", + "integrity": "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "@electric-sql/pglite": "0.4.1", + "@electric-sql/pglite-socket": "0.1.1", + "@electric-sql/pglite-tools": "0.3.1", + "@hono/node-server": "1.19.11", + "@prisma/get-platform": "7.2.0", + "@prisma/query-plan-executor": "7.2.0", + "@prisma/streams-local": "0.1.2", + "foreground-child": "3.3.1", + "get-port-please": "3.2.0", + "hono": "^4.12.8", + "http-status-codes": "2.3.0", + "pathe": "2.0.3", + "proper-lockfile": "4.1.2", + "remeda": "2.33.4", + "std-env": "3.10.0", + "valibot": "1.2.0", + "zeptomatch": "2.1.0" + } + }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz", + "integrity": "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/engines": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz", + "integrity": "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/fetch-engine": "7.8.0", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a.tgz", + "integrity": "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.8.0.tgz", + "integrity": "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0", + "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", + "@prisma/get-platform": "7.8.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.8.0.tgz", + "integrity": "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", + "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.2.0" + } + }, + "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", + "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/query-plan-executor": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", + "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/streams-local": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@prisma/streams-local/-/streams-local-0.1.2.tgz", + "integrity": "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.12.0", + "better-result": "^2.7.0", + "env-paths": "^3.0.0", + "proper-lockfile": "^4.1.2" + }, + "engines": { + "bun": ">=1.3.6", + "node": ">=22.0.0" + } + }, + "node_modules/@prisma/studio-core": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.27.3.tgz", + "integrity": "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@radix-ui/react-toggle": "1.1.10", + "chart.js": "4.5.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0", + "pnpm": "8" + }, + "peerDependencies": { + "@types/react": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@tsoa/cli": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@tsoa/cli/-/cli-6.6.0.tgz", + "integrity": "sha512-thSW0EiqjkF7HspcPIVIy0ZX65VqbWALHbxwl8Sk83j2kakOMq+fJvfo8FcBAWlMki+JDH7CO5iaAaSLHbeqtg==", + "license": "MIT", + "dependencies": { + "@tsoa/runtime": "^6.6.0", + "@types/multer": "^1.4.12", + "fs-extra": "^11.2.0", + "glob": "^10.3.10", + "handlebars": "^4.7.8", + "merge-anything": "^5.1.7", + "minimatch": "^9.0.1", + "ts-deepmerge": "^7.0.2", + "typescript": "^5.7.2", + "validator": "^13.12.0", + "yaml": "^2.6.1", + "yargs": "^17.7.1" + }, + "bin": { + "tsoa": "dist/cli.js" + }, + "engines": { + "node": ">=18.0.0", + "yarn": ">=1.9.4" + } + }, + "node_modules/@tsoa/cli/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/@tsoa/cli/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tsoa/cli/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@tsoa/cli/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@tsoa/runtime": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@tsoa/runtime/-/runtime-6.6.0.tgz", + "integrity": "sha512-+rF2gdL8CX+jQ82/IBc+MRJFNAvWPoBBl77HHJv3ESVMqbKhlhlo97JHmKyFbLcX6XOJN8zl8gfQpAEJN4SOMQ==", + "license": "MIT", + "dependencies": { + "@hapi/boom": "^10.0.1", + "@hapi/hapi": "^21.3.12", + "@types/koa": "^2.15.0", + "@types/multer": "^1.4.12", + "express": "^4.21.2", + "reflect-metadata": "^0.2.2", + "validator": "^13.12.0" + }, + "engines": { + "node": ">=18.0.0", + "yarn": ">=1.9.4" + } + }, + "node_modules/@tsoa/runtime/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/@tsoa/runtime/node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "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.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/@tsoa/runtime/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/@tsoa/runtime/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/@tsoa/runtime/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/@tsoa/runtime/node_modules/debug/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/@tsoa/runtime/node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "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.15.1", + "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/@tsoa/runtime/node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "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.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@tsoa/runtime/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/@tsoa/runtime/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/@tsoa/runtime/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/@tsoa/runtime/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/@tsoa/runtime/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/@tsoa/runtime/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/@tsoa/runtime/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/@tsoa/runtime/node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/@tsoa/runtime/node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@tsoa/runtime/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/@tsoa/runtime/node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@tsoa/runtime/node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@tsoa/runtime/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/@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/content-disposition": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz", + "integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==", + "license": "MIT" + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cookies": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.2.tgz", + "integrity": "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/http-assert": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz", + "integrity": "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "license": "MIT" + }, + "node_modules/@types/koa": { + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.2.tgz", + "integrity": "sha512-CB+iyjjh1uS5N6/CKwXvw0qA7USMS2WVc4Tjf660yCjhdvqzNr8gdFcIawB41zGGptOQ+d1fnpaQWIIUXYxR3w==", + "license": "MIT", + "dependencies": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "node_modules/@types/koa-compose": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.9.tgz", + "integrity": "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==", + "license": "MIT", + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/multer": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", + "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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/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/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/better-result": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.0.tgz", + "integrity": "sha512-NHwGDGVbRlWDOce3CwcfGIrcNR9zY37ut3SVwQVfv57DZdVhxjhA4mfaHN1n8QwWnRAR4iErpW1X/eaiaUaFYg==", + "devOptional": true, + "license": "MIT" + }, + "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": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/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/c12": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" + }, + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "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/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/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/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/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/cliui/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/cliui/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/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/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/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT", + "peer": true + }, + "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/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "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/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "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/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "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/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "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/effect": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz", + "integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "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/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "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/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 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-port-please": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", + "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", + "devOptional": true, + "license": "MIT" + }, + "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/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz", + "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", + "devOptional": true, + "license": "MIT", + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "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/glob/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/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/grammex": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", + "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/graphmatch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", + "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "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/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.15", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz", + "integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "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/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/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "devOptional": true, + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/mariadb": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/mariadb/-/mariadb-3.4.5.tgz", + "integrity": "sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ==", + "license": "LGPL-2.1-or-later", + "dependencies": { + "@types/geojson": "^7946.0.16", + "@types/node": "^24.0.13", + "denque": "^2.1.0", + "iconv-lite": "^0.6.3", + "lru-cache": "^10.4.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/mariadb/node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/mariadb/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "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": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-anything": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", + "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==", + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "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.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/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/morgan/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/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "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/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/nodemon/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/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/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/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "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/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "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/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "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-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "devOptional": true, + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/prisma": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.8.0.tgz", + "integrity": "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "7.8.0", + "@prisma/dev": "0.24.3", + "@prisma/engines": "7.8.0", + "@prisma/studio-core": "0.27.3", + "mysql2": "3.15.3", + "postgres": "3.4.7" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24.0" + }, + "peerDependencies": { + "better-sqlite3": ">=9.0.0", + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/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==", + "devOptional": true, + "license": "ISC" + }, + "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/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "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": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/rc9": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz", + "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.6", + "destr": "^2.0.5" + } + }, + "node_modules/react": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", + "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", + "devOptional": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.5" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/remeda": { + "version": "2.33.4", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", + "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/remeda" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "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/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "devOptional": true, + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", + "devOptional": true + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "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/string-width-cjs/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/string-width-cjs/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/string-width-cjs/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/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "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/strip-ansi-cjs/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/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/swagger-ui-dist": { + "version": "5.32.6", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz", + "integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "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/ts-deepmerge": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-7.0.3.tgz", + "integrity": "sha512-Du/ZW2RfwV/D4cmA5rXafYjBQVuvu4qGiEEla4EmEHVHgRdx68Gftx7i66jn2bzHPwSVZY36Ae6OuDn9el4ZKA==", + "license": "ISC", + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/tsoa": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/tsoa/-/tsoa-6.6.0.tgz", + "integrity": "sha512-7FudRojmbEpbSQ3t1pyG5EjV3scF7/X75giQt1q+tnuGjjJppB8BOEmIdCK/G8S5Dqnmpwz5Q3vxluKozpIW9A==", + "license": "MIT", + "dependencies": { + "@tsoa/cli": "^6.6.0", + "@tsoa/runtime": "^6.6.0" + }, + "bin": { + "tsoa": "dist/cli.js" + }, + "engines": { + "node": ">=18.0.0", + "yarn": ">=1.9.4" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "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/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "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/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/valibot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", + "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/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/yargs/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/yargs/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/yargs/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/zeptomatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", + "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "grammex": "^3.1.11", + "graphmatch": "^1.1.0" + } + } + } +} diff --git "a/\353\217\204\354\226\217/week8/package.json" "b/\353\217\204\354\226\217/week8/package.json" new file mode 100644 index 0000000..9bce09c --- /dev/null +++ "b/\353\217\204\354\226\217/week8/package.json" @@ -0,0 +1,33 @@ +{ + "scripts": { + "build": "tsoa spec-and-routes && tsc", + "start": "tsoa spec-and-routes && tsx src/index.ts", + "dev": "tsoa spec-and-routes && nodemon --ext ts,prisma --ignore src/generated --exec \"npx prisma generate && tsx src/index.ts\"" + }, + "dependencies": { + "@prisma/adapter-mariadb": "^7.8.0", + "@prisma/client": "^7.8.0", + "bcryptjs": "^3.0.3", + "cookie-parser": "^1.4.7", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "http-status-codes": "^2.3.0", + "morgan": "^1.10.1", + "swagger-ui-express": "^5.0.1", + "tsoa": "^6.6.0" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/morgan": "^1.9.10", + "@types/node": "^25.6.0", + "@types/swagger-ui-express": "^4.1.8", + "nodemon": "^3.1.14", + "prisma": "^7.8.0", + "tsx": "^4.21.0", + "typescript": "^6.0.3" + } +} diff --git "a/\353\217\204\354\226\217/week8/prisma.config.ts" "b/\353\217\204\354\226\217/week8/prisma.config.ts" new file mode 100644 index 0000000..5170cc4 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/prisma.config.ts" @@ -0,0 +1,12 @@ +/// +import "dotenv/config"; +import { defineConfig } from "prisma/config"; + +const { DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME } = process.env; + +export default defineConfig({ + schema: "prisma/schema.prisma", + datasource: { + url: `mysql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT ?? 3306}/${DB_NAME}`, + }, +}); diff --git "a/\353\217\204\354\226\217/week8/prisma/migrations/20260428071132_init_database/migration.sql" "b/\353\217\204\354\226\217/week8/prisma/migrations/20260428071132_init_database/migration.sql" new file mode 100644 index 0000000..31b12a1 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/prisma/migrations/20260428071132_init_database/migration.sql" @@ -0,0 +1,39 @@ +-- CreateTable +CREATE TABLE `user` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `email` VARCHAR(255) NOT NULL, + `name` VARCHAR(100) NOT NULL, + `gender` VARCHAR(15) NOT NULL, + `birth` DATE NOT NULL, + `address` VARCHAR(255) NOT NULL, + `detail_address` VARCHAR(255) NULL, + `phone_number` VARCHAR(15) NOT NULL, + + UNIQUE INDEX `email`(`email`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `food_category` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `user_favor_category` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `user_id` INTEGER NOT NULL, + `food_category_id` INTEGER NOT NULL, + + INDEX `f_category_id`(`food_category_id`), + INDEX `user_id`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `user_favor_category` ADD CONSTRAINT `user_favor_category_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_favor_category` ADD CONSTRAINT `user_favor_category_food_category_id_fkey` FOREIGN KEY (`food_category_id`) REFERENCES `food_category`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git "a/\353\217\204\354\226\217/week8/prisma/migrations/20260428071239_add_store_and_review_tables/migration.sql" "b/\353\217\204\354\226\217/week8/prisma/migrations/20260428071239_add_store_and_review_tables/migration.sql" new file mode 100644 index 0000000..99445d4 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/prisma/migrations/20260428071239_add_store_and_review_tables/migration.sql" @@ -0,0 +1,25 @@ +-- CreateTable +CREATE TABLE `store` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(100) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `user_store_review` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `store_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `content` TEXT NOT NULL, + + INDEX `store_id`(`store_id`), + INDEX `user_id`(`user_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `user_store_review` ADD CONSTRAINT `user_store_review_store_id_fkey` FOREIGN KEY (`store_id`) REFERENCES `store`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `user_store_review` ADD CONSTRAINT `user_store_review_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git "a/\353\217\204\354\226\217/week8/prisma/migrations/20260428072732_npx_prisma_migrate_dev/migration.sql" "b/\353\217\204\354\226\217/week8/prisma/migrations/20260428072732_npx_prisma_migrate_dev/migration.sql" new file mode 100644 index 0000000..a6dd8b0 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/prisma/migrations/20260428072732_npx_prisma_migrate_dev/migration.sql" @@ -0,0 +1,33 @@ +-- CreateTable +CREATE TABLE `mission` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `store_id` INTEGER NOT NULL, + `title` VARCHAR(200) NOT NULL, + `reward` INTEGER NOT NULL, + `spec` TEXT NULL, + `dead_line` DATETIME(3) NULL, + + INDEX `store_id`(`store_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `member_mission` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `member_id` INTEGER NOT NULL, + `mission_id` INTEGER NOT NULL, + `status` VARCHAR(15) NOT NULL, + + INDEX `member_id`(`member_id`), + INDEX `mission_id`(`mission_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- AddForeignKey +ALTER TABLE `mission` ADD CONSTRAINT `mission_store_id_fkey` FOREIGN KEY (`store_id`) REFERENCES `store`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `member_mission` ADD CONSTRAINT `member_mission_member_id_fkey` FOREIGN KEY (`member_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE `member_mission` ADD CONSTRAINT `member_mission_mission_id_fkey` FOREIGN KEY (`mission_id`) REFERENCES `mission`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git "a/\353\217\204\354\226\217/week8/prisma/migrations/migration_lock.toml" "b/\353\217\204\354\226\217/week8/prisma/migrations/migration_lock.toml" new file mode 100644 index 0000000..592fc0b --- /dev/null +++ "b/\353\217\204\354\226\217/week8/prisma/migrations/migration_lock.toml" @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "mysql" diff --git "a/\353\217\204\354\226\217/week8/prisma/schema.prisma" "b/\353\217\204\354\226\217/week8/prisma/schema.prisma" new file mode 100644 index 0000000..a5758e0 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/prisma/schema.prisma" @@ -0,0 +1,100 @@ +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +datasource db { + provider = "mysql" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique(map: "email") @db.VarChar(255) + name String @db.VarChar(100) + gender String @db.VarChar(15) + birth DateTime @db.Date + address String @db.VarChar(255) + detailAddress String? @map("detail_address") @db.VarChar(255) + phoneNumber String @map("phone_number") @db.VarChar(15) + + userFavorCategories UserFavorCategory[] + reviews UserStoreReview[] + memberMissions MemberMission[] + + @@map("user") +} + +model FoodCategory { + id Int @id @default(autoincrement()) + name String @db.VarChar(100) + + userFavorCategories UserFavorCategory[] + + @@map("food_category") +} + +model UserFavorCategory { + id Int @id @default(autoincrement()) + userId Int @map("user_id") + foodCategoryId Int @map("food_category_id") + user User @relation(fields: [userId], references: [id]) + foodCategory FoodCategory @relation(fields: [foodCategoryId], references: [id]) + + @@index([foodCategoryId], map: "f_category_id") + @@index([userId], map: "user_id") + @@map("user_favor_category") +} + +model Store { + id Int @id @default(autoincrement()) + name String @db.VarChar(100) + + reviews UserStoreReview[] + missions Mission[] + + @@map("store") +} + +model UserStoreReview { + id Int @id @default(autoincrement()) + storeId Int @map("store_id") + userId Int @map("user_id") + content String @db.Text + + store Store @relation(fields: [storeId], references: [id]) + user User @relation(fields: [userId], references: [id]) + + @@index([storeId], map: "store_id") + @@index([userId], map: "user_id") + @@map("user_store_review") +} + +model Mission { + id Int @id @default(autoincrement()) + storeId Int @map("store_id") + title String @db.VarChar(200) + reward Int + spec String? @db.Text + deadLine DateTime? @map("dead_line") + + store Store @relation(fields: [storeId], references: [id]) + memberMissions MemberMission[] + + @@index([storeId], map: "store_id") + @@map("mission") +} + +model MemberMission { + id Int @id @default(autoincrement()) + userId Int @map("member_id") + missionId Int @map("mission_id") + status String @db.VarChar(15) + + user User @relation(fields: [userId], references: [id]) + mission Mission @relation(fields: [missionId], references: [id]) + + @@index([userId], map: "member_id") + @@index([missionId], map: "mission_id") + @@map("member_mission") +} + diff --git "a/\353\217\204\354\226\217/week8/reset_db.sql" "b/\353\217\204\354\226\217/week8/reset_db.sql" new file mode 100644 index 0000000..454f20b --- /dev/null +++ "b/\353\217\204\354\226\217/week8/reset_db.sql" @@ -0,0 +1,215 @@ +-- ============================================================ +-- DB 초기화 및 재생성 스크립트 +-- ============================================================ + +CREATE DATABASE IF NOT EXISTS umc_mission DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +USE umc_mission; + +-- FK 체크 비활성화 후 전체 DROP +SET FOREIGN_KEY_CHECKS = 0; + +DROP TABLE IF EXISTS review_image; +DROP TABLE IF EXISTS review; +DROP TABLE IF EXISTS member_mission; +DROP TABLE IF EXISTS mission; +DROP TABLE IF EXISTS store_hours; +DROP TABLE IF EXISTS store_image; +DROP TABLE IF EXISTS store; +DROP TABLE IF EXISTS member_prefer; +DROP TABLE IF EXISTS member_agree; +DROP TABLE IF EXISTS member; +DROP TABLE IF EXISTS terms; +DROP TABLE IF EXISTS food_category; +DROP TABLE IF EXISTS region; + +SET FOREIGN_KEY_CHECKS = 1; + +-- ============================================================ +-- 테이블 재생성 +-- ============================================================ + +CREATE TABLE region ( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL COMMENT '지역명', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE food_category ( + id BIGINT NOT NULL AUTO_INCREMENT, + name VARCHAR(50) NOT NULL COMMENT '카테고리명 (한식, 중식, 일식 등)', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE terms ( + id BIGINT NOT NULL AUTO_INCREMENT, + title VARCHAR(100) NOT NULL COMMENT '약관 제목', + content TEXT NOT NULL COMMENT '약관 내용', + optional BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'TRUE: 선택 동의, FALSE: 필수 동의', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id) +); + +CREATE TABLE member ( + id BIGINT NOT NULL AUTO_INCREMENT, + social_type VARCHAR(20) NULL, + social_id VARCHAR(100) NULL, + email VARCHAR(100) NULL, + password VARCHAR(255) NULL, + name VARCHAR(50) NOT NULL, + nickname VARCHAR(50) NOT NULL, + profile_image_url VARCHAR(500) NULL, + phone_num VARCHAR(20) NULL, + phone_verified BOOLEAN NOT NULL DEFAULT FALSE, + birth DATE NULL, + gender ENUM('MALE', 'FEMALE', 'OTHER') NULL, + address VARCHAR(200) NULL, + spec_address VARCHAR(200) NULL, + point INT NOT NULL DEFAULT 0, + status ENUM('ACTIVE', 'INACTIVE', 'BANNED') NOT NULL DEFAULT 'ACTIVE', + inactive_date DATETIME NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_member_email (email), + UNIQUE KEY uq_member_social (social_type, social_id) +); + +CREATE TABLE member_agree ( + member_id BIGINT NOT NULL, + terms_id BIGINT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (member_id, terms_id), + CONSTRAINT fk_member_agree_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_member_agree_terms FOREIGN KEY (terms_id) REFERENCES terms (id) +); + +CREATE TABLE member_prefer ( + member_id BIGINT NOT NULL, + food_id BIGINT NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (member_id, food_id), + CONSTRAINT fk_member_prefer_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_member_prefer_food FOREIGN KEY (food_id) REFERENCES food_category (id) +); + +CREATE TABLE store ( + id BIGINT NOT NULL AUTO_INCREMENT, + region_id BIGINT NOT NULL, + food_category_id BIGINT NOT NULL, + name VARCHAR(100) NOT NULL, + description TEXT NULL, + lat DECIMAL(10,7) NULL, + lng DECIMAL(10,7) NULL, + address VARCHAR(200) NOT NULL, + status ENUM('OPEN', 'CLOSED', 'PENDING') NOT NULL DEFAULT 'OPEN', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_store_region FOREIGN KEY (region_id) REFERENCES region (id), + CONSTRAINT fk_store_category FOREIGN KEY (food_category_id) REFERENCES food_category (id) +); + +CREATE TABLE store_image ( + id BIGINT NOT NULL AUTO_INCREMENT, + store_id BIGINT NOT NULL, + image_url VARCHAR(500) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_store_image_store FOREIGN KEY (store_id) REFERENCES store (id) +); + +CREATE TABLE store_hours ( + id BIGINT NOT NULL AUTO_INCREMENT, + store_id BIGINT NOT NULL, + day_of_week VARCHAR(3) NOT NULL, + open_time TIME NOT NULL, + close_time TIME NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_store_hours_day (store_id, day_of_week), + CONSTRAINT fk_store_hours_store FOREIGN KEY (store_id) REFERENCES store (id) +); + +CREATE TABLE mission ( + id BIGINT NOT NULL AUTO_INCREMENT, + store_id BIGINT NOT NULL, + title VARCHAR(200) NOT NULL, + reward INT NOT NULL DEFAULT 0, + spec VARCHAR(500) NULL, + dead_line DATE NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_mission_store FOREIGN KEY (store_id) REFERENCES store (id) +); + +CREATE TABLE member_mission ( + id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + mission_id BIGINT NOT NULL, + status ENUM('CHALLENGING', 'COMPLETE') NOT NULL DEFAULT 'CHALLENGING', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + UNIQUE KEY uq_member_mission (member_id, mission_id), + CONSTRAINT fk_member_mission_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_member_mission_mission FOREIGN KEY (mission_id) REFERENCES mission (id) +); + +CREATE TABLE review ( + id BIGINT NOT NULL AUTO_INCREMENT, + member_id BIGINT NOT NULL, + store_id BIGINT NOT NULL, + member_mission_id BIGINT NULL, + content TEXT NOT NULL, + score DECIMAL(2,1) NOT NULL, + owner_reply VARCHAR(500) NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_review_member FOREIGN KEY (member_id) REFERENCES member (id), + CONSTRAINT fk_review_store FOREIGN KEY (store_id) REFERENCES store (id), + CONSTRAINT fk_review_member_mission FOREIGN KEY (member_mission_id) REFERENCES member_mission (id), + CONSTRAINT chk_review_score CHECK (score BETWEEN 1.0 AND 5.0) +); + +CREATE TABLE review_image ( + id BIGINT NOT NULL AUTO_INCREMENT, + review_id BIGINT NOT NULL, + image_url VARCHAR(500) NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_review_image_review FOREIGN KEY (review_id) REFERENCES review (id) +); + +-- ============================================================ +-- 시드 데이터 (API 테스트용 초기 데이터) +-- ============================================================ + +INSERT INTO region (name) VALUES + ('서울'), + ('경기'), + ('인천'), + ('부산'), + ('대구'); + +INSERT INTO food_category (name) VALUES + ('한식'), + ('중식'), + ('일식'), + ('양식'), + ('분식'), + ('카페/디저트'), + ('치킨'), + ('피자'), + ('패스트푸드'); diff --git "a/\353\217\204\354\226\217/week8/src/db.config.ts" "b/\353\217\204\354\226\217/week8/src/db.config.ts" new file mode 100644 index 0000000..7b88907 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/db.config.ts" @@ -0,0 +1,17 @@ +import "dotenv/config"; +import { PrismaClient } from "./generated/prisma/client.js"; +import { PrismaMariaDb } from "@prisma/adapter-mariadb"; + +const adapter = new PrismaMariaDb({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + port: process.env.DB_PORT ? parseInt(process.env.DB_PORT, 10) : 3306, + connectionLimit: 10, +}); + +export const prisma = new PrismaClient({ + adapter, + log: ["query", "info", "error", "warn"], +}); diff --git "a/\353\217\204\354\226\217/week8/src/generated/routes.ts" "b/\353\217\204\354\226\217/week8/src/generated/routes.ts" new file mode 100644 index 0000000..020aa77 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/generated/routes.ts" @@ -0,0 +1,622 @@ +/* tslint:disable */ +/* eslint-disable */ +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import type { TsoaRoute } from '@tsoa/runtime'; +import { fetchMiddlewares, ExpressTemplateService } from '@tsoa/runtime'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { StoreController } from './../modules/stores/controllers/store.controller'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { MissionController } from './../modules/missions/controllers/mission.controller'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { UserController } from './../modules/members/controllers/user.controller'; +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +import { MemberController } from './../modules/members/controllers/member.controller'; +import type { Request as ExRequest, Response as ExResponse, RequestHandler, Router } from 'express'; + + + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + +const models: TsoaRoute.Models = { + "StoreCreateResponse": { + "dataType": "refObject", + "properties": { + "storeId": {"dataType":"double","required":true}, + "name": {"dataType":"string","required":true}, + "address": {"dataType":"string","required":true}, + "regionId": {"dataType":"double","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ApiResponse_StoreCreateResponse_": { + "dataType": "refObject", + "properties": { + "isSuccess": {"dataType":"boolean","required":true}, + "code": {"dataType":"string","required":true}, + "message": {"dataType":"string","required":true}, + "result": {"dataType":"union","subSchemas":[{"ref":"StoreCreateResponse"},{"dataType":"enum","enums":[null]}],"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "StoreCreateRequest": { + "dataType": "refObject", + "properties": { + "regionId": {"dataType":"double","required":true}, + "foodCategoryId": {"dataType":"double","required":true}, + "name": {"dataType":"string","required":true}, + "description": {"dataType":"string"}, + "address": {"dataType":"string","required":true}, + "lat": {"dataType":"double"}, + "lng": {"dataType":"double"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ReviewItem": { + "dataType": "refObject", + "properties": { + "reviewId": {"dataType":"double","required":true}, + "memberId": {"dataType":"double","required":true}, + "storeId": {"dataType":"double","required":true}, + "content": {"dataType":"string","required":true}, + "score": {"dataType":"double","required":true}, + "createdAt": {"dataType":"datetime","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ReviewListResponse": { + "dataType": "refObject", + "properties": { + "data": {"dataType":"array","array":{"dataType":"refObject","ref":"ReviewItem"},"required":true}, + "pagination": {"dataType":"nestedObjectLiteral","nestedProperties":{"cursor":{"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"enum","enums":[null]}],"required":true}},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ApiResponse_ReviewListResponse_": { + "dataType": "refObject", + "properties": { + "isSuccess": {"dataType":"boolean","required":true}, + "code": {"dataType":"string","required":true}, + "message": {"dataType":"string","required":true}, + "result": {"dataType":"union","subSchemas":[{"ref":"ReviewListResponse"},{"dataType":"enum","enums":[null]}],"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ReviewCreateResponse": { + "dataType": "refObject", + "properties": { + "reviewId": {"dataType":"double","required":true}, + "memberId": {"dataType":"double","required":true}, + "storeId": {"dataType":"double","required":true}, + "content": {"dataType":"string","required":true}, + "score": {"dataType":"double","required":true}, + "createdAt": {"dataType":"datetime","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ApiResponse_ReviewCreateResponse_": { + "dataType": "refObject", + "properties": { + "isSuccess": {"dataType":"boolean","required":true}, + "code": {"dataType":"string","required":true}, + "message": {"dataType":"string","required":true}, + "result": {"dataType":"union","subSchemas":[{"ref":"ReviewCreateResponse"},{"dataType":"enum","enums":[null]}],"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ReviewCreateRequest": { + "dataType": "refObject", + "properties": { + "memberId": {"dataType":"double","required":true}, + "content": {"dataType":"string","required":true}, + "score": {"dataType":"double","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "MissionCreateResponse": { + "dataType": "refObject", + "properties": { + "missionId": {"dataType":"double","required":true}, + "storeId": {"dataType":"double","required":true}, + "title": {"dataType":"string","required":true}, + "reward": {"dataType":"double","required":true}, + "spec": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true}, + "deadLine": {"dataType":"union","subSchemas":[{"dataType":"datetime"},{"dataType":"enum","enums":[null]}],"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "StoreMissionListResponse": { + "dataType": "refObject", + "properties": { + "data": {"dataType":"array","array":{"dataType":"refObject","ref":"MissionCreateResponse"},"required":true}, + "pagination": {"dataType":"nestedObjectLiteral","nestedProperties":{"cursor":{"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"enum","enums":[null]}],"required":true}},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ApiResponse_StoreMissionListResponse_": { + "dataType": "refObject", + "properties": { + "isSuccess": {"dataType":"boolean","required":true}, + "code": {"dataType":"string","required":true}, + "message": {"dataType":"string","required":true}, + "result": {"dataType":"union","subSchemas":[{"ref":"StoreMissionListResponse"},{"dataType":"enum","enums":[null]}],"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ApiResponse_MissionCreateResponse_": { + "dataType": "refObject", + "properties": { + "isSuccess": {"dataType":"boolean","required":true}, + "code": {"dataType":"string","required":true}, + "message": {"dataType":"string","required":true}, + "result": {"dataType":"union","subSchemas":[{"ref":"MissionCreateResponse"},{"dataType":"enum","enums":[null]}],"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "MissionCreateRequest": { + "dataType": "refObject", + "properties": { + "title": {"dataType":"string","required":true}, + "reward": {"dataType":"double","required":true}, + "spec": {"dataType":"string"}, + "deadLine": {"dataType":"string"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "MissionChallengeResponse": { + "dataType": "refObject", + "properties": { + "memberMissionId": {"dataType":"double","required":true}, + "memberId": {"dataType":"double","required":true}, + "missionId": {"dataType":"double","required":true}, + "status": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ApiResponse_MissionChallengeResponse_": { + "dataType": "refObject", + "properties": { + "isSuccess": {"dataType":"boolean","required":true}, + "code": {"dataType":"string","required":true}, + "message": {"dataType":"string","required":true}, + "result": {"dataType":"union","subSchemas":[{"ref":"MissionChallengeResponse"},{"dataType":"enum","enums":[null]}],"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "MissionChallengeRequest": { + "dataType": "refObject", + "properties": { + "memberId": {"dataType":"double","required":true}, + "status": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["CHALLENGING"]},{"dataType":"enum","enums":["COMPLETE"]}],"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "UserReviewListResponse": { + "dataType": "refObject", + "properties": { + "data": {"dataType":"array","array":{"dataType":"refObject","ref":"ReviewCreateResponse"},"required":true}, + "pagination": {"dataType":"nestedObjectLiteral","nestedProperties":{"cursor":{"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"enum","enums":[null]}],"required":true}},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ApiResponse_UserReviewListResponse_": { + "dataType": "refObject", + "properties": { + "isSuccess": {"dataType":"boolean","required":true}, + "code": {"dataType":"string","required":true}, + "message": {"dataType":"string","required":true}, + "result": {"dataType":"union","subSchemas":[{"ref":"UserReviewListResponse"},{"dataType":"enum","enums":[null]}],"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "OngoingMissionListResponse": { + "dataType": "refObject", + "properties": { + "data": {"dataType":"array","array":{"dataType":"refObject","ref":"MissionCreateResponse"},"required":true}, + "pagination": {"dataType":"nestedObjectLiteral","nestedProperties":{"cursor":{"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"enum","enums":[null]}],"required":true}},"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ApiResponse_OngoingMissionListResponse_": { + "dataType": "refObject", + "properties": { + "isSuccess": {"dataType":"boolean","required":true}, + "code": {"dataType":"string","required":true}, + "message": {"dataType":"string","required":true}, + "result": {"dataType":"union","subSchemas":[{"ref":"OngoingMissionListResponse"},{"dataType":"enum","enums":[null]}],"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "MemberSignUpResponse": { + "dataType": "refObject", + "properties": { + "memberId": {"dataType":"double","required":true}, + "name": {"dataType":"string","required":true}, + "nickname": {"dataType":"string","required":true}, + "email": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true}, + "phoneNum": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true}, + "status": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ApiResponse_MemberSignUpResponse_": { + "dataType": "refObject", + "properties": { + "isSuccess": {"dataType":"boolean","required":true}, + "code": {"dataType":"string","required":true}, + "message": {"dataType":"string","required":true}, + "result": {"dataType":"union","subSchemas":[{"ref":"MemberSignUpResponse"},{"dataType":"enum","enums":[null]}],"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "MemberSignUpRequest": { + "dataType": "refObject", + "properties": { + "name": {"dataType":"string","required":true}, + "nickname": {"dataType":"string","required":true}, + "email": {"dataType":"string"}, + "password": {"dataType":"string"}, + "phoneNum": {"dataType":"string"}, + "birth": {"dataType":"string"}, + "gender": {"dataType":"string"}, + "address": {"dataType":"string"}, + "specAddress": {"dataType":"string"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +}; +const templateService = new ExpressTemplateService(models, {"noImplicitAdditionalProperties":"throw-on-extras","bodyCoercion":true}); + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + + + +export function RegisterRoutes(app: Router) { + + // ########################################################################################################### + // NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look + // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa + // ########################################################################################################### + + + + const argsStoreController_handleCreateStore: Record = { + body: {"in":"body","name":"body","required":true,"ref":"StoreCreateRequest"}, + }; + app.post('/stores', + ...(fetchMiddlewares(StoreController)), + ...(fetchMiddlewares(StoreController.prototype.handleCreateStore)), + + async function StoreController_handleCreateStore(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsStoreController_handleCreateStore, request, response }); + + const controller = new StoreController(); + + await templateService.apiHandler({ + methodName: 'handleCreateStore', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsStoreController_handleListStoreReviews: Record = { + storeId: {"in":"path","name":"storeId","required":true,"dataType":"double"}, + cursor: {"in":"query","name":"cursor","dataType":"double"}, + }; + app.get('/stores/:storeId/reviews', + ...(fetchMiddlewares(StoreController)), + ...(fetchMiddlewares(StoreController.prototype.handleListStoreReviews)), + + async function StoreController_handleListStoreReviews(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsStoreController_handleListStoreReviews, request, response }); + + const controller = new StoreController(); + + await templateService.apiHandler({ + methodName: 'handleListStoreReviews', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsStoreController_handleCreateReview: Record = { + storeId: {"in":"path","name":"storeId","required":true,"dataType":"double"}, + body: {"in":"body","name":"body","required":true,"ref":"ReviewCreateRequest"}, + }; + app.post('/stores/:storeId/reviews', + ...(fetchMiddlewares(StoreController)), + ...(fetchMiddlewares(StoreController.prototype.handleCreateReview)), + + async function StoreController_handleCreateReview(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsStoreController_handleCreateReview, request, response }); + + const controller = new StoreController(); + + await templateService.apiHandler({ + methodName: 'handleCreateReview', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsStoreController_handleListStoreMissions: Record = { + storeId: {"in":"path","name":"storeId","required":true,"dataType":"double"}, + cursor: {"in":"query","name":"cursor","dataType":"double"}, + }; + app.get('/stores/:storeId/missions', + ...(fetchMiddlewares(StoreController)), + ...(fetchMiddlewares(StoreController.prototype.handleListStoreMissions)), + + async function StoreController_handleListStoreMissions(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsStoreController_handleListStoreMissions, request, response }); + + const controller = new StoreController(); + + await templateService.apiHandler({ + methodName: 'handleListStoreMissions', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsStoreController_handleCreateMission: Record = { + storeId: {"in":"path","name":"storeId","required":true,"dataType":"double"}, + body: {"in":"body","name":"body","required":true,"ref":"MissionCreateRequest"}, + }; + app.post('/stores/:storeId/missions', + ...(fetchMiddlewares(StoreController)), + ...(fetchMiddlewares(StoreController.prototype.handleCreateMission)), + + async function StoreController_handleCreateMission(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsStoreController_handleCreateMission, request, response }); + + const controller = new StoreController(); + + await templateService.apiHandler({ + methodName: 'handleCreateMission', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsMissionController_handleChallengeMission: Record = { + missionId: {"in":"path","name":"missionId","required":true,"dataType":"double"}, + body: {"in":"body","name":"body","required":true,"ref":"MissionChallengeRequest"}, + }; + app.post('/missions/:missionId/challenge', + ...(fetchMiddlewares(MissionController)), + ...(fetchMiddlewares(MissionController.prototype.handleChallengeMission)), + + async function MissionController_handleChallengeMission(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsMissionController_handleChallengeMission, request, response }); + + const controller = new MissionController(); + + await templateService.apiHandler({ + methodName: 'handleChallengeMission', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUserController_handleListUserReviews: Record = { + userId: {"in":"path","name":"userId","required":true,"dataType":"double"}, + cursor: {"in":"query","name":"cursor","dataType":"double"}, + }; + app.get('/users/:userId/reviews', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.handleListUserReviews)), + + async function UserController_handleListUserReviews(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUserController_handleListUserReviews, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'handleListUserReviews', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUserController_handleListOngoingMissions: Record = { + userId: {"in":"path","name":"userId","required":true,"dataType":"double"}, + cursor: {"in":"query","name":"cursor","dataType":"double"}, + }; + app.get('/users/:userId/missions', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.handleListOngoingMissions)), + + async function UserController_handleListOngoingMissions(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUserController_handleListOngoingMissions, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'handleListOngoingMissions', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsUserController_handleCompleteMission: Record = { + userId: {"in":"path","name":"userId","required":true,"dataType":"double"}, + missionId: {"in":"path","name":"missionId","required":true,"dataType":"double"}, + }; + app.patch('/users/:userId/missions/:missionId', + ...(fetchMiddlewares(UserController)), + ...(fetchMiddlewares(UserController.prototype.handleCompleteMission)), + + async function UserController_handleCompleteMission(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsUserController_handleCompleteMission, request, response }); + + const controller = new UserController(); + + await templateService.apiHandler({ + methodName: 'handleCompleteMission', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsMemberController_handleSignUp: Record = { + body: {"in":"body","name":"body","required":true,"ref":"MemberSignUpRequest"}, + }; + app.post('/members/signup', + ...(fetchMiddlewares(MemberController)), + ...(fetchMiddlewares(MemberController.prototype.handleSignUp)), + + async function MemberController_handleSignUp(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsMemberController_handleSignUp, request, response }); + + const controller = new MemberController(); + + await templateService.apiHandler({ + methodName: 'handleSignUp', + controller, + response, + next, + validatedArgs, + successStatus: 201, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa +} + +// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa diff --git "a/\353\217\204\354\226\217/week8/src/index.ts" "b/\353\217\204\354\226\217/week8/src/index.ts" new file mode 100644 index 0000000..90404e2 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/index.ts" @@ -0,0 +1,44 @@ +import dotenv from 'dotenv' +import express, { Express, Request, Response, NextFunction } from 'express' +import cors from 'cors' +import morgan from 'morgan' +import cookieParser from 'cookie-parser' +import swaggerUi from 'swagger-ui-express' +import path from 'path' +import fs from 'fs' +import { RegisterRoutes } from './generated/routes.js' +import { errorMiddleware } from './middleware/error.middleware.js' + +// 1. 환경 변수 설정 (가장 먼저 호출) +dotenv.config() + +const app: Express = express() +const port = process.env.PORT ?? 3000 + +// 2. 미들웨어 설정 +app.use(morgan('dev')) +app.use(cookieParser()) +app.use(cors()) +app.use(express.json()) +app.use(express.urlencoded({ extended: false })) + +// 3. TSOA가 생성한 라우트 등록 +const router = express.Router() +RegisterRoutes(router) +app.use('/api/v1', router) + +// 4. Swagger UI 연결 (dist/swagger.json 이 존재할 때만) +const swaggerPath = path.resolve('dist/swagger.json') +if (fs.existsSync(swaggerPath)) { + const swaggerFile = JSON.parse(fs.readFileSync(swaggerPath, 'utf8')) + app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerFile)) +} + +// 5. 전역 에러 핸들러 (반드시 라우터 등록 이후에 위치) +app.use(errorMiddleware) + +// 6. 서버 시작 +app.listen(port, () => { + console.log(`[server]: Server is running at http://localhost:${port}`) + console.log(`[docs]: Swagger UI at http://localhost:${port}/docs`) +}) diff --git "a/\353\217\204\354\226\217/week8/src/middleware/error.middleware.ts" "b/\353\217\204\354\226\217/week8/src/middleware/error.middleware.ts" new file mode 100644 index 0000000..abd0e3a --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/middleware/error.middleware.ts" @@ -0,0 +1,38 @@ +import { Request, Response, NextFunction } from 'express' +import { BaseError } from '../utils/errors.js' + +export const errorMiddleware = ( + err: unknown, + _req: Request, + res: Response, + _next: NextFunction, +): void => { + if (err instanceof BaseError) { + res.status(err.status).json({ + isSuccess: false, + code: err.code, + message: err.message, + result: null, + }) + return + } + + // Prisma unique constraint 에러 + if (typeof err === 'object' && err !== null && (err as { code?: string }).code === 'P2002') { + res.status(409).json({ + isSuccess: false, + code: 'USER4002', + message: '이미 존재하는 데이터입니다.', + result: null, + }) + return + } + + console.error(err) + res.status(500).json({ + isSuccess: false, + code: 'COMMON500', + message: '서버 내부 오류입니다.', + result: null, + }) +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/members/controllers/member.controller.ts" "b/\353\217\204\354\226\217/week8/src/modules/members/controllers/member.controller.ts" new file mode 100644 index 0000000..6c42629 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/members/controllers/member.controller.ts" @@ -0,0 +1,24 @@ +import { Body, Controller, Post, Route, Tags, Response as TsoaResponse, SuccessResponse } from 'tsoa' +import { MemberSignUpRequest, MemberSignUpResponse } from '../dtos/member.dto.js' +import { signUp } from '../services/member.service.js' +import { ApiResponse, successResponse } from '../../../utils/response.js' + +@Route('members') +@Tags('Member') +export class MemberController extends Controller { + /** + * 회원가입 API + * @summary 새로운 회원을 등록합니다. + */ + @Post('signup') + @SuccessResponse(201, '회원가입 성공') + @TsoaResponse>(409, '이미 사용 중인 이메일 (USER4002)') + @TsoaResponse>(400, 'name 또는 nickname 누락 (USER4003)') + public async handleSignUp( + @Body() body: MemberSignUpRequest, + ): Promise> { + this.setStatus(201) + const result = await signUp(body) + return successResponse(result) + } +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/members/controllers/user.controller.ts" "b/\353\217\204\354\226\217/week8/src/modules/members/controllers/user.controller.ts" new file mode 100644 index 0000000..b36650d --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/members/controllers/user.controller.ts" @@ -0,0 +1,71 @@ +import { Controller, Get, Patch, Path, Query, Route, Tags, Response as TsoaResponse } from 'tsoa' +import { listUserReviews } from '../../reviews/services/review.service.js' +import { listOngoingMissions, finishMission } from '../../missions/services/mission.service.js' +import { UserReviewListResponse } from '../../reviews/dtos/review.dto.js' +import { OngoingMissionListResponse, MissionChallengeResponse } from '../../missions/dtos/mission.dto.js' +import { ApiResponse, successResponse } from '../../../utils/response.js' + +@Route('users') +@Tags('User') +export class UserController extends Controller { + /** + * 내 리뷰 목록 조회 API + * @summary 특정 사용자가 작성한 리뷰 목록을 조회합니다. + */ + @Get('{userId}/reviews') + @TsoaResponse>(404, '사용자를 찾을 수 없음 (USER4001)') + public async handleListUserReviews( + /** + * 리뷰를 조회할 사용자 ID + */ + @Path() userId: number, + /** + * 커서 기반 페이지네이션 값 + */ + @Query() cursor?: number, + ): Promise> { + const result = await listUserReviews(userId, cursor ?? 0) + return successResponse(result) + } + + /** + * 진행 중인 미션 목록 조회 API + * @summary 특정 사용자가 현재 진행 중인 미션 목록을 조회합니다. + */ + @Get('{userId}/missions') + @TsoaResponse>(404, '사용자를 찾을 수 없음 (USER4001)') + public async handleListOngoingMissions( + /** + * 미션 목록을 조회할 사용자 ID + */ + @Path() userId: number, + /** + * 커서 기반 페이지네이션 값 + */ + @Query() cursor?: number, + ): Promise> { + const result = await listOngoingMissions(userId, cursor ?? 0) + return successResponse(result) + } + + /** + * 미션 완료 처리 API + * @summary 진행 중인 미션을 완료 상태로 변경합니다. + */ + @Patch('{userId}/missions/{missionId}') + @TsoaResponse>(404, '진행 중인 미션 없음 (MISSION4003)') + @TsoaResponse>(404, '사용자를 찾을 수 없음 (USER4001)') + public async handleCompleteMission( + /** + * 미션을 완료할 사용자 ID + */ + @Path() userId: number, + /** + * 완료할 미션 ID + */ + @Path() missionId: number, + ): Promise> { + const result = await finishMission(userId, missionId) + return successResponse(result) + } +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/members/dtos/member.dto.ts" "b/\353\217\204\354\226\217/week8/src/modules/members/dtos/member.dto.ts" new file mode 100644 index 0000000..9f8cfd0 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/members/dtos/member.dto.ts" @@ -0,0 +1,85 @@ +// 회원가입 요청 인터페이스 +export interface MemberSignUpRequest { + /** 회원 이름 */ + name: string + /** 닉네임 */ + nickname: string + /** + * 이메일 + * @example "test@example.com" + */ + email?: string + /** + * 비밀번호 + * @example "qwer1234!" + */ + password?: string + /** + * 전화번호 + * @example "010-1234-5678" + */ + phoneNum?: string + /** + * 생년월일 (YYYY-MM-DD) + * @example "2000-01-01" + */ + birth?: string + /** + * 성별 + * @example "FEMALE" + */ + gender?: string + /** 주소 */ + address?: string + /** 상세 주소 */ + specAddress?: string +} + +// 회원가입 응답 인터페이스 +export interface MemberSignUpResponse { + /** 생성된 회원 ID */ + memberId: number + /** 회원 이름 */ + name: string + /** 닉네임 */ + nickname: string + /** 이메일 */ + email: string | null + /** 전화번호 */ + phoneNum: string | null + /** 회원 상태 */ + status: string +} + +// req.body → 내부 데이터로 변환 +export const bodyToMember = (body: MemberSignUpRequest) => { + return { + name: body.name, + nickname: body.nickname, + email: body.email ?? null, + phoneNum: body.phoneNum ?? null, + birth: body.birth ? new Date(body.birth) : null, + gender: body.gender ?? null, + address: body.address ?? null, + specAddress: body.specAddress ?? null, + } +} + +// DB 결과 → 응답 형태로 변환 +export const responseFromMember = (member: { + id: number + name: string + nickname: string + email: string | null + phone_num: string | null + status: string +}) => { + return { + memberId: member.id, + name: member.name, + nickname: member.nickname, + email: member.email, + phoneNum: member.phone_num, + status: member.status, + } +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/members/repositories/member.repository.ts" "b/\353\217\204\354\226\217/week8/src/modules/members/repositories/member.repository.ts" new file mode 100644 index 0000000..549030e --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/members/repositories/member.repository.ts" @@ -0,0 +1,26 @@ +import { prisma } from '../../../db.config.js' + +// 유저 생성 +export const addUser = async (data: any) => { + const exists = await prisma.user.findFirst({ where: { email: data.email } }) + if (exists) return null + + const created = await prisma.user.create({ data }) + return created.id +} + +// 유저 조회 (없으면 예외 throw) +export const getUser = async (userId: number) => + prisma.user.findFirstOrThrow({ where: { id: userId } }) + +// 선호 음식 카테고리 등록 +export const setPreference = async (userId: number, foodCategoryId: number) => + prisma.userFavorCategory.create({ data: { userId, foodCategoryId } }) + +// 선호 카테고리 목록 조회 (JOIN 포함) +export const getUserPreferencesByUserId = async (userId: number) => + prisma.userFavorCategory.findMany({ + where: { userId }, + include: { foodCategory: true }, + orderBy: { foodCategoryId: 'asc' }, + }) diff --git "a/\353\217\204\354\226\217/week8/src/modules/members/services/member.service.ts" "b/\353\217\204\354\226\217/week8/src/modules/members/services/member.service.ts" new file mode 100644 index 0000000..2cf928d --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/members/services/member.service.ts" @@ -0,0 +1,39 @@ +import bcrypt from 'bcryptjs' +import { MemberSignUpRequest, bodyToMember, responseFromMember } from '../dtos/member.dto.js' +import { addUser, getUser } from '../repositories/member.repository.js' +import { BaseError } from '../../../utils/errors.js' +import { ErrorCode } from '../../../utils/errorCode.js' + +export const signUp = async (data: MemberSignUpRequest) => { + if (!data.name || !data.nickname) { + throw new BaseError( + ErrorCode.MEMBER_REQUIRED_FIELD.message, + ErrorCode.MEMBER_REQUIRED_FIELD.status, + ErrorCode.MEMBER_REQUIRED_FIELD.code, + ) + } + + const hashedPassword = data.password ? await bcrypt.hash(data.password, 10) : null + + const memberData = bodyToMember(data) + const memberId = await addUser({ ...memberData, hashedPassword }) + + if (memberId === null) { + throw new BaseError( + ErrorCode.DUPLICATE_EMAIL.message, + ErrorCode.DUPLICATE_EMAIL.status, + ErrorCode.DUPLICATE_EMAIL.code, + ) + } + + const member = await getUser(memberId) + + return responseFromMember(member as { + id: number + name: string + nickname: string + email: string | null + phone_num: string | null + status: string + }) +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/missions/controllers/mission.controller.ts" "b/\353\217\204\354\226\217/week8/src/modules/missions/controllers/mission.controller.ts" new file mode 100644 index 0000000..df4b236 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/missions/controllers/mission.controller.ts" @@ -0,0 +1,28 @@ +import { Body, Controller, Path, Post, Route, Tags, Response as TsoaResponse, SuccessResponse } from 'tsoa' +import { MissionChallengeRequest, MissionChallengeResponse } from '../dtos/mission.dto.js' +import { challengeMission } from '../services/mission.service.js' +import { ApiResponse, successResponse } from '../../../utils/response.js' + +@Route('missions') +@Tags('Mission') +export class MissionController extends Controller { + /** + * 미션 도전 API + * @summary 특정 미션에 도전(참여)합니다. + */ + @Post('{missionId}/challenge') + @SuccessResponse(201, '미션 도전 성공') + @TsoaResponse>(409, '이미 도전 중인 미션 (MISSION4002)') + @TsoaResponse>(404, '존재하지 않는 미션 (MISSION4001)') + public async handleChallengeMission( + /** + * 도전할 미션 ID + */ + @Path() missionId: number, + @Body() body: MissionChallengeRequest, + ): Promise> { + this.setStatus(201) + const result = await challengeMission(missionId, body) + return successResponse(result) + } +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/missions/dtos/mission.dto.ts" "b/\353\217\204\354\226\217/week8/src/modules/missions/dtos/mission.dto.ts" new file mode 100644 index 0000000..d6bc435 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/missions/dtos/mission.dto.ts" @@ -0,0 +1,120 @@ +// 미션 추가 요청 인터페이스 +export interface MissionCreateRequest { + /** 미션 제목 */ + title: string + /** + * 포인트 보상 + * @example 500 + */ + reward: number + /** 미션 상세 설명 */ + spec?: string + /** + * 마감일 (YYYY-MM-DD) + * @example "2026-12-31" + */ + deadLine?: string +} + +// 미션 도전 요청 인터페이스 +export interface MissionChallengeRequest { + /** + * 도전할 회원 ID + * @example 1 + */ + memberId: number + /** + * 미션 상태 + * @example "CHALLENGING" + */ + status: 'CHALLENGING' | 'COMPLETE' +} + +// 미션 생성 응답 인터페이스 +export interface MissionCreateResponse { + /** 생성된 미션 ID */ + missionId: number + /** 가게 ID */ + storeId: number + /** 미션 제목 */ + title: string + /** 포인트 보상 */ + reward: number + /** 미션 설명 */ + spec: string | null + /** 마감일 */ + deadLine: Date | null +} + +// 미션 도전 응답 인터페이스 +export interface MissionChallengeResponse { + /** 회원-미션 매핑 ID */ + memberMissionId: number + /** 회원 ID */ + memberId: number + /** 미션 ID */ + missionId: number + /** 미션 상태 */ + status: string +} + +// 진행 중 미션 목록 응답 인터페이스 +export interface OngoingMissionListResponse { + data: MissionCreateResponse[] + pagination: { + /** 다음 페이지 커서 */ + cursor: number | null + } +} + +// 가게 미션 목록 응답 인터페이스 +export interface StoreMissionListResponse { + data: MissionCreateResponse[] + pagination: { + cursor: number | null + } +} + +// req.body → 내부 데이터로 변환 (미션 추가) +export const bodyToMission = (body: MissionCreateRequest) => { + return { + title: body.title, + reward: body.reward, + spec: body.spec ?? null, + deadLine: body.deadLine ? new Date(body.deadLine) : null, + } +} + +// DB 결과 → 응답 형태로 변환 (미션) +export const responseFromMission = (mission: { + id: number + store_id: number + title: string + reward: number + spec: string | null + dead_line: Date | null +}) => { + return { + missionId: mission.id, + storeId: mission.store_id, + title: mission.title, + reward: mission.reward, + spec: mission.spec, + deadLine: mission.dead_line, + } +} + +// DB 결과 → 응답 형태로 변환 (미션 도전) +export const responseFromMemberMission = (mm: { + id: number + member_id: number + mission_id: number + status: string +}) => { + return { + memberMissionId: mm.id, + memberId: mm.member_id, + missionId: mm.mission_id, + status: mm.status, + } +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/missions/repositories/mission.repository.ts" "b/\353\217\204\354\226\217/week8/src/modules/missions/repositories/mission.repository.ts" new file mode 100644 index 0000000..bee3e62 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/missions/repositories/mission.repository.ts" @@ -0,0 +1,72 @@ +import { prisma } from '../../../db.config.js' + +// 미션 추가 +export const addMission = async (data: { + storeId: number + title: string + reward: number + spec: string | null + deadLine: Date | null +}): Promise => { + const created = await prisma.mission.create({ + data: { + storeId: data.storeId, + title: data.title, + reward: data.reward, + spec: data.spec, + deadLine: data.deadLine, + }, + }) + return created.id +} + +// 미션 조회 +export const getMissionById = async (missionId: number) => + prisma.mission.findFirst({ where: { id: missionId } }) + +// 이미 도전 중인 미션인지 확인 +export const findMemberMission = async (memberId: number, missionId: number) => + prisma.memberMission.findFirst({ + where: { userId: memberId, missionId }, + }) + +// 미션 도전 추가 +export const addMemberMission = async ( + memberId: number, + missionId: number, + status: string, +): Promise => { + const created = await prisma.memberMission.create({ + data: { userId: memberId, missionId, status }, + }) + return created.id +} + +// 미션 도전 기록 조회 +export const getMemberMissionById = async (memberMissionId: number) => + prisma.memberMission.findFirst({ where: { id: memberMissionId } }) + +// 특정 가게의 미션 목록 조회 (커서 기반 페이지네이션) +export const getStoreMissions = async (storeId: number, cursor: number) => + prisma.mission.findMany({ + where: { storeId, id: { gt: cursor } }, + orderBy: { id: 'asc' }, + take: 5, + }) + + +// 유저가 진행 중인 미션 목록 조회 +export const getOngoingMissions = async (userId: number, cursor: number) => + prisma.memberMission.findMany({ + where: { userId, status: '진행중', id: { gt: cursor } }, + include: { mission: { include: { store: true } } }, + orderBy: { id: 'asc' }, + take: 5, + }) + +// 진행 중인 미션을 완료로 변경 +export const completeMission = async (userId: number, missionId: number) => + prisma.memberMission.updateMany({ + where: { userId, missionId, status: '진행중' }, + data: { status: '완료' }, + }) \ No newline at end of file diff --git "a/\353\217\204\354\226\217/week8/src/modules/missions/services/mission.service.ts" "b/\353\217\204\354\226\217/week8/src/modules/missions/services/mission.service.ts" new file mode 100644 index 0000000..92018a3 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/missions/services/mission.service.ts" @@ -0,0 +1,128 @@ +import { + MissionCreateRequest, + MissionChallengeRequest, + bodyToMission, + responseFromMission, + responseFromMemberMission, +} from '../dtos/mission.dto.js' +import { + addMission, + getMissionById, + findMemberMission, + addMemberMission, + getMemberMissionById, + getStoreMissions, + getOngoingMissions, + completeMission, +} from '../repositories/mission.repository.js' +import { getStoreById } from '../../stores/repositories/store.repository.js' +import { BaseError } from '../../../utils/errors.js' +import { ErrorCode } from '../../../utils/errorCode.js' + +export const createMission = async (storeId: number, data: MissionCreateRequest) => { + const store = await getStoreById(storeId) + if (!store) { + throw new BaseError( + ErrorCode.STORE_NOT_FOUND.message, + ErrorCode.STORE_NOT_FOUND.status, + ErrorCode.STORE_NOT_FOUND.code, + ) + } + + const missionData = bodyToMission(data) + const missionId = await addMission({ ...missionData, storeId }) + + const mission = await getMissionById(missionId) + if (!mission) { + throw new BaseError( + ErrorCode.MISSION_CREATE_FAILED.message, + ErrorCode.MISSION_CREATE_FAILED.status, + ErrorCode.MISSION_CREATE_FAILED.code, + ) + } + + return responseFromMission(mission as unknown as { + id: number + store_id: number + title: string + reward: number + spec: string | null + dead_line: Date | null + }) +} + +export const listStoreMissions = async (storeId: number, cursor: number) => { + const missions = await getStoreMissions(storeId, cursor) + const last = missions[missions.length - 1] + return { + data: missions, + pagination: { cursor: last ? last.id : null }, + } +} + +export const listOngoingMissions = async (userId: number, cursor: number) => { + const missions = await getOngoingMissions(userId, cursor) + const last = missions[missions.length - 1] + return { + data: missions, + pagination: { cursor: last ? last.id : null }, + } +} + +export const finishMission = async (userId: number, missionId: number) => { + const result = await completeMission(userId, missionId) + if (result.count === 0) { + throw new BaseError( + ErrorCode.ONGOING_MISSION_NOT_FOUND.message, + ErrorCode.ONGOING_MISSION_NOT_FOUND.status, + ErrorCode.ONGOING_MISSION_NOT_FOUND.code, + ) + } + return { message: '미션이 완료 처리됐습니다.' } +} + +export const challengeMission = async (missionId: number, data: MissionChallengeRequest) => { + const mission = await getMissionById(missionId) + if (!mission) { + throw new BaseError( + ErrorCode.MISSION_NOT_FOUND.message, + ErrorCode.MISSION_NOT_FOUND.status, + ErrorCode.MISSION_NOT_FOUND.code, + ) + } + + if (!data.status) { + throw new BaseError( + ErrorCode.MISSION_STATUS_REQUIRED.message, + ErrorCode.MISSION_STATUS_REQUIRED.status, + ErrorCode.MISSION_STATUS_REQUIRED.code, + ) + } + + const existing = await findMemberMission(data.memberId, missionId) + if (existing) { + throw new BaseError( + ErrorCode.MISSION_ALREADY_CHALLENGING.message, + ErrorCode.MISSION_ALREADY_CHALLENGING.status, + ErrorCode.MISSION_ALREADY_CHALLENGING.code, + ) + } + + const memberMissionId = await addMemberMission(data.memberId, missionId, data.status) + + const memberMission = await getMemberMissionById(memberMissionId) + if (!memberMission) { + throw new BaseError( + ErrorCode.MISSION_CHALLENGE_FAILED.message, + ErrorCode.MISSION_CHALLENGE_FAILED.status, + ErrorCode.MISSION_CHALLENGE_FAILED.code, + ) + } + + return responseFromMemberMission(memberMission as unknown as { + id: number + member_id: number + mission_id: number + status: string + }) +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/reviews/controllers/review.controller.ts" "b/\353\217\204\354\226\217/week8/src/modules/reviews/controllers/review.controller.ts" new file mode 100644 index 0000000..77d8ccf --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/reviews/controllers/review.controller.ts" @@ -0,0 +1,7 @@ +// review 관련 엔드포인트는 StoreController(가게 리뷰 작성/조회)와 +// UserController(사용자 리뷰 목록)로 분산 관리합니다. +// - POST /api/v1/stores/:storeId/reviews → StoreController +// - GET /api/v1/stores/:storeId/reviews → StoreController +// - GET /api/v1/users/:userId/reviews → UserController +export {} + diff --git "a/\353\217\204\354\226\217/week8/src/modules/reviews/dtos/review.dto.ts" "b/\353\217\204\354\226\217/week8/src/modules/reviews/dtos/review.dto.ts" new file mode 100644 index 0000000..ea89781 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/reviews/dtos/review.dto.ts" @@ -0,0 +1,80 @@ +// 리뷰 추가 요청 인터페이스 +export interface ReviewCreateRequest { + /** + * 작성자 회원 ID + * @example 1 + */ + memberId: number + /** + * 리뷰 내용 + * @example "음식이 정말 맛있었어요!" + */ + content: string + /** + * 별점 (1~5) + * @example 4.5 + */ + score: number +} + +// 리뷰 생성 응답 인터페이스 +export interface ReviewCreateResponse { + /** 생성된 리뷰 ID */ + reviewId: number + /** 작성자 회원 ID */ + memberId: number + /** 가게 ID */ + storeId: number + /** 리뷰 내용 */ + content: string + /** 별점 */ + score: number + /** 작성일시 */ + createdAt: Date +} + +// 사용자 리뷰 목록 응답 인터페이스 +export interface UserReviewListResponse { + data: ReviewCreateResponse[] + pagination: { + /** 다음 페이지 커서 */ + cursor: number | null + } +} + +// req.body → 내부 데이터로 변환 +export const bodyToReview = (body: ReviewCreateRequest) => { + return { + memberId: body.memberId, + content: body.content, + score: body.score, + } +} + +// 리뷰 목록 → 응답 형태로 변환 (커서 기반 페이지네이션) +export const responseFromUserReviews = (reviews: any[]) => { + const last = reviews[reviews.length - 1] + return { + data: reviews, + pagination: { cursor: last ? last.id : null }, + } +} + +// DB 결과 → 응답 형태로 변환 +export const responseFromReview = (review: { + id: number + member_id: number + store_id: number + content: string + score: number + created_at: Date +}) => { + return { + reviewId: review.id, + memberId: review.member_id, + storeId: review.store_id, + content: review.content, + score: review.score, + createdAt: review.created_at, + } +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/reviews/repositories/review.repository.ts" "b/\353\217\204\354\226\217/week8/src/modules/reviews/repositories/review.repository.ts" new file mode 100644 index 0000000..adca307 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/reviews/repositories/review.repository.ts" @@ -0,0 +1,30 @@ +import { prisma } from '../../../db.config.js' + +// 리뷰 추가 +export const addReview = async (data: { + memberId: number + storeId: number + content: string +}): Promise => { + const created = await prisma.userStoreReview.create({ + data: { + userId: data.memberId, + storeId: data.storeId, + content: data.content, + }, + }) + return created.id +} + +// 리뷰 조회 +export const getReviewById = async (reviewId: number) => + prisma.userStoreReview.findFirst({ where: { id: reviewId } }) + +// 내가 작성한 리뷰 목록 조회 (커서 기반 페이지네이션) +export const getUserReviews = async (userId: number, cursor: number) => + prisma.userStoreReview.findMany({ + where: { userId, id: { gt: cursor } }, + include: { store: true }, + orderBy: { id: 'asc' }, + take: 5, + }) diff --git "a/\353\217\204\354\226\217/week8/src/modules/reviews/services/review.service.ts" "b/\353\217\204\354\226\217/week8/src/modules/reviews/services/review.service.ts" new file mode 100644 index 0000000..13b2281 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/reviews/services/review.service.ts" @@ -0,0 +1,50 @@ +import { ReviewCreateRequest, bodyToReview, responseFromReview, responseFromUserReviews } from '../dtos/review.dto.js' +import { addReview, getReviewById, getUserReviews } from '../repositories/review.repository.js' +import { getStoreById } from '../../stores/repositories/store.repository.js' +import { BaseError } from '../../../utils/errors.js' +import { ErrorCode } from '../../../utils/errorCode.js' + +export const listUserReviews = async (userId: number, cursor: number) => { + const reviews = await getUserReviews(userId, cursor) + return responseFromUserReviews(reviews) +} + +export const createReview = async (storeId: number, data: ReviewCreateRequest) => { + const store = await getStoreById(storeId) + if (!store) { + throw new BaseError( + ErrorCode.STORE_NOT_FOUND.message, + ErrorCode.STORE_NOT_FOUND.status, + ErrorCode.STORE_NOT_FOUND.code, + ) + } + + if (data.score < 1 || data.score > 5) { + throw new BaseError( + ErrorCode.INVALID_SCORE.message, + ErrorCode.INVALID_SCORE.status, + ErrorCode.INVALID_SCORE.code, + ) + } + + const reviewData = bodyToReview(data) + const reviewId = await addReview({ ...reviewData, storeId }) + + const review = await getReviewById(reviewId) + if (!review) { + throw new BaseError( + ErrorCode.REVIEW_CREATE_FAILED.message, + ErrorCode.REVIEW_CREATE_FAILED.status, + ErrorCode.REVIEW_CREATE_FAILED.code, + ) + } + + return responseFromReview(review as { + id: number + member_id: number + store_id: number + content: string + score: number + created_at: Date + }) +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/stores/controllers/store.controller.ts" "b/\353\217\204\354\226\217/week8/src/modules/stores/controllers/store.controller.ts" new file mode 100644 index 0000000..a20c3f9 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/stores/controllers/store.controller.ts" @@ -0,0 +1,106 @@ +import { Body, Controller, Get, Path, Post, Query, Route, Tags, Response as TsoaResponse, SuccessResponse } from 'tsoa' +import { StoreCreateRequest, StoreCreateResponse, ReviewListResponse } from '../dtos/store.dto.js' +import { createStore, listStoreReviews } from '../services/store.service.js' +import { createReview } from '../../reviews/services/review.service.js' +import { createMission, listStoreMissions } from '../../missions/services/mission.service.js' +import { ReviewCreateRequest, ReviewCreateResponse } from '../../reviews/dtos/review.dto.js' +import { MissionCreateRequest, MissionCreateResponse, StoreMissionListResponse } from '../../missions/dtos/mission.dto.js' +import { ApiResponse, successResponse } from '../../../utils/response.js' + +@Route('stores') +@Tags('Store') +export class StoreController extends Controller { + /** + * 가게 생성 API + * @summary 새로운 가게를 등록합니다. + */ + @Post() + @SuccessResponse(201, '가게 생성 성공') + @TsoaResponse>(400, '잘못된 요청 (COMMON400)') + public async handleCreateStore( + @Body() body: StoreCreateRequest, + ): Promise> { + this.setStatus(201) + const result = await createStore(body) + return successResponse(result) + } + + /** + * 가게 리뷰 목록 조회 API + * @summary 특정 가게의 리뷰 목록을 커서 기반으로 조회합니다. + */ + @Get('{storeId}/reviews') + @TsoaResponse>(404, '존재하지 않는 가게 (STORE4001)') + public async handleListStoreReviews( + /** + * 리뷰를 조회할 가게 ID + */ + @Path() storeId: number, + /** + * 커서 기반 페이지네이션 값 (이전 응답의 cursor) + */ + @Query() cursor?: number, + ): Promise> { + const result = await listStoreReviews(storeId, cursor ?? 0) + return successResponse(result) + } + + /** + * 리뷰 작성 API + * @summary 특정 가게에 리뷰를 작성합니다. + */ + @Post('{storeId}/reviews') + @SuccessResponse(201, '리뷰 작성 성공') + @TsoaResponse>(400, '별점 범위 오류 (REVIEW4001)') + @TsoaResponse>(404, '존재하지 않는 가게 (STORE4001)') + public async handleCreateReview( + /** + * 리뷰를 작성할 가게 ID + */ + @Path() storeId: number, + @Body() body: ReviewCreateRequest, + ): Promise> { + this.setStatus(201) + const result = await createReview(storeId, body) + return successResponse(result) + } + + /** + * 가게 미션 목록 조회 API + * @summary 특정 가게에 등록된 미션 목록을 조회합니다. + */ + @Get('{storeId}/missions') + @TsoaResponse>(404, '존재하지 않는 가게 (STORE4001)') + public async handleListStoreMissions( + /** + * 미션 목록을 조회할 가게 ID + */ + @Path() storeId: number, + /** + * 커서 기반 페이지네이션 값 + */ + @Query() cursor?: number, + ): Promise> { + const result = await listStoreMissions(storeId, cursor ?? 0) + return successResponse(result) + } + + /** + * 미션 생성 API + * @summary 특정 가게에 새로운 미션을 등록합니다. + */ + @Post('{storeId}/missions') + @SuccessResponse(201, '미션 생성 성공') + @TsoaResponse>(404, '존재하지 않는 가게 (STORE4001)') + public async handleCreateMission( + /** + * 미션을 등록할 가게 ID + */ + @Path() storeId: number, + @Body() body: MissionCreateRequest, + ): Promise> { + this.setStatus(201) + const result = await createMission(storeId, body) + return successResponse(result) + } +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/stores/dtos/store.dto.ts" "b/\353\217\204\354\226\217/week8/src/modules/stores/dtos/store.dto.ts" new file mode 100644 index 0000000..aa19060 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/stores/dtos/store.dto.ts" @@ -0,0 +1,108 @@ +// 가게 추가 요청 인터페이스 +export interface StoreCreateRequest { + /** + * 지역 ID + * @example 1 + */ + regionId: number + /** + * 음식 카테고리 ID + * @example 2 + */ + foodCategoryId: number + /** 가게 이름 */ + name: string + /** 가게 설명 */ + description?: string + /** + * 가게 주소 + * @example "서울시 강남구 테헤란로 123" + */ + address: string + /** + * 위도 + * @example 37.5665 + */ + lat?: number + /** + * 경도 + * @example 126.978 + */ + lng?: number +} + +// 가게 생성 응답 인터페이스 +export interface StoreCreateResponse { + /** 생성된 가게 ID */ + storeId: number + /** 가게 이름 */ + name: string + /** 가게 주소 */ + address: string + /** 지역 ID */ + regionId: number +} + +// 리뷰 아이템 인터페이스 +export interface ReviewItem { + /** 리뷰 ID */ + reviewId: number + /** 작성자 회원 ID */ + memberId: number + /** 가게 ID */ + storeId: number + /** 리뷰 내용 */ + content: string + /** 별점 (1~5) */ + score: number + /** 작성일시 */ + createdAt: Date +} + +// 리뷰 목록 응답 인터페이스 (커서 기반 페이지네이션) +export interface ReviewListResponse { + data: ReviewItem[] + pagination: { + /** 다음 페이지 커서 (마지막 항목의 ID, 없으면 null) */ + cursor: number | null + } +} + +// req.body → 내부 데이터로 변환 +export const bodyToStore = (body: StoreCreateRequest) => { + return { + regionId: body.regionId, + foodCategoryId: body.foodCategoryId, + name: body.name, + description: body.description ?? null, + address: body.address, + lat: body.lat ?? null, + lng: body.lng ?? null, + } +} + +// 리뷰 목록 → 응답 형태로 변환 (커서 기반 페이지네이션) +export const responseFromReviews = (reviews: any[]) => { + const last = reviews[reviews.length - 1] + return { + data: reviews, + pagination: { + cursor: last ? last.id : null, + }, + } +} + +// DB 조회 결과 → 응답 형태로 변환 +export const responseFromStore = (store: { + id: number + name: string + address: string + region_id: number +}) => { + return { + storeId: store.id, + name: store.name, + address: store.address, + regionId: store.region_id, + } +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/stores/repositories/store.repository.ts" "b/\353\217\204\354\226\217/week8/src/modules/stores/repositories/store.repository.ts" new file mode 100644 index 0000000..9a9ac1e --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/stores/repositories/store.repository.ts" @@ -0,0 +1,28 @@ +import { prisma } from '../../../db.config.js' + +// 가게 추가 +export const addStore = async (data: { name: string }): Promise => { + const created = await prisma.store.create({ data }) + return created.id +} + +// 가게 조회 +export const getStoreById = async (storeId: number) => + prisma.store.findFirst({ where: { id: storeId } }) + +// 가게 리뷰 목록 조회 (커서 기반 페이지네이션) +export const getAllStoreReviews = async (storeId: number, cursor: number) => + prisma.userStoreReview.findMany({ + select: { + id: true, + content: true, + store: true, + user: true, + }, + where: { + storeId, + id: { gt: cursor }, + }, + orderBy: { id: 'asc' }, + take: 5, + }) diff --git "a/\353\217\204\354\226\217/week8/src/modules/stores/services/store.service.ts" "b/\353\217\204\354\226\217/week8/src/modules/stores/services/store.service.ts" new file mode 100644 index 0000000..91f2c48 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/stores/services/store.service.ts" @@ -0,0 +1,30 @@ +import { StoreCreateRequest, bodyToStore, responseFromStore, responseFromReviews } from '../dtos/store.dto.js' +import { addStore, getStoreById, getAllStoreReviews } from '../repositories/store.repository.js' +import { BaseError } from '../../../utils/errors.js' +import { ErrorCode } from '../../../utils/errorCode.js' + +export const listStoreReviews = async (storeId: number, cursor: number) => { + const reviews = await getAllStoreReviews(storeId, cursor) + return responseFromReviews(reviews) +} + +export const createStore = async (data: StoreCreateRequest) => { + const storeData = bodyToStore(data) + const storeId = await addStore(storeData) + + const store = await getStoreById(storeId) + if (!store) { + throw new BaseError( + ErrorCode.STORE_CREATE_FAILED.message, + ErrorCode.STORE_CREATE_FAILED.status, + ErrorCode.STORE_CREATE_FAILED.code, + ) + } + + return responseFromStore(store as { + id: number + name: string + address: string + region_id: number + }) +} diff --git "a/\353\217\204\354\226\217/week8/src/utils/errorCode.ts" "b/\353\217\204\354\226\217/week8/src/utils/errorCode.ts" new file mode 100644 index 0000000..1a271dd --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/utils/errorCode.ts" @@ -0,0 +1,26 @@ +export const ErrorCode = { + // 공통 + INTERNAL_ERROR: { status: 500, code: 'COMMON500', message: '서버 내부 오류입니다.' }, + INVALID_INPUT: { status: 400, code: 'COMMON400', message: '잘못된 입력값입니다.' }, + + // 회원 + USER_NOT_FOUND: { status: 404, code: 'USER4001', message: '사용자를 찾을 수 없습니다.' }, + DUPLICATE_EMAIL: { status: 409, code: 'USER4002', message: '이미 사용 중인 이메일입니다.' }, + MEMBER_REQUIRED_FIELD: { status: 400, code: 'USER4003', message: 'name과 nickname은 필수 입력값입니다.' }, + + // 가게 + STORE_NOT_FOUND: { status: 404, code: 'STORE4001', message: '존재하지 않는 가게입니다.' }, + STORE_CREATE_FAILED: { status: 500, code: 'STORE5001', message: '가게 생성 후 조회에 실패했습니다.' }, + + // 리뷰 + INVALID_SCORE: { status: 400, code: 'REVIEW4001', message: '별점은 1~5 사이여야 합니다.' }, + REVIEW_CREATE_FAILED: { status: 500, code: 'REVIEW5001', message: '리뷰 생성 후 조회에 실패했습니다.' }, + + // 미션 + MISSION_NOT_FOUND: { status: 404, code: 'MISSION4001', message: '존재하지 않는 미션입니다.' }, + MISSION_ALREADY_CHALLENGING: { status: 409, code: 'MISSION4002', message: '이미 도전 중인 미션입니다.' }, + ONGOING_MISSION_NOT_FOUND: { status: 404, code: 'MISSION4003', message: '진행 중인 미션이 없습니다.' }, + MISSION_STATUS_REQUIRED: { status: 400, code: 'MISSION4004', message: 'status는 필수 입력값입니다. (CHALLENGING 또는 COMPLETE)' }, + MISSION_CREATE_FAILED: { status: 500, code: 'MISSION5001', message: '미션 생성 후 조회에 실패했습니다.' }, + MISSION_CHALLENGE_FAILED: { status: 500, code: 'MISSION5002', message: '미션 도전 후 조회에 실패했습니다.' }, +} as const diff --git "a/\353\217\204\354\226\217/week8/src/utils/errors.ts" "b/\353\217\204\354\226\217/week8/src/utils/errors.ts" new file mode 100644 index 0000000..c237d36 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/utils/errors.ts" @@ -0,0 +1,11 @@ +export class BaseError extends Error { + status: number + code: string + + constructor(message: string, statusCode = 500, code = 'INTERNAL_ERROR') { + super(message) + this.name = 'BaseError' + this.status = statusCode + this.code = code + } +} diff --git "a/\353\217\204\354\226\217/week8/src/utils/response.ts" "b/\353\217\204\354\226\217/week8/src/utils/response.ts" new file mode 100644 index 0000000..c3d3b92 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/utils/response.ts" @@ -0,0 +1,25 @@ +// TSOA Swagger 문서화에 사용할 표준 응답 타입 +export interface ApiResponse { + isSuccess: boolean + code: string + message: string + result: T | null +} + +// 성공 응답 생성 헬퍼 +export const successResponse = (result: T): ApiResponse => ({ + isSuccess: true, + code: 'COMMON200', + message: '성공입니다.', + result, +}) + +// 기존 BaseResponse (하위 호환 유지) +export const BaseResponse = (result: T, message = '성공입니다.', code = 'COMMON200') => { + return { + isSuccess: true, + code, + message, + result, + } +} diff --git "a/\353\217\204\354\226\217/week8/todolist.json" "b/\353\217\204\354\226\217/week8/todolist.json" new file mode 100644 index 0000000..830b572 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/todolist.json" @@ -0,0 +1,353 @@ +{ + "chapter": "Chapter 5. API 및 프로젝트 설정 기초", + "branch": "feature/chapter-05", + "week4_reference": "../week4", + "keywords": [ + { + "term": "환경 변수 (Environment Variables)", + "summary": "DB 비밀번호·API Key 등 민감한 값을 코드에 하드코딩하지 않고 .env 파일로 관리. dotenv 라이브러리로 로드하며 .gitignore에 반드시 추가해야 함.", + "example": "process.env.DB_PASSWORD" + }, + { + "term": "CORS (Cross-Origin Resource Sharing)", + "summary": "브라우저가 다른 Origin(도메인·포트)의 서버에 요청할 때 발생하는 보안 정책. Express에서는 cors 미들웨어로 허용 처리.", + "example": "app.use(cors())" + }, + { + "term": "DB Connection Pool", + "summary": "매 요청마다 DB 커넥션을 새로 생성·해제하지 않고, 미리 만들어 둔 커넥션 풀에서 빌려 쓰는 방식. mysql2의 createPool()로 구현. finally 블록에서 conn.release()로 반환해야 함.", + "example": "const pool = mysql.createPool({ connectionLimit: 10 })" + }, + { + "term": "비동기 (async / await)", + "summary": "DB 쿼리·외부 API 호출처럼 응답 대기가 필요한 작업을 Promise 기반으로 처리. async 함수 안에서 await로 결과를 기다리며, 동기 코드처럼 읽기 쉽게 작성 가능.", + "example": "const [rows] = await pool.query('SELECT ...')" + }, + { + "term": "try / catch / finally", + "summary": "비동기 작업에서 발생할 수 있는 에러를 구조적으로 처리. try: 정상 로직, catch: 에러 처리, finally: 커넥션 반환(conn.release()) 등 항상 실행되어야 하는 정리 코드.", + "example": "try { ... } catch(err) { throw new Error(...) } finally { conn.release() }" + }, + { + "term": "Interface (인터페이스)", + "summary": "TypeScript에서 객체의 형태(속성·타입)를 정의하는 설계도. DTO·Repository 반환 타입 등에 활용. ?를 붙이면 선택적 프로퍼티.", + "example": "export interface StoreCreateRequest { regionId: number; name: string; address: string; }" + }, + { + "term": "Type Assertion (as 키워드)", + "summary": "TypeScript 컴파일러에게 '이 값은 이 타입이야'라고 강제로 알려주는 문법. req.body는 기본적으로 any이므로, as 로 정의한 인터페이스 타입으로 변환해 사용.", + "example": "req.body as StoreCreateRequest" + } + ], + "project_structure": { + "root": "week5/", + "note": "week4의 in-memory DB → MySQL 실제 DB 연결로 전환. 모듈형 모노리스 구조 채택.", + "files": [ + "src/index.ts - Express 앱 진입점, 미들웨어·라우터 등록", + "src/db.config.ts - MySQL Connection Pool 설정", + "src/modules/stores/controllers/store.controller.ts", + "src/modules/stores/services/store.service.ts", + "src/modules/stores/repositories/store.repository.ts", + "src/modules/stores/dtos/store.dto.ts", + "src/modules/reviews/controllers/review.controller.ts", + "src/modules/reviews/services/review.service.ts", + "src/modules/reviews/repositories/review.repository.ts", + "src/modules/reviews/dtos/review.dto.ts", + "src/modules/missions/controllers/mission.controller.ts", + "src/modules/missions/services/mission.service.ts", + "src/modules/missions/repositories/mission.repository.ts", + "src/modules/missions/dtos/mission.dto.ts", + ".env - DB 접속 정보·PORT (gitignore 필수)", + ".gitignore", + "package.json", + "tsconfig.json", + "schema.sql - week4 schema.sql 재사용 (테이블 이미 정의됨)" + ] + }, + "todos": [ + { + "id": 1, + "phase": "사전 준비", + "title": "GitHub 이슈 생성 및 브랜치 분기", + "status": "todo", + "details": [ + "GitHub 저장소 Issues 탭에서 라벨 정리: bug, docs, feature, refactor", + "이슈 제목: '[feat] Chapter 5 - API 구현 (가게 추가 / 리뷰 / 미션)'", + "Assignee: 본인, Label: feature 로 이슈 생성", + "이슈에서 'Create a branch' 클릭 → feature/chapter-05 브랜치 생성", + "로컬에서: git fetch origin && git checkout feature/chapter-05" + ] + }, + { + "id": 2, + "phase": "사전 준비", + "title": "Postman 설치 및 기본 사용법 확인", + "status": "todo", + "details": [ + "Postman 설치 (https://www.postman.com/downloads/)", + "Params / Authorization / Headers / Body 탭 역할 이해", + "Body > raw > JSON 선택 방법 숙지", + "나중에 API 테스트 시 스크린샷 저장할 준비" + ] + }, + { + "id": 3, + "phase": "프로젝트 세팅", + "title": "week5 폴더 초기화 및 의존성 설치", + "status": "todo", + "details": [ + "cd week5 && npm init -y", + "npm install express cors dotenv http-status-codes mysql2", + "npm install -D typescript @types/node @types/express @types/cors @types/dotenv nodemon tsx", + "npx tsc --init 후 tsconfig.json 수정 (rootDir: ./src, outDir: ./dist, module: NodeNext, strict: true 등)", + "week4/tsconfig.json을 참고해 module/moduleResolution 설정 일치시키기", + "package.json scripts 추가: start / dev (nodemon --exec tsx src/index.ts)" + ] + }, + { + "id": 4, + "phase": "프로젝트 세팅", + "title": ".env 파일 및 .gitignore 작성", + "status": "todo", + "details": [ + ".gitignore에 node_modules/ / .env / .env.* 추가", + ".env에 PORT=3000, DB_HOST=localhost, DB_PORT=3306, DB_USER=root, DB_PASSWORD=비밀번호, DB_NAME=umc_mission 작성", + "DB_NAME은 week4/schema.sql 기준 umc_mission 사용 (이미 테이블 있음)" + ] + }, + { + "id": 5, + "phase": "프로젝트 세팅", + "title": "src/db.config.ts 작성 - MySQL Connection Pool", + "status": "todo", + "details": [ + "mysql2/promise의 createPool 사용", + "환경 변수(DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME)로 설정", + "connectionLimit: 10, waitForConnections: true", + "week4 schema.sql의 DB(umc_mission)와 연결" + ], + "reference_week4": "week4/src/db/index.ts 구조 참고 (단, in-memory→MySQL로 변경)" + }, + { + "id": 6, + "phase": "프로젝트 세팅", + "title": "src/index.ts 작성 - Express 앱 진입점", + "status": "todo", + "details": [ + "dotenv.config() 최상단 호출", + "cors(), express.json(), express.urlencoded() 미들웨어 등록", + "각 모듈 라우터 등록: /api/v1/stores, /api/v1/reviews, /api/v1/missions", + "전역 에러 핸들러 미들웨어 등록 (시니어 미션: JSON 에러 응답)", + "app.listen(process.env.PORT || 3000)" + ], + "reference_week4": "week4/src/index.ts 구조 그대로 활용, cors·dotenv 추가" + }, + { + "id": 7, + "phase": "DB 준비", + "title": "MySQL에 week5용 테이블 확인 및 더미데이터 삽입", + "status": "todo", + "details": [ + "week4/schema.sql로 테이블 생성 (이미 되어있으면 생략)", + "food_category 더미데이터 삽입: INSERT INTO food_category(name) VALUES ('한식'),('중식'),('일식'),('양식'),('치킨'),('분식'),('고기/구이'),('도시락'),('아식'),('패스트푸드'),('다저트'),('아시안푸드')", + "region 더미데이터 삽입: INSERT INTO region(name) VALUES ('서울'),('경기'),('부산')...", + "member 더미데이터 1건 삽입 (API 테스트용 첫 번째 사용자)" + ] + }, + { + "id": 8, + "phase": "API 구현 - 1-1", + "title": "[필수] 특정 지역에 가게 추가하기 API", + "status": "todo", + "priority": "optional", + "endpoint": "POST /api/v1/stores", + "request_body": { + "regionId": "number", + "foodCategoryId": "number", + "name": "string", + "description": "string (선택)", + "address": "string", + "lat": "number (선택)", + "lng": "number (선택)" + }, + "details": [ + "store.dto.ts - StoreCreateRequest 인터페이스 정의", + "store.dto.ts - bodyToStore() 변환 함수 작성", + "store.repository.ts - addStore(): INSERT INTO store(...) VALUES(?)", + "store.service.ts - createStore(data): addStore 호출 후 결과 반환", + "store.controller.ts - handleCreateStore: bodyToStore(req.body as ...) → service 호출 → 201 응답", + "index.ts에 라우터 등록: app.post('/api/v1/stores', handleCreateStore)" + ], + "reference_week4": "week4/src/controllers/store.controller.ts 패턴 참고" + }, + { + "id": 9, + "phase": "API 구현 - 1-2", + "title": "[필수★] 가게에 리뷰 추가하기 API", + "status": "todo", + "priority": "required", + "endpoint": "POST /api/v1/stores/:storeId/reviews", + "request_body": { + "memberId": "number (특정 사용자로 가정, DB 첫 번째 사용자)", + "content": "string", + "score": "number (1.0~5.0)" + }, + "details": [ + "review.dto.ts - ReviewCreateRequest 인터페이스 (content, score, memberId)", + "review.dto.ts - bodyToReview() 변환 함수", + "review.repository.ts - addReview(): INSERT INTO review(...)", + "store.repository.ts (또는 review.repository.ts) - findStoreById(): SELECT * FROM store WHERE id=?", + "review.service.ts - createReview(storeId, data): 가게 존재 검증 → addReview 호출", + " 검증 실패 시 throw new Error('존재하지 않는 가게입니다.')", + "review.controller.ts - handleCreateReview: storeId = parseInt(req.params.storeId) → service 호출", + "index.ts에 라우터 등록" + ], + "validation": "리뷰를 추가하려는 가게가 존재하는지 검증 필요", + "reference_week4": "week4/src/services/store.service.ts의 createReview 패턴, week4 스키마의 review 테이블" + }, + { + "id": 10, + "phase": "API 구현 - 1-3", + "title": "가게에 미션 추가하기 API", + "status": "todo", + "priority": "optional", + "endpoint": "POST /api/v1/stores/:storeId/missions", + "request_body": { + "title": "string", + "reward": "number", + "spec": "string (선택)", + "deadLine": "string (YYYY-MM-DD, 선택)" + }, + "details": [ + "mission.dto.ts - MissionCreateRequest 인터페이스", + "mission.dto.ts - bodyToMission() 변환 함수 (deadLine → Date 변환)", + "mission.repository.ts - addMission(): INSERT INTO mission(store_id, title, reward, spec, dead_line)", + "mission.service.ts - createMission(storeId, data): 가게 존재 검증 → addMission", + "mission.controller.ts - handleCreateMission", + "index.ts에 라우터 등록" + ], + "reference_week4": "week4/src/repositories/mission.repository.ts 패턴, week4 schema.sql의 mission 테이블" + }, + { + "id": 11, + "phase": "API 구현 - 1-4", + "title": "[필수★] 가게의 미션을 도전 중인 미션에 추가(미션 도전하기) API", + "status": "todo", + "priority": "required", + "endpoint": "POST /api/v1/missions/:missionId/challenge", + "request_body": { + "memberId": "number (특정 사용자로 가정, DB 첫 번째 사용자)" + }, + "details": [ + "mission.dto.ts - MissionChallengeRequest 인터페이스 ({ memberId: number })", + "mission.repository.ts - findMemberMission(): SELECT * FROM member_mission WHERE member_id=? AND mission_id=?", + "mission.repository.ts - addMemberMission(): INSERT INTO member_mission(member_id, mission_id, status) VALUES(?,?,'CHALLENGING')", + "mission.service.ts - challengeMission(missionId, data): 중복 도전 검증 → addMemberMission", + " 검증 실패 시 throw new Error('이미 도전 중인 미션입니다.')", + "mission.controller.ts - handleChallengeMission: missionId = parseInt(req.params.missionId) → service", + "index.ts에 라우터 등록" + ], + "validation": "도전하려는 미션이 이미 도전 중이지는 않은지 검증 필요", + "reference_week4": "week4/src/repositories/mission.repository.ts, week4 schema.sql의 member_mission 테이블" + }, + { + "id": 12, + "phase": "추가 미션", + "title": "[공통 미션 2번] Controller → Service → Repository → DB 요청 흐름 정리", + "status": "todo", + "details": [ + "예: POST /api/v1/stores/:storeId/reviews 요청 흐름을 순서대로 작성", + "1. 사용자가 POST /api/v1/stores/1/reviews 요청 전송", + "2. index.ts의 라우터가 handleCreateReview 컨트롤러 호출", + "3. Controller: req.body를 ReviewCreateRequest 타입으로 변환(bodyToReview), storeId 파싱", + "4. Service: 가게 존재 여부 검증(findStoreById) → 없으면 Error throw", + "5. Repository: INSERT INTO review 쿼리 실행 → insertId 반환", + "6. Service: 결과를 DTO로 변환해 Controller에 반환", + "7. Controller: 201 JSON 응답 전송", + "워크북의 요약 정리 섹션에 이 내용 포함" + ] + }, + { + "id": 13, + "phase": "추가 미션", + "title": "[공통 미션 3번] 회원가입 API에 bcrypt 비밀번호 해싱 추가", + "status": "todo", + "details": [ + "npm install bcryptjs && npm install -D @types/bcryptjs (week4에 이미 설치됨)", + "member.dto.ts - MemberSignUpRequest 인터페이스에 password 필드 추가", + "member.repository.ts - addMember(): INSERT INTO member(..., password) VALUES(...)", + "member.service.ts - signUp(): const hashedPw = await bcrypt.hash(data.password, 10) → addMember에 전달", + "member.controller.ts - handleSignUp 작성", + "POST /api/v1/members/signup 라우터 등록" + ], + "reference_week4": "week4에서 bcryptjs 이미 사용 중 - week4/package.json 참고" + }, + { + "id": 14, + "phase": "시니어 미션", + "title": "[시니어] 전역 에러 핸들러 - JSON 형태 에러 응답", + "status": "todo", + "details": [ + "src/middleware/error.middleware.ts 생성", + "ErrorRequestHandler 타입 사용: (err, req, res, next) => void", + "res.status(err.status || 500).json({ success: false, message: err.message || '서버 에러' })", + "index.ts 맨 마지막에 app.use(errorMiddleware) 등록", + "Controller에서 try-catch 제거하거나 next(err) 사용으로 변경", + "기존 HTML 에러 응답 → JSON 에러 응답으로 개선" + ], + "reference_week4": "week4/src/middleware/error.middleware.ts 그대로 활용 가능" + }, + { + "id": 15, + "phase": "테스트", + "title": "Postman / curl로 각 API 호출 및 스크린샷 저장", + "status": "todo", + "details": [ + "npm run dev 로 서버 실행", + "API 1-1: POST /api/v1/stores - 가게 추가 성공 스크린샷", + "API 1-2: POST /api/v1/stores/:storeId/reviews - 리뷰 추가 성공 스크린샷", + "API 1-2: 존재하지 않는 storeId로 요청 → 에러 응답 스크린샷", + "API 1-3: POST /api/v1/stores/:storeId/missions - 미션 추가 성공 스크린샷", + "API 1-4: POST /api/v1/missions/:missionId/challenge - 도전 성공 스크린샷", + "API 1-4: 동일 미션 재도전 → '이미 도전 중' 에러 스크린샷", + "DB에서 SELECT로 데이터 삽입 확인 스크린샷" + ] + }, + { + "id": 16, + "phase": "마무리", + "title": "feature/chapter-05 브랜치에 push 및 PR 생성", + "status": "todo", + "details": [ + "git add . && git commit -m 'feat: 5주차 미션 - API 구현 (가게/리뷰/미션)'", + "git push origin feature/chapter-05", + "GitHub에서 PR 생성 (main 브랜치에 merge하지 말 것!)", + "워크북의 미션 기록란에 GitHub 링크 제출", + "GitHub 이슈 Close" + ] + }, + { + "id": 17, + "phase": "마무리", + "title": "핵심 키워드 및 요약 정리 작성", + "status": "todo", + "details": [ + "이 파일 상단의 keywords 섹션을 참고해 워크북에 기입", + "요약 정리: Controller→Service→Repository→DB 흐름 설명", + "위클리 스크럼 질문 답변: DTO 없이 사용할 때의 문제점 / Service Layer 필요성" + ] + } + ], + "required_apis_summary": { + "must_implement": ["1-2 (가게에 리뷰 추가하기)", "1-4 (미션 도전하기)"], + "minimum_count": "필수 2개 포함 총 3개 이상", + "senior_mission": "4개 전부 + JSON 에러 응답 개선" + }, + "key_differences_from_week4": { + "database": "in-memory(week4) → MySQL Connection Pool(week5)", + "modules": "flat 구조(week4) → 모듈형 모노리스(week5: src/modules/{도메인}/)", + "env": ".env 파일 추가 (dotenv 사용)", + "cors": "cors 미들웨어 추가", + "error_response": "HTML 에러(기본) → JSON 에러(시니어 미션 개선)" + } +} diff --git "a/\353\217\204\354\226\217/week8/tsconfig.json" "b/\353\217\204\354\226\217/week8/tsconfig.json" new file mode 100644 index 0000000..f43e7d2 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/tsconfig.json" @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "rootDir": ".", + "outDir": "./dist", + "target": "ESNext", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ESNext"], + "types": ["node"], + "strict": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "sourceMap": true, + "experimentalDecorators": true + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git "a/\353\217\204\354\226\217/week8/tsoa.json" "b/\353\217\204\354\226\217/week8/tsoa.json" new file mode 100644 index 0000000..ca6c134 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/tsoa.json" @@ -0,0 +1,13 @@ +{ + "entryFile": "src/index.ts", + "noImplicitAdditionalProperties": "throw-on-extras", + "controllerPathGlobs": ["src/**/*.controller.ts"], + "spec": { + "outputDirectory": "dist", + "specVersion": 3, + "basePath": "/api/v1" + }, + "routes": { + "routesDir": "src/generated" + } +} diff --git "a/\353\217\204\354\226\217/week8/week7-commands.md" "b/\353\217\204\354\226\217/week8/week7-commands.md" new file mode 100644 index 0000000..37beced --- /dev/null +++ "b/\353\217\204\354\226\217/week8/week7-commands.md" @@ -0,0 +1,116 @@ +# Week 7 실행 명령어 순서 정리 + +> 아래 순서대로 실행하세요. 각 단계 완료 후 다음 단계로 진행합니다. + +--- + +## 1단계. 프로젝트 디렉토리 이동 + +```bash +cd +``` + +> week6 코드가 있는 프로젝트 루트 폴더로 이동합니다. + +--- + +## 2단계. 필수 미들웨어 패키지 설치 + +```bash +npm install morgan cookie-parser +``` + +--- + +## 3단계. 설치 확인 + +```bash +cat package.json +``` + +> `dependencies`에 `morgan`과 `cookie-parser`가 추가되었는지 확인합니다. + +--- + +## 4단계. Git 초기화 및 원격 저장소 연결 + +> GitHub에서 새 레포지토리를 먼저 만든 후 실행합니다. + +```bash +git init +git remote add origin https://github.com//.git +``` + +--- + +## 5단계. 작업 브랜치 생성 및 이동 + +```bash +git checkout -b feature/chapter-07 +``` + +> **주의:** `main` 브랜치에는 절대 push하지 않습니다. (Week 11 CI/CD 전용) + +--- + +## 6단계. 코드 작성 후 스테이징 + +> `index.js`, `BaseResponse`, `BaseError`, `errorCode.js` 등 수정/생성 후 실행 + +```bash +git add . +``` + +--- + +## 7단계. 커밋 + +```bash +git commit -m "feat: add middleware, standard response, and error handling" +``` + +--- + +## 8단계. feature 브랜치에 push + +```bash +git push origin feature/chapter-07 +``` + +--- + +## (선택) Prisma 관련 명령어 + +### 스키마 변경 후 마이그레이션 실행 + +```bash +npx prisma migrate dev --name +``` + +### DB 상태와 스키마 동기화 확인 + +```bash +npx prisma db pull +``` + +### Prisma Studio (GUI로 DB 확인) + +```bash +npx prisma studio +``` + +--- + +## 전체 순서 요약 + +| 순서 | 명령어 | 설명 | +|---|---|---| +| 1 | `cd ` | 프로젝트 폴더 이동 | +| 2 | `npm install morgan cookie-parser` | 미들웨어 설치 | +| 3 | `cat package.json` | 설치 확인 | +| 4 | `git init` | Git 초기화 | +| 5 | `git remote add origin ` | 원격 저장소 연결 | +| 6 | `git checkout -b feature/chapter-07` | 작업 브랜치 생성 | +| 7 | `git add .` | 변경 파일 스테이징 | +| 8 | `git commit -m "..."` | 커밋 | +| 9 | `git push origin feature/chapter-07` | 브랜치에 push | diff --git "a/\353\217\204\354\226\217/week8/week7-workbook.md" "b/\353\217\204\354\226\217/week8/week7-workbook.md" new file mode 100644 index 0000000..c235612 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/week7-workbook.md" @@ -0,0 +1,282 @@ +# Week 7 워크북 - 미들웨어, 표준 응답/에러 처리, Git 전략 + +> **프로젝트 참조:** week6_directory_contents 기반으로 week7 리팩토링 + +--- + +## Step 1. 필수 미들웨어 설치 및 설정 + +### 설치 패키지 + +```bash +npm install morgan cookie-parser +``` + +### `index.js` 적용 예시 + +```js +const express = require('express'); +const morgan = require('morgan'); +const cookieParser = require('cookie-parser'); + +const app = express(); + +// 요청 로깅 미들웨어 +app.use(morgan('dev')); + +// 쿠키 파싱 미들웨어 +app.use(cookieParser()); + +// JSON 및 URL-encoded body 파싱 +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +``` + +### 미들웨어 역할 설명 + +| 미들웨어 | 역할 | +|---|---| +| `morgan('dev')` | HTTP 요청/응답 로그를 터미널에 출력 (개발 환경용) | +| `cookieParser()` | `req.cookies` 객체로 쿠키 값 파싱 | +| `express.json()` | `Content-Type: application/json` 요청 바디 파싱 | +| `express.urlencoded()` | HTML form 데이터 파싱 | + +--- + +## Step 2. 표준 API 성공 응답 형식 통일 + +### 응답 형식 정의 + +모든 API의 성공 응답은 아래 구조를 따릅니다: + +```json +{ + "isSuccess": true, + "code": "COMMON200", + "message": "성공입니다.", + "result": { } +} +``` + +| 필드 | 타입 | 설명 | +|---|---|---| +| `isSuccess` | boolean | 요청 성공 여부 | +| `code` | string | 응답 코드 (예: `COMMON200`, `USER404`) | +| `message` | string | 사람이 읽을 수 있는 메시지 | +| `result` | object \| array | 실제 데이터 | + +### BaseResponse 유틸리티 구현 예시 + +```js +// src/utils/response.js + +const BaseResponse = (result, message = '성공입니다.', code = 'COMMON200') => { + return { + isSuccess: true, + code, + message, + result, + }; +}; + +module.exports = { BaseResponse }; +``` + +### 컨트롤러에서 사용 + +```js +const { BaseResponse } = require('../utils/response'); + +// Before (기존 방식) +res.status(200).json(user); + +// After (표준 응답 적용) +res.status(200).json(BaseResponse(user)); +``` + +--- + +## Step 3. 중앙 집중식 커스텀 에러 처리 + +### BaseError 클래스 정의 + +```js +// src/utils/errors.js + +class BaseError extends Error { + constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') { + super(message); + this.name = 'BaseError'; + this.status = statusCode; + this.code = code; + } +} + +module.exports = { BaseError }; +``` + +### 에러 코드 상수 예시 + +```js +// src/utils/errorCode.js + +const ErrorCode = { + USER_NOT_FOUND: { status: 404, code: 'USER4001', message: '사용자를 찾을 수 없습니다.' }, + DUPLICATE_EMAIL: { status: 409, code: 'USER4002', message: '이미 사용 중인 이메일입니다.' }, + INTERNAL_ERROR: { status: 500, code: 'COMMON500', message: '서버 내부 오류입니다.' }, +}; + +module.exports = { ErrorCode }; +``` + +### 글로벌 에러 핸들러 미들웨어 (`index.js` 하단에 추가) + +```js +// 글로벌 에러 핸들러 - 반드시 다른 app.use() 아래에 위치해야 함 +app.use((err, req, res, next) => { + if (err instanceof BaseError) { + return res.status(err.status).json({ + isSuccess: false, + code: err.code, + message: err.message, + result: null, + }); + } + + // Prisma unique constraint 에러 처리 + if (err.code === 'P2002') { + return res.status(409).json({ + isSuccess: false, + code: 'USER4002', + message: '이미 존재하는 데이터입니다.', + result: null, + }); + } + + console.error(err); + return res.status(500).json({ + isSuccess: false, + code: 'COMMON500', + message: '서버 내부 오류입니다.', + result: null, + }); +}); +``` + +### 컨트롤러에서 에러 던지기 + +```js +const { BaseError } = require('../utils/errors'); +const { ErrorCode } = require('../utils/errorCode'); + +// Before (기존 방식) +res.status(404).send('User Not Found'); + +// After (next()로 에러 위임) +const user = await prisma.user.findUnique({ where: { id } }); +if (!user) { + return next(new BaseError( + ErrorCode.USER_NOT_FOUND.message, + ErrorCode.USER_NOT_FOUND.status, + ErrorCode.USER_NOT_FOUND.code + )); +} +``` + +--- + +## Step 4. GitHub 브랜치 전략 + +### 브랜치 규칙 + +| 브랜치 | 용도 | +|---|---| +| `main` | Week 11 CI/CD 파이프라인 전용 — **직접 push 금지** | +| `feature/chapter-07` | Week 7 작업 브랜치 | + +### 주의사항 + +> `main` 브랜치는 Week 11 CI/CD 설정을 위해 보호되어 있습니다. +> 모든 코드는 반드시 `feature/chapter-07` 브랜치에 push 하세요. + +--- + +## Step 5. Prisma ORM vs Raw SQL (mysql2) 비교 분석 + +### 비교표 + +| 항목 | Prisma ORM | Raw SQL (mysql2) | +|---|---|---| +| **문법** | TypeScript 기반 타입 안전 API | 직접 SQL 문자열 작성 | +| **가독성** | 높음 (직관적 메서드 체이닝) | 낮음 (SQL 숙련도 필요) | +| **타입 안전성** | 자동 타입 추론 지원 | 없음 (직접 캐스팅 필요) | +| **성능 튜닝** | 제한적 | 완전한 SQL 제어 가능 | +| **마이그레이션** | `prisma migrate dev` 자동화 | 수동 관리 | +| **학습 비용** | Prisma 문법 학습 필요 | SQL 지식만 있으면 됨 | +| **복잡 쿼리** | 한계 있음 (rawQuery 혼용) | 무제한 | + +--- + +### `prisma migrate dev` 장단점 분석 + +#### 장점 + +- **자동 마이그레이션 파일 생성:** `schema.prisma` 변경 사항을 감지해 SQL 파일 자동 생성 +- **히스토리 관리:** `prisma/migrations/` 폴더에 변경 이력 누적 보관 +- **개발 편의성:** 스키마 → DB 동기화를 명령어 한 줄로 처리 +- **Seed 연동:** `prisma db seed`와 연계하여 초기 데이터 삽입 가능 + +#### 단점 + +- **팀 협업 시 충돌 위험:** 여러 사람이 `schema.prisma`를 동시에 수정하면 migration 충돌 발생 +- **프로덕션 부적합:** 운영 환경에서는 `prisma migrate deploy` 사용 권장 (`dev`는 개발 전용) +- **자동화의 불투명성:** 생성된 SQL을 검토하지 않으면 의도치 않은 데이터 손실 위험 + +--- + +### 협업 시 Migration 충돌 방지 전략 + +1. **스키마 담당자 단일화:** `schema.prisma` 수정은 한 명이 담당하거나 PR 리뷰를 필수로 설정 +2. **브랜치 전략 준수:** 기능 브랜치에서 스키마 수정 후 PR 머지 순서 지키기 +3. **migration 파일 커밋 필수:** `prisma/migrations/` 폴더를 `.gitignore`에 추가하지 말고 항상 커밋 +4. **머지 전 `prisma migrate dev` 재실행:** 머지 후 로컬에서 반드시 재실행하여 동기화 확인 +5. **팀 내 DB 상태 공유:** 마이그레이션 실행 여부를 팀 채널(슬랙 등)에 공유 + +--- + +### 개인 선호도: Prisma vs mysql2 + +**선택: Prisma ORM** + +**이유:** + +- TypeScript 환경에서 타입 자동 완성과 컴파일 타임 에러 검출이 가능해 런타임 버그 감소 +- 복잡한 JOIN보다 단순 CRUD 위주인 현재 프로젝트 규모에 충분 +- 마이그레이션 자동화로 팀 전체의 DB 상태를 일관되게 유지 가능 +- `prisma studio`를 통한 GUI 데이터 확인이 개발 생산성을 높임 + +**mysql2가 더 적합한 경우:** + +- 복잡한 집계 쿼리나 서브쿼리가 많은 서비스 +- 극도의 성능 최적화가 필요한 대규모 트래픽 환경 + +--- + +## 디렉토리 구조 제안 + +``` +project/ +├── index.js +├── prisma/ +│ ├── schema.prisma +│ └── migrations/ +├── src/ +│ ├── controllers/ +│ ├── services/ +│ ├── repositories/ +│ └── utils/ +│ ├── response.js ← BaseResponse +│ ├── errors.js ← BaseError +│ └── errorCode.js ← 에러 코드 상수 +└── package.json +``` From 5288d0b49cb6320bf0f6360c8a56ce9f0d8120b1 Mon Sep 17 00:00:00 2001 From: higashiaka Date: Wed, 27 May 2026 17:45:39 +0900 Subject: [PATCH 9/9] =?UTF-8?q?fix:=20week8=20=ED=94=BC=EB=93=9C=EB=B0=B1?= =?UTF-8?q?=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "\353\217\204\354\226\217/week8/package.json" | 2 +- .../migration.sql" | 7 + .../migration.sql" | 10 ++ .../week8/prisma/schema.prisma" | 50 ++++---- .../week8/prismaConfig.ts" | 10 +- .../week8/src/db.config.ts" | 17 --- .../week8/src/dbConfig.ts" | 17 +++ .../week8/src/generated/routes.ts" | 112 +++++++++++----- "\353\217\204\354\226\217/week8/src/index.ts" | 2 +- .../week8/src/middleware/error.middleware.ts" | 38 ------ .../week8/src/middleware/errorMiddleware.ts" | 53 ++++++++ .../members/controllers/member.controller.ts" | 24 ---- .../members/controllers/memberController.ts" | 43 +++++++ .../members/controllers/userController.ts" | 8 +- .../src/modules/members/dtos/memberDto.ts" | 62 +++++---- .../members/repositories/memberRepository.ts" | 26 ++-- .../members/services/member.service.ts" | 39 ------ .../members/services/memberService.ts" | 52 ++++++++ .../controllers/missionController.ts" | 4 +- .../src/modules/missions/dtos/missionDto.ts" | 42 +++--- .../repositories/missionRepository.ts" | 51 ++++---- .../missions/services/missionService.ts" | 120 ++++++++---------- .../reviews/controllers/reviewController.ts" | 0 .../src/modules/reviews/dtos/reviewDto.ts" | 40 +++--- .../reviews/repositories/reviewRepository.ts" | 8 +- .../reviews/services/review.service.ts" | 50 -------- .../reviews/services/reviewService.ts" | 42 ++++++ .../stores/controllers/storeController.ts" | 12 +- .../src/modules/stores/dtos/store.dto.ts" | 108 ---------------- .../src/modules/stores/dtos/storeDto.ts" | 66 ++++++++++ .../stores/repositories/storeRepository.ts" | 11 +- .../modules/stores/services/storeService.ts" | 22 ++-- .../week8/src/utils/errorCode.ts" | 19 ++- .../week8/src/utils/errors.ts" | 10 +- .../week8/src/utils/response.ts" | 4 +- "\353\217\204\354\226\217/week8/tsoa.json" | 2 +- 36 files changed, 619 insertions(+), 564 deletions(-) create mode 100644 "\353\217\204\354\226\217/week8/prisma/migrations/20260527000000_add_member_auth_fields/migration.sql" create mode 100644 "\353\217\204\354\226\217/week8/prisma/migrations/20260527001000_member_mission_status_enum/migration.sql" rename "\353\217\204\354\226\217/week8/prisma.config.ts" => "\353\217\204\354\226\217/week8/prismaConfig.ts" (66%) delete mode 100644 "\353\217\204\354\226\217/week8/src/db.config.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/dbConfig.ts" delete mode 100644 "\353\217\204\354\226\217/week8/src/middleware/error.middleware.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/middleware/errorMiddleware.ts" delete mode 100644 "\353\217\204\354\226\217/week8/src/modules/members/controllers/member.controller.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/members/controllers/memberController.ts" rename "\353\217\204\354\226\217/week8/src/modules/members/controllers/user.controller.ts" => "\353\217\204\354\226\217/week8/src/modules/members/controllers/userController.ts" (93%) rename "\353\217\204\354\226\217/week8/src/modules/members/dtos/member.dto.ts" => "\353\217\204\354\226\217/week8/src/modules/members/dtos/memberDto.ts" (56%) rename "\353\217\204\354\226\217/week8/src/modules/members/repositories/member.repository.ts" => "\353\217\204\354\226\217/week8/src/modules/members/repositories/memberRepository.ts" (52%) delete mode 100644 "\353\217\204\354\226\217/week8/src/modules/members/services/member.service.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/members/services/memberService.ts" rename "\353\217\204\354\226\217/week8/src/modules/missions/controllers/mission.controller.ts" => "\353\217\204\354\226\217/week8/src/modules/missions/controllers/missionController.ts" (91%) rename "\353\217\204\354\226\217/week8/src/modules/missions/dtos/mission.dto.ts" => "\353\217\204\354\226\217/week8/src/modules/missions/dtos/missionDto.ts" (70%) rename "\353\217\204\354\226\217/week8/src/modules/missions/repositories/mission.repository.ts" => "\353\217\204\354\226\217/week8/src/modules/missions/repositories/missionRepository.ts" (53%) rename "\353\217\204\354\226\217/week8/src/modules/missions/services/mission.service.ts" => "\353\217\204\354\226\217/week8/src/modules/missions/services/missionService.ts" (50%) rename "\353\217\204\354\226\217/week8/src/modules/reviews/controllers/review.controller.ts" => "\353\217\204\354\226\217/week8/src/modules/reviews/controllers/reviewController.ts" (100%) rename "\353\217\204\354\226\217/week8/src/modules/reviews/dtos/review.dto.ts" => "\353\217\204\354\226\217/week8/src/modules/reviews/dtos/reviewDto.ts" (55%) rename "\353\217\204\354\226\217/week8/src/modules/reviews/repositories/review.repository.ts" => "\353\217\204\354\226\217/week8/src/modules/reviews/repositories/reviewRepository.ts" (73%) delete mode 100644 "\353\217\204\354\226\217/week8/src/modules/reviews/services/review.service.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/reviews/services/reviewService.ts" rename "\353\217\204\354\226\217/week8/src/modules/stores/controllers/store.controller.ts" => "\353\217\204\354\226\217/week8/src/modules/stores/controllers/storeController.ts" (93%) delete mode 100644 "\353\217\204\354\226\217/week8/src/modules/stores/dtos/store.dto.ts" create mode 100644 "\353\217\204\354\226\217/week8/src/modules/stores/dtos/storeDto.ts" rename "\353\217\204\354\226\217/week8/src/modules/stores/repositories/store.repository.ts" => "\353\217\204\354\226\217/week8/src/modules/stores/repositories/storeRepository.ts" (71%) rename "\353\217\204\354\226\217/week8/src/modules/stores/services/store.service.ts" => "\353\217\204\354\226\217/week8/src/modules/stores/services/storeService.ts" (63%) diff --git "a/\353\217\204\354\226\217/week8/package.json" "b/\353\217\204\354\226\217/week8/package.json" index 9bce09c..2bdc34c 100644 --- "a/\353\217\204\354\226\217/week8/package.json" +++ "b/\353\217\204\354\226\217/week8/package.json" @@ -2,7 +2,7 @@ "scripts": { "build": "tsoa spec-and-routes && tsc", "start": "tsoa spec-and-routes && tsx src/index.ts", - "dev": "tsoa spec-and-routes && nodemon --ext ts,prisma --ignore src/generated --exec \"npx prisma generate && tsx src/index.ts\"" + "dev": "tsoa spec-and-routes && nodemon --ext ts,prisma --ignore src/generated --exec \"npx prisma generate --config prismaConfig.ts && tsx src/index.ts\"" }, "dependencies": { "@prisma/adapter-mariadb": "^7.8.0", diff --git "a/\353\217\204\354\226\217/week8/prisma/migrations/20260527000000_add_member_auth_fields/migration.sql" "b/\353\217\204\354\226\217/week8/prisma/migrations/20260527000000_add_member_auth_fields/migration.sql" new file mode 100644 index 0000000..127921c --- /dev/null +++ "b/\353\217\204\354\226\217/week8/prisma/migrations/20260527000000_add_member_auth_fields/migration.sql" @@ -0,0 +1,7 @@ +ALTER TABLE `user` + ADD COLUMN `nickname` VARCHAR(50) NULL AFTER `name`, + ADD COLUMN `password` VARCHAR(255) NULL AFTER `nickname`, + MODIFY COLUMN `gender` VARCHAR(15) NULL, + MODIFY COLUMN `birth` DATE NULL, + MODIFY COLUMN `address` VARCHAR(255) NULL, + MODIFY COLUMN `phone_number` VARCHAR(15) NULL; diff --git "a/\353\217\204\354\226\217/week8/prisma/migrations/20260527001000_member_mission_status_enum/migration.sql" "b/\353\217\204\354\226\217/week8/prisma/migrations/20260527001000_member_mission_status_enum/migration.sql" new file mode 100644 index 0000000..a68a3d8 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/prisma/migrations/20260527001000_member_mission_status_enum/migration.sql" @@ -0,0 +1,10 @@ +UPDATE `member_mission` +SET `status` = 'CHALLENGING' +WHERE `status` IN ('진행중', 'CHALLENGING'); + +UPDATE `member_mission` +SET `status` = 'COMPLETE' +WHERE `status` IN ('완료', 'COMPLETE'); + +ALTER TABLE `member_mission` + MODIFY COLUMN `status` ENUM('CHALLENGING', 'COMPLETE') NOT NULL DEFAULT 'CHALLENGING'; diff --git "a/\353\217\204\354\226\217/week8/prisma/schema.prisma" "b/\353\217\204\354\226\217/week8/prisma/schema.prisma" index a5758e0..82f453a 100644 --- "a/\353\217\204\354\226\217/week8/prisma/schema.prisma" +++ "b/\353\217\204\354\226\217/week8/prisma/schema.prisma" @@ -7,15 +7,22 @@ datasource db { provider = "mysql" } +enum MissionStatus { + CHALLENGING + COMPLETE +} + model User { - id Int @id @default(autoincrement()) - email String @unique(map: "email") @db.VarChar(255) - name String @db.VarChar(100) - gender String @db.VarChar(15) - birth DateTime @db.Date - address String @db.VarChar(255) - detailAddress String? @map("detail_address") @db.VarChar(255) - phoneNumber String @map("phone_number") @db.VarChar(15) + id Int @id @default(autoincrement()) + email String @unique(map: "email") @db.VarChar(255) + name String @db.VarChar(100) + nickname String? @db.VarChar(50) + password String? @db.VarChar(255) + gender String? @db.VarChar(15) + birth DateTime? @db.Date + address String? @db.VarChar(255) + detailAddress String? @map("detail_address") @db.VarChar(255) + phoneNumber String? @map("phone_number") @db.VarChar(15) userFavorCategories UserFavorCategory[] reviews UserStoreReview[] @@ -41,7 +48,7 @@ model UserFavorCategory { foodCategory FoodCategory @relation(fields: [foodCategoryId], references: [id]) @@index([foodCategoryId], map: "f_category_id") - @@index([userId], map: "user_id") + @@index([userId], map: "user_id") @@map("user_favor_category") } @@ -65,19 +72,19 @@ model UserStoreReview { user User @relation(fields: [userId], references: [id]) @@index([storeId], map: "store_id") - @@index([userId], map: "user_id") + @@index([userId], map: "user_id") @@map("user_store_review") } model Mission { - id Int @id @default(autoincrement()) - storeId Int @map("store_id") - title String @db.VarChar(200) + id Int @id @default(autoincrement()) + storeId Int @map("store_id") + title String @db.VarChar(200) reward Int - spec String? @db.Text + spec String? @db.Text deadLine DateTime? @map("dead_line") - store Store @relation(fields: [storeId], references: [id]) + store Store @relation(fields: [storeId], references: [id]) memberMissions MemberMission[] @@index([storeId], map: "store_id") @@ -85,16 +92,15 @@ model Mission { } model MemberMission { - id Int @id @default(autoincrement()) - userId Int @map("member_id") - missionId Int @map("mission_id") - status String @db.VarChar(15) + id Int @id @default(autoincrement()) + memberId Int @map("member_id") + missionId Int @map("mission_id") + status MissionStatus @default(CHALLENGING) - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [memberId], references: [id]) mission Mission @relation(fields: [missionId], references: [id]) - @@index([userId], map: "member_id") + @@index([memberId], map: "member_id") @@index([missionId], map: "mission_id") @@map("member_mission") } - diff --git "a/\353\217\204\354\226\217/week8/prisma.config.ts" "b/\353\217\204\354\226\217/week8/prismaConfig.ts" similarity index 66% rename from "\353\217\204\354\226\217/week8/prisma.config.ts" rename to "\353\217\204\354\226\217/week8/prismaConfig.ts" index 5170cc4..0f93b24 100644 --- "a/\353\217\204\354\226\217/week8/prisma.config.ts" +++ "b/\353\217\204\354\226\217/week8/prismaConfig.ts" @@ -1,12 +1,12 @@ /// -import "dotenv/config"; -import { defineConfig } from "prisma/config"; +import 'dotenv/config' +import { defineConfig } from 'prisma/config' -const { DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME } = process.env; +const { DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME } = process.env export default defineConfig({ - schema: "prisma/schema.prisma", + schema: 'prisma/schema.prisma', datasource: { url: `mysql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT ?? 3306}/${DB_NAME}`, }, -}); +}) diff --git "a/\353\217\204\354\226\217/week8/src/db.config.ts" "b/\353\217\204\354\226\217/week8/src/db.config.ts" deleted file mode 100644 index 7b88907..0000000 --- "a/\353\217\204\354\226\217/week8/src/db.config.ts" +++ /dev/null @@ -1,17 +0,0 @@ -import "dotenv/config"; -import { PrismaClient } from "./generated/prisma/client.js"; -import { PrismaMariaDb } from "@prisma/adapter-mariadb"; - -const adapter = new PrismaMariaDb({ - host: process.env.DB_HOST, - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - port: process.env.DB_PORT ? parseInt(process.env.DB_PORT, 10) : 3306, - connectionLimit: 10, -}); - -export const prisma = new PrismaClient({ - adapter, - log: ["query", "info", "error", "warn"], -}); diff --git "a/\353\217\204\354\226\217/week8/src/dbConfig.ts" "b/\353\217\204\354\226\217/week8/src/dbConfig.ts" new file mode 100644 index 0000000..0d2808c --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/dbConfig.ts" @@ -0,0 +1,17 @@ +import 'dotenv/config' +import { PrismaClient } from './generated/prisma/client.js' +import { PrismaMariaDb } from '@prisma/adapter-mariadb' + +const adapter = new PrismaMariaDb({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + port: process.env.DB_PORT ? parseInt(process.env.DB_PORT, 10) : 3306, + connectionLimit: 10, +}) + +export const prisma = new PrismaClient({ + adapter, + log: ['query', 'info', 'error', 'warn'], +}) diff --git "a/\353\217\204\354\226\217/week8/src/generated/routes.ts" "b/\353\217\204\354\226\217/week8/src/generated/routes.ts" index 020aa77..88a7eab 100644 --- "a/\353\217\204\354\226\217/week8/src/generated/routes.ts" +++ "b/\353\217\204\354\226\217/week8/src/generated/routes.ts" @@ -4,13 +4,13 @@ import type { TsoaRoute } from '@tsoa/runtime'; import { fetchMiddlewares, ExpressTemplateService } from '@tsoa/runtime'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa -import { StoreController } from './../modules/stores/controllers/store.controller'; +import { StoreController } from './../modules/stores/controllers/storeController'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa -import { MissionController } from './../modules/missions/controllers/mission.controller'; +import { MissionController } from './../modules/missions/controllers/missionController'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa -import { UserController } from './../modules/members/controllers/user.controller'; +import { UserController } from './../modules/members/controllers/userController'; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa -import { MemberController } from './../modules/members/controllers/member.controller'; +import { MemberController } from './../modules/members/controllers/memberController'; import type { Request as ExRequest, Response as ExResponse, RequestHandler, Router } from 'express'; @@ -23,8 +23,6 @@ const models: TsoaRoute.Models = { "properties": { "storeId": {"dataType":"double","required":true}, "name": {"dataType":"string","required":true}, - "address": {"dataType":"string","required":true}, - "regionId": {"dataType":"double","required":true}, }, "additionalProperties": false, }, @@ -43,13 +41,7 @@ const models: TsoaRoute.Models = { "StoreCreateRequest": { "dataType": "refObject", "properties": { - "regionId": {"dataType":"double","required":true}, - "foodCategoryId": {"dataType":"double","required":true}, "name": {"dataType":"string","required":true}, - "description": {"dataType":"string"}, - "address": {"dataType":"string","required":true}, - "lat": {"dataType":"double"}, - "lng": {"dataType":"double"}, }, "additionalProperties": false, }, @@ -61,8 +53,6 @@ const models: TsoaRoute.Models = { "memberId": {"dataType":"double","required":true}, "storeId": {"dataType":"double","required":true}, "content": {"dataType":"string","required":true}, - "score": {"dataType":"double","required":true}, - "createdAt": {"dataType":"datetime","required":true}, }, "additionalProperties": false, }, @@ -94,8 +84,6 @@ const models: TsoaRoute.Models = { "memberId": {"dataType":"double","required":true}, "storeId": {"dataType":"double","required":true}, "content": {"dataType":"string","required":true}, - "score": {"dataType":"double","required":true}, - "createdAt": {"dataType":"datetime","required":true}, }, "additionalProperties": false, }, @@ -116,7 +104,7 @@ const models: TsoaRoute.Models = { "properties": { "memberId": {"dataType":"double","required":true}, "content": {"dataType":"string","required":true}, - "score": {"dataType":"double","required":true}, + "score": {"dataType":"double"}, }, "additionalProperties": false, }, @@ -176,13 +164,18 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "MissionStatus": { + "dataType": "refEnum", + "enums": ["CHALLENGING","COMPLETE"], + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "MissionChallengeResponse": { "dataType": "refObject", "properties": { "memberMissionId": {"dataType":"double","required":true}, "memberId": {"dataType":"double","required":true}, "missionId": {"dataType":"double","required":true}, - "status": {"dataType":"string","required":true}, + "status": {"ref":"MissionStatus","required":true}, }, "additionalProperties": false, }, @@ -202,7 +195,7 @@ const models: TsoaRoute.Models = { "dataType": "refObject", "properties": { "memberId": {"dataType":"double","required":true}, - "status": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["CHALLENGING"]},{"dataType":"enum","enums":["COMPLETE"]}],"required":true}, + "status": {"ref":"MissionStatus"}, }, "additionalProperties": false, }, @@ -230,7 +223,7 @@ const models: TsoaRoute.Models = { "OngoingMissionListResponse": { "dataType": "refObject", "properties": { - "data": {"dataType":"array","array":{"dataType":"refObject","ref":"MissionCreateResponse"},"required":true}, + "data": {"dataType":"array","array":{"dataType":"refObject","ref":"MissionChallengeResponse"},"required":true}, "pagination": {"dataType":"nestedObjectLiteral","nestedProperties":{"cursor":{"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"enum","enums":[null]}],"required":true}},"required":true}, }, "additionalProperties": false, @@ -247,19 +240,23 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "MemberSignUpResponse": { + "MemberResponse": { "dataType": "refObject", "properties": { "memberId": {"dataType":"double","required":true}, "name": {"dataType":"string","required":true}, - "nickname": {"dataType":"string","required":true}, - "email": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true}, - "phoneNum": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true}, - "status": {"dataType":"string","required":true}, + "nickname": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true}, + "email": {"dataType":"string","required":true}, + "phoneNumber": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"required":true}, }, "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "MemberSignUpResponse": { + "dataType": "refAlias", + "type": {"ref":"MemberResponse","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa "ApiResponse_MemberSignUpResponse_": { "dataType": "refObject", "properties": { @@ -275,14 +272,39 @@ const models: TsoaRoute.Models = { "dataType": "refObject", "properties": { "name": {"dataType":"string","required":true}, - "nickname": {"dataType":"string","required":true}, - "email": {"dataType":"string"}, - "password": {"dataType":"string"}, - "phoneNum": {"dataType":"string"}, + "nickname": {"dataType":"string"}, + "email": {"dataType":"string","required":true}, + "password": {"dataType":"string","required":true}, + "phoneNumber": {"dataType":"string"}, "birth": {"dataType":"string"}, "gender": {"dataType":"string"}, "address": {"dataType":"string"}, - "specAddress": {"dataType":"string"}, + "detailAddress": {"dataType":"string"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "MemberLoginResponse": { + "dataType": "refAlias", + "type": {"ref":"MemberResponse","validators":{}}, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "ApiResponse_MemberLoginResponse_": { + "dataType": "refObject", + "properties": { + "isSuccess": {"dataType":"boolean","required":true}, + "code": {"dataType":"string","required":true}, + "message": {"dataType":"string","required":true}, + "result": {"dataType":"union","subSchemas":[{"ref":"MemberLoginResponse"},{"dataType":"enum","enums":[null]}],"required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "MemberLoginRequest": { + "dataType": "refObject", + "properties": { + "email": {"dataType":"string","required":true}, + "password": {"dataType":"string","required":true}, }, "additionalProperties": false, }, @@ -612,6 +634,36 @@ export function RegisterRoutes(app: Router) { } }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + const argsMemberController_handleLogin: Record = { + body: {"in":"body","name":"body","required":true,"ref":"MemberLoginRequest"}, + }; + app.post('/members/login', + ...(fetchMiddlewares(MemberController)), + ...(fetchMiddlewares(MemberController.prototype.handleLogin)), + + async function MemberController_handleLogin(request: ExRequest, response: ExResponse, next: any) { + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = templateService.getValidatedArgs({ args: argsMemberController_handleLogin, request, response }); + + const controller = new MemberController(); + + await templateService.apiHandler({ + methodName: 'handleLogin', + controller, + response, + next, + validatedArgs, + successStatus: undefined, + }); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa diff --git "a/\353\217\204\354\226\217/week8/src/index.ts" "b/\353\217\204\354\226\217/week8/src/index.ts" index 90404e2..0fb501d 100644 --- "a/\353\217\204\354\226\217/week8/src/index.ts" +++ "b/\353\217\204\354\226\217/week8/src/index.ts" @@ -7,7 +7,7 @@ import swaggerUi from 'swagger-ui-express' import path from 'path' import fs from 'fs' import { RegisterRoutes } from './generated/routes.js' -import { errorMiddleware } from './middleware/error.middleware.js' +import { errorMiddleware } from './middleware/errorMiddleware.js' // 1. 환경 변수 설정 (가장 먼저 호출) dotenv.config() diff --git "a/\353\217\204\354\226\217/week8/src/middleware/error.middleware.ts" "b/\353\217\204\354\226\217/week8/src/middleware/error.middleware.ts" deleted file mode 100644 index abd0e3a..0000000 --- "a/\353\217\204\354\226\217/week8/src/middleware/error.middleware.ts" +++ /dev/null @@ -1,38 +0,0 @@ -import { Request, Response, NextFunction } from 'express' -import { BaseError } from '../utils/errors.js' - -export const errorMiddleware = ( - err: unknown, - _req: Request, - res: Response, - _next: NextFunction, -): void => { - if (err instanceof BaseError) { - res.status(err.status).json({ - isSuccess: false, - code: err.code, - message: err.message, - result: null, - }) - return - } - - // Prisma unique constraint 에러 - if (typeof err === 'object' && err !== null && (err as { code?: string }).code === 'P2002') { - res.status(409).json({ - isSuccess: false, - code: 'USER4002', - message: '이미 존재하는 데이터입니다.', - result: null, - }) - return - } - - console.error(err) - res.status(500).json({ - isSuccess: false, - code: 'COMMON500', - message: '서버 내부 오류입니다.', - result: null, - }) -} diff --git "a/\353\217\204\354\226\217/week8/src/middleware/errorMiddleware.ts" "b/\353\217\204\354\226\217/week8/src/middleware/errorMiddleware.ts" new file mode 100644 index 0000000..48c76bd --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/middleware/errorMiddleware.ts" @@ -0,0 +1,53 @@ +import { Request, Response, NextFunction } from 'express' +import { BaseError } from '../utils/errors.js' +import { ErrorCode } from '../utils/errorCode.js' + +export const errorMiddleware = ( + err: unknown, + _req: Request, + res: Response, + _next: NextFunction, +): void => { + const sendErrorResponse = (error: (typeof ErrorCode)[keyof typeof ErrorCode]) => { + res.status(error.status).json({ + isSuccess: false, + code: error.code, + message: error.message, + result: null, + }) + } + + if (err instanceof BaseError) { + res.status(err.status).json({ + isSuccess: false, + code: err.code, + message: err.message, + result: null, + }) + return + } + + const prismaCode = typeof err === 'object' && err !== null ? (err as { code?: string }).code : undefined + + if (prismaCode === 'P2002') { + const target = (err as { meta?: { target?: string[] | string } }).meta?.target + const fields = Array.isArray(target) ? target : [target] + const error = fields.includes('email') ? ErrorCode.DUPLICATE_EMAIL : ErrorCode.INVALID_INPUT + sendErrorResponse(error) + return + } + + if (prismaCode === 'P2025') { + sendErrorResponse(ErrorCode.USER_NOT_FOUND) + return + } + + console.error(err) + const error = ErrorCode.INTERNAL_ERROR + res.status(500).json({ + isSuccess: false, + code: error.code, + message: error.message, + result: null, + }) +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/members/controllers/member.controller.ts" "b/\353\217\204\354\226\217/week8/src/modules/members/controllers/member.controller.ts" deleted file mode 100644 index 6c42629..0000000 --- "a/\353\217\204\354\226\217/week8/src/modules/members/controllers/member.controller.ts" +++ /dev/null @@ -1,24 +0,0 @@ -import { Body, Controller, Post, Route, Tags, Response as TsoaResponse, SuccessResponse } from 'tsoa' -import { MemberSignUpRequest, MemberSignUpResponse } from '../dtos/member.dto.js' -import { signUp } from '../services/member.service.js' -import { ApiResponse, successResponse } from '../../../utils/response.js' - -@Route('members') -@Tags('Member') -export class MemberController extends Controller { - /** - * 회원가입 API - * @summary 새로운 회원을 등록합니다. - */ - @Post('signup') - @SuccessResponse(201, '회원가입 성공') - @TsoaResponse>(409, '이미 사용 중인 이메일 (USER4002)') - @TsoaResponse>(400, 'name 또는 nickname 누락 (USER4003)') - public async handleSignUp( - @Body() body: MemberSignUpRequest, - ): Promise> { - this.setStatus(201) - const result = await signUp(body) - return successResponse(result) - } -} diff --git "a/\353\217\204\354\226\217/week8/src/modules/members/controllers/memberController.ts" "b/\353\217\204\354\226\217/week8/src/modules/members/controllers/memberController.ts" new file mode 100644 index 0000000..69f3e21 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/members/controllers/memberController.ts" @@ -0,0 +1,43 @@ +import { Body, Controller, Post, Route, Tags, Response as TsoaResponse, SuccessResponse } from 'tsoa' +import { + MemberLoginRequest, + MemberLoginResponse, + MemberSignUpRequest, + MemberSignUpResponse, +} from '../dtos/memberDto.js' +import { login, signUp } from '../services/memberService.js' +import { ApiResponse, successResponse } from '../../../utils/response.js' + +@Route('members') +@Tags('Member') +export class MemberController extends Controller { + /** + * 회원가입 API + * @summary 새로운 회원을 등록합니다. + */ + @Post('signup') + @SuccessResponse(201, '회원가입 성공') + @TsoaResponse>(409, '이미 사용 중인 이메일 (USER4002)') + @TsoaResponse>(400, '필수 입력값 누락 (USER4003)') + public async handleSignUp( + @Body() body: MemberSignUpRequest, + ): Promise> { + this.setStatus(201) + const result = await signUp(body) + return successResponse(result) + } + + /** + * 로그인 API + * @summary 이메일과 비밀번호로 로그인합니다. + */ + @Post('login') + @TsoaResponse>(401, '이메일 또는 비밀번호 불일치 (AUTH4001)') + @TsoaResponse>(400, '필수 입력값 누락 (USER4003)') + public async handleLogin( + @Body() body: MemberLoginRequest, + ): Promise> { + const result = await login(body) + return successResponse(result) + } +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/members/controllers/user.controller.ts" "b/\353\217\204\354\226\217/week8/src/modules/members/controllers/userController.ts" similarity index 93% rename from "\353\217\204\354\226\217/week8/src/modules/members/controllers/user.controller.ts" rename to "\353\217\204\354\226\217/week8/src/modules/members/controllers/userController.ts" index b36650d..1401b5b 100644 --- "a/\353\217\204\354\226\217/week8/src/modules/members/controllers/user.controller.ts" +++ "b/\353\217\204\354\226\217/week8/src/modules/members/controllers/userController.ts" @@ -1,8 +1,8 @@ import { Controller, Get, Patch, Path, Query, Route, Tags, Response as TsoaResponse } from 'tsoa' -import { listUserReviews } from '../../reviews/services/review.service.js' -import { listOngoingMissions, finishMission } from '../../missions/services/mission.service.js' -import { UserReviewListResponse } from '../../reviews/dtos/review.dto.js' -import { OngoingMissionListResponse, MissionChallengeResponse } from '../../missions/dtos/mission.dto.js' +import { listUserReviews } from '../../reviews/services/reviewService.js' +import { listOngoingMissions, finishMission } from '../../missions/services/missionService.js' +import { UserReviewListResponse } from '../../reviews/dtos/reviewDto.js' +import { OngoingMissionListResponse, MissionChallengeResponse } from '../../missions/dtos/missionDto.js' import { ApiResponse, successResponse } from '../../../utils/response.js' @Route('users') diff --git "a/\353\217\204\354\226\217/week8/src/modules/members/dtos/member.dto.ts" "b/\353\217\204\354\226\217/week8/src/modules/members/dtos/memberDto.ts" similarity index 56% rename from "\353\217\204\354\226\217/week8/src/modules/members/dtos/member.dto.ts" rename to "\353\217\204\354\226\217/week8/src/modules/members/dtos/memberDto.ts" index 9f8cfd0..61cf5e8 100644 --- "a/\353\217\204\354\226\217/week8/src/modules/members/dtos/member.dto.ts" +++ "b/\353\217\204\354\226\217/week8/src/modules/members/dtos/memberDto.ts" @@ -1,24 +1,23 @@ -// 회원가입 요청 인터페이스 export interface MemberSignUpRequest { /** 회원 이름 */ name: string /** 닉네임 */ - nickname: string + nickname?: string /** * 이메일 * @example "test@example.com" */ - email?: string + email: string /** * 비밀번호 * @example "qwer1234!" */ - password?: string + password: string /** * 전화번호 * @example "010-1234-5678" */ - phoneNum?: string + phoneNumber?: string /** * 생년월일 (YYYY-MM-DD) * @example "2000-01-01" @@ -32,54 +31,63 @@ export interface MemberSignUpRequest { /** 주소 */ address?: string /** 상세 주소 */ - specAddress?: string + detailAddress?: string } -// 회원가입 응답 인터페이스 -export interface MemberSignUpResponse { - /** 생성된 회원 ID */ +export interface MemberLoginRequest { + /** + * 이메일 + * @example "test@example.com" + */ + email: string + /** + * 비밀번호 + * @example "qwer1234!" + */ + password: string +} + +export interface MemberResponse { + /** 회원 ID */ memberId: number /** 회원 이름 */ name: string /** 닉네임 */ - nickname: string + nickname: string | null /** 이메일 */ - email: string | null + email: string /** 전화번호 */ - phoneNum: string | null - /** 회원 상태 */ - status: string + phoneNumber: string | null } -// req.body → 내부 데이터로 변환 +export type MemberSignUpResponse = MemberResponse +export type MemberLoginResponse = MemberResponse + export const bodyToMember = (body: MemberSignUpRequest) => { return { name: body.name, - nickname: body.nickname, - email: body.email ?? null, - phoneNum: body.phoneNum ?? null, + nickname: body.nickname ?? null, + email: body.email, + phoneNumber: body.phoneNumber ?? null, birth: body.birth ? new Date(body.birth) : null, gender: body.gender ?? null, address: body.address ?? null, - specAddress: body.specAddress ?? null, + detailAddress: body.detailAddress ?? null, } } -// DB 결과 → 응답 형태로 변환 export const responseFromMember = (member: { id: number name: string - nickname: string - email: string | null - phone_num: string | null - status: string -}) => { + nickname: string | null + email: string + phoneNumber: string | null +}): MemberResponse => { return { memberId: member.id, name: member.name, nickname: member.nickname, email: member.email, - phoneNum: member.phone_num, - status: member.status, + phoneNumber: member.phoneNumber, } } diff --git "a/\353\217\204\354\226\217/week8/src/modules/members/repositories/member.repository.ts" "b/\353\217\204\354\226\217/week8/src/modules/members/repositories/memberRepository.ts" similarity index 52% rename from "\353\217\204\354\226\217/week8/src/modules/members/repositories/member.repository.ts" rename to "\353\217\204\354\226\217/week8/src/modules/members/repositories/memberRepository.ts" index 549030e..aa89599 100644 --- "a/\353\217\204\354\226\217/week8/src/modules/members/repositories/member.repository.ts" +++ "b/\353\217\204\354\226\217/week8/src/modules/members/repositories/memberRepository.ts" @@ -1,23 +1,29 @@ -import { prisma } from '../../../db.config.js' - -// 유저 생성 -export const addUser = async (data: any) => { - const exists = await prisma.user.findFirst({ where: { email: data.email } }) - if (exists) return null +import { prisma } from '../../../dbConfig.js' +export const addUser = async (data: { + name: string + nickname: string | null + email: string + password: string + phoneNumber: string | null + birth: Date | null + gender: string | null + address: string | null + detailAddress: string | null +}) => { const created = await prisma.user.create({ data }) return created.id } -// 유저 조회 (없으면 예외 throw) export const getUser = async (userId: number) => - prisma.user.findFirstOrThrow({ where: { id: userId } }) + prisma.user.findFirst({ where: { id: userId } }) + +export const getUserByEmail = async (email: string) => + prisma.user.findFirst({ where: { email } }) -// 선호 음식 카테고리 등록 export const setPreference = async (userId: number, foodCategoryId: number) => prisma.userFavorCategory.create({ data: { userId, foodCategoryId } }) -// 선호 카테고리 목록 조회 (JOIN 포함) export const getUserPreferencesByUserId = async (userId: number) => prisma.userFavorCategory.findMany({ where: { userId }, diff --git "a/\353\217\204\354\226\217/week8/src/modules/members/services/member.service.ts" "b/\353\217\204\354\226\217/week8/src/modules/members/services/member.service.ts" deleted file mode 100644 index 2cf928d..0000000 --- "a/\353\217\204\354\226\217/week8/src/modules/members/services/member.service.ts" +++ /dev/null @@ -1,39 +0,0 @@ -import bcrypt from 'bcryptjs' -import { MemberSignUpRequest, bodyToMember, responseFromMember } from '../dtos/member.dto.js' -import { addUser, getUser } from '../repositories/member.repository.js' -import { BaseError } from '../../../utils/errors.js' -import { ErrorCode } from '../../../utils/errorCode.js' - -export const signUp = async (data: MemberSignUpRequest) => { - if (!data.name || !data.nickname) { - throw new BaseError( - ErrorCode.MEMBER_REQUIRED_FIELD.message, - ErrorCode.MEMBER_REQUIRED_FIELD.status, - ErrorCode.MEMBER_REQUIRED_FIELD.code, - ) - } - - const hashedPassword = data.password ? await bcrypt.hash(data.password, 10) : null - - const memberData = bodyToMember(data) - const memberId = await addUser({ ...memberData, hashedPassword }) - - if (memberId === null) { - throw new BaseError( - ErrorCode.DUPLICATE_EMAIL.message, - ErrorCode.DUPLICATE_EMAIL.status, - ErrorCode.DUPLICATE_EMAIL.code, - ) - } - - const member = await getUser(memberId) - - return responseFromMember(member as { - id: number - name: string - nickname: string - email: string | null - phone_num: string | null - status: string - }) -} diff --git "a/\353\217\204\354\226\217/week8/src/modules/members/services/memberService.ts" "b/\353\217\204\354\226\217/week8/src/modules/members/services/memberService.ts" new file mode 100644 index 0000000..40622fb --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/members/services/memberService.ts" @@ -0,0 +1,52 @@ +import bcrypt from 'bcryptjs' +import { + MemberLoginRequest, + MemberSignUpRequest, + bodyToMember, + responseFromMember, +} from '../dtos/memberDto.js' +import { addUser, getUser, getUserByEmail } from '../repositories/memberRepository.js' +import { BaseError } from '../../../utils/errors.js' +import { ErrorCode } from '../../../utils/errorCode.js' + +export const signUp = async (data: MemberSignUpRequest) => { + // TSOA required 검증 이후에도 빈 문자열 입력은 서비스에서 한 번 더 방어합니다. + if (!data.name || !data.email || !data.password) { + throw new BaseError(ErrorCode.MEMBER_REQUIRED_FIELD) + } + + const existingUser = await getUserByEmail(data.email) + if (existingUser) { + throw new BaseError(ErrorCode.DUPLICATE_EMAIL) + } + + const hashedPassword = await bcrypt.hash(data.password, 10) + const memberData = bodyToMember(data) + const memberId = await addUser({ ...memberData, password: hashedPassword }) + const member = await getUser(memberId) + + if (!member) { + throw new BaseError(ErrorCode.USER_NOT_FOUND) + } + + return responseFromMember(member) +} + +export const login = async (data: MemberLoginRequest) => { + // TSOA required 검증 이후에도 빈 문자열 입력은 서비스에서 한 번 더 방어합니다. + if (!data.email || !data.password) { + throw new BaseError(ErrorCode.MEMBER_REQUIRED_FIELD) + } + + const member = await getUserByEmail(data.email) + if (!member?.password) { + throw new BaseError(ErrorCode.AUTH_INVALID_CREDENTIALS) + } + + const isPasswordMatched = await bcrypt.compare(data.password, member.password) + if (!isPasswordMatched) { + throw new BaseError(ErrorCode.AUTH_INVALID_CREDENTIALS) + } + + return responseFromMember(member) +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/missions/controllers/mission.controller.ts" "b/\353\217\204\354\226\217/week8/src/modules/missions/controllers/missionController.ts" similarity index 91% rename from "\353\217\204\354\226\217/week8/src/modules/missions/controllers/mission.controller.ts" rename to "\353\217\204\354\226\217/week8/src/modules/missions/controllers/missionController.ts" index df4b236..0d72b45 100644 --- "a/\353\217\204\354\226\217/week8/src/modules/missions/controllers/mission.controller.ts" +++ "b/\353\217\204\354\226\217/week8/src/modules/missions/controllers/missionController.ts" @@ -1,6 +1,6 @@ import { Body, Controller, Path, Post, Route, Tags, Response as TsoaResponse, SuccessResponse } from 'tsoa' -import { MissionChallengeRequest, MissionChallengeResponse } from '../dtos/mission.dto.js' -import { challengeMission } from '../services/mission.service.js' +import { MissionChallengeRequest, MissionChallengeResponse } from '../dtos/missionDto.js' +import { challengeMission } from '../services/missionService.js' import { ApiResponse, successResponse } from '../../../utils/response.js' @Route('missions') diff --git "a/\353\217\204\354\226\217/week8/src/modules/missions/dtos/mission.dto.ts" "b/\353\217\204\354\226\217/week8/src/modules/missions/dtos/missionDto.ts" similarity index 70% rename from "\353\217\204\354\226\217/week8/src/modules/missions/dtos/mission.dto.ts" rename to "\353\217\204\354\226\217/week8/src/modules/missions/dtos/missionDto.ts" index d6bc435..7636293 100644 --- "a/\353\217\204\354\226\217/week8/src/modules/missions/dtos/mission.dto.ts" +++ "b/\353\217\204\354\226\217/week8/src/modules/missions/dtos/missionDto.ts" @@ -1,4 +1,8 @@ -// 미션 추가 요청 인터페이스 +export enum MissionStatus { + CHALLENGING = 'CHALLENGING', + COMPLETE = 'COMPLETE', +} + export interface MissionCreateRequest { /** 미션 제목 */ title: string @@ -16,7 +20,6 @@ export interface MissionCreateRequest { deadLine?: string } -// 미션 도전 요청 인터페이스 export interface MissionChallengeRequest { /** * 도전할 회원 ID @@ -27,10 +30,9 @@ export interface MissionChallengeRequest { * 미션 상태 * @example "CHALLENGING" */ - status: 'CHALLENGING' | 'COMPLETE' + status?: MissionStatus } -// 미션 생성 응답 인터페이스 export interface MissionCreateResponse { /** 생성된 미션 ID */ missionId: number @@ -46,7 +48,6 @@ export interface MissionCreateResponse { deadLine: Date | null } -// 미션 도전 응답 인터페이스 export interface MissionChallengeResponse { /** 회원-미션 매핑 ID */ memberMissionId: number @@ -55,19 +56,17 @@ export interface MissionChallengeResponse { /** 미션 ID */ missionId: number /** 미션 상태 */ - status: string + status: MissionStatus } -// 진행 중 미션 목록 응답 인터페이스 export interface OngoingMissionListResponse { - data: MissionCreateResponse[] + data: MissionChallengeResponse[] pagination: { /** 다음 페이지 커서 */ cursor: number | null } } -// 가게 미션 목록 응답 인터페이스 export interface StoreMissionListResponse { data: MissionCreateResponse[] pagination: { @@ -75,7 +74,6 @@ export interface StoreMissionListResponse { } } -// req.body → 내부 데이터로 변환 (미션 추가) export const bodyToMission = (body: MissionCreateRequest) => { return { title: body.title, @@ -85,36 +83,34 @@ export const bodyToMission = (body: MissionCreateRequest) => { } } -// DB 결과 → 응답 형태로 변환 (미션) export const responseFromMission = (mission: { id: number - store_id: number + storeId: number title: string reward: number spec: string | null - dead_line: Date | null -}) => { + deadLine: Date | null +}): MissionCreateResponse => { return { missionId: mission.id, - storeId: mission.store_id, + storeId: mission.storeId, title: mission.title, reward: mission.reward, spec: mission.spec, - deadLine: mission.dead_line, + deadLine: mission.deadLine, } } -// DB 결과 → 응답 형태로 변환 (미션 도전) export const responseFromMemberMission = (mm: { id: number - member_id: number - mission_id: number + memberId: number + missionId: number status: string -}) => { +}): MissionChallengeResponse => { return { memberMissionId: mm.id, - memberId: mm.member_id, - missionId: mm.mission_id, - status: mm.status, + memberId: mm.memberId, + missionId: mm.missionId, + status: mm.status as MissionStatus, } } diff --git "a/\353\217\204\354\226\217/week8/src/modules/missions/repositories/mission.repository.ts" "b/\353\217\204\354\226\217/week8/src/modules/missions/repositories/missionRepository.ts" similarity index 53% rename from "\353\217\204\354\226\217/week8/src/modules/missions/repositories/mission.repository.ts" rename to "\353\217\204\354\226\217/week8/src/modules/missions/repositories/missionRepository.ts" index bee3e62..6263db8 100644 --- "a/\353\217\204\354\226\217/week8/src/modules/missions/repositories/mission.repository.ts" +++ "b/\353\217\204\354\226\217/week8/src/modules/missions/repositories/missionRepository.ts" @@ -1,52 +1,50 @@ -import { prisma } from '../../../db.config.js' +import { prisma } from '../../../dbConfig.js' +import { MissionStatus } from '../dtos/missionDto.js' -// 미션 추가 export const addMission = async (data: { - storeId: number - title: string - reward: number - spec: string | null + storeId: number + title: string + reward: number + spec: string | null deadLine: Date | null }): Promise => { const created = await prisma.mission.create({ data: { - storeId: data.storeId, - title: data.title, - reward: data.reward, - spec: data.spec, + storeId: data.storeId, + title: data.title, + reward: data.reward, + spec: data.spec, deadLine: data.deadLine, }, }) return created.id } -// 미션 조회 export const getMissionById = async (missionId: number) => prisma.mission.findFirst({ where: { id: missionId } }) -// 이미 도전 중인 미션인지 확인 export const findMemberMission = async (memberId: number, missionId: number) => prisma.memberMission.findFirst({ - where: { userId: memberId, missionId }, + where: { memberId, missionId }, }) -// 미션 도전 추가 export const addMemberMission = async ( - memberId: number, + memberId: number, missionId: number, - status: string, + status: MissionStatus, ): Promise => { const created = await prisma.memberMission.create({ - data: { userId: memberId, missionId, status }, + data: { memberId, missionId, status }, }) return created.id } -// 미션 도전 기록 조회 export const getMemberMissionById = async (memberMissionId: number) => prisma.memberMission.findFirst({ where: { id: memberMissionId } }) -// 특정 가게의 미션 목록 조회 (커서 기반 페이지네이션) +export const getMemberMissionByMemberAndMission = async (memberId: number, missionId: number) => + prisma.memberMission.findFirst({ where: { memberId, missionId } }) + export const getStoreMissions = async (storeId: number, cursor: number) => prisma.mission.findMany({ where: { storeId, id: { gt: cursor } }, @@ -54,19 +52,16 @@ export const getStoreMissions = async (storeId: number, cursor: number) => take: 5, }) - -// 유저가 진행 중인 미션 목록 조회 -export const getOngoingMissions = async (userId: number, cursor: number) => +export const getOngoingMissions = async (memberId: number, cursor: number) => prisma.memberMission.findMany({ - where: { userId, status: '진행중', id: { gt: cursor } }, + where: { memberId, status: MissionStatus.CHALLENGING, id: { gt: cursor } }, include: { mission: { include: { store: true } } }, orderBy: { id: 'asc' }, take: 5, }) -// 진행 중인 미션을 완료로 변경 -export const completeMission = async (userId: number, missionId: number) => +export const completeMission = async (memberId: number, missionId: number) => prisma.memberMission.updateMany({ - where: { userId, missionId, status: '진행중' }, - data: { status: '완료' }, - }) \ No newline at end of file + where: { memberId, missionId, status: MissionStatus.CHALLENGING }, + data: { status: MissionStatus.COMPLETE }, + }) diff --git "a/\353\217\204\354\226\217/week8/src/modules/missions/services/mission.service.ts" "b/\353\217\204\354\226\217/week8/src/modules/missions/services/missionService.ts" similarity index 50% rename from "\353\217\204\354\226\217/week8/src/modules/missions/services/mission.service.ts" rename to "\353\217\204\354\226\217/week8/src/modules/missions/services/missionService.ts" index 92018a3..a3d828b 100644 --- "a/\353\217\204\354\226\217/week8/src/modules/missions/services/mission.service.ts" +++ "b/\353\217\204\354\226\217/week8/src/modules/missions/services/missionService.ts" @@ -1,128 +1,118 @@ import { - MissionCreateRequest, MissionChallengeRequest, + MissionCreateRequest, + MissionStatus, bodyToMission, - responseFromMission, responseFromMemberMission, -} from '../dtos/mission.dto.js' + responseFromMission, +} from '../dtos/missionDto.js' import { + addMemberMission, addMission, - getMissionById, + completeMission, findMemberMission, - addMemberMission, getMemberMissionById, - getStoreMissions, + getMemberMissionByMemberAndMission, + getMissionById, getOngoingMissions, - completeMission, -} from '../repositories/mission.repository.js' -import { getStoreById } from '../../stores/repositories/store.repository.js' + getStoreMissions, +} from '../repositories/missionRepository.js' +import { getUser } from '../../members/repositories/memberRepository.js' +import { getStoreById } from '../../stores/repositories/storeRepository.js' import { BaseError } from '../../../utils/errors.js' import { ErrorCode } from '../../../utils/errorCode.js' export const createMission = async (storeId: number, data: MissionCreateRequest) => { const store = await getStoreById(storeId) if (!store) { - throw new BaseError( - ErrorCode.STORE_NOT_FOUND.message, - ErrorCode.STORE_NOT_FOUND.status, - ErrorCode.STORE_NOT_FOUND.code, - ) + throw new BaseError(ErrorCode.STORE_NOT_FOUND) } const missionData = bodyToMission(data) const missionId = await addMission({ ...missionData, storeId }) - const mission = await getMissionById(missionId) + if (!mission) { - throw new BaseError( - ErrorCode.MISSION_CREATE_FAILED.message, - ErrorCode.MISSION_CREATE_FAILED.status, - ErrorCode.MISSION_CREATE_FAILED.code, - ) + throw new BaseError(ErrorCode.MISSION_CREATE_FAILED) } - return responseFromMission(mission as unknown as { - id: number - store_id: number - title: string - reward: number - spec: string | null - dead_line: Date | null - }) + return responseFromMission(mission) } export const listStoreMissions = async (storeId: number, cursor: number) => { + const store = await getStoreById(storeId) + if (!store) { + throw new BaseError(ErrorCode.STORE_NOT_FOUND) + } + const missions = await getStoreMissions(storeId, cursor) const last = missions[missions.length - 1] return { - data: missions, + data: missions.map(responseFromMission), pagination: { cursor: last ? last.id : null }, } } export const listOngoingMissions = async (userId: number, cursor: number) => { + const user = await getUser(userId) + if (!user) { + throw new BaseError(ErrorCode.USER_NOT_FOUND) + } + const missions = await getOngoingMissions(userId, cursor) const last = missions[missions.length - 1] return { - data: missions, + data: missions.map(responseFromMemberMission), pagination: { cursor: last ? last.id : null }, } } export const finishMission = async (userId: number, missionId: number) => { + const user = await getUser(userId) + if (!user) { + throw new BaseError(ErrorCode.USER_NOT_FOUND) + } + const result = await completeMission(userId, missionId) if (result.count === 0) { - throw new BaseError( - ErrorCode.ONGOING_MISSION_NOT_FOUND.message, - ErrorCode.ONGOING_MISSION_NOT_FOUND.status, - ErrorCode.ONGOING_MISSION_NOT_FOUND.code, - ) + throw new BaseError(ErrorCode.ONGOING_MISSION_NOT_FOUND) } - return { message: '미션이 완료 처리됐습니다.' } + + const memberMission = await getMemberMissionByMemberAndMission(userId, missionId) + if (!memberMission) { + throw new BaseError(ErrorCode.ONGOING_MISSION_NOT_FOUND) + } + + return responseFromMemberMission(memberMission) } export const challengeMission = async (missionId: number, data: MissionChallengeRequest) => { const mission = await getMissionById(missionId) if (!mission) { - throw new BaseError( - ErrorCode.MISSION_NOT_FOUND.message, - ErrorCode.MISSION_NOT_FOUND.status, - ErrorCode.MISSION_NOT_FOUND.code, - ) + throw new BaseError(ErrorCode.MISSION_NOT_FOUND) } - if (!data.status) { - throw new BaseError( - ErrorCode.MISSION_STATUS_REQUIRED.message, - ErrorCode.MISSION_STATUS_REQUIRED.status, - ErrorCode.MISSION_STATUS_REQUIRED.code, - ) + const user = await getUser(data.memberId) + if (!user) { + throw new BaseError(ErrorCode.USER_NOT_FOUND) + } + + const status = data.status ?? MissionStatus.CHALLENGING + if (!Object.values(MissionStatus).includes(status)) { + throw new BaseError(ErrorCode.MISSION_STATUS_REQUIRED) } const existing = await findMemberMission(data.memberId, missionId) if (existing) { - throw new BaseError( - ErrorCode.MISSION_ALREADY_CHALLENGING.message, - ErrorCode.MISSION_ALREADY_CHALLENGING.status, - ErrorCode.MISSION_ALREADY_CHALLENGING.code, - ) + throw new BaseError(ErrorCode.MISSION_ALREADY_CHALLENGING) } - const memberMissionId = await addMemberMission(data.memberId, missionId, data.status) - + const memberMissionId = await addMemberMission(data.memberId, missionId, status) const memberMission = await getMemberMissionById(memberMissionId) + if (!memberMission) { - throw new BaseError( - ErrorCode.MISSION_CHALLENGE_FAILED.message, - ErrorCode.MISSION_CHALLENGE_FAILED.status, - ErrorCode.MISSION_CHALLENGE_FAILED.code, - ) + throw new BaseError(ErrorCode.MISSION_CHALLENGE_FAILED) } - return responseFromMemberMission(memberMission as unknown as { - id: number - member_id: number - mission_id: number - status: string - }) + return responseFromMemberMission(memberMission) } diff --git "a/\353\217\204\354\226\217/week8/src/modules/reviews/controllers/review.controller.ts" "b/\353\217\204\354\226\217/week8/src/modules/reviews/controllers/reviewController.ts" similarity index 100% rename from "\353\217\204\354\226\217/week8/src/modules/reviews/controllers/review.controller.ts" rename to "\353\217\204\354\226\217/week8/src/modules/reviews/controllers/reviewController.ts" diff --git "a/\353\217\204\354\226\217/week8/src/modules/reviews/dtos/review.dto.ts" "b/\353\217\204\354\226\217/week8/src/modules/reviews/dtos/reviewDto.ts" similarity index 55% rename from "\353\217\204\354\226\217/week8/src/modules/reviews/dtos/review.dto.ts" rename to "\353\217\204\354\226\217/week8/src/modules/reviews/dtos/reviewDto.ts" index ea89781..0177664 100644 --- "a/\353\217\204\354\226\217/week8/src/modules/reviews/dtos/review.dto.ts" +++ "b/\353\217\204\354\226\217/week8/src/modules/reviews/dtos/reviewDto.ts" @@ -1,4 +1,3 @@ -// 리뷰 추가 요청 인터페이스 export interface ReviewCreateRequest { /** * 작성자 회원 ID @@ -7,17 +6,16 @@ export interface ReviewCreateRequest { memberId: number /** * 리뷰 내용 - * @example "음식이 정말 맛있었어요!" + * @example "음식이 정말 맛있었어요." */ content: string /** - * 별점 (1~5) + * 별점 (1~5). 현재 DB에는 저장하지 않고 입력 검증에만 사용합니다. * @example 4.5 */ - score: number + score?: number } -// 리뷰 생성 응답 인터페이스 export interface ReviewCreateResponse { /** 생성된 리뷰 ID */ reviewId: number @@ -27,13 +25,8 @@ export interface ReviewCreateResponse { storeId: number /** 리뷰 내용 */ content: string - /** 별점 */ - score: number - /** 작성일시 */ - createdAt: Date } -// 사용자 리뷰 목록 응답 인터페이스 export interface UserReviewListResponse { data: ReviewCreateResponse[] pagination: { @@ -42,39 +35,36 @@ export interface UserReviewListResponse { } } -// req.body → 내부 데이터로 변환 export const bodyToReview = (body: ReviewCreateRequest) => { return { memberId: body.memberId, content: body.content, - score: body.score, } } -// 리뷰 목록 → 응답 형태로 변환 (커서 기반 페이지네이션) -export const responseFromUserReviews = (reviews: any[]) => { +export const responseFromUserReviews = (reviews: Array<{ + id: number + userId: number + storeId: number + content: string +}>): UserReviewListResponse => { const last = reviews[reviews.length - 1] return { - data: reviews, + data: reviews.map(responseFromReview), pagination: { cursor: last ? last.id : null }, } } -// DB 결과 → 응답 형태로 변환 export const responseFromReview = (review: { id: number - member_id: number - store_id: number + userId: number + storeId: number content: string - score: number - created_at: Date -}) => { +}): ReviewCreateResponse => { return { reviewId: review.id, - memberId: review.member_id, - storeId: review.store_id, + memberId: review.userId, + storeId: review.storeId, content: review.content, - score: review.score, - createdAt: review.created_at, } } diff --git "a/\353\217\204\354\226\217/week8/src/modules/reviews/repositories/review.repository.ts" "b/\353\217\204\354\226\217/week8/src/modules/reviews/repositories/reviewRepository.ts" similarity index 73% rename from "\353\217\204\354\226\217/week8/src/modules/reviews/repositories/review.repository.ts" rename to "\353\217\204\354\226\217/week8/src/modules/reviews/repositories/reviewRepository.ts" index adca307..e367900 100644 --- "a/\353\217\204\354\226\217/week8/src/modules/reviews/repositories/review.repository.ts" +++ "b/\353\217\204\354\226\217/week8/src/modules/reviews/repositories/reviewRepository.ts" @@ -1,6 +1,5 @@ -import { prisma } from '../../../db.config.js' +import { prisma } from '../../../dbConfig.js' -// 리뷰 추가 export const addReview = async (data: { memberId: number storeId: number @@ -8,7 +7,7 @@ export const addReview = async (data: { }): Promise => { const created = await prisma.userStoreReview.create({ data: { - userId: data.memberId, + userId: data.memberId, storeId: data.storeId, content: data.content, }, @@ -16,15 +15,12 @@ export const addReview = async (data: { return created.id } -// 리뷰 조회 export const getReviewById = async (reviewId: number) => prisma.userStoreReview.findFirst({ where: { id: reviewId } }) -// 내가 작성한 리뷰 목록 조회 (커서 기반 페이지네이션) export const getUserReviews = async (userId: number, cursor: number) => prisma.userStoreReview.findMany({ where: { userId, id: { gt: cursor } }, - include: { store: true }, orderBy: { id: 'asc' }, take: 5, }) diff --git "a/\353\217\204\354\226\217/week8/src/modules/reviews/services/review.service.ts" "b/\353\217\204\354\226\217/week8/src/modules/reviews/services/review.service.ts" deleted file mode 100644 index 13b2281..0000000 --- "a/\353\217\204\354\226\217/week8/src/modules/reviews/services/review.service.ts" +++ /dev/null @@ -1,50 +0,0 @@ -import { ReviewCreateRequest, bodyToReview, responseFromReview, responseFromUserReviews } from '../dtos/review.dto.js' -import { addReview, getReviewById, getUserReviews } from '../repositories/review.repository.js' -import { getStoreById } from '../../stores/repositories/store.repository.js' -import { BaseError } from '../../../utils/errors.js' -import { ErrorCode } from '../../../utils/errorCode.js' - -export const listUserReviews = async (userId: number, cursor: number) => { - const reviews = await getUserReviews(userId, cursor) - return responseFromUserReviews(reviews) -} - -export const createReview = async (storeId: number, data: ReviewCreateRequest) => { - const store = await getStoreById(storeId) - if (!store) { - throw new BaseError( - ErrorCode.STORE_NOT_FOUND.message, - ErrorCode.STORE_NOT_FOUND.status, - ErrorCode.STORE_NOT_FOUND.code, - ) - } - - if (data.score < 1 || data.score > 5) { - throw new BaseError( - ErrorCode.INVALID_SCORE.message, - ErrorCode.INVALID_SCORE.status, - ErrorCode.INVALID_SCORE.code, - ) - } - - const reviewData = bodyToReview(data) - const reviewId = await addReview({ ...reviewData, storeId }) - - const review = await getReviewById(reviewId) - if (!review) { - throw new BaseError( - ErrorCode.REVIEW_CREATE_FAILED.message, - ErrorCode.REVIEW_CREATE_FAILED.status, - ErrorCode.REVIEW_CREATE_FAILED.code, - ) - } - - return responseFromReview(review as { - id: number - member_id: number - store_id: number - content: string - score: number - created_at: Date - }) -} diff --git "a/\353\217\204\354\226\217/week8/src/modules/reviews/services/reviewService.ts" "b/\353\217\204\354\226\217/week8/src/modules/reviews/services/reviewService.ts" new file mode 100644 index 0000000..2272f74 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/reviews/services/reviewService.ts" @@ -0,0 +1,42 @@ +import { ReviewCreateRequest, bodyToReview, responseFromReview, responseFromUserReviews } from '../dtos/reviewDto.js' +import { addReview, getReviewById, getUserReviews } from '../repositories/reviewRepository.js' +import { getStoreById } from '../../stores/repositories/storeRepository.js' +import { getUser } from '../../members/repositories/memberRepository.js' +import { BaseError } from '../../../utils/errors.js' +import { ErrorCode } from '../../../utils/errorCode.js' + +export const listUserReviews = async (userId: number, cursor: number) => { + const user = await getUser(userId) + if (!user) { + throw new BaseError(ErrorCode.USER_NOT_FOUND) + } + + const reviews = await getUserReviews(userId, cursor) + return responseFromUserReviews(reviews) +} + +export const createReview = async (storeId: number, data: ReviewCreateRequest) => { + const store = await getStoreById(storeId) + if (!store) { + throw new BaseError(ErrorCode.STORE_NOT_FOUND) + } + + const user = await getUser(data.memberId) + if (!user) { + throw new BaseError(ErrorCode.USER_NOT_FOUND) + } + + if (data.score !== undefined && (data.score < 1 || data.score > 5)) { + throw new BaseError(ErrorCode.INVALID_SCORE) + } + + const reviewData = bodyToReview(data) + const reviewId = await addReview({ ...reviewData, storeId }) + + const review = await getReviewById(reviewId) + if (!review) { + throw new BaseError(ErrorCode.REVIEW_CREATE_FAILED) + } + + return responseFromReview(review) +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/stores/controllers/store.controller.ts" "b/\353\217\204\354\226\217/week8/src/modules/stores/controllers/storeController.ts" similarity index 93% rename from "\353\217\204\354\226\217/week8/src/modules/stores/controllers/store.controller.ts" rename to "\353\217\204\354\226\217/week8/src/modules/stores/controllers/storeController.ts" index a20c3f9..c702e6b 100644 --- "a/\353\217\204\354\226\217/week8/src/modules/stores/controllers/store.controller.ts" +++ "b/\353\217\204\354\226\217/week8/src/modules/stores/controllers/storeController.ts" @@ -1,10 +1,10 @@ import { Body, Controller, Get, Path, Post, Query, Route, Tags, Response as TsoaResponse, SuccessResponse } from 'tsoa' -import { StoreCreateRequest, StoreCreateResponse, ReviewListResponse } from '../dtos/store.dto.js' -import { createStore, listStoreReviews } from '../services/store.service.js' -import { createReview } from '../../reviews/services/review.service.js' -import { createMission, listStoreMissions } from '../../missions/services/mission.service.js' -import { ReviewCreateRequest, ReviewCreateResponse } from '../../reviews/dtos/review.dto.js' -import { MissionCreateRequest, MissionCreateResponse, StoreMissionListResponse } from '../../missions/dtos/mission.dto.js' +import { StoreCreateRequest, StoreCreateResponse, ReviewListResponse } from '../dtos/storeDto.js' +import { createStore, listStoreReviews } from '../services/storeService.js' +import { createReview } from '../../reviews/services/reviewService.js' +import { createMission, listStoreMissions } from '../../missions/services/missionService.js' +import { ReviewCreateRequest, ReviewCreateResponse } from '../../reviews/dtos/reviewDto.js' +import { MissionCreateRequest, MissionCreateResponse, StoreMissionListResponse } from '../../missions/dtos/missionDto.js' import { ApiResponse, successResponse } from '../../../utils/response.js' @Route('stores') diff --git "a/\353\217\204\354\226\217/week8/src/modules/stores/dtos/store.dto.ts" "b/\353\217\204\354\226\217/week8/src/modules/stores/dtos/store.dto.ts" deleted file mode 100644 index aa19060..0000000 --- "a/\353\217\204\354\226\217/week8/src/modules/stores/dtos/store.dto.ts" +++ /dev/null @@ -1,108 +0,0 @@ -// 가게 추가 요청 인터페이스 -export interface StoreCreateRequest { - /** - * 지역 ID - * @example 1 - */ - regionId: number - /** - * 음식 카테고리 ID - * @example 2 - */ - foodCategoryId: number - /** 가게 이름 */ - name: string - /** 가게 설명 */ - description?: string - /** - * 가게 주소 - * @example "서울시 강남구 테헤란로 123" - */ - address: string - /** - * 위도 - * @example 37.5665 - */ - lat?: number - /** - * 경도 - * @example 126.978 - */ - lng?: number -} - -// 가게 생성 응답 인터페이스 -export interface StoreCreateResponse { - /** 생성된 가게 ID */ - storeId: number - /** 가게 이름 */ - name: string - /** 가게 주소 */ - address: string - /** 지역 ID */ - regionId: number -} - -// 리뷰 아이템 인터페이스 -export interface ReviewItem { - /** 리뷰 ID */ - reviewId: number - /** 작성자 회원 ID */ - memberId: number - /** 가게 ID */ - storeId: number - /** 리뷰 내용 */ - content: string - /** 별점 (1~5) */ - score: number - /** 작성일시 */ - createdAt: Date -} - -// 리뷰 목록 응답 인터페이스 (커서 기반 페이지네이션) -export interface ReviewListResponse { - data: ReviewItem[] - pagination: { - /** 다음 페이지 커서 (마지막 항목의 ID, 없으면 null) */ - cursor: number | null - } -} - -// req.body → 내부 데이터로 변환 -export const bodyToStore = (body: StoreCreateRequest) => { - return { - regionId: body.regionId, - foodCategoryId: body.foodCategoryId, - name: body.name, - description: body.description ?? null, - address: body.address, - lat: body.lat ?? null, - lng: body.lng ?? null, - } -} - -// 리뷰 목록 → 응답 형태로 변환 (커서 기반 페이지네이션) -export const responseFromReviews = (reviews: any[]) => { - const last = reviews[reviews.length - 1] - return { - data: reviews, - pagination: { - cursor: last ? last.id : null, - }, - } -} - -// DB 조회 결과 → 응답 형태로 변환 -export const responseFromStore = (store: { - id: number - name: string - address: string - region_id: number -}) => { - return { - storeId: store.id, - name: store.name, - address: store.address, - regionId: store.region_id, - } -} diff --git "a/\353\217\204\354\226\217/week8/src/modules/stores/dtos/storeDto.ts" "b/\353\217\204\354\226\217/week8/src/modules/stores/dtos/storeDto.ts" new file mode 100644 index 0000000..0a5e8a7 --- /dev/null +++ "b/\353\217\204\354\226\217/week8/src/modules/stores/dtos/storeDto.ts" @@ -0,0 +1,66 @@ +export interface StoreCreateRequest { + /** 가게 이름 */ + name: string +} + +export interface StoreCreateResponse { + /** 생성된 가게 ID */ + storeId: number + /** 가게 이름 */ + name: string +} + +export interface ReviewItem { + /** 리뷰 ID */ + reviewId: number + /** 작성자 회원 ID */ + memberId: number + /** 가게 ID */ + storeId: number + /** 리뷰 내용 */ + content: string +} + +export interface ReviewListResponse { + data: ReviewItem[] + pagination: { + /** 다음 페이지 커서 */ + cursor: number | null + } +} + +export const bodyToStore = (body: StoreCreateRequest) => { + return { + name: body.name, + } +} + +export const responseFromReviews = (reviews: Array<{ + id: number + userId: number + storeId: number + content: string +}>): ReviewListResponse => { + const last = reviews[reviews.length - 1] + return { + data: reviews.map((review) => ({ + reviewId: review.id, + memberId: review.userId, + storeId: review.storeId, + content: review.content, + })), + pagination: { + cursor: last ? last.id : null, + }, + } +} + +export const responseFromStore = (store: { + id: number + name: string +}): StoreCreateResponse => { + return { + storeId: store.id, + name: store.name, + } +} diff --git "a/\353\217\204\354\226\217/week8/src/modules/stores/repositories/store.repository.ts" "b/\353\217\204\354\226\217/week8/src/modules/stores/repositories/storeRepository.ts" similarity index 71% rename from "\353\217\204\354\226\217/week8/src/modules/stores/repositories/store.repository.ts" rename to "\353\217\204\354\226\217/week8/src/modules/stores/repositories/storeRepository.ts" index 9a9ac1e..8e8631b 100644 --- "a/\353\217\204\354\226\217/week8/src/modules/stores/repositories/store.repository.ts" +++ "b/\353\217\204\354\226\217/week8/src/modules/stores/repositories/storeRepository.ts" @@ -1,23 +1,20 @@ -import { prisma } from '../../../db.config.js' +import { prisma } from '../../../dbConfig.js' -// 가게 추가 export const addStore = async (data: { name: string }): Promise => { const created = await prisma.store.create({ data }) return created.id } -// 가게 조회 export const getStoreById = async (storeId: number) => prisma.store.findFirst({ where: { id: storeId } }) -// 가게 리뷰 목록 조회 (커서 기반 페이지네이션) export const getAllStoreReviews = async (storeId: number, cursor: number) => prisma.userStoreReview.findMany({ select: { - id: true, + id: true, + userId: true, + storeId: true, content: true, - store: true, - user: true, }, where: { storeId, diff --git "a/\353\217\204\354\226\217/week8/src/modules/stores/services/store.service.ts" "b/\353\217\204\354\226\217/week8/src/modules/stores/services/storeService.ts" similarity index 63% rename from "\353\217\204\354\226\217/week8/src/modules/stores/services/store.service.ts" rename to "\353\217\204\354\226\217/week8/src/modules/stores/services/storeService.ts" index 91f2c48..e12fd45 100644 --- "a/\353\217\204\354\226\217/week8/src/modules/stores/services/store.service.ts" +++ "b/\353\217\204\354\226\217/week8/src/modules/stores/services/storeService.ts" @@ -1,9 +1,14 @@ -import { StoreCreateRequest, bodyToStore, responseFromStore, responseFromReviews } from '../dtos/store.dto.js' -import { addStore, getStoreById, getAllStoreReviews } from '../repositories/store.repository.js' +import { StoreCreateRequest, bodyToStore, responseFromStore, responseFromReviews } from '../dtos/storeDto.js' +import { addStore, getStoreById, getAllStoreReviews } from '../repositories/storeRepository.js' import { BaseError } from '../../../utils/errors.js' import { ErrorCode } from '../../../utils/errorCode.js' export const listStoreReviews = async (storeId: number, cursor: number) => { + const store = await getStoreById(storeId) + if (!store) { + throw new BaseError(ErrorCode.STORE_NOT_FOUND) + } + const reviews = await getAllStoreReviews(storeId, cursor) return responseFromReviews(reviews) } @@ -14,17 +19,8 @@ export const createStore = async (data: StoreCreateRequest) => { const store = await getStoreById(storeId) if (!store) { - throw new BaseError( - ErrorCode.STORE_CREATE_FAILED.message, - ErrorCode.STORE_CREATE_FAILED.status, - ErrorCode.STORE_CREATE_FAILED.code, - ) + throw new BaseError(ErrorCode.STORE_CREATE_FAILED) } - return responseFromStore(store as { - id: number - name: string - address: string - region_id: number - }) + return responseFromStore(store) } diff --git "a/\353\217\204\354\226\217/week8/src/utils/errorCode.ts" "b/\353\217\204\354\226\217/week8/src/utils/errorCode.ts" index 1a271dd..f2caa9f 100644 --- "a/\353\217\204\354\226\217/week8/src/utils/errorCode.ts" +++ "b/\353\217\204\354\226\217/week8/src/utils/errorCode.ts" @@ -1,26 +1,31 @@ export const ErrorCode = { - // 공통 + // Common INTERNAL_ERROR: { status: 500, code: 'COMMON500', message: '서버 내부 오류입니다.' }, INVALID_INPUT: { status: 400, code: 'COMMON400', message: '잘못된 입력값입니다.' }, - // 회원 + // Auth + AUTH_INVALID_CREDENTIALS: { status: 401, code: 'AUTH4001', message: '이메일 또는 비밀번호가 올바르지 않습니다.' }, + + // Member USER_NOT_FOUND: { status: 404, code: 'USER4001', message: '사용자를 찾을 수 없습니다.' }, DUPLICATE_EMAIL: { status: 409, code: 'USER4002', message: '이미 사용 중인 이메일입니다.' }, - MEMBER_REQUIRED_FIELD: { status: 400, code: 'USER4003', message: 'name과 nickname은 필수 입력값입니다.' }, + MEMBER_REQUIRED_FIELD: { status: 400, code: 'USER4003', message: '필수 입력값이 누락되었습니다.' }, - // 가게 + // Store STORE_NOT_FOUND: { status: 404, code: 'STORE4001', message: '존재하지 않는 가게입니다.' }, STORE_CREATE_FAILED: { status: 500, code: 'STORE5001', message: '가게 생성 후 조회에 실패했습니다.' }, - // 리뷰 + // Review INVALID_SCORE: { status: 400, code: 'REVIEW4001', message: '별점은 1~5 사이여야 합니다.' }, REVIEW_CREATE_FAILED: { status: 500, code: 'REVIEW5001', message: '리뷰 생성 후 조회에 실패했습니다.' }, - // 미션 + // Mission MISSION_NOT_FOUND: { status: 404, code: 'MISSION4001', message: '존재하지 않는 미션입니다.' }, MISSION_ALREADY_CHALLENGING: { status: 409, code: 'MISSION4002', message: '이미 도전 중인 미션입니다.' }, ONGOING_MISSION_NOT_FOUND: { status: 404, code: 'MISSION4003', message: '진행 중인 미션이 없습니다.' }, - MISSION_STATUS_REQUIRED: { status: 400, code: 'MISSION4004', message: 'status는 필수 입력값입니다. (CHALLENGING 또는 COMPLETE)' }, + MISSION_STATUS_REQUIRED: { status: 400, code: 'MISSION4004', message: 'status는 필수 입력값입니다.' }, MISSION_CREATE_FAILED: { status: 500, code: 'MISSION5001', message: '미션 생성 후 조회에 실패했습니다.' }, MISSION_CHALLENGE_FAILED: { status: 500, code: 'MISSION5002', message: '미션 도전 후 조회에 실패했습니다.' }, } as const + +export type ErrorCodeValue = (typeof ErrorCode)[keyof typeof ErrorCode] diff --git "a/\353\217\204\354\226\217/week8/src/utils/errors.ts" "b/\353\217\204\354\226\217/week8/src/utils/errors.ts" index c237d36..643ea5b 100644 --- "a/\353\217\204\354\226\217/week8/src/utils/errors.ts" +++ "b/\353\217\204\354\226\217/week8/src/utils/errors.ts" @@ -1,11 +1,13 @@ +import { ErrorCode, ErrorCodeValue } from './errorCode.js' + export class BaseError extends Error { status: number code: string - constructor(message: string, statusCode = 500, code = 'INTERNAL_ERROR') { - super(message) + constructor(errorCode: ErrorCodeValue = ErrorCode.INTERNAL_ERROR) { + super(errorCode.message) this.name = 'BaseError' - this.status = statusCode - this.code = code + this.status = errorCode.status + this.code = errorCode.code } } diff --git "a/\353\217\204\354\226\217/week8/src/utils/response.ts" "b/\353\217\204\354\226\217/week8/src/utils/response.ts" index c3d3b92..d0028b3 100644 --- "a/\353\217\204\354\226\217/week8/src/utils/response.ts" +++ "b/\353\217\204\354\226\217/week8/src/utils/response.ts" @@ -14,7 +14,9 @@ export const successResponse = (result: T): ApiResponse => ({ result, }) -// 기존 BaseResponse (하위 호환 유지) +/** + * @deprecated successResponse를 사용하세요. + */ export const BaseResponse = (result: T, message = '성공입니다.', code = 'COMMON200') => { return { isSuccess: true, diff --git "a/\353\217\204\354\226\217/week8/tsoa.json" "b/\353\217\204\354\226\217/week8/tsoa.json" index ca6c134..f57729f 100644 --- "a/\353\217\204\354\226\217/week8/tsoa.json" +++ "b/\353\217\204\354\226\217/week8/tsoa.json" @@ -1,7 +1,7 @@ { "entryFile": "src/index.ts", "noImplicitAdditionalProperties": "throw-on-extras", - "controllerPathGlobs": ["src/**/*.controller.ts"], + "controllerPathGlobs": ["src/**/*Controller.ts"], "spec": { "outputDirectory": "dist", "specVersion": 3,