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/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; 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 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"] +} 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..454f20b --- /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 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/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..8786670 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/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/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..65a6e13 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/missions/repositories/mission.repository.ts" @@ -0,0 +1,100 @@ +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, + status: string, +): Promise => { + const conn = await pool.getConnection() + try { + const [result] = await conn.query( + `INSERT INTO member_mission (member_id, mission_id, status) + VALUES (?, ?, ?)`, + [memberId, missionId, status], + ) + 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..65869d4 --- /dev/null +++ "b/\353\217\204\354\226\217/week5/src/modules/missions/services/mission.service.ts" @@ -0,0 +1,84 @@ +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) + } + + // 필수값 검증 + 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 { + 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"] +} 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"] +}