From 0163d06cf127834e655080757cd74c66c4df99b2 Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Mon, 24 Nov 2025 20:13:47 +0900 Subject: [PATCH 01/18] =?UTF-8?q?=F0=9F=94=84=20chore:=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=204=20=EC=B4=88=EA=B8=B0=20=EC=84=B8=ED=8C=85=20?= =?UTF-8?q?=EB=B0=8F=20=EC=9D=B8=EC=A6=9D=20=EA=B4=80=EB=A0=A8=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 17 +- .prettierrc | 3 +- README.md | 118 -- package-lock.json | 336 +++- package.json | 40 +- sprint-mission-2 | 1 - sprint-mission-3/.prettierrc | 8 + sprint-mission-3/README.md | 118 ++ {class => sprint-mission-3/class}/Article.js | 0 .../class}/ElectronicProduct.js | 0 {class => sprint-mission-3/class}/Product.js | 0 {main => sprint-mission-3/main}/main.js | 0 sprint-mission-3/package-lock.json | 1489 +++++++++++++++++ sprint-mission-3/package.json | 33 + .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../migration.sql | 0 .../20251031011131_add_authro/migration.sql | 0 .../migrations/20251031072401_/migration.sql | 0 .../migrations/20251103022615_/migration.sql | 0 .../prisma}/migrations/migration_lock.toml | 0 {prisma => sprint-mission-3/prisma}/mock.js | 0 .../prisma}/schema.prisma | 0 {prisma => sprint-mission-3/prisma}/seed.js | 0 .../service}/ArticleService.js | 0 .../service}/ProductService.js | 0 sprint-mission-3/sprint-mission-2/.gitignore | 1 + sprint-mission-3/sprint-mission-2/.prettierrc | 6 + sprint-mission-3/sprint-mission-2/README.md | 101 ++ .../sprint-mission-2/class/Article.js | 14 + .../class/ElectronicProduct.js | 16 + .../sprint-mission-2/class/Product.js | 15 + .../sprint-mission-2/main/main.js | 112 ++ .../sprint-mission-2/package-lock.json | 291 ++++ .../sprint-mission-2/package.json | 5 + .../service/ArticleService.js | 124 ++ .../service/ProductService.js | 106 ++ {src => sprint-mission-3/src}/app.js | 0 .../src}/controllers/articleController.js | 0 .../src}/controllers/commentController.js | 0 .../src}/controllers/productController.js | 0 .../src}/controllers/userController.js | 0 .../src}/lib/prismaClient.js | 0 .../src}/middlewares/errorHandler.js | 0 .../src}/middlewares/uploadImages.js | 0 .../src}/middlewares/validator.js | 0 .../src}/routers/articleRouter.js | 0 .../src}/routers/commentRouter.js | 0 .../src}/routers/productRouter.js | 0 .../src}/routers/uploadRouter.js | 0 .../src}/routers/userRouter.js | 0 .../uploads}/kuppo.png_1762155800599.png | 0 .../uploads}/test-image.png_1762155697729.png | 0 .../uploads}/test-image.png_1762155750185.png | 0 56 files changed, 2751 insertions(+), 203 deletions(-) delete mode 160000 sprint-mission-2 create mode 100644 sprint-mission-3/.prettierrc create mode 100644 sprint-mission-3/README.md rename {class => sprint-mission-3/class}/Article.js (100%) rename {class => sprint-mission-3/class}/ElectronicProduct.js (100%) rename {class => sprint-mission-3/class}/Product.js (100%) rename {main => sprint-mission-3/main}/main.js (100%) create mode 100644 sprint-mission-3/package-lock.json create mode 100644 sprint-mission-3/package.json rename {prisma => sprint-mission-3/prisma}/migrations/20251028093410_add_users_products_articles/migration.sql (100%) rename {prisma => sprint-mission-3/prisma}/migrations/20251028110145_delete_in_tags/migration.sql (100%) rename {prisma => sprint-mission-3/prisma}/migrations/20251029025527_refactor_model_name/migration.sql (100%) rename {prisma => sprint-mission-3/prisma}/migrations/20251030012316_add_comment_model/migration.sql (100%) rename {prisma => sprint-mission-3/prisma}/migrations/20251030080553_add_index_di_tags/migration.sql (100%) rename {prisma => sprint-mission-3/prisma}/migrations/20251031011131_add_authro/migration.sql (100%) rename {prisma => sprint-mission-3/prisma}/migrations/20251031072401_/migration.sql (100%) rename {prisma => sprint-mission-3/prisma}/migrations/20251103022615_/migration.sql (100%) rename {prisma => sprint-mission-3/prisma}/migrations/migration_lock.toml (100%) rename {prisma => sprint-mission-3/prisma}/mock.js (100%) rename {prisma => sprint-mission-3/prisma}/schema.prisma (100%) rename {prisma => sprint-mission-3/prisma}/seed.js (100%) rename {service => sprint-mission-3/service}/ArticleService.js (100%) rename {service => sprint-mission-3/service}/ProductService.js (100%) create mode 100644 sprint-mission-3/sprint-mission-2/.gitignore create mode 100644 sprint-mission-3/sprint-mission-2/.prettierrc create mode 100644 sprint-mission-3/sprint-mission-2/README.md create mode 100644 sprint-mission-3/sprint-mission-2/class/Article.js create mode 100644 sprint-mission-3/sprint-mission-2/class/ElectronicProduct.js create mode 100644 sprint-mission-3/sprint-mission-2/class/Product.js create mode 100644 sprint-mission-3/sprint-mission-2/main/main.js create mode 100644 sprint-mission-3/sprint-mission-2/package-lock.json create mode 100644 sprint-mission-3/sprint-mission-2/package.json create mode 100644 sprint-mission-3/sprint-mission-2/service/ArticleService.js create mode 100644 sprint-mission-3/sprint-mission-2/service/ProductService.js rename {src => sprint-mission-3/src}/app.js (100%) rename {src => sprint-mission-3/src}/controllers/articleController.js (100%) rename {src => sprint-mission-3/src}/controllers/commentController.js (100%) rename {src => sprint-mission-3/src}/controllers/productController.js (100%) rename {src => sprint-mission-3/src}/controllers/userController.js (100%) rename {src => sprint-mission-3/src}/lib/prismaClient.js (100%) rename {src => sprint-mission-3/src}/middlewares/errorHandler.js (100%) rename {src => sprint-mission-3/src}/middlewares/uploadImages.js (100%) rename {src => sprint-mission-3/src}/middlewares/validator.js (100%) rename {src => sprint-mission-3/src}/routers/articleRouter.js (100%) rename {src => sprint-mission-3/src}/routers/commentRouter.js (100%) rename {src => sprint-mission-3/src}/routers/productRouter.js (100%) rename {src => sprint-mission-3/src}/routers/uploadRouter.js (100%) rename {src => sprint-mission-3/src}/routers/userRouter.js (100%) rename {uploads => sprint-mission-3/uploads}/kuppo.png_1762155800599.png (100%) rename {uploads => sprint-mission-3/uploads}/test-image.png_1762155697729.png (100%) rename {uploads => sprint-mission-3/uploads}/test-image.png_1762155750185.png (100%) diff --git a/.gitignore b/.gitignore index 1c03a83c..9f91dfdf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,16 @@ -node_modules/ -__http__/ +# 1. 환경 변수 .env -/generated/prisma +.env.* + +# 팀원 공유용 예시 파일은 깃에 올려줘 (느낌표는 '이건 제외하고'라는 뜻) +!.env.example + +# 2. 로컬 설정 및 모듈 +__http__/ +node_modules/ + +# 3. 업로드 파일 관리 +# image 폴더가 업로드용이라면 유지, 아니라면 지워도 됨 image/ +public/* +!public/.gitkeep \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 351ac92b..503a5da4 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,5 +4,6 @@ "semi": true, "printWidth": 100, "endOfLine": "auto", - "arrowParens": "always" + "arrowParens": "always", + "tabWidth": 2 } diff --git a/README.md b/README.md index cc9ce75c..e69de29b 100644 --- a/README.md +++ b/README.md @@ -1,118 +0,0 @@ -# 기본 요구 사항 - -## 공통 - -- [x] PostgreSQL를 이용해 주세요. -- [x] 데이터 모델 간의 관계를 고려하여 onDelete를 설정해 주세요. -- [x] 데이터베이스 시딩 코드를 작성해 주세요. -- [x] 각 API에 적절한 에러 처리를 해 주세요. -- [x] 각 API 응답에 적절한 상태 코드를 리턴하도록 해 주세요. - -## 스키마 - -### 중고마켓 - -- [x] Product 스키마를 작성해 주세요. - - - [x] id, name, description, price, tags, createdAt, updatedAt필드를 가집니다. - 필요한 필드가 있다면 자유롭게 추가해 주세요. - -- [x] 상품 등록 API를 만들어 주세요. == POST == - - - [x] name, description, price, tags를 입력하여 상품을 등록합니다. - -- [x] 상품 목록 조회 API를 만들어 주세요. == GET LIST == - - - [x] id, name, price, createdAt를 조회합니다. - - [x] offset 방식의 페이지네이션 기능을 포함해 주세요. - - [x] 최신순(recent)으로 정렬할 수 있습니다. - - [x] name, description에 포함된 단어로 검색할 수 있습니다. - -- [x] 상품 상세 조회 API를 만들어 주세요. == GET ID == - - - [x] id, name, description, price, tags, createdAt를 조회합니다. - -- [x] 상품 수정 API를 만들어 주세요. == PATCH ID == - - - [x] PATCH 메서드를 사용해 주세요. - -- [x] 상품 삭제 API를 만들어 주세요. == DELETE ID == - -- [] 각 API에 적절한 에러 처리를 해 주세요. - -- [] 각 API 응답에 적절한 상태 코드를 리턴하도록 해 주세요. - -### 자유게시판 - -- [x] Article 스키마를 작성해 주세요. - - - [x] id, title, content, createdAt, updatedAt 필드를 가집니다. - -- [x] 게시글 등록 API를 만들어 주세요. == POST == - - - [x] title, content를 입력해 게시글을 등록합니다. - -- [x] 게시글 목록 조회 API를 만들어 주세요. LIST - - - [x] id, title, content, createdAt를 조회합니다. - - [x] offset 방식의 페이지네이션 기능을 포함해 주세요. - - [x] 최신순(recent)으로 정렬할 수 있습니다. - - [x] title, content에 포함된 단어로 검색할 수 있습니다. - -- [x] 게시글 상세 조회 API를 만들어 주세요. == GET ID == - - - [x] id, title, content, createdAt를 조회합니다. - -- [x] 게시글 수정 API를 만들어 주세요. == PATCH ID == - -- [x] 게시글 삭제 API를 만들어 주세요. == DELETE ID == - -### 댓글 - -- [x] 댓글 등록 API를 만들어 주세요. == POST == - - - [x] content를 입력하여 댓글을 등록합니다. - - [x] 중고마켓, 자유게시판 댓글 등록 API를 따로 만들어 주세요. - -- [x] 댓글 목록 조회 API를 만들어 주세요. == GET LIST == - - - [x] id, content, createdAt 를 조회합니다. - - [x] cursor 방식의 페이지네이션 기능을 포함해 주세요. - - [x] 중고마켓, 자유게시판 댓글 목록 조회 API를 따로 만들어 주세요. - -- [x] 댓글 수정 API를 만들어 주세요. == PATCH == - - - [x] PATCH 메서드를 사용해 주세요. - -- [x] 댓글 삭제 API를 만들어 주세요. == DELETE == - -## 미들웨어 - -### 유효성 검증 - -- [x] 상품 등록 시 필요한 필드(이름, 설명, 가격 등)의 유효성을 검증하는 미들웨어를 구현합니다. - -- [x] 게시물 등록 시 필요한 필드(제목, 내용 등)의 유효성 검증하는 미들웨어를 구현합니다. - -### 이미지 업로드 - -- [x] multer 미들웨어를 사용하여 이미지 업로드 API를 구현해주세요. - - [] 업로드된 이미지는 서버에 저장하고, 해당 이미지의 경로를 response 객체에 포함해 반환합니다. - -### 에러 처리 - -- [x] 모든 예외 상황을 처리할 수 있는 에러 핸들러 미들웨어를 구현합니다. -- [x] 서버 오류(500), 사용자 입력 오류(400 시리즈), 리소스 찾을 수 없음(404) 등 상황에 맞는 상태값을 반환합니다. - -## 라우터 - -### 라우트 중복 제거 - -- [x] 중복되는 라우트 경로(예: /users에 대한 get 및 post 요청)를 app.route()로 통합해 중복을 제거합니다. -- [x] express.Router()를 활용하여 중고마켓/자유게시판 관련 라우트를 별도의 모듈로 구분합니다. - -# 배포 - -- [x] .env 파일에 환경 변수를 설정해 주세요. -- [x] CORS를 설정해 주세요. -- [] render.com으로 배포해 주세요. diff --git a/package-lock.json b/package-lock.json index 913282a3..bbaca887 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,27 +1,31 @@ { - "name": "sprint-mission-3", + "name": "sprint-mission-4", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "sprint-mission-3", + "name": "sprint-mission-4", "version": "1.0.0", "license": "ISC", "dependencies": { - "@prisma/client": "^5.4.2", + "@prisma/client": "^5.16.2", + "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", - "dotenv": "^16.3.1", - "express": "^4.18.2", + "dotenv": "^16.4.5", + "express": "^4.19.2", "express-async-handler": "^1.2.0", "is-email": "^1.0.2", - "is-uuid": "^1.0.2", - "multer": "^2.0.2", - "prisma": "^5.4.2", - "superstruct": "^1.0.3" + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "superstruct": "^2.0.2", + "uuid": "^11.0.5" }, "devDependencies": { - "nodemon": "^3.1.10" + "nodemon": "^3.1.10", + "prettier": "^3.3.2", + "prisma": "^5.16.2" } }, "node_modules/@prisma/client": { @@ -46,12 +50,14 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -65,12 +71,14 @@ "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "5.22.0", @@ -82,6 +90,7 @@ "version": "5.22.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, "license": "Apache-2.0", "dependencies": { "@prisma/debug": "5.22.0" @@ -133,6 +142,20 @@ "dev": true, "license": "MIT" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -194,6 +217,12 @@ "node": ">=8" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -282,17 +311,17 @@ "license": "MIT" }, "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "engines": [ - "node >= 6.0" + "node >= 0.8" ], "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^3.0.2", + "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, @@ -326,12 +355,40 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "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.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -399,6 +456,15 @@ "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", @@ -560,20 +626,6 @@ "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==", - "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", @@ -781,10 +833,101 @@ "node": ">=0.12.0" } }, - "node_modules/is-uuid": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-uuid/-/is-uuid-1.0.2.tgz", - "integrity": "sha512-tCByphFcJgf2qmiMo5hMCgNAquNSagOetVetDvBXswGkNfoyEMvGH1yDlF8cbZbKnbVBr4Y5/rlpMz9umxyBkQ==", + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, "node_modules/math-intrinsics": { @@ -897,21 +1040,22 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", - "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", "license": "MIT", "dependencies": { "append-field": "^1.0.0", - "busboy": "^1.6.0", - "concat-stream": "^2.0.0", - "mkdirp": "^0.5.6", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", "object-assign": "^4.1.1", - "type-is": "^1.6.18", - "xtend": "^4.0.2" + "type-is": "^1.6.4", + "xtend": "^4.0.0" }, "engines": { - "node": ">= 10.16.0" + "node": ">= 6.0.0" } }, "node_modules/negotiator": { @@ -923,10 +1067,30 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/nodemon": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", - "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", "dev": true, "license": "MIT", "dependencies": { @@ -1048,10 +1212,27 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prisma": { "version": "5.22.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -1067,6 +1248,12 @@ "fsevents": "2.3.3" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1127,19 +1314,26 @@ } }, "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -1183,7 +1377,6 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1355,18 +1548,24 @@ } }, "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "license": "MIT", "dependencies": { - "safe-buffer": "~5.2.0" + "safe-buffer": "~5.1.0" } }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/superstruct": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz", - "integrity": "sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -1467,6 +1666,19 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 16755e82..6afb99e5 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,35 @@ { - "name": "sprint-mission-3", + "name": "sprint-mission-4", "version": "1.0.0", - "description": "", - "main": "src/app.js", "type": "module", + "main": "src/app.js", + "description": "", "keywords": [], "author": "", "license": "ISC", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "nodemon src/app.js", + "start": "node src/app.js" + }, + "devDependencies": { + "nodemon": "^3.1.10", + "prettier": "^3.3.2", + "prisma": "^5.16.2" + }, "dependencies": { - "@prisma/client": "^5.4.2", + "@prisma/client": "^5.16.2", + "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", - "dotenv": "^16.3.1", - "express": "^4.18.2", + "dotenv": "^16.4.5", + "express": "^4.19.2", "express-async-handler": "^1.2.0", "is-email": "^1.0.2", - "is-uuid": "^1.0.2", - "multer": "^2.0.2", - "prisma": "^5.4.2", - "superstruct": "^1.0.3" - }, - "devDependencies": { - "nodemon": "^3.1.10" - }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "dev": "nodemon src/app.js", - "start": "node app.js" + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1", + "superstruct": "^2.0.2", + "uuid": "^11.0.5" }, "prisma": { "seed": "node prisma/seed.js" diff --git a/sprint-mission-2 b/sprint-mission-2 deleted file mode 160000 index 005ef323..00000000 --- a/sprint-mission-2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 005ef3233a3b300b0f55b63a7a4bc085986bda76 diff --git a/sprint-mission-3/.prettierrc b/sprint-mission-3/.prettierrc new file mode 100644 index 00000000..351ac92b --- /dev/null +++ b/sprint-mission-3/.prettierrc @@ -0,0 +1,8 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "semi": true, + "printWidth": 100, + "endOfLine": "auto", + "arrowParens": "always" +} diff --git a/sprint-mission-3/README.md b/sprint-mission-3/README.md new file mode 100644 index 00000000..cc9ce75c --- /dev/null +++ b/sprint-mission-3/README.md @@ -0,0 +1,118 @@ +# 기본 요구 사항 + +## 공통 + +- [x] PostgreSQL를 이용해 주세요. +- [x] 데이터 모델 간의 관계를 고려하여 onDelete를 설정해 주세요. +- [x] 데이터베이스 시딩 코드를 작성해 주세요. +- [x] 각 API에 적절한 에러 처리를 해 주세요. +- [x] 각 API 응답에 적절한 상태 코드를 리턴하도록 해 주세요. + +## 스키마 + +### 중고마켓 + +- [x] Product 스키마를 작성해 주세요. + + - [x] id, name, description, price, tags, createdAt, updatedAt필드를 가집니다. + 필요한 필드가 있다면 자유롭게 추가해 주세요. + +- [x] 상품 등록 API를 만들어 주세요. == POST == + + - [x] name, description, price, tags를 입력하여 상품을 등록합니다. + +- [x] 상품 목록 조회 API를 만들어 주세요. == GET LIST == + + - [x] id, name, price, createdAt를 조회합니다. + - [x] offset 방식의 페이지네이션 기능을 포함해 주세요. + - [x] 최신순(recent)으로 정렬할 수 있습니다. + - [x] name, description에 포함된 단어로 검색할 수 있습니다. + +- [x] 상품 상세 조회 API를 만들어 주세요. == GET ID == + + - [x] id, name, description, price, tags, createdAt를 조회합니다. + +- [x] 상품 수정 API를 만들어 주세요. == PATCH ID == + + - [x] PATCH 메서드를 사용해 주세요. + +- [x] 상품 삭제 API를 만들어 주세요. == DELETE ID == + +- [] 각 API에 적절한 에러 처리를 해 주세요. + +- [] 각 API 응답에 적절한 상태 코드를 리턴하도록 해 주세요. + +### 자유게시판 + +- [x] Article 스키마를 작성해 주세요. + + - [x] id, title, content, createdAt, updatedAt 필드를 가집니다. + +- [x] 게시글 등록 API를 만들어 주세요. == POST == + + - [x] title, content를 입력해 게시글을 등록합니다. + +- [x] 게시글 목록 조회 API를 만들어 주세요. LIST + + - [x] id, title, content, createdAt를 조회합니다. + - [x] offset 방식의 페이지네이션 기능을 포함해 주세요. + - [x] 최신순(recent)으로 정렬할 수 있습니다. + - [x] title, content에 포함된 단어로 검색할 수 있습니다. + +- [x] 게시글 상세 조회 API를 만들어 주세요. == GET ID == + + - [x] id, title, content, createdAt를 조회합니다. + +- [x] 게시글 수정 API를 만들어 주세요. == PATCH ID == + +- [x] 게시글 삭제 API를 만들어 주세요. == DELETE ID == + +### 댓글 + +- [x] 댓글 등록 API를 만들어 주세요. == POST == + + - [x] content를 입력하여 댓글을 등록합니다. + - [x] 중고마켓, 자유게시판 댓글 등록 API를 따로 만들어 주세요. + +- [x] 댓글 목록 조회 API를 만들어 주세요. == GET LIST == + + - [x] id, content, createdAt 를 조회합니다. + - [x] cursor 방식의 페이지네이션 기능을 포함해 주세요. + - [x] 중고마켓, 자유게시판 댓글 목록 조회 API를 따로 만들어 주세요. + +- [x] 댓글 수정 API를 만들어 주세요. == PATCH == + + - [x] PATCH 메서드를 사용해 주세요. + +- [x] 댓글 삭제 API를 만들어 주세요. == DELETE == + +## 미들웨어 + +### 유효성 검증 + +- [x] 상품 등록 시 필요한 필드(이름, 설명, 가격 등)의 유효성을 검증하는 미들웨어를 구현합니다. + +- [x] 게시물 등록 시 필요한 필드(제목, 내용 등)의 유효성 검증하는 미들웨어를 구현합니다. + +### 이미지 업로드 + +- [x] multer 미들웨어를 사용하여 이미지 업로드 API를 구현해주세요. + - [] 업로드된 이미지는 서버에 저장하고, 해당 이미지의 경로를 response 객체에 포함해 반환합니다. + +### 에러 처리 + +- [x] 모든 예외 상황을 처리할 수 있는 에러 핸들러 미들웨어를 구현합니다. +- [x] 서버 오류(500), 사용자 입력 오류(400 시리즈), 리소스 찾을 수 없음(404) 등 상황에 맞는 상태값을 반환합니다. + +## 라우터 + +### 라우트 중복 제거 + +- [x] 중복되는 라우트 경로(예: /users에 대한 get 및 post 요청)를 app.route()로 통합해 중복을 제거합니다. +- [x] express.Router()를 활용하여 중고마켓/자유게시판 관련 라우트를 별도의 모듈로 구분합니다. + +# 배포 + +- [x] .env 파일에 환경 변수를 설정해 주세요. +- [x] CORS를 설정해 주세요. +- [] render.com으로 배포해 주세요. diff --git a/class/Article.js b/sprint-mission-3/class/Article.js similarity index 100% rename from class/Article.js rename to sprint-mission-3/class/Article.js diff --git a/class/ElectronicProduct.js b/sprint-mission-3/class/ElectronicProduct.js similarity index 100% rename from class/ElectronicProduct.js rename to sprint-mission-3/class/ElectronicProduct.js diff --git a/class/Product.js b/sprint-mission-3/class/Product.js similarity index 100% rename from class/Product.js rename to sprint-mission-3/class/Product.js diff --git a/main/main.js b/sprint-mission-3/main/main.js similarity index 100% rename from main/main.js rename to sprint-mission-3/main/main.js diff --git a/sprint-mission-3/package-lock.json b/sprint-mission-3/package-lock.json new file mode 100644 index 00000000..913282a3 --- /dev/null +++ b/sprint-mission-3/package-lock.json @@ -0,0 +1,1489 @@ +{ + "name": "sprint-mission-3", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sprint-mission-3", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.4.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-async-handler": "^1.2.0", + "is-email": "^1.0.2", + "is-uuid": "^1.0.2", + "multer": "^2.0.2", + "prisma": "^5.4.2", + "superstruct": "^1.0.3" + }, + "devDependencies": { + "nodemon": "^3.1.10" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "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/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": 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": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/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/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-async-handler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz", + "integrity": "sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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/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.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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-email": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-email/-/is-email-1.0.2.tgz", + "integrity": "sha512-UojUgD2EhDTBQ2SGKwrK9edce5phRzgLsP+V5+Uu2Swi+uvjVXgH3zduM3HhT9iaC/9Kq19/TYUbP0jPoi6ioA==", + "license": "SEE LICENSE IN LICENSE" + }, + "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-uuid": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-uuid/-/is-uuid-1.0.2.tgz", + "integrity": "sha512-tCByphFcJgf2qmiMo5hMCgNAquNSagOetVetDvBXswGkNfoyEMvGH1yDlF8cbZbKnbVBr4Y5/rlpMz9umxyBkQ==", + "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": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/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.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/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.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/superstruct": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-1.0.4.tgz", + "integrity": "sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/sprint-mission-3/package.json b/sprint-mission-3/package.json new file mode 100644 index 00000000..16755e82 --- /dev/null +++ b/sprint-mission-3/package.json @@ -0,0 +1,33 @@ +{ + "name": "sprint-mission-3", + "version": "1.0.0", + "description": "", + "main": "src/app.js", + "type": "module", + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.4.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-async-handler": "^1.2.0", + "is-email": "^1.0.2", + "is-uuid": "^1.0.2", + "multer": "^2.0.2", + "prisma": "^5.4.2", + "superstruct": "^1.0.3" + }, + "devDependencies": { + "nodemon": "^3.1.10" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "nodemon src/app.js", + "start": "node app.js" + }, + "prisma": { + "seed": "node prisma/seed.js" + } +} diff --git a/prisma/migrations/20251028093410_add_users_products_articles/migration.sql b/sprint-mission-3/prisma/migrations/20251028093410_add_users_products_articles/migration.sql similarity index 100% rename from prisma/migrations/20251028093410_add_users_products_articles/migration.sql rename to sprint-mission-3/prisma/migrations/20251028093410_add_users_products_articles/migration.sql diff --git a/prisma/migrations/20251028110145_delete_in_tags/migration.sql b/sprint-mission-3/prisma/migrations/20251028110145_delete_in_tags/migration.sql similarity index 100% rename from prisma/migrations/20251028110145_delete_in_tags/migration.sql rename to sprint-mission-3/prisma/migrations/20251028110145_delete_in_tags/migration.sql diff --git a/prisma/migrations/20251029025527_refactor_model_name/migration.sql b/sprint-mission-3/prisma/migrations/20251029025527_refactor_model_name/migration.sql similarity index 100% rename from prisma/migrations/20251029025527_refactor_model_name/migration.sql rename to sprint-mission-3/prisma/migrations/20251029025527_refactor_model_name/migration.sql diff --git a/prisma/migrations/20251030012316_add_comment_model/migration.sql b/sprint-mission-3/prisma/migrations/20251030012316_add_comment_model/migration.sql similarity index 100% rename from prisma/migrations/20251030012316_add_comment_model/migration.sql rename to sprint-mission-3/prisma/migrations/20251030012316_add_comment_model/migration.sql diff --git a/prisma/migrations/20251030080553_add_index_di_tags/migration.sql b/sprint-mission-3/prisma/migrations/20251030080553_add_index_di_tags/migration.sql similarity index 100% rename from prisma/migrations/20251030080553_add_index_di_tags/migration.sql rename to sprint-mission-3/prisma/migrations/20251030080553_add_index_di_tags/migration.sql diff --git a/prisma/migrations/20251031011131_add_authro/migration.sql b/sprint-mission-3/prisma/migrations/20251031011131_add_authro/migration.sql similarity index 100% rename from prisma/migrations/20251031011131_add_authro/migration.sql rename to sprint-mission-3/prisma/migrations/20251031011131_add_authro/migration.sql diff --git a/prisma/migrations/20251031072401_/migration.sql b/sprint-mission-3/prisma/migrations/20251031072401_/migration.sql similarity index 100% rename from prisma/migrations/20251031072401_/migration.sql rename to sprint-mission-3/prisma/migrations/20251031072401_/migration.sql diff --git a/prisma/migrations/20251103022615_/migration.sql b/sprint-mission-3/prisma/migrations/20251103022615_/migration.sql similarity index 100% rename from prisma/migrations/20251103022615_/migration.sql rename to sprint-mission-3/prisma/migrations/20251103022615_/migration.sql diff --git a/prisma/migrations/migration_lock.toml b/sprint-mission-3/prisma/migrations/migration_lock.toml similarity index 100% rename from prisma/migrations/migration_lock.toml rename to sprint-mission-3/prisma/migrations/migration_lock.toml diff --git a/prisma/mock.js b/sprint-mission-3/prisma/mock.js similarity index 100% rename from prisma/mock.js rename to sprint-mission-3/prisma/mock.js diff --git a/prisma/schema.prisma b/sprint-mission-3/prisma/schema.prisma similarity index 100% rename from prisma/schema.prisma rename to sprint-mission-3/prisma/schema.prisma diff --git a/prisma/seed.js b/sprint-mission-3/prisma/seed.js similarity index 100% rename from prisma/seed.js rename to sprint-mission-3/prisma/seed.js diff --git a/service/ArticleService.js b/sprint-mission-3/service/ArticleService.js similarity index 100% rename from service/ArticleService.js rename to sprint-mission-3/service/ArticleService.js diff --git a/service/ProductService.js b/sprint-mission-3/service/ProductService.js similarity index 100% rename from service/ProductService.js rename to sprint-mission-3/service/ProductService.js diff --git a/sprint-mission-3/sprint-mission-2/.gitignore b/sprint-mission-3/sprint-mission-2/.gitignore new file mode 100644 index 00000000..b512c09d --- /dev/null +++ b/sprint-mission-3/sprint-mission-2/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/sprint-mission-3/sprint-mission-2/.prettierrc b/sprint-mission-3/sprint-mission-2/.prettierrc new file mode 100644 index 00000000..92f97e75 --- /dev/null +++ b/sprint-mission-3/sprint-mission-2/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/sprint-mission-3/sprint-mission-2/README.md b/sprint-mission-3/sprint-mission-2/README.md new file mode 100644 index 00000000..e73bfd2e --- /dev/null +++ b/sprint-mission-3/sprint-mission-2/README.md @@ -0,0 +1,101 @@ +## 요구사항 + +### 기본 + +#### 파일 만들기 ( 1 ) + +- [x] main.js 만들기 +- [x] Product.js 만들기 +- [x] ElectronicProduct.js 만들기 +- [x] Article.js 만들기

+ +**Product 클래스를 만들어 주세요.** + +- [x] Product 클래스는 name(상품명) description(상품 설명), price(판매 가격), tags(해시태그 배열), images(이미지 배열), favoriteCount(찜하기 수)프로퍼티를 가집니다. +- [x] Product 클래스는 favorite 메소드를 가집니다. favorite 메소드가 호출될 경우 찜하기 수가 1 증가합니다.

+ +**ElectronicProduct 클래스를 만들어 주세요.** + +- [x] ElectronicProduct 클래스는 Product를 상속하며, 추가로 manufacturer(제조사) 프로퍼티를 가집니다.

+ +**Article 클래스를 만들어 주세요.** + +- [x] Article 클래스는 title(제목), content(내용), writer(작성자), likeCount(좋아요 수) 프로퍼티를 가집니다. +- [x] Article 클래스는 like 메소드를 가집니다. like 메소드가 호출될 경우 좋아요 수가 1 증가합니다.

+ +- [x] 각 클래스 마다 constructor를 작성해 주세요. +- [x] 추상화/캡슐화/상속/다형성을 고려하여 코드를 작성해 주세요.

+ +--- + +#### 파일 만들기 ( 2 ) + +- [x] ArticleService.js 만들기 +- [x] ProductService.js 만들기

+ +**Article 요청 함수 구현하기**
+ +- [x] https://panda-market-api-crud.vercel.app/docs 의 Article API를 이용하여 아래 함수들을 구현해 주세요.

+ - [x] getArticleList() : GET 메소드를 사용해 주세요. + - [x] page, pageSize, keyword 쿼리 파라미터를 이용해 주세요.

+ - [x] getArticle() : GET 메소드를 사용해 주세요. + - [x] createArticle() : POST 메소드를 사용해 주세요. + - [x] request body에 title, content, image 를 포함해 주세요.

+ - [x] patchArticle() : PATCH 메소드를 사용해 주세요. + - [x] deleteArticle() : DELETE 메소드를 사용해 주세요.

+- [x] fetch 혹은 axios를 이용해 주세요. + - [x] 응답의 상태 코드가 2XX가 아닐 경우, 에러 메시지를 콘솔에 출력해 주세요.

+- [x] .then() 메소드를 이용하여 비동기 처리를 해주세요. +- [x] .catch() 를 이용하여 오류 처리를 해주세요. + +--- + +### 심화 + +- [x] Article 클래스에 createdAt(생성일자) 프로퍼티를 만들어 주세요. +- [x] 새로운 객체가 생성되어 constructor가 호출될 시 createdAt에 현재 시간을 저장합니다.

+ +--- + +**Product 요청 함수 구현하기**

+ +- [x] https://panda-market-api-crud.vercel.app/docs 의 Product API를 이용하여 아래 함수들을 구현해 주세요.

+ - [x] getProductList() : GET 메소드를 사용해 주세요. + - [x] page, pageSize, keyword 쿼리 파라미터를 이용해 주세요.

+ - [x] getProduct() : GET 메소드를 사용해 주세요. + - [x] createProduct() : POST 메소드를 사용해 주세요. + - [x] request body에 name, description, price, tags, images 를 포함해 주세요.

+ - [x] patchProduct() : PATCH 메소드를 사용해 주세요. + - [x] deleteProduct() : DELETE 메소드를 사용해 주세요. +- [x] async/await 을 이용하여 비동기 처리를 해주세요. +- [x] try/catch 를 이용하여 오류 처리를 해주세요. +- [x] getProductList()를 통해서 받아온 상품 리스트를 각각 인스턴스로 만들어 products 배열에 저장해 주세요.

+ - [x] 해시태그에 "전자제품"이 포함되어 있는 상품들은 Product 클래스 대신 ElectronicProduct 클래스를 사용해 인스턴스를 생성해 주세요. + - [x] 나머지 상품들은 모두 Product 클래스를 사용해 인스턴스를 생성해 주세요.

+ +--- + +- [x] 구현한 함수들을 아래와 같이 파일을 분리해 주세요. + - [x] export를 활용해 주세요. + - [x] ProductService.js 파일 Product API 관련 함수들을 작성해 주세요. + - [x] ArticleService.js 파일에 Article API 관련 함수들을 작성해 주세요.

+- [x] 이외의 코드들은 모두 main.js 파일에 작성해 주세요. + - [x] import를 활용해 주세요. + - [x] 각 함수를 실행하는 코드를 작성하고, 제대로 동작하는지 확인해 주세요.

+ +--- + +## 주요 변경사항 + +- +- + +## 스크린샷 + +(![image]image +) + +## 멘토에게 + +- 아직 잘 모르겠습니다.... +- diff --git a/sprint-mission-3/sprint-mission-2/class/Article.js b/sprint-mission-3/sprint-mission-2/class/Article.js new file mode 100644 index 00000000..4f2d498c --- /dev/null +++ b/sprint-mission-3/sprint-mission-2/class/Article.js @@ -0,0 +1,14 @@ +export default class Article { + constructor(title, content, writer, likeCount) { + this.title = title; + this.content = content; + this.writer = writer; + this.likeCount = likeCount; + //생성일자 넣기 + this.createdAt = new Date(); + } + like() { + this.likeCount++; + return this.likeCount; + } +} diff --git a/sprint-mission-3/sprint-mission-2/class/ElectronicProduct.js b/sprint-mission-3/sprint-mission-2/class/ElectronicProduct.js new file mode 100644 index 00000000..6a5cc781 --- /dev/null +++ b/sprint-mission-3/sprint-mission-2/class/ElectronicProduct.js @@ -0,0 +1,16 @@ +import Product from './Product.js'; + +export default class ElectronicProduct extends Product { + constructor( + name, + description, + price, + tags, + images, + favoriteCount, + manufacturer + ) { + super(name, description, price, tags, images, favoriteCount); + this.manufacturer = manufacturer || '-'; //undefined 값 방지 + } +} diff --git a/sprint-mission-3/sprint-mission-2/class/Product.js b/sprint-mission-3/sprint-mission-2/class/Product.js new file mode 100644 index 00000000..d06e786a --- /dev/null +++ b/sprint-mission-3/sprint-mission-2/class/Product.js @@ -0,0 +1,15 @@ +export default class Product { + constructor(name, description, price, tags, images, favoriteCount) { + this.name = name; + this.description = description; + this.price = price; + this.tags = tags || []; //undefined 값 방지 || [] / 0 + this.images = images || []; + this.favoriteCount = favoriteCount; + } + + favorite() { + this.favoriteCount++; + return this.favoriteCount; + } +} diff --git a/sprint-mission-3/sprint-mission-2/main/main.js b/sprint-mission-3/sprint-mission-2/main/main.js new file mode 100644 index 00000000..62790b5e --- /dev/null +++ b/sprint-mission-3/sprint-mission-2/main/main.js @@ -0,0 +1,112 @@ +import Product from '../class/Product.js'; +import ElectronicProduct from '../class/ElectronicProduct.js'; +import Article from '../class/Article.js'; +import { + getArticleList, + getArticle, + createArticle, + patchArticle, + deleteArticle, +} from '../service/ArticleService.js'; +import { + getProduct, + getProductList, + createProduct, + deleteProduct, + patchProduct, +} from '../service/ProductService.js'; + +//------------------------------------------------------------ +console.log('=====테스트 시작====='); + +//----올릴 포스트/패치 내용 쓰는 곳---- +// const articleData = { +// title: '검은 고양이', +// content: '검은 고양이', +// image: +// 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSdlFJ7CNFqVBVVHTq3Vjfio8TTIFoktx9-sQ&s', +// }; + +//--------------리스트 가져오기-------------- +// getArticleList(1, 10, '고양이'); + +//----------------고양이 포스트하기---------------------- +// createArticle(articleData); + +//-------------- 포스트 한것 구하기--------------4715, 4718 ,34,35,36 +// getArticle(4715); + +// ---------------patch 하기-----------------4718 +// patchArticle(4715, articleData); + +//-------------------------------------------------delete-------------------------------------------------------------------4734, 35, 36 +// deleteArticle(4734); +// deleteArticle(4735); +// deleteArticle(4736); + +//============ getProductList (page, pageSize, keyword) ========== +// getProductList(1, 10, ''); + +//======= 겟 프로덕트(ID) ======= +// getProduct(1234); + +//======= 프로덕트 생성 ======== +// const myProduct = new Product( +// '태블릿', +// '평범한 태블릿이다.', +// 500000, +// ['전자제품'], +// [] +// ); +// createProduct(myProduct); + +//============== 프로덕트 패치 ============== +// patchProduct(2407, myProduct); + +//======= delete product ======= 2405 +// deleteProduct(); +// deleteProduct(); + +//======== instance 어쩌구 저쩌구 ================== +const products = []; //최종적으로 모든게 담겨야 할 상자 + +async function productsItems() { + try { + let temp = []; //임시 리스트를 만들어준다. + temp = await getProductList(1, 10, ''); //임시 리스트 안에 넣을 리스트를, await으로 받아올 때까지 기다린다 + temp.forEach((item) => { + //임시리스트 안에 있는 값들을 forEach로 하나씩 돌면서 + let instance; //새로운 인스턴스안에 + if (item.tags && item.tags.includes('전자제품')) { + //아이템 태그에 전자제품이 있다면 전자제품으로 분류, 전자제품 클래스를 가져와서 인스턴스에 집어넣는다 + instance = new ElectronicProduct( //오류가 나던 item을 item.name … 이런식으로 변경 + item.name, + item.description, + item.price, + item.tags, + item.image, + item.favoriteCount, + item.manufacturer + ); + } else { + //그게 아니라면 일반 제품으로 집어 넣는다 + instance = new Product( + item.name, + item.description, + item.price, + item.tags, + item.image, + item.favoriteCount + ); + } //instance안에 있는 제품들을 product로 집어 넣는다 + products.push(instance); + }); //성공 시 products 안에 있는 제품들이 뭔지 알 수 있도록 한번 출력해줬다 + console.log('성공!: ', products); + } catch (error) { + console.error('실패!!!: ', error.message); + } finally { + console.log('========= 테스트 끝 ======='); + } +} + +productsItems(); //함수 실행 diff --git a/sprint-mission-3/sprint-mission-2/package-lock.json b/sprint-mission-3/sprint-mission-2/package-lock.json new file mode 100644 index 00000000..52c7df30 --- /dev/null +++ b/sprint-mission-3/sprint-mission-2/package-lock.json @@ -0,0 +1,291 @@ +{ + "name": "6-sprint-mission", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "axios": "^1.12.2" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "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/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "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/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/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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/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-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "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/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/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/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + } + } +} diff --git a/sprint-mission-3/sprint-mission-2/package.json b/sprint-mission-3/sprint-mission-2/package.json new file mode 100644 index 00000000..6795e640 --- /dev/null +++ b/sprint-mission-3/sprint-mission-2/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "axios": "^1.12.2" + } +} diff --git a/sprint-mission-3/sprint-mission-2/service/ArticleService.js b/sprint-mission-3/sprint-mission-2/service/ArticleService.js new file mode 100644 index 00000000..45f1884a --- /dev/null +++ b/sprint-mission-3/sprint-mission-2/service/ArticleService.js @@ -0,0 +1,124 @@ +import axios from 'axios'; +//악시오스를 쓸 것이기 때문에 불러온다. 설치법은 npm install axios + +// https://panda-market-api-crud.vercel.app/docs <<판다 마켓 주소 + +const BASE_URL = `https://panda-market-api-crud.vercel.app`; +//매번 이걸 쳐주기 귀찮으니, 베이스로 사용할 URL 선언. + +//-------------getArticleList(1,1,'')------------------------ +function getArticleList(page, pageSize, keyword) { + const q = `?page=${page}&pageSize=${pageSize}&keyword=${keyword}`; + const finURL = `${BASE_URL}/articles/${q}`; + //쿼리 파라미터? 형식으로 하라고 미션에 적혀있어서 이렇게 q 를 선언 후, + //위에서 만들었던 BASE_URL과 /article/ 방금만든 q 를 합쳐 full 주소(finURL)를 완성시킨다. + + return axios + .get(finURL) //완성된 주소에서 GET 해오기 + .then((response) => { + console.log(`성공!: `, response.data); //답이 왔을 때, 성공!과 response.data를 출력하도록 + }) + .catch((error) => { + console.error('실패!!! :', error.message); //근데 만약 에러가 잡히면? 실패!, 에러 메시지 출력 + if (error.response) { + console.log('에러 코드: ', error.response.status); //미션에서, 2xx(성공코드)가 아니라면, 에러 코드를 출력 해달라 함. + console.log('에러 내용: ', error.response.data); //에러 내용을 출력 해달라 함. + } + }) + .finally(() => { + console.log('=====[gerArticleList] 리스트 불러오기 끝===='); //전 제가 보기 편하라고 넣었습니다. + }); +} + +//------------getArticle(id)-------------------------------------- + +function getArticle(id) { + //함수 호출때 getArticle(‘여기’) 적은 아이디가, 여기로 들어갈 예정 + return ( + axios + .get(`${BASE_URL}/articles/${id}`) + //여긴 쿼리 부분에 특정 게시물이 올라온 주소의 아이디를 바로 넣을 것이기 때문에 + //finURL말고 이렇게 사용.(맞는지는 잘 모르나 일단 잘 동작했습니다.) + .then((response) => { + console.log('성공!: ', response.data); + return response.data; + }) + .catch((error) => { + console.error('실패!!!: ', error.message); + if (error.response) { + console.log('에러 코드: ', error.response.status); + console.log('에러 내용: ', error.response.data); + } + }) + .finally(() => { + console.log('=====[gerArticle] 게시글 불러오기 끝====='); + }) + ); +} + +//------------createArticle-------------------------------------- + +function createArticle(articleData) { + return axios + .post(BASE_URL + '/articles', articleData) + .then((response) => { + return console.log(`성공!: `, response.data); + }) + .catch((error) => { + console.error('실패!!!: ', error.message); + if (error.response) { + console.log('에러 코드: ', error.response.status); + console.log('에러 내용: ', error.response.data); + } + }) + .finally(() => { + console.log(`===생성 실험 끝===`); + }); +} + +//--------------------patchArticle------------------ +function patchArticle(id, articleData) { + return axios + .patch(`${BASE_URL}/articles/${id}`, articleData) + .then((response) => { + return console.log(`성공!: `, response.data); + }) + .catch((error) => { + console.error(`실패!!!: `, error.message); + if (error.response) { + console.log('에러 코드: ', error.response.status); + console.log('에러 내용: ', error.response.data); + } + }) + .finally(() => { + console.log(`===패치 실험 끝===`); + }); +} + +//-----------------deleteArticle---------------------- +function deleteArticle(id) { + return axios + .delete(`${BASE_URL}/articles/${id}`) + .then((response) => { + return console.log(`성공!: `, response.data); + }) + .catch((error) => { + console.error(`실패!!!: `, error.message); + if (error.response) { + console.log('에러 코드: ', error.response.status); + console.log('에러 내용: ', error.response.data); + } + }) + .finally(() => { + console.log(`=== 게시물 삭제 실험 끝===`); + }); +} + +//한번에 내보내기 (보기 쉽게) +export { + getArticleList, + getArticle, + createArticle, + patchArticle, + deleteArticle, +}; diff --git a/sprint-mission-3/sprint-mission-2/service/ProductService.js b/sprint-mission-3/sprint-mission-2/service/ProductService.js new file mode 100644 index 00000000..ba558956 --- /dev/null +++ b/sprint-mission-3/sprint-mission-2/service/ProductService.js @@ -0,0 +1,106 @@ +import axios from 'axios'; + +const BASE_URL = `https://panda-market-api-crud.vercel.app`; // /products/ ${BASE_URL}/products + +//=============== 겟 프로덕트 리스트 ============ +async function getProductList(page, pageSize, keyword) { + try { + const response = await axios.get(BASE_URL + '/products', { + params: { + page, + pageSize, + keyword, + }, + }); + const productData = response.data.list; //list: [{...}, {...}, {...}] => list 자리가 products라면 response.data.products + //아직 원리를 제대로 이해하지 못했다. + console.log('성공!: ', response.data); //내용 출력 + return productData; //다른곳에서 쓸 수 있게 리턴해준다. ? + } catch (error) { + console.error('실패!!!: ', error.message); + if (error.response) { + console.log('에러 코드: ', error.response.status); + console.log('에러 내용: ', error.response.data); + } + } finally { + console.log(`======겟 프로덕트 리스트 테스트 완료======`); + } +} + +//================ 겟 프로덕트 ================ + +async function getProduct(id) { + try { + const response = await axios.get(BASE_URL + '/products/' + id); + const productData = response.data; + console.log('성공!: ', productData); + return productData; + } catch (error) { + console.error('실패!!!: ', error.message); + if (error.response) { + console.log('에러 코드: ', error.response.status); + console.log('에러 내용: ', error.response.data); + } + } finally { + console.log('======== 겟 프로닥 실험 끝 ========'); + } +} + +//================ 프로덕트 생성 ================ +async function createProduct(myProduct) { + try { + const response = await axios.post(BASE_URL + '/products', myProduct); + const createdProductData = response.data; + console.log('생성 성공!: ', createdProductData); + } catch (error) { + console.error('실패!!!: ', error.message); + if (error.response) { + console.log('에러 코드: ', error.response.status); + console.log('에러 내용: ', error.response.data); + } + } finally { + console.log('======== 생성 실험 끝 ========'); + } +} + +//========= 프로덕트 패치하기 ========== +async function patchProduct(id, myProduct) { + try { + const response = await axios.patch(`${BASE_URL}/products/${id}`, myProduct); + const productData = response.data; + console.log('성공!: ', productData); + } catch (error) { + console.error('실패!!!: ', error.message); + if (error.response) { + console.log('에러 코드: ', error.response.status); + console.log('에러 내용: ', error.response.data); + } + } finally { + console.log('======= 프로덕트 패치 실험 끝 ======='); + } +} + +// ========= 프로덕트 삭제 ========= +async function deleteProduct(id) { + try { + const response = await axios.delete(BASE_URL + '/products/' + id); + const responseData = response.data; + console.log('성공!: ', responseData); + } catch (error) { + console.error('실패!!!: ', error.message); + if (error.response) { + console.log('에러 코드: ', error.response.status); + console.log('에러 내용: ', error.response.data); + } + } finally { + console.log('======= 게시글 삭제 실험 끝 ========'); + } +} + +export { + getProductList, + getProduct, + createProduct, + patchProduct, + deleteProduct, +}; diff --git a/src/app.js b/sprint-mission-3/src/app.js similarity index 100% rename from src/app.js rename to sprint-mission-3/src/app.js diff --git a/src/controllers/articleController.js b/sprint-mission-3/src/controllers/articleController.js similarity index 100% rename from src/controllers/articleController.js rename to sprint-mission-3/src/controllers/articleController.js diff --git a/src/controllers/commentController.js b/sprint-mission-3/src/controllers/commentController.js similarity index 100% rename from src/controllers/commentController.js rename to sprint-mission-3/src/controllers/commentController.js diff --git a/src/controllers/productController.js b/sprint-mission-3/src/controllers/productController.js similarity index 100% rename from src/controllers/productController.js rename to sprint-mission-3/src/controllers/productController.js diff --git a/src/controllers/userController.js b/sprint-mission-3/src/controllers/userController.js similarity index 100% rename from src/controllers/userController.js rename to sprint-mission-3/src/controllers/userController.js diff --git a/src/lib/prismaClient.js b/sprint-mission-3/src/lib/prismaClient.js similarity index 100% rename from src/lib/prismaClient.js rename to sprint-mission-3/src/lib/prismaClient.js diff --git a/src/middlewares/errorHandler.js b/sprint-mission-3/src/middlewares/errorHandler.js similarity index 100% rename from src/middlewares/errorHandler.js rename to sprint-mission-3/src/middlewares/errorHandler.js diff --git a/src/middlewares/uploadImages.js b/sprint-mission-3/src/middlewares/uploadImages.js similarity index 100% rename from src/middlewares/uploadImages.js rename to sprint-mission-3/src/middlewares/uploadImages.js diff --git a/src/middlewares/validator.js b/sprint-mission-3/src/middlewares/validator.js similarity index 100% rename from src/middlewares/validator.js rename to sprint-mission-3/src/middlewares/validator.js diff --git a/src/routers/articleRouter.js b/sprint-mission-3/src/routers/articleRouter.js similarity index 100% rename from src/routers/articleRouter.js rename to sprint-mission-3/src/routers/articleRouter.js diff --git a/src/routers/commentRouter.js b/sprint-mission-3/src/routers/commentRouter.js similarity index 100% rename from src/routers/commentRouter.js rename to sprint-mission-3/src/routers/commentRouter.js diff --git a/src/routers/productRouter.js b/sprint-mission-3/src/routers/productRouter.js similarity index 100% rename from src/routers/productRouter.js rename to sprint-mission-3/src/routers/productRouter.js diff --git a/src/routers/uploadRouter.js b/sprint-mission-3/src/routers/uploadRouter.js similarity index 100% rename from src/routers/uploadRouter.js rename to sprint-mission-3/src/routers/uploadRouter.js diff --git a/src/routers/userRouter.js b/sprint-mission-3/src/routers/userRouter.js similarity index 100% rename from src/routers/userRouter.js rename to sprint-mission-3/src/routers/userRouter.js diff --git a/uploads/kuppo.png_1762155800599.png b/sprint-mission-3/uploads/kuppo.png_1762155800599.png similarity index 100% rename from uploads/kuppo.png_1762155800599.png rename to sprint-mission-3/uploads/kuppo.png_1762155800599.png diff --git a/uploads/test-image.png_1762155697729.png b/sprint-mission-3/uploads/test-image.png_1762155697729.png similarity index 100% rename from uploads/test-image.png_1762155697729.png rename to sprint-mission-3/uploads/test-image.png_1762155697729.png diff --git a/uploads/test-image.png_1762155750185.png b/sprint-mission-3/uploads/test-image.png_1762155750185.png similarity index 100% rename from uploads/test-image.png_1762155750185.png rename to sprint-mission-3/uploads/test-image.png_1762155750185.png From 15ac458e1eeb2fff10d9c5073886f2dcba9eb3f8 Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Mon, 24 Nov 2025 20:34:11 +0900 Subject: [PATCH 02/18] =?UTF-8?q?=F0=9F=93=9A=20docs(README):=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=20=EC=B2=B4=ED=81=AC=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=8F=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EA=B0=80=EC=9D=B4=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/README.md b/README.md index e69de29b..159dc36c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,64 @@ +# 🛡️ 스프린트 미션: 인증/인가 및 관계형 DB 구현 + +## 📅 프로젝트 개요 + +- **목표:** 토큰 기반의 인증(Authentication)과 인가(Authorization) 시스템을 구현하고, Prisma의 관계형 데이터 모델링을 적용합니다. +- **핵심 기술:** Node.js, Express, Prisma, JWT, bcrypt + +--- + +## ✅ 개발 체크리스트 (To-Do List) + +### 🛠️ 1. 초기 세팅 & 데이터베이스 (Prisma) + +- [ ] **환경변수(.env) 설정** + - [ ] `DATABASE_URL` 확인 + - [ ] `JWT_SECRET` (토큰 비밀키) 추가 👈 이거! +- [ ] **User 스키마 작성** + - [ ] 필드 구성: `id`, `email`, `nickname`, `image`, `password`, `createdAt`, `updatedAt` + - [ ] 기존 모델(`Product`, `Article`, `Comment`)과 1:N 관계 설정 (`relation` 연결) + +### 🔐 2. 인증 (Authentication) - 로그인/회원가입 + +- [ ] **회원가입 API 구현** + - [ ] 입력: `email`, `nickname`, `password` + - [ ] **중요:** 비밀번호는 반드시 **해싱(Hashing)** 하여 저장 (bcrypt 등 사용) +- [ ] **로그인 API 구현** + - [ ] 입력: `email`, `password` 검증 + - [ ] 성공 시: **Access Token (JWT)** 발급 및 반환 + +### 👮 3. 인가 (Authorization) - 권한 체크 + +> **공통 규칙:** 로그인한 유저만 등록 가능 / 본인만 수정, 삭제 가능 + +- [ ] **인가 미들웨어(Middleware) 구현** (토큰 검증 및 유저 확인) +- [ ] **상품(Product) 기능 인가** + - [ ] 등록: 로그인한 유저만 가능 + - [ ] 수정/삭제: 상품 등록자(본인)만 가능 +- [ ] **게시글(Article) 기능 인가** + - [ ] 등록: 로그인한 유저만 가능 + - [ ] 수정/삭제: 게시글 작성자(본인)만 가능 +- [ ] **댓글(Comment) 기능 인가** + - [ ] 등록: 상품/게시글에 댓글 달기 (로그인 유저만) + - [ ] 수정/삭제: 댓글 작성자(본인)만 가능 + +### 👤 4. 유저 정보 관리 (My Page) + +- [ ] **내 정보 조회 API** + - [ ] 응답에 `password` 제외할 것 +- [ ] **내 정보 수정 API** +- [ ] **비밀번호 변경 API** (기존 비번 확인 과정 권장) +- [ ] **내가 등록한 상품 목록 조회 API** + +--- + +## 🔥 심화 요구사항 (Advanced) - 시간 남으면 도전! + +- [ ] **Refresh Token 구현** (토큰 갱신 기능) +- [ ] **좋아요(Like) 기능 - 상품** + - [ ] 좋아요 / 좋아요 취소 토글 + - [ ] 조회 시 `isLiked` 필드 포함 +- [ ] **좋아요(Like) 기능 - 게시글** + - [ ] 좋아요 / 좋아요 취소 토글 + - [ ] 조회 시 `isLiked` 필드 포함 +- [ ] **좋아요한 목록 조회 기능** From c52c37d00f123a5c28f97f50dc3a8304767bb60b Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Mon, 24 Nov 2025 20:57:16 +0900 Subject: [PATCH 03/18] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20=20remove(mission?= =?UTF-8?q?3):=20=EC=9E=98=EB=AA=BB=20=EC=9C=84=EC=B9=98=ED=95=9C=20?= =?UTF-8?q?=EB=AF=B8=EC=85=982=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20=EB=B0=8F=20=ED=8F=B4=EB=8D=94=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EC=A0=95=EC=83=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sprint-mission-3/class/Article.js | 14 --- sprint-mission-3/class/ElectronicProduct.js | 16 --- sprint-mission-3/class/Product.js | 15 --- sprint-mission-3/main/main.js | 112 ------------------ sprint-mission-3/service/ArticleService.js | 124 -------------------- sprint-mission-3/service/ProductService.js | 106 ----------------- 6 files changed, 387 deletions(-) delete mode 100644 sprint-mission-3/class/Article.js delete mode 100644 sprint-mission-3/class/ElectronicProduct.js delete mode 100644 sprint-mission-3/class/Product.js delete mode 100644 sprint-mission-3/main/main.js delete mode 100644 sprint-mission-3/service/ArticleService.js delete mode 100644 sprint-mission-3/service/ProductService.js diff --git a/sprint-mission-3/class/Article.js b/sprint-mission-3/class/Article.js deleted file mode 100644 index 4f2d498c..00000000 --- a/sprint-mission-3/class/Article.js +++ /dev/null @@ -1,14 +0,0 @@ -export default class Article { - constructor(title, content, writer, likeCount) { - this.title = title; - this.content = content; - this.writer = writer; - this.likeCount = likeCount; - //생성일자 넣기 - this.createdAt = new Date(); - } - like() { - this.likeCount++; - return this.likeCount; - } -} diff --git a/sprint-mission-3/class/ElectronicProduct.js b/sprint-mission-3/class/ElectronicProduct.js deleted file mode 100644 index 6a5cc781..00000000 --- a/sprint-mission-3/class/ElectronicProduct.js +++ /dev/null @@ -1,16 +0,0 @@ -import Product from './Product.js'; - -export default class ElectronicProduct extends Product { - constructor( - name, - description, - price, - tags, - images, - favoriteCount, - manufacturer - ) { - super(name, description, price, tags, images, favoriteCount); - this.manufacturer = manufacturer || '-'; //undefined 값 방지 - } -} diff --git a/sprint-mission-3/class/Product.js b/sprint-mission-3/class/Product.js deleted file mode 100644 index d06e786a..00000000 --- a/sprint-mission-3/class/Product.js +++ /dev/null @@ -1,15 +0,0 @@ -export default class Product { - constructor(name, description, price, tags, images, favoriteCount) { - this.name = name; - this.description = description; - this.price = price; - this.tags = tags || []; //undefined 값 방지 || [] / 0 - this.images = images || []; - this.favoriteCount = favoriteCount; - } - - favorite() { - this.favoriteCount++; - return this.favoriteCount; - } -} diff --git a/sprint-mission-3/main/main.js b/sprint-mission-3/main/main.js deleted file mode 100644 index 62790b5e..00000000 --- a/sprint-mission-3/main/main.js +++ /dev/null @@ -1,112 +0,0 @@ -import Product from '../class/Product.js'; -import ElectronicProduct from '../class/ElectronicProduct.js'; -import Article from '../class/Article.js'; -import { - getArticleList, - getArticle, - createArticle, - patchArticle, - deleteArticle, -} from '../service/ArticleService.js'; -import { - getProduct, - getProductList, - createProduct, - deleteProduct, - patchProduct, -} from '../service/ProductService.js'; - -//------------------------------------------------------------ -console.log('=====테스트 시작====='); - -//----올릴 포스트/패치 내용 쓰는 곳---- -// const articleData = { -// title: '검은 고양이', -// content: '검은 고양이', -// image: -// 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSdlFJ7CNFqVBVVHTq3Vjfio8TTIFoktx9-sQ&s', -// }; - -//--------------리스트 가져오기-------------- -// getArticleList(1, 10, '고양이'); - -//----------------고양이 포스트하기---------------------- -// createArticle(articleData); - -//-------------- 포스트 한것 구하기--------------4715, 4718 ,34,35,36 -// getArticle(4715); - -// ---------------patch 하기-----------------4718 -// patchArticle(4715, articleData); - -//-------------------------------------------------delete-------------------------------------------------------------------4734, 35, 36 -// deleteArticle(4734); -// deleteArticle(4735); -// deleteArticle(4736); - -//============ getProductList (page, pageSize, keyword) ========== -// getProductList(1, 10, ''); - -//======= 겟 프로덕트(ID) ======= -// getProduct(1234); - -//======= 프로덕트 생성 ======== -// const myProduct = new Product( -// '태블릿', -// '평범한 태블릿이다.', -// 500000, -// ['전자제품'], -// [] -// ); -// createProduct(myProduct); - -//============== 프로덕트 패치 ============== -// patchProduct(2407, myProduct); - -//======= delete product ======= 2405 -// deleteProduct(); -// deleteProduct(); - -//======== instance 어쩌구 저쩌구 ================== -const products = []; //최종적으로 모든게 담겨야 할 상자 - -async function productsItems() { - try { - let temp = []; //임시 리스트를 만들어준다. - temp = await getProductList(1, 10, ''); //임시 리스트 안에 넣을 리스트를, await으로 받아올 때까지 기다린다 - temp.forEach((item) => { - //임시리스트 안에 있는 값들을 forEach로 하나씩 돌면서 - let instance; //새로운 인스턴스안에 - if (item.tags && item.tags.includes('전자제품')) { - //아이템 태그에 전자제품이 있다면 전자제품으로 분류, 전자제품 클래스를 가져와서 인스턴스에 집어넣는다 - instance = new ElectronicProduct( //오류가 나던 item을 item.name … 이런식으로 변경 - item.name, - item.description, - item.price, - item.tags, - item.image, - item.favoriteCount, - item.manufacturer - ); - } else { - //그게 아니라면 일반 제품으로 집어 넣는다 - instance = new Product( - item.name, - item.description, - item.price, - item.tags, - item.image, - item.favoriteCount - ); - } //instance안에 있는 제품들을 product로 집어 넣는다 - products.push(instance); - }); //성공 시 products 안에 있는 제품들이 뭔지 알 수 있도록 한번 출력해줬다 - console.log('성공!: ', products); - } catch (error) { - console.error('실패!!!: ', error.message); - } finally { - console.log('========= 테스트 끝 ======='); - } -} - -productsItems(); //함수 실행 diff --git a/sprint-mission-3/service/ArticleService.js b/sprint-mission-3/service/ArticleService.js deleted file mode 100644 index 45f1884a..00000000 --- a/sprint-mission-3/service/ArticleService.js +++ /dev/null @@ -1,124 +0,0 @@ -import axios from 'axios'; -//악시오스를 쓸 것이기 때문에 불러온다. 설치법은 npm install axios - -// https://panda-market-api-crud.vercel.app/docs <<판다 마켓 주소 - -const BASE_URL = `https://panda-market-api-crud.vercel.app`; -//매번 이걸 쳐주기 귀찮으니, 베이스로 사용할 URL 선언. - -//-------------getArticleList(1,1,'')------------------------ -function getArticleList(page, pageSize, keyword) { - const q = `?page=${page}&pageSize=${pageSize}&keyword=${keyword}`; - const finURL = `${BASE_URL}/articles/${q}`; - //쿼리 파라미터? 형식으로 하라고 미션에 적혀있어서 이렇게 q 를 선언 후, - //위에서 만들었던 BASE_URL과 /article/ 방금만든 q 를 합쳐 full 주소(finURL)를 완성시킨다. - - return axios - .get(finURL) //완성된 주소에서 GET 해오기 - .then((response) => { - console.log(`성공!: `, response.data); //답이 왔을 때, 성공!과 response.data를 출력하도록 - }) - .catch((error) => { - console.error('실패!!! :', error.message); //근데 만약 에러가 잡히면? 실패!, 에러 메시지 출력 - if (error.response) { - console.log('에러 코드: ', error.response.status); //미션에서, 2xx(성공코드)가 아니라면, 에러 코드를 출력 해달라 함. - console.log('에러 내용: ', error.response.data); //에러 내용을 출력 해달라 함. - } - }) - .finally(() => { - console.log('=====[gerArticleList] 리스트 불러오기 끝===='); //전 제가 보기 편하라고 넣었습니다. - }); -} - -//------------getArticle(id)-------------------------------------- - -function getArticle(id) { - //함수 호출때 getArticle(‘여기’) 적은 아이디가, 여기로 들어갈 예정 - return ( - axios - .get(`${BASE_URL}/articles/${id}`) - //여긴 쿼리 부분에 특정 게시물이 올라온 주소의 아이디를 바로 넣을 것이기 때문에 - //finURL말고 이렇게 사용.(맞는지는 잘 모르나 일단 잘 동작했습니다.) - .then((response) => { - console.log('성공!: ', response.data); - return response.data; - }) - .catch((error) => { - console.error('실패!!!: ', error.message); - if (error.response) { - console.log('에러 코드: ', error.response.status); - console.log('에러 내용: ', error.response.data); - } - }) - .finally(() => { - console.log('=====[gerArticle] 게시글 불러오기 끝====='); - }) - ); -} - -//------------createArticle-------------------------------------- - -function createArticle(articleData) { - return axios - .post(BASE_URL + '/articles', articleData) - .then((response) => { - return console.log(`성공!: `, response.data); - }) - .catch((error) => { - console.error('실패!!!: ', error.message); - if (error.response) { - console.log('에러 코드: ', error.response.status); - console.log('에러 내용: ', error.response.data); - } - }) - .finally(() => { - console.log(`===생성 실험 끝===`); - }); -} - -//--------------------patchArticle------------------ -function patchArticle(id, articleData) { - return axios - .patch(`${BASE_URL}/articles/${id}`, articleData) - .then((response) => { - return console.log(`성공!: `, response.data); - }) - .catch((error) => { - console.error(`실패!!!: `, error.message); - if (error.response) { - console.log('에러 코드: ', error.response.status); - console.log('에러 내용: ', error.response.data); - } - }) - .finally(() => { - console.log(`===패치 실험 끝===`); - }); -} - -//-----------------deleteArticle---------------------- -function deleteArticle(id) { - return axios - .delete(`${BASE_URL}/articles/${id}`) - .then((response) => { - return console.log(`성공!: `, response.data); - }) - .catch((error) => { - console.error(`실패!!!: `, error.message); - if (error.response) { - console.log('에러 코드: ', error.response.status); - console.log('에러 내용: ', error.response.data); - } - }) - .finally(() => { - console.log(`=== 게시물 삭제 실험 끝===`); - }); -} - -//한번에 내보내기 (보기 쉽게) -export { - getArticleList, - getArticle, - createArticle, - patchArticle, - deleteArticle, -}; diff --git a/sprint-mission-3/service/ProductService.js b/sprint-mission-3/service/ProductService.js deleted file mode 100644 index ba558956..00000000 --- a/sprint-mission-3/service/ProductService.js +++ /dev/null @@ -1,106 +0,0 @@ -import axios from 'axios'; - -const BASE_URL = `https://panda-market-api-crud.vercel.app`; // /products/ ${BASE_URL}/products - -//=============== 겟 프로덕트 리스트 ============ -async function getProductList(page, pageSize, keyword) { - try { - const response = await axios.get(BASE_URL + '/products', { - params: { - page, - pageSize, - keyword, - }, - }); - const productData = response.data.list; //list: [{...}, {...}, {...}] => list 자리가 products라면 response.data.products - //아직 원리를 제대로 이해하지 못했다. - console.log('성공!: ', response.data); //내용 출력 - return productData; //다른곳에서 쓸 수 있게 리턴해준다. ? - } catch (error) { - console.error('실패!!!: ', error.message); - if (error.response) { - console.log('에러 코드: ', error.response.status); - console.log('에러 내용: ', error.response.data); - } - } finally { - console.log(`======겟 프로덕트 리스트 테스트 완료======`); - } -} - -//================ 겟 프로덕트 ================ - -async function getProduct(id) { - try { - const response = await axios.get(BASE_URL + '/products/' + id); - const productData = response.data; - console.log('성공!: ', productData); - return productData; - } catch (error) { - console.error('실패!!!: ', error.message); - if (error.response) { - console.log('에러 코드: ', error.response.status); - console.log('에러 내용: ', error.response.data); - } - } finally { - console.log('======== 겟 프로닥 실험 끝 ========'); - } -} - -//================ 프로덕트 생성 ================ -async function createProduct(myProduct) { - try { - const response = await axios.post(BASE_URL + '/products', myProduct); - const createdProductData = response.data; - console.log('생성 성공!: ', createdProductData); - } catch (error) { - console.error('실패!!!: ', error.message); - if (error.response) { - console.log('에러 코드: ', error.response.status); - console.log('에러 내용: ', error.response.data); - } - } finally { - console.log('======== 생성 실험 끝 ========'); - } -} - -//========= 프로덕트 패치하기 ========== -async function patchProduct(id, myProduct) { - try { - const response = await axios.patch(`${BASE_URL}/products/${id}`, myProduct); - const productData = response.data; - console.log('성공!: ', productData); - } catch (error) { - console.error('실패!!!: ', error.message); - if (error.response) { - console.log('에러 코드: ', error.response.status); - console.log('에러 내용: ', error.response.data); - } - } finally { - console.log('======= 프로덕트 패치 실험 끝 ======='); - } -} - -// ========= 프로덕트 삭제 ========= -async function deleteProduct(id) { - try { - const response = await axios.delete(BASE_URL + '/products/' + id); - const responseData = response.data; - console.log('성공!: ', responseData); - } catch (error) { - console.error('실패!!!: ', error.message); - if (error.response) { - console.log('에러 코드: ', error.response.status); - console.log('에러 내용: ', error.response.data); - } - } finally { - console.log('======= 게시글 삭제 실험 끝 ========'); - } -} - -export { - getProductList, - getProduct, - createProduct, - patchProduct, - deleteProduct, -}; From ffdbb93a5f24e47b29facaeb590533f86d55982e Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Mon, 24 Nov 2025 21:28:48 +0900 Subject: [PATCH 04/18] =?UTF-8?q?=F0=9F=94=84=20chore(mission3):=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=B0=A9=EC=A7=80=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20=EB=B0=B1=EC=97=85=20=EC=8A=A4=ED=82=A4=EB=A7=88=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sprint-mission-3/prisma/schema.prisma | 140 +++++++++++++------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/sprint-mission-3/prisma/schema.prisma b/sprint-mission-3/prisma/schema.prisma index 086f8c7e..d9f86161 100644 --- a/sprint-mission-3/prisma/schema.prisma +++ b/sprint-mission-3/prisma/schema.prisma @@ -1,78 +1,78 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema +// // This is your Prisma schema file, +// // learn more about it in the docs: https://pris.ly/d/prisma-schema -// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? -// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init +// // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? +// // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init -generator client { - provider = "prisma-client-js" - // output = "../generated/prisma" -} +// generator client { +// provider = "prisma-client-js" +// // output = "../generated/prisma" +// } -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} +// datasource db { +// provider = "postgresql" +// url = env("DATABASE_URL") +// } -//--- User --- -model User { - id String @id @default(uuid()) - firstName String - lastName String - email String @unique - description String? - address String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - sellingProducts Product[] - Article Article[] - comment comment[] -} +// //--- User --- +// model User { +// id String @id @default(uuid()) +// firstName String +// lastName String +// email String @unique +// description String? +// address String +// createdAt DateTime @default(now()) +// updatedAt DateTime @updatedAt +// sellingProducts Product[] +// Article Article[] +// comment comment[] +// } -//--- 중고 마켓 --- //id, name, description, price, tags, createdAt, updatedAt필드를 가집니다. -model Product { - id String @id @default(uuid()) - name String - description String - price Float - tags String[] - status ProductStatus @default(SALE) - seller User @relation(fields: [sellerId], references: [id], onDelete: Cascade) - sellerId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - comment comment[] -} +// //--- 중고 마켓 --- //id, name, description, price, tags, createdAt, updatedAt필드를 가집니다. +// model Product { +// id String @id @default(uuid()) +// name String +// description String +// price Float +// tags String[] +// status ProductStatus @default(SALE) +// seller User @relation(fields: [sellerId], references: [id], onDelete: Cascade) +// sellerId String +// createdAt DateTime @default(now()) +// updatedAt DateTime @updatedAt +// comment comment[] +// } -//--- 자유 게시판 --- //id, title, content, createdAt, updatedAt 필드를 가집니다. -model Article { - id String @id @default(uuid()) - author User? @relation(fields: [authorId], references: [id], onDelete: Cascade) - title String - content String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - comment comment[] - authorId String? -} +// //--- 자유 게시판 --- //id, title, content, createdAt, updatedAt 필드를 가집니다. +// model Article { +// id String @id @default(uuid()) +// author User? @relation(fields: [authorId], references: [id], onDelete: Cascade) +// title String +// content String +// createdAt DateTime @default(now()) +// updatedAt DateTime @updatedAt +// comment comment[] +// authorId String? +// } -//--- 댓글 --- id, content,createdAt,updatedAt -model comment { - id String @id @default(uuid()) - author User @relation(fields: [authorId], references: [id], onDelete: Cascade) - content String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - productId String? - product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) - articleId String? - article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) - authorId String -} +// //--- 댓글 --- id, content,createdAt,updatedAt +// model comment { +// id String @id @default(uuid()) +// author User @relation(fields: [authorId], references: [id], onDelete: Cascade) +// content String +// createdAt DateTime @default(now()) +// updatedAt DateTime @updatedAt +// productId String? +// product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) +// articleId String? +// article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) +// authorId String +// } -//제품 판매 상태 -enum ProductStatus { - SALE //판매중 - RESERVED //예약중 - SOLD //판매완료 -} +// //제품 판매 상태 +// enum ProductStatus { +// SALE //판매중 +// RESERVED //예약중 +// SOLD //판매완료 +// } From 84551a8a64b2c27fa986ea780c5bc19457bf9976 Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Tue, 25 Nov 2025 13:07:14 +0900 Subject: [PATCH 05/18] =?UTF-8?q?=F0=9F=9A=80=20feat(db):=20Prisma=20?= =?UTF-8?q?=EC=8A=A4=ED=82=A4=EB=A7=88=20=EA=B4=80=EA=B3=84=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EC=8B=9C=EB=93=9C=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0(bcrypt)=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 +- .../20251125010145_init/migration.sql | 103 +++++++++++ prisma/migrations/migration_lock.toml | 3 + prisma/mock.js | 175 ++++++++++++++++++ prisma/schema.prisma | 102 ++++++++++ prisma/seed.js | 54 ++++++ 6 files changed, 442 insertions(+), 5 deletions(-) create mode 100644 prisma/migrations/20251125010145_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/mock.js create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.js diff --git a/README.md b/README.md index 159dc36c..5e3ba9de 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,16 @@ ### 🛠️ 1. 초기 세팅 & 데이터베이스 (Prisma) - [ ] **환경변수(.env) 설정** - - [ ] `DATABASE_URL` 확인 - - [ ] `JWT_SECRET` (토큰 비밀키) 추가 👈 이거! + - [x] `DATABASE_URL` 확인 + - [ ] `JWT_SECRET` (토큰 비밀키) - [ ] **User 스키마 작성** - - [ ] 필드 구성: `id`, `email`, `nickname`, `image`, `password`, `createdAt`, `updatedAt` - - [ ] 기존 모델(`Product`, `Article`, `Comment`)과 1:N 관계 설정 (`relation` 연결) + - [x] 필드 구성: `id`, `email`, `nickname`, `image`, `password`, `createdAt`, `updatedAt` + - [x] 기존 모델(`Product`, `Article`, `Comment`)과 1:N 관계 설정 (`relation` 연결) ### 🔐 2. 인증 (Authentication) - 로그인/회원가입 - [ ] **회원가입 API 구현** - - [ ] 입력: `email`, `nickname`, `password` + - [x] 입력: `email`, `nickname`, `password` - [ ] **중요:** 비밀번호는 반드시 **해싱(Hashing)** 하여 저장 (bcrypt 등 사용) - [ ] **로그인 API 구현** - [ ] 입력: `email`, `password` 검증 diff --git a/prisma/migrations/20251125010145_init/migration.sql b/prisma/migrations/20251125010145_init/migration.sql new file mode 100644 index 00000000..cb564702 --- /dev/null +++ b/prisma/migrations/20251125010145_init/migration.sql @@ -0,0 +1,103 @@ +-- CreateEnum +CREATE TYPE "ProductStatus" AS ENUM ('SALE', 'RESERVED', 'SOLD'); + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "nickname" TEXT NOT NULL, + "password" TEXT NOT NULL, + "description" TEXT, + "address" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "image" TEXT, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Product" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "price" DOUBLE PRECISION NOT NULL, + "tags" TEXT[], + "status" "ProductStatus" NOT NULL DEFAULT 'SALE', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "sellerId" INTEGER NOT NULL, + "buyerId" INTEGER, + + CONSTRAINT "Product_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Article" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "authorId" INTEGER NOT NULL, + + CONSTRAINT "Article_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Comment" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "authorId" INTEGER NOT NULL, + "productId" INTEGER, + "articleId" INTEGER, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Image" ( + "id" SERIAL NOT NULL, + "imageUrl" TEXT NOT NULL, + "userId" INTEGER, + "articleId" INTEGER, + "productId" INTEGER, + "commentId" INTEGER, + + CONSTRAINT "Image_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- AddForeignKey +ALTER TABLE "Product" ADD CONSTRAINT "Product_sellerId_fkey" FOREIGN KEY ("sellerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Product" ADD CONSTRAINT "Product_buyerId_fkey" FOREIGN KEY ("buyerId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Article" ADD CONSTRAINT "Article_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Image" ADD CONSTRAINT "Image_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Image" ADD CONSTRAINT "Image_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Image" ADD CONSTRAINT "Image_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Image" ADD CONSTRAINT "Image_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..fbffa92c --- /dev/null +++ b/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 (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/mock.js b/prisma/mock.js new file mode 100644 index 00000000..f8698aed --- /dev/null +++ b/prisma/mock.js @@ -0,0 +1,175 @@ +export const USERS = [ + { + id: 1, + email: 'wakingSands@example.com', + nickname: '민필리아', + password: '1234', + address: '서부 다날란, 저녁별 만, 모래의 집', + }, + { + id: 2, + email: 'gunBreaker@example.com', + nickname: '산크레드', + password: '1234', + address: '울다하 날 회랑, 모래늪 여관', + }, + { + id: 3, + email: 'gillionaire@example.com', + nickname: '타타루', + password: '1234', + address: '모르도나 망자의 종소리, 돌의 집', + }, + { + id: 4, + email: 'matoyaMom@example.com', + nickname: '야슈톨라', + password: '1234', + address: '저지 드라바니아, 마토야의 동굴', + }, + { + id: 5, + email: 'finalHeaven@example.com', + nickname: '이다', + password: '1234', + address: '기라바니아 변방지대, 랄거의 손길', + }, + { + id: 6, + email: 'smartCaster@example.com', + nickname: '파파리모', + password: '1234', + address: '검은장막 숲, 그리다니아 구시가지', + }, + { + id: 7, + email: 'thouArt@example.com', + nickname: '위리앙제', + password: '1234', + address: '일 메그, 몽환의 숲, 문지기의 서재', + }, +]; + +export const PRODUCTS = [ + { + id: 1, + name: '백금 만년필', + description: '행정 업무에 최적화된 고급 만년필입니다. 부드러운 필기감을 자랑합니다.', + tags: ['STATIONERY'], + price: 2500, + sellerId: 1, //민필리아 + }, + { + id: 2, + name: '다홍색 장미', + description: '진하고 매혹적인 색을 띤 장미입니다. 선물용으로 적합합니다.', + tags: ['FLOWER'], + price: 500, + sellerId: 2, //산크레드 + }, + { + id: 3, + name: '꼬마 친구 포실포실 털뭉치', + description: '부드러운 털실로 짜인 귀여운 꼬마 친구입니다. 희귀한 확률로 발견됩니다.', + tags: ['DOLL'], + price: 15000000, + sellerId: 3, //타타루 + }, + { + id: 4, + name: '마법의 빗자루', + description: '마력을 주입하여 스스로 움직이는 빗자루입니다. 하우징 마당을 꾸미기에 좋습니다.', + tags: ['FURNITURE'], + price: 450000, + sellerId: 4, //야슈톨라 + }, + { + id: 5, + name: '경화 가죽 격투무기', + description: '튼튼한 가죽으로 감싼 격투가용 무기입니다. 내구성이 뛰어납니다.', + tags: ['SPORTS'], + price: 15000, + sellerId: 5, //이다 + }, + { + id: 6, + name: '샬레이안 고글', + description: + '지식의 도시 샬레이안 양식으로 제작된 고글입니다. 에테르의 흐름을 관찰하기 좋습니다.', + tags: ['FASHION'], + price: 85000, + sellerId: 6, //파파리모 + }, + { + id: 7, + name: '점성술사 카드 세트', + description: '여섯 별의 운명을 점칠 수 있는 카드 세트입니다. 점성술 입문자에게 추천합니다.', + tags: ['HOBBY'], + price: 33000, + sellerId: 7, //위리앙제 + }, + { + id: 8, + name: '미스릴 곡괭이', + description: '미스릴 주괴로 날을 세운 곡괭이입니다. 광석 채집에 필수적인 도구입니다.', + tags: ['TOOL'], + price: 5000, + sellerId: 1, //민필리아 + }, + { + id: 9, + name: '미스릴 반지', + description: '세공된 미스릴로 만든 반지입니다. 깔끔한 디자인으로 인기가 많습니다.', + tags: ['ACCESSORY'], + price: 25000, + sellerId: 2, //산크레드 + }, + { + id: 10, + name: '사베네어 뷔스티에', + description: '사베네어 지방의 전통 의상입니다. 화려한 자수와 고급 원단으로 제작되었습니다.', + tags: ['FASHION'], + price: 5000000, + sellerId: 3, //타타루 + }, + { + id: 11, + name: '고대 롱카의 비석', + description: '롱카 문명이 새겨진 고대 비석입니다. 하우징 조경물로 사용할 수 있습니다.', + tags: ['FURNITURE'], + price: 200000, + sellerId: 4, //야슈톨라 + }, + { + id: 12, + name: '동방의 목인', + description: '동방 지역에서 수련용으로 사용하는 목인입니다. 튼튼한 나무로 만들어졌습니다.', + tags: ['FURNITURE'], + price: 10000, + sellerId: 5, //이다 + }, + { + id: 13, + name: '묵직한 철제 화분', + description: '어떤 식물이든 심을 수 있는 튼튼한 화분입니다. 실내 장식용으로 적합합니다.', + tags: ['FURNITURE'], + price: 5000, + sellerId: 7, //위리앙제 + }, + { + id: 14, + name: '에테르학 개론', + description: '에테르의 기본 원리와 응용법이 적힌 학술서입니다. 학자들에게 필독서로 꼽힙니다.', + tags: ['BOOKS'], + price: 12000, + sellerId: 6, //파파리모 + }, + { + id: 15, + name: '프론티어 드레스', + description: '고급 옷감을 사용하여 제작된 드레스입니다. 우아한 실루엣을 연출합니다.', + tags: ['FASHION'], + price: 8000000, + sellerId: 3, //타타루 + }, +]; diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 00000000..d8188cbe --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,102 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? +// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init + +generator client { + provider = "prisma-client-js" + // output = "../generated/prisma" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +//`id`, `email`, `nickname`, `image`, `password`, `createdAt`, `updatedAt` +// 기존 모델(`Product`, `Article`, `Comment`)과 1:N 관계 설정 (`relation` 연결) + +//--- User --- +model User { + id Int @id @default(autoincrement()) + email String @unique + nickname String + password String + description String? + address String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + sellingProducts Product[] @relation("selling") + boughtProducts Product[] @relation("buying") + articles Article[] + comments Comment[] + image String? + images Image[] +} + +//--- 중고 마켓 --- //id, name, description, price, tags, createdAt, updatedAt필드를 가집니다. +model Product { + id Int @id @default(autoincrement()) + name String + description String + price Float + tags String[] + status ProductStatus @default(SALE) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + seller User @relation("selling", fields: [sellerId], references: [id], onDelete: Cascade) + sellerId Int + comment Comment[] + buyer User? @relation("buying", fields: [buyerId], references: [id]) + buyerId Int? + images Image[] +} + +//--- 자유 게시판 --- //id, title, content, createdAt, updatedAt 필드를 가집니다. +model Article { + id Int @id @default(autoincrement()) + title String + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId Int + comment Comment[] + images Image[] +} + +//--- 댓글 --- id, content,createdAt,updatedAt +model Comment { + id Int @id @default(autoincrement()) + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId Int + product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) + productId Int? + article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) + articleId Int? + images Image[] +} + +model Image { + id Int @id @default(autoincrement()) + imageUrl String + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int? + article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) + articleId Int? + product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) + productId Int? + comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade) + commentId Int? +} + +//제품 판매 상태 +enum ProductStatus { + SALE //판매중 + RESERVED //예약중 + SOLD //판매완료 +} diff --git a/prisma/seed.js b/prisma/seed.js new file mode 100644 index 00000000..d4a9e3a8 --- /dev/null +++ b/prisma/seed.js @@ -0,0 +1,54 @@ +import { PrismaClient } from '@prisma/client'; +import { USERS, PRODUCTS } from './mock.js'; +import bcrypt from 'bcrypt'; + +const prisma = new PrismaClient(); + +async function main() { + //기존에 데이터가 남아있을 시 지우기 위해 사용 + await prisma.product.deleteMany(); + await prisma.user.deleteMany(); + + /*기존 데이터를 싹 날리고, ID 번호표도 1번으로 리셋. 안그러면 db 내부 카운터가 안줄어서 8, 10 이럴 수 있다고 함 + TRUNCATE는 "테이블 비우기" + RESTART IDENTITY는 "번호 초기화". + (이걸 해야 sellerId: 1이 정확히 민필리아를 가리킴)*/ + await prisma.$executeRaw`TRUNCATE TABLE "User", "Product", "Article", "Comment", "Image" RESTART IDENTITY CASCADE;`; + + //===== 유저 데이터 생성 ===== + console.log('👥 유저(새벽의 혈맹) 등록 중...'); + + const usersToCreate = await Promise.all( + USERS.map(async (user) => { + const HashedPassword = await bcrypt.hash(user.password, 10); //10은 보안 정도라고 함 10에서 12가 적당하다고... //1234 -> $2b$ + return { + ...user, + password: HashedPassword, + }; + }), + ); + await prisma.user.createMany({ + data: usersToCreate, + skipDuplicates: true, + }); + + //===== 상품 생성 ===== + console.log('📦 장터 물품 진열 중...'); + await prisma.product.createMany({ + data: PRODUCTS, + skipDuplicates: true, + }); +} +console.log('에오르제아 중고장터 개방 완료'); +console.log(`➡️ 등록된 유저: ${USERS.length}명`); +console.log(`➡️ 진열된 상품: ${PRODUCTS.length}개`); + +//===== 실행버튼 ===== +main() + .catch((e) => { + console.error('에러 캐치!:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + console.log('데이터베이스 연결 종료'); + }); From 9e78a4eeef0a5d2bc5edf07b07a41b166bd36301 Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Tue, 25 Nov 2025 13:58:54 +0900 Subject: [PATCH 06/18] =?UTF-8?q?=F0=9F=9A=80=20feat(server):=20app.js=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EC=A7=84=EC=9E=85=EC=A0=90=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=ED=99=98=EA=B2=BD=20=EC=83=81=EC=88=98?= =?UTF-8?q?(constants)=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.js | 38 ++++++++++++++++++++++++++++++++++++++ src/lib/constants.js | 7 +++++++ 2 files changed, 45 insertions(+) create mode 100644 src/app.js create mode 100644 src/lib/constants.js diff --git a/src/app.js b/src/app.js new file mode 100644 index 00000000..ce20f2f3 --- /dev/null +++ b/src/app.js @@ -0,0 +1,38 @@ +import express from 'express'; +import cors from 'cors'; +import dotenv from 'dotenv'; +// import errorHandler from './middlewares/errorHandler.js'; +// import userRouter from './routes/user.router.js'; +// import productRouter from './routes/product.router.js'; +// import articleRouter from './routes/article.router.js'; +// import commentRouter from './routes/comment.router.js'; +// import uploadRouter from './routes/upload.router.js'; + +dotenv.config(); +const app = express(); + +app.use(cors()); +app.use(express.json()); + +app.get('/', (req, res) => { + res.send('두려워하지 마십시오. 죽음이 끝은 아닙니다.'); +}); + +// //중고마켓 +// app.use('/products', productRouter); + +// //자유게시판 +// app.use('/articles', articleRouter); + +// //댓글 +// app.use('/comments', commentRouter); + +// //이미지 +// app.use('/uploads', uploadRouter); + +// //마지막에 실행. +// app.use(errorHandler); + +app.listen(process.env.PORT || 3000, () => { + console.log(`열려라 참깨! 시스템: ${process.env.PORT || 3000} 문이 열립니다. ( b^-^)b`); +}); diff --git a/src/lib/constants.js b/src/lib/constants.js new file mode 100644 index 00000000..ed1ee1f5 --- /dev/null +++ b/src/lib/constants.js @@ -0,0 +1,7 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +export const DATABASE_URL = process.env.DATABASE_URL; +export const PORT = process.env.PORT || 3000; +// export const PUBLIC_PATH = './public'; +// export const STATIC_PATH = '/public'; From 093e9a74128cd623d09c16f6e0816f632c534c81 Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Tue, 25 Nov 2025 19:37:07 +0900 Subject: [PATCH 07/18] =?UTF-8?q?=F0=9F=9A=80=20feat(core):=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=EC=9C=A0=ED=8B=B8=EB=A6=AC=ED=8B=B0(Prisma,=20Stru?= =?UTF-8?q?cts)=20=EB=B0=8F=20=EB=AF=B8=EB=93=A4=EC=9B=A8=EC=96=B4(Validat?= =?UTF-8?q?or,=20ErrorHandler)=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/prisma.js | 5 +++ src/lib/structs.js | 68 +++++++++++++++++++++++++++++++++ src/lib/utils.js | 10 +++++ src/lib/withAsync.js | 9 +++++ src/middlewares/errorHandler.js | 27 +++++++++++++ src/middlewares/validator.js | 24 ++++++++++++ 6 files changed, 143 insertions(+) create mode 100644 src/lib/prisma.js create mode 100644 src/lib/structs.js create mode 100644 src/lib/utils.js create mode 100644 src/lib/withAsync.js create mode 100644 src/middlewares/errorHandler.js create mode 100644 src/middlewares/validator.js diff --git a/src/lib/prisma.js b/src/lib/prisma.js new file mode 100644 index 00000000..4e54f7a7 --- /dev/null +++ b/src/lib/prisma.js @@ -0,0 +1,5 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +export default prisma; diff --git a/src/lib/structs.js b/src/lib/structs.js new file mode 100644 index 00000000..a643d829 --- /dev/null +++ b/src/lib/structs.js @@ -0,0 +1,68 @@ +import * as s from 'superstruct'; + +//===== 문자열을 숫자로 ===== +//Integer = 1보다 큰 정수: 패치 및 삭제 id 유효성 검사) +//예: Coerce를 이용해 문자열을 숫자로 변환한 후 refine적용, 이제 문자열 "3"도 Number 으로 바뀌어 들어감 +const CoercedNumber = s.coerce(s.number(), s.union([s.string(), s.number()]), (value) => + Number(value), +); +const Integer = s.refine(CoercedNumber, 'Integer', (value) => Number.isInteger(value) && value > 0); + +//===== ID 검사 ===== +//----- productId, articleId, commentId ----- +export const ProductIdStruct = s.object({ + productId: Integer, +}); + +export const ArticleIdStruct = s.object({ + articleId: Integer, +}); + +export const CommentIdStruct = s.object({ + commentId: Integer, +}); + +//===== req.body 검사 ===== + +//----- User ----- +export const CreateUserStruct = s.object({ + email: s.size(s.string(), 1, 100), + nickname: s.size(s.string(), 1, 10), + password: s.size(s.string(), 4, 20), +}); + +// partial을 쓰면 내부의 모든 필드가 자동으로 s.optional() 처리가 됨. +export const PatchUserStruct = s.partial(CreateUserStruct); + +//----- product ----- +export const CreateProductStruct = s.object({ + name: s.size(s.string(), 1, 30), + description: s.size(s.string(), 1, 500), + price: s.min(s.number(), 0), + tags: s.optional(s.array(s.string())), + // 🚧 [임시] 로그인 만들기 전까지만 + sellerId: Integer, +}); + +export const PatchProductStruct = s.partial(CreateProductStruct); + +//----- article ----- +export const CreateArticleStruct = s.object({ + title: s.size(s.string(), 1, 30), + content: s.size(s.string(), 10, 1000), + + // 🚧 [임시] 로그인 만들기 전까지만 + authorId: Integer, +}); + +export const PatchArticleStruct = s.partial(CreateArticleStruct); + +//----- comment ----- +export const CreateCommentStruct = s.object({ + content: s.size(s.string(), 10, 200), + + // 🚧 [임시] 로그인 만들기 전까지만 + authorId: Integer, +}); + +export const PatchCommentStruct = s.partial(CreateCommentStruct); diff --git a/src/lib/utils.js b/src/lib/utils.js new file mode 100644 index 00000000..0ab2fbac --- /dev/null +++ b/src/lib/utils.js @@ -0,0 +1,10 @@ +//최신순 정렬 +export const getOrderBy = (order) => { + switch (order) { + case 'oldest': + return { createdAt: 'asc' }; + case 'recent': + default: + return { createdAt: 'desc' }; + } +}; diff --git a/src/lib/withAsync.js b/src/lib/withAsync.js new file mode 100644 index 00000000..bf007aab --- /dev/null +++ b/src/lib/withAsync.js @@ -0,0 +1,9 @@ +export default function withAsync(handler) { + return async function (req, res, next) { + try { + await handler(req, res, next); + } catch (e) { + next(e); + } + }; +} diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js new file mode 100644 index 00000000..d8b3f7e2 --- /dev/null +++ b/src/middlewares/errorHandler.js @@ -0,0 +1,27 @@ +const errorHandler = (err, req, res, next) => { + console.error('❌ Error Log:', err); + + //Prisma 에러: P2025 (찾을 수 없음) + if (err.code === 'P2025') { + return res.status(404).json({ + success: false, + message: '요청하신 내용을 찾을 수 없습니다.', + }); + } + + //Prisma 에러 : P2002 (중복 데이터) + if (err.code === 'P2002') { + return res.status(409).json({ + success: false, + message: '이미 존재하는 데이터입니다.', // 혹은 "중복된 값입니다." + }); + } else { + const statusCode = err.status || 500; + return res.status(statusCode).json({ + success: false, + message: err.message || '서버 오류가 발생했습니다. 다시 시도해주십시오.', + }); + } +}; + +export default errorHandler; diff --git a/src/middlewares/validator.js b/src/middlewares/validator.js new file mode 100644 index 00000000..7244d2c7 --- /dev/null +++ b/src/middlewares/validator.js @@ -0,0 +1,24 @@ +import * as s from 'superstruct'; + +//.post (validate(CreateProductStruct, 'body'), createProduct); +export const validate = (struct, target = 'body') => { + return (req, res, next) => { + try { + // 1. 검사할 데이터 선택 (body냐 params냐) + const data = req[target]; + + // 2. 설계도와 대조 (검증) + const validatedData = s.create(data, struct); + + // 3. 검증된 데이터로 덮어쓰기 (Sanitization 효과) + req[target] = validatedData; + + next(); // 통과! + } catch (error) { + // 4. 실패 시 에러 던지기 + error.status = 400; + error.message = `유효성 검사 에러: ${error.message}`; + next(error); + } + }; +}; From cd584bf052f217fbba6cf7463280855d1a45c3c2 Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Tue, 25 Nov 2025 19:38:27 +0900 Subject: [PATCH 08/18] =?UTF-8?q?=F0=9F=9A=80=20feat(product):=20=EC=83=81?= =?UTF-8?q?=ED=92=88=20CRUD=20API,=20=EA=B2=80=EC=83=89/=EC=A0=95=EB=A0=AC?= =?UTF-8?q?,=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/product.controller.js | 153 ++++++++++++++++++++++++++ src/routes/product.router.js | 60 ++++++++++ 2 files changed, 213 insertions(+) create mode 100644 src/controllers/product.controller.js create mode 100644 src/routes/product.router.js diff --git a/src/controllers/product.controller.js b/src/controllers/product.controller.js new file mode 100644 index 00000000..e72acfd0 --- /dev/null +++ b/src/controllers/product.controller.js @@ -0,0 +1,153 @@ +import prisma from '../lib/prisma.js'; +import { getOrderBy } from '../lib/utils.js'; + +//POST========== +const createProduct = async (req, res, next) => { + const inputData = req.body; + + const productData = await prisma.product.create({ + data: inputData, + include: { + seller: { + select: { id: true, nickname: true, email: true }, + }, + }, + }); + + const responseData = { + id: productData.id, + status: productData.status, + productName: productData.name, + description: productData.description, + price: productData.price, + tags: productData.tags, + sellerName: productData.seller.nickname, + sellerId: productData.seller.id, + email: productData.seller.email, + createdAt: productData.createdAt, + updatedAt: productData.updatedAt, + }; + + return res.status(201).json(responseData); +}; + +//GET========== +const getListProducts = async (req, res, next) => { + const { offset = 0, limit = 10, order = 'recent', search } = req.query; + + //포함 검색 + const where = search + ? { + OR: [ + { name: { contains: search, mode: 'insensitive' } }, // 대소문자 구분없음 mode: 'insensitive' + { description: { contains: search, mode: 'insensitive' } }, + { seller: { nickname: { contains: search, mode: 'insensitive' } } }, + ], + } + : undefined; + + const [productData, totalCount] = await Promise.all([ + prisma.product.findMany({ + where, + orderBy: getOrderBy(order), + skip: parseInt(offset), + take: parseInt(limit), + include: { + seller: { select: { id: true, nickname: true, email: true } }, + }, + }), + + //파모 '총 50건 중 1~10건 표시' 같은거하려면 50건 구하는걸 해야됨 (페이지네이션) + prisma.product.count({ where }), + ]); + + const responseData = productData.map((product) => { + const seller = product.seller || {}; + + return { + id: product.id, + status: product.status, + productName: product.name, + description: product.description, + price: product.price, + tags: product.tags, + sellerName: seller.nickname, + sellerId: product.sellerId, + email: seller.email, + createdAt: product.createdAt, + updatedAt: product.updatedAt, + // favoriteCount: 0 // 나중에 기능 구현하면 넣기 + }; + }); + + return res.status(200).json({ totalCount: totalCount, list: responseData }); +}; + +//GET id========== +const getProductById = async (req, res, next) => { + const { productId } = req.params; + + const productData = await prisma.product.findUniqueOrThrow({ + where: { id: productId }, + include: { + seller: { select: { id: true, nickname: true, email: true } }, + }, + }); + + const responseData = { + id: productData.id, + status: productData.status, + productName: productData.name, + description: productData.description, + price: productData.price, + tags: productData.tags, + sellerName: productData.seller.nickname, + sellerId: productData.seller.id, + email: productData.seller.email, + createdAt: productData.createdAt, + updatedAt: productData.updatedAt, + }; + + return res.status(200).json(responseData); +}; + +//PATCH id========== +const patchProductById = async (req, res, next) => { + const { productId } = req.params; + const inputData = req.body; + + const newPatchData = await prisma.product.update({ + where: { id: productId }, + data: inputData, + include: { + seller: { select: { id: true, nickname: true, email: true } }, + }, + }); + + const responseData = { + id: newPatchData.id, + status: newPatchData.status, + productName: newPatchData.name, + description: newPatchData.description, + price: newPatchData.price, + tags: newPatchData.tags, + sellerName: newPatchData.seller.nickname, + sellerId: newPatchData.seller.id, + email: newPatchData.seller.email, + createdAt: newPatchData.createdAt, + updatedAt: newPatchData.updatedAt, + }; + res.status(200).json(responseData); +}; + +//DELETE id========== +const deleteProductById = async (req, res, next) => { + const { productId } = req.params; + + await prisma.product.delete({ + where: { id: productId }, + }); + res.status(200).json({ message: '제품 삭제 성공' }); +}; + +export { createProduct, getListProducts, getProductById, patchProductById, deleteProductById }; diff --git a/src/routes/product.router.js b/src/routes/product.router.js new file mode 100644 index 00000000..8d020f87 --- /dev/null +++ b/src/routes/product.router.js @@ -0,0 +1,60 @@ +import express from 'express'; +import withAsync from '../lib/withAsync.js'; +import { validate } from '../middlewares/validator.js'; +import { + ProductIdStruct, + CreateProductStruct, + PatchProductStruct, + CreateCommentStruct, +} from '../lib/structs.js'; +import { + createProduct, + getListProducts, + getProductById, + patchProductById, + deleteProductById, +} from '../controllers/product.controller.js'; +import { + createCommentForProduct, + getCommentListProduct, +} from '../controllers/comment.controller.js'; + +//.post (validate(CreateProductStruct, 'body'), createProduct); + +const router = express.Router(); + +router + .route('/') + //POST, GET + .post(validate(CreateProductStruct, 'body'), withAsync(createProduct)) + .get(withAsync(getListProducts)); + +router + .route('/:productId') + //GET id, + .get(validate(ProductIdStruct, 'params'), withAsync(getProductById)) + + //PATCH id, + .patch( + validate(ProductIdStruct, 'params'), + validate(PatchProductStruct, 'body'), + withAsync(patchProductById), + ) + + //DELETE id + .delete(validate(ProductIdStruct, 'params'), withAsync(deleteProductById)); + +//중고 장터 +router + .route('/:productId/comments') + //POST + .post( + validate(ProductIdStruct, 'params'), + validate(CreateCommentStruct, 'body'), + withAsync(createCommentForProduct), + ) //중고장터 댓글 + + //GET + .get(validate(ProductIdStruct, 'params'), withAsync(getCommentListProduct)); //중고장터 댓글 + +export default router; From 5c3decdf6cd966fcc3d06224624373b304009101 Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Tue, 25 Nov 2025 19:38:45 +0900 Subject: [PATCH 09/18] =?UTF-8?q?=F0=9F=9A=80=20feat(server):=20Product=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=84=B0=20=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20?= =?UTF-8?q?=EC=A0=84=EC=97=AD=20=EC=97=90=EB=9F=AC=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app.js b/src/app.js index ce20f2f3..8bd1bdc0 100644 --- a/src/app.js +++ b/src/app.js @@ -1,9 +1,9 @@ import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; -// import errorHandler from './middlewares/errorHandler.js'; +import errorHandler from './middlewares/errorHandler.js'; // import userRouter from './routes/user.router.js'; -// import productRouter from './routes/product.router.js'; +import productRouter from './routes/product.router.js'; // import articleRouter from './routes/article.router.js'; // import commentRouter from './routes/comment.router.js'; // import uploadRouter from './routes/upload.router.js'; @@ -18,8 +18,8 @@ app.get('/', (req, res) => { res.send('두려워하지 마십시오. 죽음이 끝은 아닙니다.'); }); -// //중고마켓 -// app.use('/products', productRouter); +//중고마켓 +app.use('/products', productRouter); // //자유게시판 // app.use('/articles', articleRouter); @@ -30,8 +30,8 @@ app.get('/', (req, res) => { // //이미지 // app.use('/uploads', uploadRouter); -// //마지막에 실행. -// app.use(errorHandler); +//마지막에 실행. +app.use(errorHandler); app.listen(process.env.PORT || 3000, () => { console.log(`열려라 참깨! 시스템: ${process.env.PORT || 3000} 문이 열립니다. ( b^-^)b`); From c2f42a2f185f9dbcf828cb4d5e76b7b85daeda9d Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Wed, 26 Nov 2025 14:25:02 +0900 Subject: [PATCH 10/18] =?UTF-8?q?=F0=9F=9A=80=20feat(auth):=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=EA=B0=80=EC=9E=85=20API=20=EA=B5=AC=ED=98=84=20(bcryp?= =?UTF-8?q?t=20=EC=95=94=ED=98=B8=ED=99=94=20=EB=B0=8F=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EC=A0=81=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.js | 3 +++ src/controllers/auth.controller.js | 39 ++++++++++++++++++++++++++++++ src/routes/auth.router.js | 21 ++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 src/controllers/auth.controller.js create mode 100644 src/routes/auth.router.js diff --git a/src/app.js b/src/app.js index 8bd1bdc0..2cb98dc3 100644 --- a/src/app.js +++ b/src/app.js @@ -2,6 +2,7 @@ import express from 'express'; import cors from 'cors'; import dotenv from 'dotenv'; import errorHandler from './middlewares/errorHandler.js'; +import authRouter from './routes/auth.router.js'; // import userRouter from './routes/user.router.js'; import productRouter from './routes/product.router.js'; // import articleRouter from './routes/article.router.js'; @@ -18,6 +19,8 @@ app.get('/', (req, res) => { res.send('두려워하지 마십시오. 죽음이 끝은 아닙니다.'); }); +app.use('/auth', authRouter); + //중고마켓 app.use('/products', productRouter); diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js new file mode 100644 index 00000000..5bb39827 --- /dev/null +++ b/src/controllers/auth.controller.js @@ -0,0 +1,39 @@ +import bcrypt from 'bcrypt'; +import prisma from '../lib/prisma.js'; + +// 회원가입 POST (/auth/sign-up) +export const signUp = async (req, res, next) => { + //요청바디 (입력 내용들에서 정보 꺼내기) + const { email, nickname, password } = req.body; + + // 비밀번호 암호화 + const hashedPassword = await bcrypt.hash(password, 10); + + // 해싱된 비밀번호로 유저 생성하기 + const user = await prisma.user.create({ + data: { + email, + nickname, + password: hashedPassword, + }, + }); + + // 비번을 제외한 정보 보여주기 + const responseData = { + id: user.id, + email: user.email, + nickname: user.nickname, + description: user.description, + image: user.image, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }; + + return res.status(201).json(responseData); +}; + +// 로그인 POST (/auth/login) + +// 토큰 재발급 POST (/auth/refresh) + +// 로그아웃 POST (/auth/logout) diff --git a/src/routes/auth.router.js b/src/routes/auth.router.js new file mode 100644 index 00000000..5bce7420 --- /dev/null +++ b/src/routes/auth.router.js @@ -0,0 +1,21 @@ +import express from 'express'; +import withAsync from '../lib/withAsync.js'; +import { validate } from '../middlewares/validator.js'; +import { signUp } from '../controllers/auth.controller.js'; +import { CreateUserStruct } from '../lib/structs.js'; + +const router = express.Router(); + +// =========== 회원가입 POST (/auth/signUp) +router.route('/sign-up').post(validate(CreateUserStruct), withAsync(signUp)); + +// =========== 로그인 POST (/auth/login) +// router.route('/login').post(validate(), withAsync()); + +// =========== 토큰 재발급 POST (/auth/refresh) +// router.route('/refresh').post(validate(), withAsync()); + +// =========== 로그아웃 POST (/auth/logout) +// router.route('/logout').post(validate(), withAsync()); + +export default router; From c4dcd32b1fc152ca803c64b9c88e149f1e45a7dd Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Wed, 26 Nov 2025 16:05:52 +0900 Subject: [PATCH 11/18] =?UTF-8?q?=F0=9F=9A=80=20feat(auth):=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20API=20=EA=B5=AC=ED=98=84=20(JWT=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EB=A1=9C=EC=A7=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/auth.controller.js | 24 ++++++++++++++++++++++++ src/lib/constants.js | 1 + src/lib/structs.js | 10 +++++++++- src/routes/auth.router.js | 6 +++--- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 5bb39827..933e81a0 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -1,5 +1,7 @@ +import jwt from 'jsonwebtoken'; import bcrypt from 'bcrypt'; import prisma from '../lib/prisma.js'; +import { JWT_SECRET } from '../lib/constants.js'; // 회원가입 POST (/auth/sign-up) export const signUp = async (req, res, next) => { @@ -33,7 +35,29 @@ export const signUp = async (req, res, next) => { }; // 로그인 POST (/auth/login) +export const login = async (req, res, next) => { + const { email, password } = req.body; + //이메일로 유저찾기 + const user = await prisma.user.findUnique({ + //OrThrow를 안 쓰는 이유 => 보안을 위해서 정확히 무슨 오류인지 안 알려주려고 + where: { email }, + }); + + // 유저가 없거나 비밀번호가 틀리면 -> 401 (인증실패) + if (!user || !(await bcrypt.compare(password, user.password))) { + return res.status(401).json({ message: '이메일 또는 비밀번호가 일치하지 않습니다.' }); + } + + // 로그인 성공 시 토큰 발급 + const accessToken = jwt.sign( + { id: user.id, email: user.email, nickname: user.nickname }, + JWT_SECRET, + { expiresIn: '1h' }, + ); + + return res.status(200).json({ accessToken }); +}; // 토큰 재발급 POST (/auth/refresh) // 로그아웃 POST (/auth/logout) diff --git a/src/lib/constants.js b/src/lib/constants.js index ed1ee1f5..2b9de145 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -5,3 +5,4 @@ export const DATABASE_URL = process.env.DATABASE_URL; export const PORT = process.env.PORT || 3000; // export const PUBLIC_PATH = './public'; // export const STATIC_PATH = '/public'; +export const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key'; diff --git a/src/lib/structs.js b/src/lib/structs.js index a643d829..c507e7c1 100644 --- a/src/lib/structs.js +++ b/src/lib/structs.js @@ -24,13 +24,21 @@ export const CommentIdStruct = s.object({ //===== req.body 검사 ===== -//----- User ----- +//----- User / Auth ----- + +// sign-up export const CreateUserStruct = s.object({ email: s.size(s.string(), 1, 100), nickname: s.size(s.string(), 1, 10), password: s.size(s.string(), 4, 20), }); +// login +export const LoginUserStruct = s.object({ + email: s.size(s.string(), 1, 100), + password: s.size(s.string(), 4, 20), +}); + // partial을 쓰면 내부의 모든 필드가 자동으로 s.optional() 처리가 됨. export const PatchUserStruct = s.partial(CreateUserStruct); diff --git a/src/routes/auth.router.js b/src/routes/auth.router.js index 5bce7420..bb4a6ad0 100644 --- a/src/routes/auth.router.js +++ b/src/routes/auth.router.js @@ -1,8 +1,8 @@ import express from 'express'; import withAsync from '../lib/withAsync.js'; import { validate } from '../middlewares/validator.js'; -import { signUp } from '../controllers/auth.controller.js'; -import { CreateUserStruct } from '../lib/structs.js'; +import { login, signUp } from '../controllers/auth.controller.js'; +import { CreateUserStruct, LoginUserStruct } from '../lib/structs.js'; const router = express.Router(); @@ -10,7 +10,7 @@ const router = express.Router(); router.route('/sign-up').post(validate(CreateUserStruct), withAsync(signUp)); // =========== 로그인 POST (/auth/login) -// router.route('/login').post(validate(), withAsync()); +router.route('/login').post(validate(LoginUserStruct), withAsync(login)); // =========== 토큰 재발급 POST (/auth/refresh) // router.route('/refresh').post(validate(), withAsync()); From 6e64b8e6502c5ace1aa4048c4dc8336d83d4e5df Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Wed, 26 Nov 2025 17:12:20 +0900 Subject: [PATCH 12/18] =?UTF-8?q?=F0=9F=9A=80=20feat(auth):=20JWT=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=AF=B8=EB=93=A4=EC=9B=A8=EC=96=B4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20README=20=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 10 +++++----- src/middlewares/auth.middleware.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 src/middlewares/auth.middleware.js diff --git a/README.md b/README.md index 5e3ba9de..8f3ccd42 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ - [ ] **환경변수(.env) 설정** - [x] `DATABASE_URL` 확인 - - [ ] `JWT_SECRET` (토큰 비밀키) + - [x] `JWT_SECRET` (토큰 비밀키) - [ ] **User 스키마 작성** - [x] 필드 구성: `id`, `email`, `nickname`, `image`, `password`, `createdAt`, `updatedAt` - [x] 기존 모델(`Product`, `Article`, `Comment`)과 1:N 관계 설정 (`relation` 연결) @@ -22,16 +22,16 @@ - [ ] **회원가입 API 구현** - [x] 입력: `email`, `nickname`, `password` - - [ ] **중요:** 비밀번호는 반드시 **해싱(Hashing)** 하여 저장 (bcrypt 등 사용) + - [x] **중요:** 비밀번호는 반드시 **해싱(Hashing)** 하여 저장 (bcrypt 등 사용) - [ ] **로그인 API 구현** - - [ ] 입력: `email`, `password` 검증 - - [ ] 성공 시: **Access Token (JWT)** 발급 및 반환 + - [x] 입력: `email`, `password` 검증 + - [x] 성공 시: **Access Token (JWT)** 발급 및 반환 ### 👮 3. 인가 (Authorization) - 권한 체크 > **공통 규칙:** 로그인한 유저만 등록 가능 / 본인만 수정, 삭제 가능 -- [ ] **인가 미들웨어(Middleware) 구현** (토큰 검증 및 유저 확인) +- [x] **인가 미들웨어(Middleware) 구현** (토큰 검증 및 유저 확인) - [ ] **상품(Product) 기능 인가** - [ ] 등록: 로그인한 유저만 가능 - [ ] 수정/삭제: 상품 등록자(본인)만 가능 diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js new file mode 100644 index 00000000..5c18ca03 --- /dev/null +++ b/src/middlewares/auth.middleware.js @@ -0,0 +1,29 @@ +import jwt from 'jsonwebtoken'; +import { JWT_SECRET } from '../lib/constants.js'; + +export const authMiddleware = (req, res, next) => { + // req.헤더에서 토큰 꺼내기 + const authHeader = req.headers['authorization']; + + // 토큰이 없다면 ? return 401 + if (!authHeader) { + return res.status(401).json({ message: '인증 토큰이 필요합니다.' }); + } + + // Bearer 떼어내기 (보낸사람: 내용 토큰 < 뭐 이렇게 있으면 보낸사람 빼고 '토큰'만 들고오겠다고 하는거) + // 근데 덜렁 내용만 보내져있다면 그냥 그거 갖다 쓰겠다는 뜻 + const token = authHeader.split(' ')[1] || authHeader; + + // 위조 검사 + try { + //JWT_SECRET은 대충 나의 도장이고, 토큰은 저 도장으로 찍은 것이다. => 도장 그림과 토큰에 찍힌 도장의 그림이 일치하는지 확인 + const decoded = jwt.verify(token, JWT_SECRET); + + //위조심사를 통과했다면? + req.user = decoded; + + next(); //관문 통과 + } catch (error) { + return res.status(401).json({ message: '유효하지 않은 토큰입니다.' }); + } +}; From cc9d5e14d96dd33d3f19b2764fe821f06c9ad6f2 Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Wed, 26 Nov 2025 19:16:04 +0900 Subject: [PATCH 13/18] =?UTF-8?q?=F0=9F=9A=80=20feat(user):=20=EB=82=B4=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(Auth=20Middleware=20=EC=A0=81=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +-- src/app.js | 5 +++- src/controllers/user.controller.js | 40 ++++++++++++++++++++++++++++++ src/routes/user.router.js | 14 +++++++++++ 4 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 src/controllers/user.controller.js create mode 100644 src/routes/user.router.js diff --git a/README.md b/README.md index 8f3ccd42..eb7fc5c0 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ ### 👤 4. 유저 정보 관리 (My Page) -- [ ] **내 정보 조회 API** - - [ ] 응답에 `password` 제외할 것 +- [x] **내 정보 조회 API** + - [x] 응답에 `password` 제외할 것 - [ ] **내 정보 수정 API** - [ ] **비밀번호 변경 API** (기존 비번 확인 과정 권장) - [ ] **내가 등록한 상품 목록 조회 API** diff --git a/src/app.js b/src/app.js index 2cb98dc3..b6595c46 100644 --- a/src/app.js +++ b/src/app.js @@ -3,8 +3,8 @@ import cors from 'cors'; import dotenv from 'dotenv'; import errorHandler from './middlewares/errorHandler.js'; import authRouter from './routes/auth.router.js'; -// import userRouter from './routes/user.router.js'; import productRouter from './routes/product.router.js'; +import userRouter from './routes/user.router.js'; // import articleRouter from './routes/article.router.js'; // import commentRouter from './routes/comment.router.js'; // import uploadRouter from './routes/upload.router.js'; @@ -21,6 +21,9 @@ app.get('/', (req, res) => { app.use('/auth', authRouter); +// 유저 관련 요청 +app.use('/users', userRouter); + //중고마켓 app.use('/products', productRouter); diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js new file mode 100644 index 00000000..5485c630 --- /dev/null +++ b/src/controllers/user.controller.js @@ -0,0 +1,40 @@ +import prisma from '../lib/prisma.js'; +/** +200 OK: 일반적인 성공 (GET, UPDATE 후) +201 Created: 새로운 리소스 생성 성공 (POST) +204 No Content: 성공했지만 돌려줄 데이터가 없음 (DELETE) +400 Bad Request: 클라이언트 요청 오류 (유효성 검사 실패 등) +404 Not Found: 요청한 리소스가 없음 + */ + +// 내 정보 조회 GET (/users/me) +export const getUserMe = async (req, res, next) => { + // authMiddleware 가 붙여준 req.user 확인 + // 미들웨어 통과 시 req.user 안에 {id, email , ...} 정보가 있을 것. 거기서 id 꺼내기 + const { id } = req.user; + + const user = await prisma.user.findUnique({ + where: { id }, // 위에서 가져온 아이디로 찾기 + }); + + // 만약 없거나 탈퇴/삭제된 유저라면 return 404 + if (!user) { + return res.status(404).json({ message: '유저 정보를 찾을 수 없습니다.' }); + } + + // 정보 포장하기 + const responseData = { + id: user.id, + email: user.email, + nickname: user.nickname, + description: user.description, + image: user.image, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }; + + return res.status(200).json(responseData); +}; + +// 내 정보 수정 PATCH (/users/me) +// 회원 탈퇴 DELETE (/users/me) diff --git a/src/routes/user.router.js b/src/routes/user.router.js new file mode 100644 index 00000000..aa48a785 --- /dev/null +++ b/src/routes/user.router.js @@ -0,0 +1,14 @@ +import express from 'express'; +import withAsync from '../lib/withAsync.js'; +import { authMiddleware } from '../middlewares/auth.middleware.js'; +import { getUserMe } from '../controllers/user.controller.js'; + +const router = express.Router(); + +// (/users -> /me) +router.route('/me').get(authMiddleware, withAsync(getUserMe)); +// 내 정보 조회 GET (/users/me) +// 내 정보 수정 PATCH (/users/me) +// 회원 탈퇴 DELETE (/users/me) + +export default router; From c4345c7ba8f65631b50f584c948d81e1b5edbab1 Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Wed, 26 Nov 2025 20:04:01 +0900 Subject: [PATCH 14/18] =?UTF-8?q?=F0=9F=9A=80=20feat(user):=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C/=EC=88=98?= =?UTF-8?q?=EC=A0=95/=ED=83=88=ED=87=B4=20API=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B0=8F=20README=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/controllers/user.controller.js | 31 ++++++++++++++++++++++++++++++ src/lib/structs.js | 15 ++++++--------- src/routes/user.router.js | 20 +++++++++++++++---- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index eb7fc5c0..dd7bea86 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ - [x] **내 정보 조회 API** - [x] 응답에 `password` 제외할 것 -- [ ] **내 정보 수정 API** +- [x] **내 정보 수정 API** - [ ] **비밀번호 변경 API** (기존 비번 확인 과정 권장) - [ ] **내가 등록한 상품 목록 조회 API** diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index 5485c630..5f175e3c 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -37,4 +37,35 @@ export const getUserMe = async (req, res, next) => { }; // 내 정보 수정 PATCH (/users/me) +export const patchUserMe = async (req, res, next) => { + const { id } = req.user; //토큰에서 아이디 쏙 + const inputData = req.body; + + const updatedUser = await prisma.user.update({ + where: { id }, + data: inputData, + }); + + // 정보 포장하기 + const responseData = { + id: updatedUser.id, + email: updatedUser.email, + nickname: updatedUser.nickname, + description: updatedUser.description, + image: updatedUser.image, + updatedAt: updatedUser.updatedAt, + }; + + return res.status(200).json(responseData); +}; + // 회원 탈퇴 DELETE (/users/me) +export const deleteUserMe = async (req, res, next) => { + const { id } = req.user; + + await prisma.user.delete({ + where: { id }, + }); + + return res.status(200).json({ message: '회원 탈퇴가 완료되었습니다.' }); +}; diff --git a/src/lib/structs.js b/src/lib/structs.js index c507e7c1..78d28ac7 100644 --- a/src/lib/structs.js +++ b/src/lib/structs.js @@ -40,7 +40,12 @@ export const LoginUserStruct = s.object({ }); // partial을 쓰면 내부의 모든 필드가 자동으로 s.optional() 처리가 됨. -export const PatchUserStruct = s.partial(CreateUserStruct); +export const PatchUserStruct = s.object({ + nickname: s.optional(s.size(s.string(), 1, 20)), + description: s.optional(s.size(s.string(), 1, 300)), + image: s.optional(s.size(s.string())), + // (보안상 이메일과 비밀번호 변경은 별도 API로 빼기로 함.) +}); //----- product ----- export const CreateProductStruct = s.object({ @@ -48,8 +53,6 @@ export const CreateProductStruct = s.object({ description: s.size(s.string(), 1, 500), price: s.min(s.number(), 0), tags: s.optional(s.array(s.string())), - // 🚧 [임시] 로그인 만들기 전까지만 - sellerId: Integer, }); export const PatchProductStruct = s.partial(CreateProductStruct); @@ -58,9 +61,6 @@ export const PatchProductStruct = s.partial(CreateProductStruct); export const CreateArticleStruct = s.object({ title: s.size(s.string(), 1, 30), content: s.size(s.string(), 10, 1000), - - // 🚧 [임시] 로그인 만들기 전까지만 - authorId: Integer, }); export const PatchArticleStruct = s.partial(CreateArticleStruct); @@ -68,9 +68,6 @@ export const PatchArticleStruct = s.partial(CreateArticleStruct); //----- comment ----- export const CreateCommentStruct = s.object({ content: s.size(s.string(), 10, 200), - - // 🚧 [임시] 로그인 만들기 전까지만 - authorId: Integer, }); export const PatchCommentStruct = s.partial(CreateCommentStruct); diff --git a/src/routes/user.router.js b/src/routes/user.router.js index aa48a785..b847f452 100644 --- a/src/routes/user.router.js +++ b/src/routes/user.router.js @@ -1,14 +1,26 @@ import express from 'express'; import withAsync from '../lib/withAsync.js'; import { authMiddleware } from '../middlewares/auth.middleware.js'; -import { getUserMe } from '../controllers/user.controller.js'; +import { validate } from '../middlewares/validator.js'; +import { PatchUserStruct } from '../lib/structs.js'; +import { getUserMe, patchUserMe, deleteUserMe } from '../controllers/user.controller.js'; const router = express.Router(); // (/users -> /me) -router.route('/me').get(authMiddleware, withAsync(getUserMe)); // 내 정보 조회 GET (/users/me) -// 내 정보 수정 PATCH (/users/me) -// 회원 탈퇴 DELETE (/users/me) +router + .route('/me') + .get(authMiddleware, withAsync(getUserMe)) + + // 내 정보 수정 PATCH (/users/me) + .patch( + authMiddleware, // 로그인 확인 + validate(PatchUserStruct, 'body'), // 패치 내용 유효성 검사 + withAsync(patchUserMe), // 수정 실행 + ) + + // 회원 탈퇴 DELETE (/users/me) + .delete(authMiddleware, withAsync(deleteUserMe)); export default router; From 1f75b77393890b6ae980f6ac4686304f6eeb4397 Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Wed, 26 Nov 2025 20:41:52 +0900 Subject: [PATCH 15/18] =?UTF-8?q?=F0=9F=9A=80=20feat(product):=20=EC=83=81?= =?UTF-8?q?=ED=92=88=20API=20=EC=9D=B8=EA=B0=80(Authorization)=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20=EB=B0=8F=20=EB=B3=B8=EC=9D=B8=20=ED=99=95=EC=9D=B8?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +++---- src/controllers/product.controller.js | 58 +++++++++++++++++++-------- src/lib/structs.js | 1 - src/routes/product.router.js | 10 +++-- 4 files changed, 54 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index dd7bea86..72205a07 100644 --- a/README.md +++ b/README.md @@ -11,19 +11,19 @@ ### 🛠️ 1. 초기 세팅 & 데이터베이스 (Prisma) -- [ ] **환경변수(.env) 설정** +- [x] **환경변수(.env) 설정** - [x] `DATABASE_URL` 확인 - [x] `JWT_SECRET` (토큰 비밀키) -- [ ] **User 스키마 작성** +- [x] **User 스키마 작성** - [x] 필드 구성: `id`, `email`, `nickname`, `image`, `password`, `createdAt`, `updatedAt` - [x] 기존 모델(`Product`, `Article`, `Comment`)과 1:N 관계 설정 (`relation` 연결) ### 🔐 2. 인증 (Authentication) - 로그인/회원가입 -- [ ] **회원가입 API 구현** +- [x] **회원가입 API 구현** - [x] 입력: `email`, `nickname`, `password` - [x] **중요:** 비밀번호는 반드시 **해싱(Hashing)** 하여 저장 (bcrypt 등 사용) -- [ ] **로그인 API 구현** +- [x] **로그인 API 구현** - [x] 입력: `email`, `password` 검증 - [x] 성공 시: **Access Token (JWT)** 발급 및 반환 @@ -32,9 +32,9 @@ > **공통 규칙:** 로그인한 유저만 등록 가능 / 본인만 수정, 삭제 가능 - [x] **인가 미들웨어(Middleware) 구현** (토큰 검증 및 유저 확인) -- [ ] **상품(Product) 기능 인가** - - [ ] 등록: 로그인한 유저만 가능 - - [ ] 수정/삭제: 상품 등록자(본인)만 가능 +- [x] **상품(Product) 기능 인가** + - [x] 등록: 로그인한 유저만 가능 + - [x] 수정/삭제: 상품 등록자(본인)만 가능 - [ ] **게시글(Article) 기능 인가** - [ ] 등록: 로그인한 유저만 가능 - [ ] 수정/삭제: 게시글 작성자(본인)만 가능 diff --git a/src/controllers/product.controller.js b/src/controllers/product.controller.js index e72acfd0..062ff75c 100644 --- a/src/controllers/product.controller.js +++ b/src/controllers/product.controller.js @@ -3,10 +3,11 @@ import { getOrderBy } from '../lib/utils.js'; //POST========== const createProduct = async (req, res, next) => { + const { id: sellerId } = req.user; //토큰에서 ID 꺼내기 (로그인 한 사람만 가능하니까) const inputData = req.body; const productData = await prisma.product.create({ - data: inputData, + data: { ...inputData, sellerId }, include: { seller: { select: { id: true, nickname: true, email: true }, @@ -113,41 +114,64 @@ const getProductById = async (req, res, next) => { //PATCH id========== const patchProductById = async (req, res, next) => { + const { id: userId } = req.user; const { productId } = req.params; const inputData = req.body; - const newPatchData = await prisma.product.update({ + // 1. 상품이 존재하는지, 누가 주인인지 확인 + const product = await prisma.product.findUniqueOrThrow({ + where: { id: productId }, + }); + + // 2. 본인 확인 (작성자 본인의 상품이 아니라면 return 403) + if (product.sellerId !== userId) { + return res.status(403).json({ message: '수정 권한이 없습니다.' }); + } + + // 3. 마침내 상품 업데이트 + const updatedProduct = await prisma.product.update({ where: { id: productId }, data: inputData, - include: { - seller: { select: { id: true, nickname: true, email: true } }, - }, + include: { seller: { select: { id: true, nickname: true, email: true } } }, }); const responseData = { - id: newPatchData.id, - status: newPatchData.status, - productName: newPatchData.name, - description: newPatchData.description, - price: newPatchData.price, - tags: newPatchData.tags, - sellerName: newPatchData.seller.nickname, - sellerId: newPatchData.seller.id, - email: newPatchData.seller.email, - createdAt: newPatchData.createdAt, - updatedAt: newPatchData.updatedAt, + id: updatedProduct.id, + status: updatedProduct.status, + productName: updatedProduct.name, + description: updatedProduct.description, + price: updatedProduct.price, + tags: updatedProduct.tags, + sellerName: updatedProduct.seller.nickname, + sellerId: updatedProduct.seller.id, + email: updatedProduct.seller.email, + createdAt: updatedProduct.createdAt, + updatedAt: updatedProduct.updatedAt, }; res.status(200).json(responseData); }; //DELETE id========== const deleteProductById = async (req, res, next) => { + const { id: userId } = req.user; const { productId } = req.params; + // 1. 상품이 존재하는지, 누가 주인인지 확인 + const product = await prisma.product.findUniqueOrThrow({ + where: { id: productId }, + }); + + // 2. 본인 확인 (작성자 본인의 상품이 아니라면 return 403) + if (product.sellerId !== userId) { + return res.status(403).json({ message: '삭제 권한이 없습니다.' }); + } + + //3. 상품 삭제하기 await prisma.product.delete({ where: { id: productId }, }); - res.status(200).json({ message: '제품 삭제 성공' }); + + return res.status(200).json({ message: '제품 삭제 성공' }); }; export { createProduct, getListProducts, getProductById, patchProductById, deleteProductById }; diff --git a/src/lib/structs.js b/src/lib/structs.js index 78d28ac7..b41a895c 100644 --- a/src/lib/structs.js +++ b/src/lib/structs.js @@ -23,7 +23,6 @@ export const CommentIdStruct = s.object({ }); //===== req.body 검사 ===== - //----- User / Auth ----- // sign-up diff --git a/src/routes/product.router.js b/src/routes/product.router.js index 8d020f87..e0913829 100644 --- a/src/routes/product.router.js +++ b/src/routes/product.router.js @@ -1,5 +1,6 @@ import express from 'express'; import withAsync from '../lib/withAsync.js'; +import { authMiddleware } from '../middlewares/auth.middleware.js'; import { validate } from '../middlewares/validator.js'; import { ProductIdStruct, @@ -19,14 +20,13 @@ import { getCommentListProduct, } from '../controllers/comment.controller.js'; -//.post (validate(CreateProductStruct, 'body'), createProduct); - const router = express.Router(); +// 라우트 =================================================== router .route('/') //POST, GET - .post(validate(CreateProductStruct, 'body'), withAsync(createProduct)) + .post(authMiddleware, validate(CreateProductStruct, 'body'), withAsync(createProduct)) .get(withAsync(getListProducts)); router @@ -36,19 +36,21 @@ router //PATCH id, .patch( + authMiddleware, validate(ProductIdStruct, 'params'), validate(PatchProductStruct, 'body'), withAsync(patchProductById), ) //DELETE id - .delete(validate(ProductIdStruct, 'params'), withAsync(deleteProductById)); + .delete(authMiddleware, validate(ProductIdStruct, 'params'), withAsync(deleteProductById)); //중고 장터 router .route('/:productId/comments') //POST .post( + authMiddleware, validate(ProductIdStruct, 'params'), validate(CreateCommentStruct, 'body'), withAsync(createCommentForProduct), From 947339b3f6e0e0d02777daeb3e4ce0bdabaf3ca5 Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Thu, 27 Nov 2025 14:41:34 +0900 Subject: [PATCH 16/18] =?UTF-8?q?=F0=9F=9A=80=20feat(article):=20=EC=9E=90?= =?UTF-8?q?=EC=9C=A0=EA=B2=8C=EC=8B=9C=ED=8C=90=20CRUD=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(=EC=9D=B8=EA=B0=80=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9)=20=EB=B0=8F=20README=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- src/app.js | 4 +- src/controllers/article.controller.js | 155 ++++++++++++++++++++++++++ src/routes/article.router.js | 65 +++++++++++ 4 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 src/controllers/article.controller.js create mode 100644 src/routes/article.router.js diff --git a/README.md b/README.md index 72205a07..9f0f7410 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ - [x] **상품(Product) 기능 인가** - [x] 등록: 로그인한 유저만 가능 - [x] 수정/삭제: 상품 등록자(본인)만 가능 -- [ ] **게시글(Article) 기능 인가** - - [ ] 등록: 로그인한 유저만 가능 - - [ ] 수정/삭제: 게시글 작성자(본인)만 가능 +- [x] **게시글(Article) 기능 인가** + - [x] 등록: 로그인한 유저만 가능 + - [x] 수정/삭제: 게시글 작성자(본인)만 가능 - [ ] **댓글(Comment) 기능 인가** - [ ] 등록: 상품/게시글에 댓글 달기 (로그인 유저만) - [ ] 수정/삭제: 댓글 작성자(본인)만 가능 diff --git a/src/app.js b/src/app.js index b6595c46..3ebb8324 100644 --- a/src/app.js +++ b/src/app.js @@ -5,7 +5,7 @@ import errorHandler from './middlewares/errorHandler.js'; import authRouter from './routes/auth.router.js'; import productRouter from './routes/product.router.js'; import userRouter from './routes/user.router.js'; -// import articleRouter from './routes/article.router.js'; +import articleRouter from './routes/article.router.js'; // import commentRouter from './routes/comment.router.js'; // import uploadRouter from './routes/upload.router.js'; @@ -28,7 +28,7 @@ app.use('/users', userRouter); app.use('/products', productRouter); // //자유게시판 -// app.use('/articles', articleRouter); +app.use('/articles', articleRouter); // //댓글 // app.use('/comments', commentRouter); diff --git a/src/controllers/article.controller.js b/src/controllers/article.controller.js new file mode 100644 index 00000000..1d68055f --- /dev/null +++ b/src/controllers/article.controller.js @@ -0,0 +1,155 @@ +import prisma from '../lib/prisma.js'; +import { getOrderBy } from '../lib/utils.js'; + +//[createArticle, getListArticles, getArticleById, patchArticleById, deleteArticleById] + +// POST ========== +export const createArticle = async (req, res, next) => { + const { id: authorId } = req.user; + const inputData = req.body; + + const articleData = await prisma.article.create({ + data: { ...inputData, authorId }, + include: { author: { select: { nickname: true, id: true, email: true } } }, + }); + + // 정보 포장 + const responseData = { + id: articleData.id, + title: articleData.title, + content: articleData.content, + authorName: articleData.author.nickname, + authorId: articleData.authorId, + authorEmail: articleData.author.email, + createdAt: articleData.createdAt, + updatedAt: articleData.updatedAt, + }; + + return res.status(201).json(responseData); +}; + +// GET List========== +export const getListArticles = async (req, res, next) => { + const { offset = 0, limit = 10, order = 'recent', search } = req.query; + + const where = search + ? { + OR: [ + { title: { contains: search, mode: 'insensitive' } }, + { content: { contains: search, mode: 'insensitive' } }, + { author: { nickname: { contains: search, mode: 'insensitive' } } }, + ], + } + : undefined; + + const [articleData, totalCount] = await Promise.all([ + prisma.article.findMany({ + where, + orderBy: getOrderBy(order), + skip: parseInt(offset), + take: parseInt(limit), + include: { author: { select: { nickname: true, id: true, email: true } } }, + }), + prisma.article.count({ where }), + ]); + + // 정보 포장 + const list = articleData.map((article) => ({ + id: article.id, + title: article.title, + content: article.content, + authorName: article.author.nickname, + authorId: article.authorId, + authorEmail: article.author.email, + createdAt: article.createdAt, + updatedAt: article.updatedAt, + //commentCount: 0 + })); + + return res.status(200).json({ totalCount, list }); +}; + +// GET id ========== +export const getArticleById = async (req, res, next) => { + const { articleId } = req.params; + + const articleData = await prisma.article.findUniqueOrThrow({ + where: { id: articleId }, + include: { author: { select: { id: true, nickname: true, email: true } } }, + }); + + // 정보 포장 + const responseData = { + id: articleData.id, + title: articleData.title, + content: articleData.content, + authorName: articleData.author.nickname, + authorId: articleData.authorId, + authorEmail: articleData.author.email, + createdAt: articleData.createdAt, + updatedAt: articleData.updatedAt, + }; + + return res.status(200).json(responseData); +}; + +// PATCH id ========== +export const patchArticleById = async (req, res, next) => { + const { id: userId } = req.user; //헷갈림 방지를 위한 authorId 대신 userId 사용 + const { articleId } = req.params; + const inputData = req.body; + + // 1. 자유 게시글이 존재하는지, 누가 주인인지 확인 + const article = await prisma.article.findUniqueOrThrow({ + where: { id: articleId }, + }); + + // 2. 본인 확인 (작성자 본인의 자유 게시글이 아니라면 return 403) + if (article.authorId !== userId) { + return res.status(403).json({ message: '수정 권한이 없습니다.' }); + } + + // 3. 자유 게시글 수정하기 + const updateArticle = await prisma.article.update({ + where: { id: articleId }, + data: inputData, + include: { author: { select: { id: true, nickname: true, email: true } } }, + }); + + // 정보 포장 + const responseData = { + id: updateArticle.id, + title: updateArticle.title, + content: updateArticle.content, + authorName: updateArticle.author.nickname, + authorId: updateArticle.authorId, + authorEmail: updateArticle.author.email, + createdAt: updateArticle.createdAt, + updatedAt: updateArticle.updatedAt, + }; + + return res.status(200).json(responseData); +}; + +// DELETE id ========== +export const deleteArticleById = async (req, res, next) => { + const { id: userId } = req.user; + const { articleId } = req.params; + + // 1. 자유 게시글이 존재하는지, 누가 주인인지 확인 + const article = await prisma.article.findUniqueOrThrow({ + where: { id: articleId }, + }); + + // 2. 본인 확인 (작성자 본인의 자유 게시글이 아니라면 return 403) + if (article.authorId !== userId) { + return res.status(403).json({ message: '삭제 권한이 없습니다.' }); + } + + //3. 자유 게시글 삭제하기 + await prisma.article.delete({ + where: { id: articleId }, + }); + + return res.status(200).json({ message: '게시글이 삭제되었습니다.' }); +}; diff --git a/src/routes/article.router.js b/src/routes/article.router.js new file mode 100644 index 00000000..f128ae76 --- /dev/null +++ b/src/routes/article.router.js @@ -0,0 +1,65 @@ +import express from 'express'; +import withAsync from '../lib/withAsync.js'; +import { authMiddleware } from '../middlewares/auth.middleware.js'; +import { validate } from '../middlewares/validator.js'; +// import upload from '../middlewares/uploadImages.js'; +import { + ArticleIdStruct, + CommentIdStruct, + CreateArticleStruct, + CreateCommentStruct, + PatchArticleStruct, +} from '../lib/structs.js'; +import { + createArticle, + getListArticles, + getArticleById, + patchArticleById, + deleteArticleById, +} from '../controllers/article.controller.js'; +// import { +// createCommentForArticle, +// getCommentListArticle, +// } from '../controllers/comment.controller.js'; + +const router = express.Router(); + +router + .route('/') + //GET + .get(withAsync(getListArticles)) + + //POST + .post(authMiddleware, validate(CreateArticleStruct, 'body'), withAsync(createArticle)); + +router + .route('/:articleId') + + //GET id + .get(validate(ArticleIdStruct, 'params'), withAsync(getArticleById)) + //PATCH id + .patch( + authMiddleware, + validate(ArticleIdStruct, 'params'), + validate(PatchArticleStruct, 'body'), + withAsync(patchArticleById), + ) + //DELETE id + .delete(authMiddleware, validate(ArticleIdStruct, 'params'), withAsync(deleteArticleById)); + +// 자유게시판 댓글 =================== +// router +// .route('/:articleId/comments') + +// //GET +// .get(validate(ArticleIdStruct, 'params'), withAsync(getCommentListArticle)) //자유게시판 + +// //POST +// .post( +// authMiddleware, +// validate(ArticleIdStruct, 'params'), +// validate(CreateCommentStruct, 'body'), +// withAsync(createCommentForArticle), +// ); //자유게시판 댓글 + +export default router; From 4234f271870a8e5925b6f1209c359ed158aaca43 Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Thu, 27 Nov 2025 18:55:14 +0900 Subject: [PATCH 17/18] =?UTF-8?q?=F0=9F=9A=80=20feat(comment):=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20CRUD=20=EB=B0=8F=20=EC=9D=B8=EA=B0=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C,=20README?= =?UTF-8?q?=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +- src/app.js | 4 +- src/controllers/comment.controller.js | 246 ++++++++++++++++++++++++++ src/lib/structs.js | 2 +- src/routes/article.router.js | 31 ++-- src/routes/comment.router.js | 28 +++ 6 files changed, 295 insertions(+), 22 deletions(-) create mode 100644 src/controllers/comment.controller.js create mode 100644 src/routes/comment.router.js diff --git a/README.md b/README.md index 9f0f7410..e7db36a5 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,9 @@ - [x] **게시글(Article) 기능 인가** - [x] 등록: 로그인한 유저만 가능 - [x] 수정/삭제: 게시글 작성자(본인)만 가능 -- [ ] **댓글(Comment) 기능 인가** - - [ ] 등록: 상품/게시글에 댓글 달기 (로그인 유저만) - - [ ] 수정/삭제: 댓글 작성자(본인)만 가능 +- [x] **댓글(Comment) 기능 인가** + - [x] 등록: 상품/게시글에 댓글 달기 (로그인 유저만) + - [x] 수정/삭제: 댓글 작성자(본인)만 가능 ### 👤 4. 유저 정보 관리 (My Page) diff --git a/src/app.js b/src/app.js index 3ebb8324..40d1ab48 100644 --- a/src/app.js +++ b/src/app.js @@ -6,7 +6,7 @@ import authRouter from './routes/auth.router.js'; import productRouter from './routes/product.router.js'; import userRouter from './routes/user.router.js'; import articleRouter from './routes/article.router.js'; -// import commentRouter from './routes/comment.router.js'; +import commentRouter from './routes/comment.router.js'; // import uploadRouter from './routes/upload.router.js'; dotenv.config(); @@ -31,7 +31,7 @@ app.use('/products', productRouter); app.use('/articles', articleRouter); // //댓글 -// app.use('/comments', commentRouter); +app.use('/comments', commentRouter); // //이미지 // app.use('/uploads', uploadRouter); diff --git a/src/controllers/comment.controller.js b/src/controllers/comment.controller.js new file mode 100644 index 00000000..1b385925 --- /dev/null +++ b/src/controllers/comment.controller.js @@ -0,0 +1,246 @@ +import prisma from '../lib/prisma.js'; + +/* export { + createCommentForArticle, + createCommentForProduct, + getCommentListProduct, + getCommentListArticle, + getCommentById, + patchCommentById, + deleteCommentById, +}; +*/ + +// ========================================== +// 📰 중고마켓(product) 댓글 기능 +// ========================================== + +// 상품 댓글 POST -------------- (/products/:productId/comments) +export const createCommentForProduct = async (req, res, next) => { + const { id: authorId } = req.user; // 작성자(토큰) + const { productId } = req.params; // 상품 ID + const inputData = req.body; // 내용 ( content , image ... ) + + // + // 1. 상품이 존재하는지 확인 + const product = await prisma.product.findUniqueOrThrow({ + where: { id: productId }, + }); + + // 2. 댓글 작성 + const comment = await prisma.comment.create({ + data: { + ...inputData, + authorId, + productId: product.id, // 상품과 연결 + }, + include: { author: { select: { id: true, nickname: true, email: true } } }, + }); + + // 정보 포장 + const responseData = { + id: comment.id, + content: comment.content, + authorName: comment.author.nickname, + authorId: comment.authorId, + authorEmail: comment.author.email, + createdAt: comment.createdAt, + updatedAt: comment.updatedAt, + }; + + return res.status(201).json(responseData); +}; + +// 중고마켓 댓글 list -------------------------- (/products/:productId/comments) +export const getCommentListProduct = async (req, res, next) => { + const { productId } = req.params; + const { limit = 10, cursor } = req.query; + + // 1. 상품 존재 확인 없을 시 return 404 + await prisma.product.findUniqueOrThrow({ where: { id: productId } }); + + const queryOptions = { + where: { productId }, + take: parseInt(limit), + orderBy: { createdAt: 'desc' }, + include: { author: { select: { nickname: true } } }, + }; + + if (cursor) { + queryOptions.cursor = { id: Number(cursor) }; + queryOptions.skip = 1; + } + + const comments = await prisma.comment.findMany(queryOptions); + + //다음 커서 계산: 가져온 개수가 limit과 같으면, 마지막 아이템 Id가 다음 커서가 됨 + const nextCursor = comments.length === parseInt(limit) ? comments[comments.length - 1].id : null; + + // 2. 상품이 있을 시 댓글목록 불러오기 + const list = comments.map((comment) => ({ + id: comment.id, + content: comment.content, + authorName: comment.author.nickname, + createdAt: comment.createdAt, + updatedAt: comment.updatedAt, + })); + + return res.status(200).json({ nextCursor, list }); +}; + +// ========================================== +// 📰 게시글(Article) 댓글 기능 +// ========================================== + +// 게시글 댓글 작성 (POST)------------------------------------------------------------------------------ +export const createCommentForArticle = async (req, res, next) => { + const { id: authorId } = req.user; + const { articleId } = req.params; + const inputData = req.body; + + //게시글 존재여부 확인 없다면 return 404 + const article = await prisma.article.findUniqueOrThrow({ + where: { id: articleId }, + }); + + // 댓글 달기 + const comment = await prisma.comment.create({ + data: { ...inputData, authorId, articleId: article.id }, + include: { author: { select: { id: true, nickname: true, email: true } } }, + }); + + // 정보 포장 + const responseData = { + id: comment.id, + content: comment.content, + authorName: comment.author.nickname, + authorId: comment.authorId, + authorEmail: comment.author.email, + createdAt: comment.createdAt, + updatedAt: comment.updatedAt, + }; + + return res.status(201).json(responseData); +}; + +//자유게시판, 게시글에 달린 댓글 list---------------------------------------------------------------------- +export const getCommentListArticle = async (req, res, next) => { + const { articleId } = req.params; + const { limit = 10, cursor } = req.query; + + // 1. 게시글 존재 여부 확인 + await prisma.article.findUniqueOrThrow({ where: { id: articleId } }); + + const queryOptions = { + where: { articleId }, + take: parseInt(limit), + orderBy: { createdAt: 'desc' }, + include: { author: { select: { nickname: true, email: true } } }, + }; + + if (cursor) { + queryOptions.cursor = { id: Number(cursor) }; + queryOptions.skip = 1; + } + + const comments = await prisma.comment.findMany(queryOptions); + const nextCursor = comments.length === parseInt(limit) ? comments[comments.length - 1].id : null; + + // 2. 게시글이 있을 시 댓글목록 불러오기 + const list = comments.map((comment) => ({ + id: comment.id, + content: comment.content, + authorName: comment.author.nickname, + authorEmail: comment.author.email, + createdAt: comment.createdAt, + updatedAt: comment.updatedAt, + })); + + return res.status(200).json({ nextCursor, list }); +}; + +// ========================================== +// 🛠️ 공통 기능 (수정/삭제) +// ========================================== + +//GET id 공통 ------------------------------------------------------------------------------------------ +export const getCommentById = async (req, res, next) => { + const { commentId } = req.params; // idStruct 거쳐올 것 + + const commentData = await prisma.comment.findUniqueOrThrow({ + where: { id: commentId }, + include: { author: { select: { id: true, nickname: true, email: true } } }, + }); + + const responseData = { + id: commentData.id, + content: commentData.content, + authorName: commentData.author.nickname, + authorId: commentData.author.id, + authorEmail: commentData.author.email, + createdAt: commentData.createdAt, + updatedAt: commentData.updatedAt, + }; + + return res.status(200).json(responseData); +}; + +//PATCH id 공통 ---------------------------------------------------------------------------------------- +export const patchCommentById = async (req, res, next) => { + const { id: userId } = req.user; + const { commentId } = req.params; + const inputData = req.body; + + // 1. 댓글이 존재하는지 + const comment = await prisma.comment.findUniqueOrThrow({ + where: { id: commentId }, + }); + + // 2. 본인 확인 (작성자 본인의 댓글이 아니라면 return 403) + if (comment.authorId !== userId) { + return res.status(403).json({ message: '수정 권한이 없습니다.' }); + } + + // 3. 자유 게시글 수정하기 + const newPatchData = await prisma.comment.update({ + where: { id: commentId }, + data: inputData, + include: { author: { select: { id: true, nickname: true, email: true } } }, + }); + + // 정보 포장 + const responseData = { + id: newPatchData.id, + content: newPatchData.content, + authorName: newPatchData.author.nickname, + authorId: newPatchData.author.id, + authorEmail: newPatchData.author.email, + createdAt: newPatchData.createdAt, + updatedAt: newPatchData.updatedAt, + }; + + return res.status(200).json(responseData); +}; + +//DELETE id 공통 -------------------------------------------------------------------------------------- +export const deleteCommentById = async (req, res, next) => { + const { id: userId } = req.user; + const { commentId } = req.params; + + // 1. 댓글이 존재하는지 + const comment = await prisma.comment.findUniqueOrThrow({ + where: { id: commentId }, + }); + + // 2. 본인 확인 (작성자 본인의 댓글이 아니라면 return 403) + if (comment.authorId !== userId) { + return res.status(403).json({ message: '삭제 권한이 없습니다.' }); + } + + // 3. 자유 게시글 수정하기 + const newPatchData = await prisma.comment.delete({ + where: { id: commentId }, + }); + + return res.status(200).json({ message: '댓글이 삭제되었습니다.' }); +}; diff --git a/src/lib/structs.js b/src/lib/structs.js index b41a895c..c0f63247 100644 --- a/src/lib/structs.js +++ b/src/lib/structs.js @@ -66,7 +66,7 @@ export const PatchArticleStruct = s.partial(CreateArticleStruct); //----- comment ----- export const CreateCommentStruct = s.object({ - content: s.size(s.string(), 10, 200), + content: s.size(s.string(), 1, 200), }); export const PatchCommentStruct = s.partial(CreateCommentStruct); diff --git a/src/routes/article.router.js b/src/routes/article.router.js index f128ae76..dc3eea2b 100644 --- a/src/routes/article.router.js +++ b/src/routes/article.router.js @@ -5,7 +5,6 @@ import { validate } from '../middlewares/validator.js'; // import upload from '../middlewares/uploadImages.js'; import { ArticleIdStruct, - CommentIdStruct, CreateArticleStruct, CreateCommentStruct, PatchArticleStruct, @@ -17,10 +16,10 @@ import { patchArticleById, deleteArticleById, } from '../controllers/article.controller.js'; -// import { -// createCommentForArticle, -// getCommentListArticle, -// } from '../controllers/comment.controller.js'; +import { + createCommentForArticle, + getCommentListArticle, +} from '../controllers/comment.controller.js'; const router = express.Router(); @@ -48,18 +47,18 @@ router .delete(authMiddleware, validate(ArticleIdStruct, 'params'), withAsync(deleteArticleById)); // 자유게시판 댓글 =================== -// router -// .route('/:articleId/comments') +router + .route('/:articleId/comments') -// //GET -// .get(validate(ArticleIdStruct, 'params'), withAsync(getCommentListArticle)) //자유게시판 + //GET + .get(validate(ArticleIdStruct, 'params'), withAsync(getCommentListArticle)) //자유게시판 -// //POST -// .post( -// authMiddleware, -// validate(ArticleIdStruct, 'params'), -// validate(CreateCommentStruct, 'body'), -// withAsync(createCommentForArticle), -// ); //자유게시판 댓글 + //POST + .post( + authMiddleware, + validate(ArticleIdStruct, 'params'), + validate(CreateCommentStruct, 'body'), + withAsync(createCommentForArticle), + ); //자유게시판 댓글 export default router; diff --git a/src/routes/comment.router.js b/src/routes/comment.router.js new file mode 100644 index 00000000..30151bad --- /dev/null +++ b/src/routes/comment.router.js @@ -0,0 +1,28 @@ +import express from 'express'; +import withAsync from '../lib/withAsync.js'; +import { authMiddleware } from '../middlewares/auth.middleware.js'; +import { validate } from '../middlewares/validator.js'; +import { CommentIdStruct, CreateCommentStruct, PatchCommentStruct } from '../lib/structs.js'; +import { + getCommentById, + patchCommentById, + deleteCommentById, +} from '../controllers/comment.controller.js'; +// import upload from '../middlewares/uploadImages.js'; + +const router = express.Router(); + +//Article/Product comment는 각 라우터에 있음 +router + .route('/:commentId') + //GET id //PATCH id //DELETE id + .get(validate(CommentIdStruct, 'params'), withAsync(getCommentById)) + .patch( + authMiddleware, + validate(CommentIdStruct, 'params'), + validate(PatchCommentStruct, 'body'), + withAsync(patchCommentById), + ) + .delete(authMiddleware, validate(CommentIdStruct, 'params'), withAsync(deleteCommentById)); + +export default router; From b2db241a658a0ae3bd9b72111f84a00461e5066e Mon Sep 17 00:00:00 2001 From: YM_KIM Date: Thu, 27 Nov 2025 21:17:04 +0900 Subject: [PATCH 18/18] =?UTF-8?q?=F0=9F=9A=80=20feat(user):=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20?= =?UTF-8?q?=EB=82=B4=EA=B0=80=20=EB=93=B1=EB=A1=9D=ED=95=9C=20=EC=83=81?= =?UTF-8?q?=ED=92=88=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +-- src/controllers/user.controller.js | 42 ++++++++++++++++++++++++++++++ src/lib/structs.js | 6 +++++ src/routes/user.router.js | 18 +++++++++++-- 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e7db36a5..fb383665 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,8 @@ - [x] **내 정보 조회 API** - [x] 응답에 `password` 제외할 것 - [x] **내 정보 수정 API** -- [ ] **비밀번호 변경 API** (기존 비번 확인 과정 권장) -- [ ] **내가 등록한 상품 목록 조회 API** +- [x] **비밀번호 변경 API** (기존 비번 확인 과정 권장) +- [x] **내가 등록한 상품 목록 조회 API** --- diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index 5f175e3c..71ebcb42 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -1,4 +1,6 @@ +import bcrypt from 'bcrypt'; import prisma from '../lib/prisma.js'; +import { getOrderBy } from '../lib/utils.js'; /** 200 OK: 일반적인 성공 (GET, UPDATE 후) 201 Created: 새로운 리소스 생성 성공 (POST) @@ -7,6 +9,46 @@ import prisma from '../lib/prisma.js'; 404 Not Found: 요청한 리소스가 없음 */ +//비밀번호 변경 +export const updatePassword = async (req, res) => { + const { id } = req.user; + const { oldPassword, newPassword } = req.body; + + // 유저가 있는지 확인 + const user = await prisma.user.findUniqueOrThrow({ + where: { id }, + }); + + // 기존 비밀번호가 맞는지 확인 + const isMatch = await bcrypt.compare(oldPassword, user.password); + if (!isMatch) { + return res.status(400).json({ message: '기존 비밀번호와 일치하지 않습니다.' }); + } + + //새 비밀번호 해싱 + const hashedPassword = await bcrypt.hash(newPassword, 10); + + //업데이트 + await prisma.user.update({ + where: { id }, + data: { password: hashedPassword }, + }); + return res.status(200).json({ message: '비밀번호가 성공적으로 변경되었습니다.' }); +}; + +//내가 등록한 상품 목록 조회 +export const getMyProducts = async (req, res) => { + const { id } = req.user; + + const products = await prisma.product.findMany({ + where: { sellerId: id }, + orderBy: { createdAt: 'desc' }, + include: { images: true }, + }); + + return res.status(200).json(products); +}; + // 내 정보 조회 GET (/users/me) export const getUserMe = async (req, res, next) => { // authMiddleware 가 붙여준 req.user 확인 diff --git a/src/lib/structs.js b/src/lib/structs.js index c0f63247..134108d5 100644 --- a/src/lib/structs.js +++ b/src/lib/structs.js @@ -46,6 +46,12 @@ export const PatchUserStruct = s.object({ // (보안상 이메일과 비밀번호 변경은 별도 API로 빼기로 함.) }); +// 비밀번호 변경용 +export const PatchPasswordStruct = s.object({ + oldPassword: s.size(s.string(), 4, 20), + newPassword: s.size(s.string(), 4, 20), +}); + //----- product ----- export const CreateProductStruct = s.object({ name: s.size(s.string(), 1, 30), diff --git a/src/routes/user.router.js b/src/routes/user.router.js index b847f452..a6000ad1 100644 --- a/src/routes/user.router.js +++ b/src/routes/user.router.js @@ -2,8 +2,14 @@ import express from 'express'; import withAsync from '../lib/withAsync.js'; import { authMiddleware } from '../middlewares/auth.middleware.js'; import { validate } from '../middlewares/validator.js'; -import { PatchUserStruct } from '../lib/structs.js'; -import { getUserMe, patchUserMe, deleteUserMe } from '../controllers/user.controller.js'; +import { PatchUserStruct, PatchPasswordStruct } from '../lib/structs.js'; +import { + getUserMe, + patchUserMe, + deleteUserMe, + updatePassword, + getMyProducts, +} from '../controllers/user.controller.js'; const router = express.Router(); @@ -23,4 +29,12 @@ router // 회원 탈퇴 DELETE (/users/me) .delete(authMiddleware, withAsync(deleteUserMe)); +//비밀번호 변경 (/users/me/password) +router + .route('/me/password') + .patch(authMiddleware, validate(PatchPasswordStruct, 'body'), withAsync(updatePassword)); + +//내가 등록한 상품 조회 +router.route('/me/products').get(authMiddleware, withAsync(getMyProducts)); + export default router;