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..fb383665 100644
--- a/README.md
+++ b/README.md
@@ -1,118 +1,64 @@
-# 기본 요구 사항
+# 🛡️ 스프린트 미션: 인증/인가 및 관계형 DB 구현
-## 공통
+## 📅 프로젝트 개요
-- [x] PostgreSQL를 이용해 주세요.
-- [x] 데이터 모델 간의 관계를 고려하여 onDelete를 설정해 주세요.
-- [x] 데이터베이스 시딩 코드를 작성해 주세요.
-- [x] 각 API에 적절한 에러 처리를 해 주세요.
-- [x] 각 API 응답에 적절한 상태 코드를 리턴하도록 해 주세요.
+- **목표:** 토큰 기반의 인증(Authentication)과 인가(Authorization) 시스템을 구현하고, Prisma의 관계형 데이터 모델링을 적용합니다.
+- **핵심 기술:** Node.js, Express, Prisma, JWT, bcrypt
-## 스키마
+---
-### 중고마켓
+## ✅ 개발 체크리스트 (To-Do List)
-- [x] Product 스키마를 작성해 주세요.
+### 🛠️ 1. 초기 세팅 & 데이터베이스 (Prisma)
- - [x] id, name, description, price, tags, createdAt, updatedAt필드를 가집니다.
- 필요한 필드가 있다면 자유롭게 추가해 주세요.
+- [x] **환경변수(.env) 설정**
+ - [x] `DATABASE_URL` 확인
+ - [x] `JWT_SECRET` (토큰 비밀키)
+- [x] **User 스키마 작성**
+ - [x] 필드 구성: `id`, `email`, `nickname`, `image`, `password`, `createdAt`, `updatedAt`
+ - [x] 기존 모델(`Product`, `Article`, `Comment`)과 1:N 관계 설정 (`relation` 연결)
-- [x] 상품 등록 API를 만들어 주세요. == POST ==
+### 🔐 2. 인증 (Authentication) - 로그인/회원가입
- - [x] name, description, price, tags를 입력하여 상품을 등록합니다.
+- [x] **회원가입 API 구현**
+ - [x] 입력: `email`, `nickname`, `password`
+ - [x] **중요:** 비밀번호는 반드시 **해싱(Hashing)** 하여 저장 (bcrypt 등 사용)
+- [x] **로그인 API 구현**
+ - [x] 입력: `email`, `password` 검증
+ - [x] 성공 시: **Access Token (JWT)** 발급 및 반환
-- [x] 상품 목록 조회 API를 만들어 주세요. == GET LIST ==
+### 👮 3. 인가 (Authorization) - 권한 체크
- - [x] id, name, price, createdAt를 조회합니다.
- - [x] offset 방식의 페이지네이션 기능을 포함해 주세요.
- - [x] 최신순(recent)으로 정렬할 수 있습니다.
- - [x] name, description에 포함된 단어로 검색할 수 있습니다.
+> **공통 규칙:** 로그인한 유저만 등록 가능 / 본인만 수정, 삭제 가능
-- [x] 상품 상세 조회 API를 만들어 주세요. == GET ID ==
+- [x] **인가 미들웨어(Middleware) 구현** (토큰 검증 및 유저 확인)
+- [x] **상품(Product) 기능 인가**
+ - [x] 등록: 로그인한 유저만 가능
+ - [x] 수정/삭제: 상품 등록자(본인)만 가능
+- [x] **게시글(Article) 기능 인가**
+ - [x] 등록: 로그인한 유저만 가능
+ - [x] 수정/삭제: 게시글 작성자(본인)만 가능
+- [x] **댓글(Comment) 기능 인가**
+ - [x] 등록: 상품/게시글에 댓글 달기 (로그인 유저만)
+ - [x] 수정/삭제: 댓글 작성자(본인)만 가능
- - [x] id, name, description, price, tags, createdAt를 조회합니다.
+### 👤 4. 유저 정보 관리 (My Page)
-- [x] 상품 수정 API를 만들어 주세요. == PATCH ID ==
+- [x] **내 정보 조회 API**
+ - [x] 응답에 `password` 제외할 것
+- [x] **내 정보 수정 API**
+- [x] **비밀번호 변경 API** (기존 비번 확인 과정 권장)
+- [x] **내가 등록한 상품 목록 조회 API**
- - [x] PATCH 메서드를 사용해 주세요.
+---
-- [x] 상품 삭제 API를 만들어 주세요. == DELETE ID ==
+## 🔥 심화 요구사항 (Advanced) - 시간 남으면 도전!
-- [] 각 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으로 배포해 주세요.
+- [ ] **Refresh Token 구현** (토큰 갱신 기능)
+- [ ] **좋아요(Like) 기능 - 상품**
+ - [ ] 좋아요 / 좋아요 취소 토글
+ - [ ] 조회 시 `isLiked` 필드 포함
+- [ ] **좋아요(Like) 기능 - 게시글**
+ - [ ] 좋아요 / 좋아요 취소 토글
+ - [ ] 조회 시 `isLiked` 필드 포함
+- [ ] **좋아요한 목록 조회 기능**
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/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/mock.js b/prisma/mock.js
index b8cf9368..f8698aed 100644
--- a/prisma/mock.js
+++ b/prisma/mock.js
@@ -1,651 +1,175 @@
export const USERS = [
{
- id: 'b8f11e76-0a9e-4b3f-bccf-8d9b4fbf331e',
- email: 'honggd@example.com',
- firstName: '길동',
- lastName: '홍',
- address: '서울특별시 강남구 무실로 123번길 45-6',
- createdAt: '2023-07-16T09:00:00Z',
- updatedAt: '2023-07-16T09:00:00Z',
+ id: 1,
+ email: 'wakingSands@example.com',
+ nickname: '민필리아',
+ password: '1234',
+ address: '서부 다날란, 저녁별 만, 모래의 집',
},
{
- id: '6c3a18b0-11c5-4d97-9019-9ebe3c4d1317',
- email: 'kimyh@example.com',
- firstName: '영희',
- lastName: '김',
- address: '경기도 고양시 봉명로 789번길 21',
- createdAt: '2023-07-16T09:30:00Z',
- updatedAt: '2023-07-16T09:30:00Z',
+ id: 2,
+ email: 'gunBreaker@example.com',
+ nickname: '산크레드',
+ password: '1234',
+ address: '울다하 날 회랑, 모래늪 여관',
},
{
- id: 'fd3ae0a5-8dd5-40b6-b8fd-48870f731db1',
- email: 'lee.cs@example.com',
- firstName: '철수',
- lastName: '이',
- address: '인천광역시 남구 향교로 567번길 8-2',
- createdAt: '2023-07-16T10:00:00Z',
- updatedAt: '2023-07-16T10:00:00Z',
+ id: 3,
+ email: 'gillionaire@example.com',
+ nickname: '타타루',
+ password: '1234',
+ address: '모르도나 망자의 종소리, 돌의 집',
},
{
- id: '70e1e61d-f2ae-4d7d-bf8f-d65eafdb6a45',
- email: 'parkjy@example.com',
- firstName: '지영',
- lastName: '박',
- address: '대전광역시 중구 성남로 432번길 76',
- createdAt: '2023-07-16T10:30:00Z',
- updatedAt: '2023-07-16T10:30:00Z',
+ id: 4,
+ email: 'matoyaMom@example.com',
+ nickname: '야슈톨라',
+ password: '1234',
+ address: '저지 드라바니아, 마토야의 동굴',
},
{
- id: '73cb9639-d8b7-4f11-9a62-53f4187f3f11',
- email: 'jungminsoo@example.com',
- firstName: '민수',
- lastName: '정',
- address: '부산광역시 동래구 수림로 987번길 33-7',
- createdAt: '2023-07-16T11:00:00Z',
- updatedAt: '2023-07-16T11:00:00Z',
- },
-];
-
-export const PRODUCTS = [
- {
- id: 'f8013040-b076-4dc4-8677-11be9a17162f',
- name: '랑방 샤워젤 세트',
- description:
- '랑방의 향기로운 샤워젤 세트입니다. 피부를 부드럽게 케어하며, 향기로운 샤워 시간을 선사합니다.',
- tags: ['BEAUTY'],
- price: 38000,
- createdAt: '2023-07-14T10:00:00Z',
- updatedAt: '2023-07-14T10:00:00Z',
- },
- {
- id: 'd2ff3048-83bc-425a-8ad3-d6d9af1c7c6d',
- name: '나이키 테크조그거팬츠',
- description:
- '나이키의 테크조그거팬츠입니다. 운동이나 일상 생활에서 편안하게 착용할 수 있으며, 스타일과 기능을 모두 갖추고 있습니다.',
- tags: ['FASHION'],
- price: 75000,
- createdAt: '2023-07-14T10:30:00Z',
- updatedAt: '2023-07-14T10:30:00Z',
- },
- {
- id: '7f70481b-784d-4b0e-bc3e-f05eefc17951',
- name: 'Apple AirPods 프로',
- description:
- 'Apple의 AirPods 프로는 탁월한 사운드 품질과 노이즈 캔슬링 기능을 갖춘 무선 이어폰입니다. 간편한 터치 컨트롤과 긴 배터리 수명을 제공합니다.',
- tags: ['ELECTRONICS'],
- price: 320000,
- createdAt: '2023-07-14T11:00:00Z',
- updatedAt: '2023-07-14T11:00:00Z',
- },
- {
- id: '4e0d9424-3a16-4a5e-9725-0e9d2f9722b3',
- name: '베르사체 화장품 세트',
- description:
- '베르사체의 화장품 세트로 화려하고 특별한 분위기를 연출할 수 있습니다. 다양한 아이템으로 구성되어 있으며, 고품질 성분을 사용하여 피부에 부드럽고 안정적인 관리를 제공합니다.',
- tags: ['BEAUTY'],
- price: 65000,
- createdAt: '2023-07-14T11:30:00Z',
- updatedAt: '2023-07-14T11:30:00Z',
- },
- {
- id: '39c3fd4a-dbd5-4ab1-8e0b-58ea31b8a2d3',
- name: '아이언맨 골프 클럽 세트',
- description:
- '아이언맨 디자인으로 유명한 골프 클럽 세트입니다. 고품질 소재와 최신 기술로 제작되어 정밀한 스윙과 멋진 샷을 도와줍니다.',
- tags: ['SPORTS'],
- price: 850000,
- createdAt: '2023-07-14T12:00:00Z',
- updatedAt: '2023-07-14T12:00:00Z',
- },
- {
- id: 'c2d4a2e3-7c2a-4f80-bff7-8ebcbacccf2c',
- name: '삼성 갤럭시 S21 울트라 5G 스마트폰',
- description:
- '삼성 갤럭시 S21 울트라 5G 스마트폰은 최신 플래그십 기술을 갖춘 뛰어난 성능을 제공합니다. 화려한 디스플레이, 고화질 카메라, 빠른 프로세서 등으로 사용자들에게 탁월한 사용자 경험을 선사합니다.',
- tags: ['ELECTRONICS'],
- price: 1249000,
- createdAt: '2023-07-14T14:00:00Z',
- updatedAt: '2023-07-14T14:00:00Z',
- },
- {
- id: '43c62d5b-6e66-4d1c-9f47-14d1a1970fd1',
- name: 'LG 그램 14형 노트북',
- description:
- 'LG 그램 14형 노트북은 가볍고 휴대성이 뛰어난 디자인과 우수한 성능을 제공합니다. 긴 배터리 수명과 선명한 디스플레이로 사용자들에게 편리한 사용자 경험을 선사합니다.',
- tags: ['ELECTRONICS'],
- price: 1399000,
- createdAt: '2023-07-14T14:30:00Z',
- updatedAt: '2023-07-14T14:30:00Z',
- },
- {
- id: '6e9d4e63-7246-4fc5-bf33-3e7f32fe9c02',
- name: '나이키 에어맥스 270 운동화',
- description:
- '나이키 에어맥스 270 운동화는 경량 디자인과 탁월한 고무 밑창으로 편안한 착용감과 훌륭한 트랙션을 제공합니다. 스포티한 디자인과 풍부한 컬러로 다양한 스타일에 어울립니다.',
- tags: ['FASHION'],
- price: 169000,
- createdAt: '2023-07-14T15:00:00Z',
- updatedAt: '2023-07-14T15:00:00Z',
- },
- {
- id: '80e7b377-df80-4410-9a14-622ea3cc58e0',
- name: '애플 맥북 프로 13형 노트북',
- description:
- '애플 맥북 프로 13형 노트북은 탁월한 성능과 우수한 디스플레이 품질로 유명합니다. 가벼운 무게와 편리한 사용성으로 사용자들에게 탁월한 컴퓨팅 경험을 제공합니다.',
- tags: ['ELECTRONICS'],
- price: 1999000,
- createdAt: '2023-07-14T15:30:00Z',
- updatedAt: '2023-07-14T15:30:00Z',
+ id: 5,
+ email: 'finalHeaven@example.com',
+ nickname: '이다',
+ password: '1234',
+ address: '기라바니아 변방지대, 랄거의 손길',
},
{
- id: 'a4ff201c-48f7-4963-b317-2e9e4e3e43b7',
- name: '랑방 매트 틴트',
- description:
- '랑방 매트 틴트는 풍부한 컬러와 지속력을 제공하는 제품입니다. 입술에 부드럽게 발리며 오래 지속되는 매트한 마무리를 선사합니다.',
- tags: ['BEAUTY'],
- price: 35000,
- createdAt: '2023-07-14T16:00:00Z',
- updatedAt: '2023-07-14T16:00:00Z',
- },
- {
- id: 'c6a5975a-42e7-4f7f-8b7c-72714d59f44a',
- name: '루이비통 클래식 지갑',
- description:
- '루이비통의 클래식 지갑은 고품질 가죽과 세련된 디자인으로 제작되었습니다. 실용적인 구성과 품질로 오랜 기간 동안 사용할 수 있습니다.',
- tags: ['FASHION'],
- price: 750000,
- createdAt: '2023-07-14T16:30:00Z',
- updatedAt: '2023-07-14T16:30:00Z',
+ id: 6,
+ email: 'smartCaster@example.com',
+ nickname: '파파리모',
+ password: '1234',
+ address: '검은장막 숲, 그리다니아 구시가지',
},
{
- id: 'a33d441f-57a9-4618-8f46-07e7418ef3c9',
- name: '아이폰 13 프로 맥스 스마트폰',
- description:
- '아이폰 13 프로 맥스는 탁월한 카메라 성능과 강력한 성능으로 유명한 스마트폰입니다. 선명한 디스플레이와 편리한 사용성을 제공합니다.',
- tags: ['ELECTRONICS'],
- price: 1799000,
- createdAt: '2023-07-14T17:00:00Z',
- updatedAt: '2023-07-14T17:00:00Z',
- },
- {
- id: 'f6c2a70e-32a4-4bcf-b183-8a33d97fb587',
- name: '헤라 UV 미스트 선스틱 SPF50+ PA+++',
- description:
- '헤라의 UV 미스트 선스틱은 SPF50+ PA+++로 강력한 자외선 차단과 함께 피부 보습에 도움을 주는 제품입니다. 휴대하기 편리한 스틱 형태로 사용이 간편합니다.',
- tags: ['BEAUTY'],
- price: 28000,
- createdAt: '2023-07-14T17:30:00Z',
- updatedAt: '2023-07-14T17:30:00Z',
- },
- {
- id: 'e700c4be-6e0c-40fd-bb47-2ab2b2e9270d',
- name: '올리비아 버튼 니트',
- description:
- '올리비아 버튼 니트는 스타일리시한 디자인과 편안한 착용감으로 유명한 제품입니다. 다양한 의상에 매치하기 좋으며, 따뜻한 니트 소재로 겨울철에도 활용할 수 있습니다.',
- tags: ['FASHION'],
- price: 59000,
- createdAt: '2023-07-14T18:00:00Z',
- updatedAt: '2023-07-14T18:00:00Z',
- },
- {
- id: 'be13a617-8f0e-4806-82f7-149a6e12f1a7',
- name: '삼성 55인치 QLED 4K 스마트 TV',
- description:
- '삼성의 55인치 QLED 4K 스마트 TV는 고품질 화질과 풍부한 컬러 표현력으로 몰입감 있는 시청 경험을 제공합니다. 스마트 기능과 음성 인식 기능을 지원하여 편리한 사용자 경험을 선사합니다.',
- tags: ['ELECTRONICS'],
- price: 1999000,
- createdAt: '2023-07-14T18:30:00Z',
- updatedAt: '2023-07-14T18:30:00Z',
- },
- {
- id: 'd3f74179-2cb5-4e51-8f3a-57f65c1d4e6e',
- name: '루이스 카스텔리 미니 프라이팬',
- description:
- '루이스 카스텔리의 미니 프라이팬은 작고 휴대하기 편한 사이즈로 다양한 요리에 활용할 수 있습니다. 내열성과 내마모성이 우수하며 손쉬운 청소가 가능합니다.',
- tags: ['KITCHENWARE'],
- price: 29000,
- createdAt: '2023-07-15T10:00:00Z',
- updatedAt: '2023-07-15T10:00:00Z',
- },
- {
- id: 'dc74c065-5237-4d5b-86d1-d4eeb8a4d1fd',
- name: '델롱기 헤어 드라이어',
- description:
- '델롱기의 헤어 드라이어는 강력한 풍력과 다양한 모드로 빠른 건조와 스타일링을 도와줍니다. 손쉬운 조작과 휴대성을 갖추고 있습니다.',
- tags: ['ELECTRONICS'],
- price: 69000,
- createdAt: '2023-07-15T10:30:00Z',
- updatedAt: '2023-07-15T10:30:00Z',
- },
- {
- id: '93a1c570-ee10-4961-bbdf-68001efddc49',
- name: '보노홈 스탠드 믹서기',
- description:
- '보노홈의 스탠드 믹서기는 강력한 모터와 다양한 속도 조절로 원하는 믹싱과 반죽 작업을 할 수 있습니다. 내구성이 뛰어나며 안정적인 성능을 제공합니다.',
- tags: ['KITCHENWARE'],
- price: 99000,
- createdAt: '2023-07-15T11:00:00Z',
- updatedAt: '2023-07-15T11:00:00Z',
- },
- {
- id: 'd5d60337-ccf6-404f-b615-982f2b223ab3',
- name: '나이키 에어 포스 1 운동화',
- description:
- '나이키의 에어 포스 1 운동화는 클래식한 디자인과 탁월한 편안함으로 유명합니다. 내구성이 우수하며 다양한 스타일과 컬러로 선택할 수 있습니다.',
- tags: ['FASHION'],
- price: 129000,
- createdAt: '2023-07-15T11:30:00Z',
- updatedAt: '2023-07-15T11:30:00Z',
- },
- {
- id: '209b2a1c-c6e9-4461-a17a-c8a8f141f9f9',
- name: '삼성 컬러 레이저 프린터',
- description:
- '삼성의 컬러 레이저 프린터는 고품질 컬러 출력과 빠른 인쇄 속도로 탁월한 인쇄 품질을 제공합니다. 다양한 용지 크기와 고효율 인쇄 기능을 지원합니다.',
- tags: ['ELECTRONICS'],
- price: 499000,
- createdAt: '2023-07-15T12:00:00Z',
- updatedAt: '2023-07-15T12:00:00Z',
+ id: 7,
+ email: 'thouArt@example.com',
+ nickname: '위리앙제',
+ password: '1234',
+ address: '일 메그, 몽환의 숲, 문지기의 서재',
},
+];
+
+export const PRODUCTS = [
{
- id: 'b1782c1e-8ad2-45a7-90e6-50f61c4a8de9',
- name: '코멧 피크닉 매트',
- description:
- '코멧 피크닉 매트는 내열성과 내충격성이 뛰어나며 편리한 보관과 휴대를 위한 접이식 디자인입니다. 야외 활동에 최적화되어 있습니다.',
- tags: ['SPORTS'],
- price: 35000,
- createdAt: '2023-07-15T12:30:00Z',
- updatedAt: '2023-07-15T12:30:00Z',
+ id: 1,
+ name: '백금 만년필',
+ description: '행정 업무에 최적화된 고급 만년필입니다. 부드러운 필기감을 자랑합니다.',
+ tags: ['STATIONERY'],
+ price: 2500,
+ sellerId: 1, //민필리아
},
{
- id: 'b2593dbd-5ea3-4376-9378-6d6e7a17e232',
- name: '루미에어 드라이 바디 수건',
- description:
- '루미에어의 드라이 바디 수건은 퀵 드라이와 산뜻한 사용감을 제공하는 혁신적인 수건입니다. 속건성이 뛰어나며 휴대하기 편리합니다.',
- tags: ['BEAUTY'],
- price: 19900,
- createdAt: '2023-07-15T13:00:00Z',
- updatedAt: '2023-07-15T13:00:00Z',
+ id: 2,
+ name: '다홍색 장미',
+ description: '진하고 매혹적인 색을 띤 장미입니다. 선물용으로 적합합니다.',
+ tags: ['FLOWER'],
+ price: 500,
+ sellerId: 2, //산크레드
},
{
- id: 'c28a2eaf-4d87-4f9f-ae5b-cbcf73e24253',
- name: '쿠진앤에이 오믈렛 팬',
- description:
- '쿠진앤에이의 오믈렛 팬은 오믈렛을 쉽고 빠르게 만들 수 있는 전용 팬입니다. 내열성이 뛰어나며 논스틱 처리로 편리한 사용과 청소가 가능합니다.',
- tags: ['KITCHENWARE'],
- price: 25000,
- createdAt: '2023-07-15T13:30:00Z',
- updatedAt: '2023-07-15T13:30:00Z',
+ id: 3,
+ name: '꼬마 친구 포실포실 털뭉치',
+ description: '부드러운 털실로 짜인 귀여운 꼬마 친구입니다. 희귀한 확률로 발견됩니다.',
+ tags: ['DOLL'],
+ price: 15000000,
+ sellerId: 3, //타타루
},
{
- id: 'e48d9e8b-6712-48ed-9ea2-2f7e150a2e48',
- name: '네스프레소 커피머신',
- description:
- '네스프레소 커피머신은 간편한 사용과 다양한 커피 음료 제공으로 인기 있는 제품입니다. 커피 품질과 편리한 관리 기능을 동시에 갖추고 있습니다.',
- tags: ['ELECTRONICS'],
- price: 149000,
- createdAt: '2023-07-15T14:00:00Z',
- updatedAt: '2023-07-15T14:00:00Z',
+ id: 4,
+ name: '마법의 빗자루',
+ description: '마력을 주입하여 스스로 움직이는 빗자루입니다. 하우징 마당을 꾸미기에 좋습니다.',
+ tags: ['FURNITURE'],
+ price: 450000,
+ sellerId: 4, //야슈톨라
},
{
- id: '64e7c9fc-49b6-4b20-9ce3-11a9487ed1e1',
- name: '스완스톤 운동 요가 매트',
- description:
- '스완스톤의 운동 요가 매트는 편안하고 안정적인 기준면과 내충격성으로 안전한 운동 활동을 도와줍니다. 내마모성이 우수하며 휴대하기 편리합니다.',
+ id: 5,
+ name: '경화 가죽 격투무기',
+ description: '튼튼한 가죽으로 감싼 격투가용 무기입니다. 내구성이 뛰어납니다.',
tags: ['SPORTS'],
- price: 69000,
- createdAt: '2023-07-15T14:30:00Z',
- updatedAt: '2023-07-15T14:30:00Z',
- },
- {
- id: 'e3f34fb9-2c4b-45c0-94b1-610e6cfe13a7',
- name: '샤넬 커버 파운데이션',
- description:
- '샤넬의 커버 파운데이션은 자연스럽게 피부를 커버하고 매끈한 마무리를 선사합니다. 오랜 지속력과 촉촉한 사용감을 제공합니다.',
- tags: ['BEAUTY'],
- price: 65000,
- createdAt: '2023-07-15T15:00:00Z',
- updatedAt: '2023-07-15T15:00:00Z',
- },
- {
- id: '4be1e6fd-2271-4f15-96e3-577c4a057c8b',
- name: '올바로 10단 스텐레스 냄비세트',
- description:
- '올바로의 10단 스텐레스 냄비세트는 내열성과 내식성이 우수한 제품입니다. 다양한 크기와 기능으로 요리를 즐길 수 있습니다.',
- tags: ['KITCHENWARE'],
- price: 179000,
- createdAt: '2023-07-15T15:30:00Z',
- updatedAt: '2023-07-15T15:30:00Z',
- },
- {
- id: 'a4468fc1-3cfc-4c46-9ff9-2469b5f9e88a',
- name: '아디다스 운동바지',
- description:
- '아디다스의 운동바지는 신축성과 편안한 착용감으로 활동성을 높여줍니다. 퀄리티한 소재와 다양한 스타일로 선택의 폭이 넓습니다.',
- tags: ['FASHION'],
- price: 59000,
- createdAt: '2023-07-15T16:00:00Z',
- updatedAt: '2023-07-15T16:00:00Z',
- },
- {
- id: 'be07b27a-93e9-4d1a-8a54-133c0f04541e',
- name: 'LG 울트라 HD 4K TV',
- description:
- 'LG의 울트라 HD 4K TV는 생생하고 선명한 화질로 몰입감 있는 시청 경험을 제공합니다. 스마트 기능과 다양한 연결 옵션을 갖추고 있습니다.',
- tags: ['ELECTRONICS'],
- price: 2299000,
- createdAt: '2023-07-15T16:30:00Z',
- updatedAt: '2023-07-15T16:30:00Z',
- },
- {
- id: 'de573676-2a27-4cfa-85a1-0e9dd3ed5315',
- name: '삼성 갤럭시 워치4',
- description:
- '삼성의 갤럭시 워치4는 스타일리시한 디자인과 다양한 기능으로 탁월한 착용 경험을 선사합니다. 편리한 스마트 기능과 건강 관리 기능을 갖추고 있습니다.',
- tags: ['ELECTRONICS'],
- price: 399000,
- createdAt: '2023-07-15T17:00:00Z',
- updatedAt: '2023-07-15T17:00:00Z',
- },
- {
- id: '9c6ebf9b-f666-44da-b79f-5f62517f55fc',
- name: '나이스컷 고급 칼세트',
- description:
- '나이스컷의 고급 칼세트는 품질 높은 강화 스테인리스 블레이드로 효율적인 커팅을 가능하게 합니다. 다양한 칼 종류로 다양한 요리를 할 수 있습니다.',
- tags: ['KITCHENWARE'],
- price: 99000,
- createdAt: '2023-07-15T17:30:00Z',
- updatedAt: '2023-07-15T17:30:00Z',
- },
- {
- id: '17cc4f9c-1e71-4782-914a-c8a12dc6f94b',
- name: '아벤크롬비 스니커즈',
- description:
- '아벤크롬비의 스니커즈는 스포티한 디자인과 편안한 착용감으로 인기 있는 제품입니다. 고품질 소재와 세련된 스타일로 멋진 캐주얼 룩을 완성할 수 있습니다.',
- tags: ['FASHION'],
- price: 89000,
- createdAt: '2023-07-15T18:00:00Z',
- updatedAt: '2023-07-15T18:00:00Z',
- },
- {
- id: 'c1b31e2b-6d6a-4b24-b3ea-d0746f9cc6ea',
- name: '피오라 러브포이즌 향수',
- description:
- '피오라의 러브포이즌 향수는 상쾌하고 여성스러운 향기로 매력적인 분위기를 연출합니다. 오래 지속되는 향과 우아한 디자인으로 사랑받는 제품입니다.',
- tags: ['BEAUTY'],
- price: 79000,
- createdAt: '2023-07-15T18:30:00Z',
- updatedAt: '2023-07-15T18:30:00Z',
- },
- {
- id: '5ae4a823-0076-4ae0-af49-7f3eb0002c0a',
- name: '니스 퍼퓸 캔들',
- description:
- '니스 퍼퓸의 캔들은 아로마 향으로 휴식과 힐링을 선사합니다. 품질 좋은 원료로 만들어진 캔들은 안전하고 오랜 시간 향기를 유지합니다.',
- tags: ['HOME_INTERIOR'],
- price: 35000,
- createdAt: '2023-07-15T19:00:00Z',
- updatedAt: '2023-07-15T19:00:00Z',
- },
- {
- id: '50ce94ef-cb04-4c2f-bad4-d36b18956b9a',
- name: '디젤 남성 슬림 핏 청바지',
- description:
- '디젤의 남성 슬림 핏 청바지는 모던하고 스타일리시한 디자인으로 인기를 끄는 제품입니다. 퀄리티한 소재와 세련된 실루엣으로 완벽한 핏을 선사합니다.',
- tags: ['FASHION'],
- price: 139000,
- createdAt: '2023-07-15T19:30:00Z',
- updatedAt: '2023-07-15T19:30:00Z',
- },
- {
- id: 'd6c5e7d5-225f-4f6d-ba17-6f79d32726db',
- name: '슈퍼디올 여성 선글라스',
- description:
- '슈퍼디올의 여성 선글라스는 세련된 디자인과 탁월한 안경 렌즈로 스타일과 보호를 동시에 제공합니다. 여름 휴가나 일상적인 착용에 적합합니다.',
- tags: ['FASHION'],
- price: 189000,
- createdAt: '2023-07-15T20:00:00Z',
- updatedAt: '2023-07-15T20:00:00Z',
- },
- {
- id: '9c2bc7ad-6f1e-491d-8d81-9ed78a805a20',
- name: '보타닉 프레쉬 화장품 세트',
- description:
- '보타닉 프레쉬의 화장품 세트는 천연 식물성 성분을 사용하여 피부에 영양과 보습을 제공합니다. 다양한 제품으로 피부 관리를 완벽하게 할 수 있습니다.',
- tags: ['BEAUTY'],
- price: 99000,
- createdAt: '2023-07-15T20:30:00Z',
- updatedAt: '2023-07-15T20:30:00Z',
- },
- {
- id: 'f8c95e63-ba05-4e2d-8a61-9e5b07d4ccdb',
- name: '프라다 남성 가죽 벨트',
- description:
- '프라다의 남성 가죽 벨트는 고품질 가죽 소재와 세련된 디자인으로 멋진 스타일링을 완성할 수 있습니다. 다양한 의상에 어울리는 제품입니다.',
- tags: ['FASHION'],
- price: 249000,
- createdAt: '2023-07-15T21:00:00Z',
- updatedAt: '2023-07-15T21:00:00Z',
- },
- {
- id: 'e6c6aeed-209d-4f3d-907e-9d208d5bcfd2',
- name: 'LG 스마트 인버터 에어컨',
- description:
- 'LG의 스마트 인버터 에어컨은 효율적인 냉방과 에너지 절약을 위한 기능을 갖추고 있습니다. 스마트한 제어와 시원한 바람으로 쾌적한 환경을 제공합니다.',
- tags: ['ELECTRONICS'],
- price: 1399000,
- createdAt: '2023-07-15T21:30:00Z',
- updatedAt: '2023-07-15T21:30:00Z',
- },
- {
- id: 'a81dd4df-5bc4-44b5-89a0-769a7b6d3cc0',
- name: '닌텐도 스위치 라이트',
- description:
- '닌텐도 스위치 라이트는 휴대성과 다양한 게임 플레이를 위한 제품입니다. 화질과 음질이 우수하며 다양한 게임을 즐길 수 있습니다.',
- tags: ['ELECTRONICS'],
- price: 299000,
- createdAt: '2023-07-15T22:00:00Z',
- updatedAt: '2023-07-15T22:00:00Z',
- },
- {
- id: '6e890e6d-df7b-4c50-8d3e-59a32c48ae51',
- name: '루미에어 프리미엄 베개',
- description:
- '루미에어의 프리미엄 베개는 안락한 수면을 위한 편안한 디자인과 고품질 소재로 제작되었습니다. 적절한 지지력과 통기성을 제공합니다.',
- tags: ['HOME_INTERIOR'],
- price: 59000,
- createdAt: '2023-07-15T22:30:00Z',
- updatedAt: '2023-07-15T22:30:00Z',
+ price: 15000,
+ sellerId: 5, //이다
},
{
- id: '7a14ccf9-8b08-4e9a-8f0f-624dcf7d6d74',
- name: '메종키츠네 남성 반팔 티셔츠',
+ id: 6,
+ name: '샬레이안 고글',
description:
- '메종키츠네의 남성 반팔 티셔츠는 고품질 소재와 세련된 디자인으로 스타일리시한 룩을 완성할 수 있습니다. 다양한 컬러와 패턴으로 선택의 폭을 넓힐 수 있습니다.',
+ '지식의 도시 샬레이안 양식으로 제작된 고글입니다. 에테르의 흐름을 관찰하기 좋습니다.',
tags: ['FASHION'],
- price: 149000,
- createdAt: '2023-07-15T23:00:00Z',
- updatedAt: '2023-07-15T23:00:00Z',
+ price: 85000,
+ sellerId: 6, //파파리모
},
{
- id: 'b5d2d3ad-7d92-4f17-a1e5-2260a2d69d0d',
- name: '올림포스 포터블 프린터',
- description:
- '올림포스의 포터블 프린터는 휴대성과 편리한 사용을 제공하는 제품입니다. 다양한 용지 크기와 고품질 인쇄를 지원합니다.',
- tags: ['ELECTRONICS'],
- price: 99000,
- createdAt: '2023-07-16T00:00:00Z',
- updatedAt: '2023-07-16T00:00:00Z',
+ id: 7,
+ name: '점성술사 카드 세트',
+ description: '여섯 별의 운명을 점칠 수 있는 카드 세트입니다. 점성술 입문자에게 추천합니다.',
+ tags: ['HOBBY'],
+ price: 33000,
+ sellerId: 7, //위리앙제
},
-
{
- id: '3f0ccf0a-9980-4a55-9ef0-57e929fe813c',
- name: '디올 애디크트 립스틱',
- description:
- '디올의 애디크트 립스틱은 풍부한 컬러와 윤기로운 마무리를 선사합니다. 부드러운 발림성과 오랜 지속력으로 립 메이크업을 완성합니다.',
- tags: ['BEAUTY'],
- price: 45000,
- createdAt: '2023-07-16T00:30:00Z',
- updatedAt: '2023-07-16T00:30:00Z',
+ id: 8,
+ name: '미스릴 곡괭이',
+ description: '미스릴 주괴로 날을 세운 곡괭이입니다. 광석 채집에 필수적인 도구입니다.',
+ tags: ['TOOL'],
+ price: 5000,
+ sellerId: 1, //민필리아
},
{
- id: 'f6a63b92-870e-4b54-8f80-7ae5e0b5be78',
- name: '삼성 갤럭시 버즈 프로',
- description:
- '삼성의 갤럭시 버즈 프로는 탁월한 음질과 노이즈 캔슬링 기능을 제공하는 무선 이어폰입니다. 스마트한 기능과 편안한 착용감을 동시에 즐길 수 있습니다.',
- tags: ['ELECTRONICS'],
- price: 229000,
- createdAt: '2023-07-16T01:00:00Z',
- updatedAt: '2023-07-16T01:00:00Z',
+ id: 9,
+ name: '미스릴 반지',
+ description: '세공된 미스릴로 만든 반지입니다. 깔끔한 디자인으로 인기가 많습니다.',
+ tags: ['ACCESSORY'],
+ price: 25000,
+ sellerId: 2, //산크레드
},
{
- id: 'd0ccffae-fa61-4e2f-88ff-2253e838bf2d',
- name: '지오다노 남성 반팔 티셔츠',
- description:
- '지오다노의 남성 반팔 티셔츠는 심플하면서도 스타일리시한 디자인으로 인기를 끄는 제품입니다. 퀄리티한 소재와 편안한 착용감을 제공합니다.',
+ id: 10,
+ name: '사베네어 뷔스티에',
+ description: '사베네어 지방의 전통 의상입니다. 화려한 자수와 고급 원단으로 제작되었습니다.',
tags: ['FASHION'],
- price: 29000,
- createdAt: '2023-07-16T01:30:00Z',
- updatedAt: '2023-07-16T01:30:00Z',
+ price: 5000000,
+ sellerId: 3, //타타루
},
{
- id: '8a131b86-7b3e-44c2-a02b-b17c69a2c780',
- name: '필립스 공기청정기',
- description:
- '필립스의 공기청정기는 공기 중의 먼지와 유해 물질을 효과적으로 제거하여 깨끗한 공기를 유지합니다. 소음이 적고 사용이 편리합니다.',
- tags: ['ELECTRONICS'],
- price: 119000,
- createdAt: '2023-07-16T02:00:00Z',
- updatedAt: '2023-07-16T02:00:00Z',
+ id: 11,
+ name: '고대 롱카의 비석',
+ description: '롱카 문명이 새겨진 고대 비석입니다. 하우징 조경물로 사용할 수 있습니다.',
+ tags: ['FURNITURE'],
+ price: 200000,
+ sellerId: 4, //야슈톨라
},
{
- id: 'f8cb3992-c455-4c9a-9736-1f94a83b3e6f',
- name: '후아유 촉촉 수분 선크림',
- description:
- '후아유의 촉촉 수분 선크림은 가볍고 촉촉한 텍스처로 피부에 쉽게 흡수되며 자외선 차단과 피부 보호를 동시에 제공합니다.',
- tags: ['BEAUTY'],
- price: 39000,
- createdAt: '2023-07-16T02:30:00Z',
- updatedAt: '2023-07-16T02:30:00Z',
+ id: 12,
+ name: '동방의 목인',
+ description: '동방 지역에서 수련용으로 사용하는 목인입니다. 튼튼한 나무로 만들어졌습니다.',
+ tags: ['FURNITURE'],
+ price: 10000,
+ sellerId: 5, //이다
},
{
- id: '78e5b047-f28e-4b3e-9c62-54c2d12565cd',
- name: '조지아 아이스 커피',
- description:
- '조지아 아이스 커피는 진하고 풍부한 커피 맛과 시원한 얼음으로 상쾌한 음료를 즐길 수 있습니다. 휴대하기 편리한 캔 형태로 제공됩니다.',
- tags: ['HOUSEHOLD_SUPPLIES'],
- price: 2000,
- createdAt: '2023-07-16T03:00:00Z',
- updatedAt: '2023-07-16T03:00:00Z',
- },
- {
- id: 'b8e3e8d9-0f39-4f2e-99a0-1c00c8f5d9e1',
- name: '헤스티아 화장지',
- description:
- '헤스티아의 화장지는 부드럽고 흡수력이 뛰어나며 피부 친화적인 소재로 만들어진 제품입니다. 편리한 사용감과 위생성을 제공합니다.',
- tags: ['HOUSEHOLD_SUPPLIES'],
+ id: 13,
+ name: '묵직한 철제 화분',
+ description: '어떤 식물이든 심을 수 있는 튼튼한 화분입니다. 실내 장식용으로 적합합니다.',
+ tags: ['FURNITURE'],
price: 5000,
- createdAt: '2023-07-16T03:30:00Z',
- updatedAt: '2023-07-16T03:30:00Z',
+ sellerId: 7, //위리앙제
},
{
- id: '0d8e554a-84c0-4b9c-bdc3-d04b9bbf1344',
- name: '탐스킨 리뉴얼 페이셜 클렌저',
- description:
- '탐스킨의 리뉴얼 페이셜 클렌저는 부드러운 거품과 깨끗한 세정력으로 피부를 깨끗하게 유지합니다. 피부에 자극을 주지 않는 올인원 클렌저입니다.',
- tags: ['BEAUTY'],
- price: 15000,
- createdAt: '2023-07-16T04:00:00Z',
- updatedAt: '2023-07-16T04:00:00Z',
- },
- {
- id: '1cc006a0-82a3-4e70-8d92-974d1ea9c3af',
- name: '카시오 베이비-지 시계',
- description:
- '카시오 베이비-지 시계는 귀여운 디자인과 실용적인 기능을 가진 어린이용 시계입니다. 내추럴 라이트와 방수 기능을 갖추고 있습니다.',
- tags: ['ELECTRONICS'],
- price: 59000,
- createdAt: '2023-07-16T04:30:00Z',
- updatedAt: '2023-07-16T04:30:00Z',
+ id: 14,
+ name: '에테르학 개론',
+ description: '에테르의 기본 원리와 응용법이 적힌 학술서입니다. 학자들에게 필독서로 꼽힙니다.',
+ tags: ['BOOKS'],
+ price: 12000,
+ sellerId: 6, //파파리모
},
{
- id: '85fc2182-8b7f-47f1-8f3d-911e0a3c0582',
- name: '홀리바나나 남성 가죽 슬리퍼',
- description:
- '홀리바나나 남성 가죽 슬리퍼는 편안한 착용감과 세련된 디자인으로 인기를 끄는 제품입니다. 고품질 가죽 소재와 내구성을 겸비하고 있습니다.',
+ id: 15,
+ name: '프론티어 드레스',
+ description: '고급 옷감을 사용하여 제작된 드레스입니다. 우아한 실루엣을 연출합니다.',
tags: ['FASHION'],
- price: 79000,
- createdAt: '2023-07-16T05:00:00Z',
- updatedAt: '2023-07-16T05:00:00Z',
- },
- {
- id: 'd9e3f6ae-04e5-4a7b-8c7c-2d582af08489',
- name: '티파니 실버 팔찌',
- description:
- '티파니의 실버 팔찌는 우아하고 고급스러운 디자인으로 많은 사랑을 받는 제품입니다. 실버 소재와 섬세한 장식이 특징입니다.',
- tags: ['FASHION'],
- price: 289000,
- createdAt: '2023-07-16T05:30:00Z',
- updatedAt: '2023-07-16T05:30:00Z',
- },
- {
- id: '10f12b65-20f5-43d9-860e-faf4890e2a9e',
- name: '조던 남성 농구화',
- description:
- '조던의 남성 농구화는 탁월한 풋워크와 스트라이드를 위한 디자인과 탁월한 지지력을 제공합니다. 전문 농구 선수들의 선택입니다.',
- tags: ['SPORTS'],
- price: 189000,
- createdAt: '2023-07-16T06:00:00Z',
- updatedAt: '2023-07-16T06:00:00Z',
- },
- {
- id: '6922d7f9-72d6-46fe-8b0e-890700aa8f13',
- name: '유니클로 남성 셔츠',
- description:
- '유니클로의 남성 셔츠는 심플하면서도 편안한 디자인으로 인기를 끄는 제품입니다. 다양한 컬러와 스타일로 선택의 폭을 넓힐 수 있습니다.',
- tags: ['FASHION'],
- price: 25000,
- createdAt: '2023-07-16T06:30:00Z',
- updatedAt: '2023-07-16T06:30:00Z',
- },
- {
- id: 'e5d7a4f3-9e21-4125-9e1c-216d5a226b74',
- name: '베리알 남성 어깨 가방',
- description:
- '베리알의 남성 어깨 가방은 실용적인 디자인과 고품질 소재로 제작되었습니다. 여행이나 일상적인 외출에 편리한 수납 공간을 제공합니다.',
- tags: ['FASHION'],
- price: 69000,
- createdAt: '2023-07-16T07:00:00Z',
- updatedAt: '2023-07-16T07:00:00Z',
- },
- {
- id: 'f751e63f-686d-42d4-898e-3ef6d4137908',
- name: '키친아트 커피머신',
- description:
- '키친아트의 커피머신은 다양한 커피 메뉴를 즐길 수 있는 다기능 제품입니다. 간편한 조작과 풍부한 맛을 선사합니다.',
- tags: ['KITCHENWARE'],
- price: 89000,
- createdAt: '2023-07-16T07:30:00Z',
- updatedAt: '2023-07-16T07:30:00Z',
- },
- {
- id: '1e112c02-7c92-4d52-b76e-485e41e3f64d',
- name: '파나소닉 전기 면도기',
- description:
- '파나소닉의 전기 면도기는 부드럽고 깨끗한 면도 경험을 제공합니다. 섬세한 면도와 피부 자극을 최소화합니다.',
- tags: ['ELECTRONICS'],
- price: 79000,
- createdAt: '2023-07-16T08:00:00Z',
- updatedAt: '2023-07-16T08:00:00Z',
- },
- {
- id: '19a6ce8e-70ed-4e3c-832a-ba3541a7da15',
- name: '보브의 화장 솔루션 세트',
- description:
- '보브의 화장 솔루션 세트는 다양한 솔루션으로 메이크업을 완벽하게 지원합니다. 피부에 자연스러운 톤과 미세한 마무리를 제공합니다.',
- tags: ['BEAUTY'],
- price: 39000,
- createdAt: '2023-07-16T02:15:00Z',
- updatedAt: '2023-07-16T02:15:00Z',
+ price: 8000000,
+ sellerId: 3, //타타루
},
];
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 086f8c7e..d8188cbe 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -5,74 +5,98 @@
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
- provider = "prisma-client-js"
- // output = "../generated/prisma"
+ provider = "prisma-client-js"
+ // output = "../generated/prisma"
}
datasource db {
- provider = "postgresql"
- url = env("DATABASE_URL")
+ provider = "postgresql"
+ url = env("DATABASE_URL")
}
+//`id`, `email`, `nickname`, `image`, `password`, `createdAt`, `updatedAt`
+// 기존 모델(`Product`, `Article`, `Comment`)과 1:N 관계 설정 (`relation` 연결)
+
//--- 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 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 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 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 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 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 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
+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 //판매완료
+ SALE //판매중
+ RESERVED //예약중
+ SOLD //판매완료
}
diff --git a/prisma/seed.js b/prisma/seed.js
index 48dcbe21..d4a9e3a8 100644
--- a/prisma/seed.js
+++ b/prisma/seed.js
@@ -1,38 +1,48 @@
-import prisma from '../src/lib/prismaClient.js';
+import { PrismaClient } from '@prisma/client';
import { USERS, PRODUCTS } from './mock.js';
+import bcrypt from 'bcrypt';
+
+const prisma = new PrismaClient();
async function main() {
- //기존 데이터 잔존시 시드를 재실행 했을 때 꼬일 수 있으므로 삭제 코드 추가
- await prisma.user.deleteMany();
+ //기존에 데이터가 남아있을 시 지우기 위해 사용
await prisma.product.deleteMany();
+ await prisma.user.deleteMany();
- //목데이터 삽입
- await prisma.user.createMany({
- data: USERS,
- skipDuplicates: true,
- });
+ /*기존 데이터를 싹 날리고, ID 번호표도 1번으로 리셋. 안그러면 db 내부 카운터가 안줄어서 8, 10 이럴 수 있다고 함
+ TRUNCATE는 "테이블 비우기" + RESTART IDENTITY는 "번호 초기화".
+ (이걸 해야 sellerId: 1이 정확히 민필리아를 가리킴)*/
+ await prisma.$executeRaw`TRUNCATE TABLE "User", "Product", "Article", "Comment", "Image" RESTART IDENTITY CASCADE;`;
- //셀러 아이디 부여
- const productsWithSeller = PRODUCTS.map((product) => {
- //유저 수 만큼 랜덤인덱스 뽑기
- const randomIndex = Math.floor(Math.random() * USERS.length);
- //유저스의 랜덤 인덱스에서의 아이디를 랜덤 셀러아이디에 부여
- const randomSellerId = USERS[randomIndex].id;
+ //===== 유저 데이터 생성 =====
+ console.log('👥 유저(새벽의 혈맹) 등록 중...');
- return {
- ...product,
- sellerId: randomSellerId,
- };
+ 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: productsWithSeller,
+ data: PRODUCTS,
skipDuplicates: true,
});
- console.log('시딩 완료!');
}
+console.log('에오르제아 중고장터 개방 완료');
+console.log(`➡️ 등록된 유저: ${USERS.length}명`);
+console.log(`➡️ 진열된 상품: ${PRODUCTS.length}개`);
-//실행기능
+//===== 실행버튼 =====
main()
.catch((e) => {
console.error('에러 캐치!:', e);
@@ -42,5 +52,3 @@ main()
await prisma.$disconnect();
console.log('데이터베이스 연결 종료');
});
-
-//npx prisma db seed
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/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/sprint-mission-3/prisma/migrations/migration_lock.toml b/sprint-mission-3/prisma/migrations/migration_lock.toml
new file mode 100644
index 00000000..fbffa92c
--- /dev/null
+++ b/sprint-mission-3/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/sprint-mission-3/prisma/mock.js b/sprint-mission-3/prisma/mock.js
new file mode 100644
index 00000000..b8cf9368
--- /dev/null
+++ b/sprint-mission-3/prisma/mock.js
@@ -0,0 +1,651 @@
+export const USERS = [
+ {
+ id: 'b8f11e76-0a9e-4b3f-bccf-8d9b4fbf331e',
+ email: 'honggd@example.com',
+ firstName: '길동',
+ lastName: '홍',
+ address: '서울특별시 강남구 무실로 123번길 45-6',
+ createdAt: '2023-07-16T09:00:00Z',
+ updatedAt: '2023-07-16T09:00:00Z',
+ },
+ {
+ id: '6c3a18b0-11c5-4d97-9019-9ebe3c4d1317',
+ email: 'kimyh@example.com',
+ firstName: '영희',
+ lastName: '김',
+ address: '경기도 고양시 봉명로 789번길 21',
+ createdAt: '2023-07-16T09:30:00Z',
+ updatedAt: '2023-07-16T09:30:00Z',
+ },
+ {
+ id: 'fd3ae0a5-8dd5-40b6-b8fd-48870f731db1',
+ email: 'lee.cs@example.com',
+ firstName: '철수',
+ lastName: '이',
+ address: '인천광역시 남구 향교로 567번길 8-2',
+ createdAt: '2023-07-16T10:00:00Z',
+ updatedAt: '2023-07-16T10:00:00Z',
+ },
+ {
+ id: '70e1e61d-f2ae-4d7d-bf8f-d65eafdb6a45',
+ email: 'parkjy@example.com',
+ firstName: '지영',
+ lastName: '박',
+ address: '대전광역시 중구 성남로 432번길 76',
+ createdAt: '2023-07-16T10:30:00Z',
+ updatedAt: '2023-07-16T10:30:00Z',
+ },
+ {
+ id: '73cb9639-d8b7-4f11-9a62-53f4187f3f11',
+ email: 'jungminsoo@example.com',
+ firstName: '민수',
+ lastName: '정',
+ address: '부산광역시 동래구 수림로 987번길 33-7',
+ createdAt: '2023-07-16T11:00:00Z',
+ updatedAt: '2023-07-16T11:00:00Z',
+ },
+];
+
+export const PRODUCTS = [
+ {
+ id: 'f8013040-b076-4dc4-8677-11be9a17162f',
+ name: '랑방 샤워젤 세트',
+ description:
+ '랑방의 향기로운 샤워젤 세트입니다. 피부를 부드럽게 케어하며, 향기로운 샤워 시간을 선사합니다.',
+ tags: ['BEAUTY'],
+ price: 38000,
+ createdAt: '2023-07-14T10:00:00Z',
+ updatedAt: '2023-07-14T10:00:00Z',
+ },
+ {
+ id: 'd2ff3048-83bc-425a-8ad3-d6d9af1c7c6d',
+ name: '나이키 테크조그거팬츠',
+ description:
+ '나이키의 테크조그거팬츠입니다. 운동이나 일상 생활에서 편안하게 착용할 수 있으며, 스타일과 기능을 모두 갖추고 있습니다.',
+ tags: ['FASHION'],
+ price: 75000,
+ createdAt: '2023-07-14T10:30:00Z',
+ updatedAt: '2023-07-14T10:30:00Z',
+ },
+ {
+ id: '7f70481b-784d-4b0e-bc3e-f05eefc17951',
+ name: 'Apple AirPods 프로',
+ description:
+ 'Apple의 AirPods 프로는 탁월한 사운드 품질과 노이즈 캔슬링 기능을 갖춘 무선 이어폰입니다. 간편한 터치 컨트롤과 긴 배터리 수명을 제공합니다.',
+ tags: ['ELECTRONICS'],
+ price: 320000,
+ createdAt: '2023-07-14T11:00:00Z',
+ updatedAt: '2023-07-14T11:00:00Z',
+ },
+ {
+ id: '4e0d9424-3a16-4a5e-9725-0e9d2f9722b3',
+ name: '베르사체 화장품 세트',
+ description:
+ '베르사체의 화장품 세트로 화려하고 특별한 분위기를 연출할 수 있습니다. 다양한 아이템으로 구성되어 있으며, 고품질 성분을 사용하여 피부에 부드럽고 안정적인 관리를 제공합니다.',
+ tags: ['BEAUTY'],
+ price: 65000,
+ createdAt: '2023-07-14T11:30:00Z',
+ updatedAt: '2023-07-14T11:30:00Z',
+ },
+ {
+ id: '39c3fd4a-dbd5-4ab1-8e0b-58ea31b8a2d3',
+ name: '아이언맨 골프 클럽 세트',
+ description:
+ '아이언맨 디자인으로 유명한 골프 클럽 세트입니다. 고품질 소재와 최신 기술로 제작되어 정밀한 스윙과 멋진 샷을 도와줍니다.',
+ tags: ['SPORTS'],
+ price: 850000,
+ createdAt: '2023-07-14T12:00:00Z',
+ updatedAt: '2023-07-14T12:00:00Z',
+ },
+ {
+ id: 'c2d4a2e3-7c2a-4f80-bff7-8ebcbacccf2c',
+ name: '삼성 갤럭시 S21 울트라 5G 스마트폰',
+ description:
+ '삼성 갤럭시 S21 울트라 5G 스마트폰은 최신 플래그십 기술을 갖춘 뛰어난 성능을 제공합니다. 화려한 디스플레이, 고화질 카메라, 빠른 프로세서 등으로 사용자들에게 탁월한 사용자 경험을 선사합니다.',
+ tags: ['ELECTRONICS'],
+ price: 1249000,
+ createdAt: '2023-07-14T14:00:00Z',
+ updatedAt: '2023-07-14T14:00:00Z',
+ },
+ {
+ id: '43c62d5b-6e66-4d1c-9f47-14d1a1970fd1',
+ name: 'LG 그램 14형 노트북',
+ description:
+ 'LG 그램 14형 노트북은 가볍고 휴대성이 뛰어난 디자인과 우수한 성능을 제공합니다. 긴 배터리 수명과 선명한 디스플레이로 사용자들에게 편리한 사용자 경험을 선사합니다.',
+ tags: ['ELECTRONICS'],
+ price: 1399000,
+ createdAt: '2023-07-14T14:30:00Z',
+ updatedAt: '2023-07-14T14:30:00Z',
+ },
+ {
+ id: '6e9d4e63-7246-4fc5-bf33-3e7f32fe9c02',
+ name: '나이키 에어맥스 270 운동화',
+ description:
+ '나이키 에어맥스 270 운동화는 경량 디자인과 탁월한 고무 밑창으로 편안한 착용감과 훌륭한 트랙션을 제공합니다. 스포티한 디자인과 풍부한 컬러로 다양한 스타일에 어울립니다.',
+ tags: ['FASHION'],
+ price: 169000,
+ createdAt: '2023-07-14T15:00:00Z',
+ updatedAt: '2023-07-14T15:00:00Z',
+ },
+ {
+ id: '80e7b377-df80-4410-9a14-622ea3cc58e0',
+ name: '애플 맥북 프로 13형 노트북',
+ description:
+ '애플 맥북 프로 13형 노트북은 탁월한 성능과 우수한 디스플레이 품질로 유명합니다. 가벼운 무게와 편리한 사용성으로 사용자들에게 탁월한 컴퓨팅 경험을 제공합니다.',
+ tags: ['ELECTRONICS'],
+ price: 1999000,
+ createdAt: '2023-07-14T15:30:00Z',
+ updatedAt: '2023-07-14T15:30:00Z',
+ },
+ {
+ id: 'a4ff201c-48f7-4963-b317-2e9e4e3e43b7',
+ name: '랑방 매트 틴트',
+ description:
+ '랑방 매트 틴트는 풍부한 컬러와 지속력을 제공하는 제품입니다. 입술에 부드럽게 발리며 오래 지속되는 매트한 마무리를 선사합니다.',
+ tags: ['BEAUTY'],
+ price: 35000,
+ createdAt: '2023-07-14T16:00:00Z',
+ updatedAt: '2023-07-14T16:00:00Z',
+ },
+ {
+ id: 'c6a5975a-42e7-4f7f-8b7c-72714d59f44a',
+ name: '루이비통 클래식 지갑',
+ description:
+ '루이비통의 클래식 지갑은 고품질 가죽과 세련된 디자인으로 제작되었습니다. 실용적인 구성과 품질로 오랜 기간 동안 사용할 수 있습니다.',
+ tags: ['FASHION'],
+ price: 750000,
+ createdAt: '2023-07-14T16:30:00Z',
+ updatedAt: '2023-07-14T16:30:00Z',
+ },
+ {
+ id: 'a33d441f-57a9-4618-8f46-07e7418ef3c9',
+ name: '아이폰 13 프로 맥스 스마트폰',
+ description:
+ '아이폰 13 프로 맥스는 탁월한 카메라 성능과 강력한 성능으로 유명한 스마트폰입니다. 선명한 디스플레이와 편리한 사용성을 제공합니다.',
+ tags: ['ELECTRONICS'],
+ price: 1799000,
+ createdAt: '2023-07-14T17:00:00Z',
+ updatedAt: '2023-07-14T17:00:00Z',
+ },
+ {
+ id: 'f6c2a70e-32a4-4bcf-b183-8a33d97fb587',
+ name: '헤라 UV 미스트 선스틱 SPF50+ PA+++',
+ description:
+ '헤라의 UV 미스트 선스틱은 SPF50+ PA+++로 강력한 자외선 차단과 함께 피부 보습에 도움을 주는 제품입니다. 휴대하기 편리한 스틱 형태로 사용이 간편합니다.',
+ tags: ['BEAUTY'],
+ price: 28000,
+ createdAt: '2023-07-14T17:30:00Z',
+ updatedAt: '2023-07-14T17:30:00Z',
+ },
+ {
+ id: 'e700c4be-6e0c-40fd-bb47-2ab2b2e9270d',
+ name: '올리비아 버튼 니트',
+ description:
+ '올리비아 버튼 니트는 스타일리시한 디자인과 편안한 착용감으로 유명한 제품입니다. 다양한 의상에 매치하기 좋으며, 따뜻한 니트 소재로 겨울철에도 활용할 수 있습니다.',
+ tags: ['FASHION'],
+ price: 59000,
+ createdAt: '2023-07-14T18:00:00Z',
+ updatedAt: '2023-07-14T18:00:00Z',
+ },
+ {
+ id: 'be13a617-8f0e-4806-82f7-149a6e12f1a7',
+ name: '삼성 55인치 QLED 4K 스마트 TV',
+ description:
+ '삼성의 55인치 QLED 4K 스마트 TV는 고품질 화질과 풍부한 컬러 표현력으로 몰입감 있는 시청 경험을 제공합니다. 스마트 기능과 음성 인식 기능을 지원하여 편리한 사용자 경험을 선사합니다.',
+ tags: ['ELECTRONICS'],
+ price: 1999000,
+ createdAt: '2023-07-14T18:30:00Z',
+ updatedAt: '2023-07-14T18:30:00Z',
+ },
+ {
+ id: 'd3f74179-2cb5-4e51-8f3a-57f65c1d4e6e',
+ name: '루이스 카스텔리 미니 프라이팬',
+ description:
+ '루이스 카스텔리의 미니 프라이팬은 작고 휴대하기 편한 사이즈로 다양한 요리에 활용할 수 있습니다. 내열성과 내마모성이 우수하며 손쉬운 청소가 가능합니다.',
+ tags: ['KITCHENWARE'],
+ price: 29000,
+ createdAt: '2023-07-15T10:00:00Z',
+ updatedAt: '2023-07-15T10:00:00Z',
+ },
+ {
+ id: 'dc74c065-5237-4d5b-86d1-d4eeb8a4d1fd',
+ name: '델롱기 헤어 드라이어',
+ description:
+ '델롱기의 헤어 드라이어는 강력한 풍력과 다양한 모드로 빠른 건조와 스타일링을 도와줍니다. 손쉬운 조작과 휴대성을 갖추고 있습니다.',
+ tags: ['ELECTRONICS'],
+ price: 69000,
+ createdAt: '2023-07-15T10:30:00Z',
+ updatedAt: '2023-07-15T10:30:00Z',
+ },
+ {
+ id: '93a1c570-ee10-4961-bbdf-68001efddc49',
+ name: '보노홈 스탠드 믹서기',
+ description:
+ '보노홈의 스탠드 믹서기는 강력한 모터와 다양한 속도 조절로 원하는 믹싱과 반죽 작업을 할 수 있습니다. 내구성이 뛰어나며 안정적인 성능을 제공합니다.',
+ tags: ['KITCHENWARE'],
+ price: 99000,
+ createdAt: '2023-07-15T11:00:00Z',
+ updatedAt: '2023-07-15T11:00:00Z',
+ },
+ {
+ id: 'd5d60337-ccf6-404f-b615-982f2b223ab3',
+ name: '나이키 에어 포스 1 운동화',
+ description:
+ '나이키의 에어 포스 1 운동화는 클래식한 디자인과 탁월한 편안함으로 유명합니다. 내구성이 우수하며 다양한 스타일과 컬러로 선택할 수 있습니다.',
+ tags: ['FASHION'],
+ price: 129000,
+ createdAt: '2023-07-15T11:30:00Z',
+ updatedAt: '2023-07-15T11:30:00Z',
+ },
+ {
+ id: '209b2a1c-c6e9-4461-a17a-c8a8f141f9f9',
+ name: '삼성 컬러 레이저 프린터',
+ description:
+ '삼성의 컬러 레이저 프린터는 고품질 컬러 출력과 빠른 인쇄 속도로 탁월한 인쇄 품질을 제공합니다. 다양한 용지 크기와 고효율 인쇄 기능을 지원합니다.',
+ tags: ['ELECTRONICS'],
+ price: 499000,
+ createdAt: '2023-07-15T12:00:00Z',
+ updatedAt: '2023-07-15T12:00:00Z',
+ },
+ {
+ id: 'b1782c1e-8ad2-45a7-90e6-50f61c4a8de9',
+ name: '코멧 피크닉 매트',
+ description:
+ '코멧 피크닉 매트는 내열성과 내충격성이 뛰어나며 편리한 보관과 휴대를 위한 접이식 디자인입니다. 야외 활동에 최적화되어 있습니다.',
+ tags: ['SPORTS'],
+ price: 35000,
+ createdAt: '2023-07-15T12:30:00Z',
+ updatedAt: '2023-07-15T12:30:00Z',
+ },
+ {
+ id: 'b2593dbd-5ea3-4376-9378-6d6e7a17e232',
+ name: '루미에어 드라이 바디 수건',
+ description:
+ '루미에어의 드라이 바디 수건은 퀵 드라이와 산뜻한 사용감을 제공하는 혁신적인 수건입니다. 속건성이 뛰어나며 휴대하기 편리합니다.',
+ tags: ['BEAUTY'],
+ price: 19900,
+ createdAt: '2023-07-15T13:00:00Z',
+ updatedAt: '2023-07-15T13:00:00Z',
+ },
+ {
+ id: 'c28a2eaf-4d87-4f9f-ae5b-cbcf73e24253',
+ name: '쿠진앤에이 오믈렛 팬',
+ description:
+ '쿠진앤에이의 오믈렛 팬은 오믈렛을 쉽고 빠르게 만들 수 있는 전용 팬입니다. 내열성이 뛰어나며 논스틱 처리로 편리한 사용과 청소가 가능합니다.',
+ tags: ['KITCHENWARE'],
+ price: 25000,
+ createdAt: '2023-07-15T13:30:00Z',
+ updatedAt: '2023-07-15T13:30:00Z',
+ },
+ {
+ id: 'e48d9e8b-6712-48ed-9ea2-2f7e150a2e48',
+ name: '네스프레소 커피머신',
+ description:
+ '네스프레소 커피머신은 간편한 사용과 다양한 커피 음료 제공으로 인기 있는 제품입니다. 커피 품질과 편리한 관리 기능을 동시에 갖추고 있습니다.',
+ tags: ['ELECTRONICS'],
+ price: 149000,
+ createdAt: '2023-07-15T14:00:00Z',
+ updatedAt: '2023-07-15T14:00:00Z',
+ },
+ {
+ id: '64e7c9fc-49b6-4b20-9ce3-11a9487ed1e1',
+ name: '스완스톤 운동 요가 매트',
+ description:
+ '스완스톤의 운동 요가 매트는 편안하고 안정적인 기준면과 내충격성으로 안전한 운동 활동을 도와줍니다. 내마모성이 우수하며 휴대하기 편리합니다.',
+ tags: ['SPORTS'],
+ price: 69000,
+ createdAt: '2023-07-15T14:30:00Z',
+ updatedAt: '2023-07-15T14:30:00Z',
+ },
+ {
+ id: 'e3f34fb9-2c4b-45c0-94b1-610e6cfe13a7',
+ name: '샤넬 커버 파운데이션',
+ description:
+ '샤넬의 커버 파운데이션은 자연스럽게 피부를 커버하고 매끈한 마무리를 선사합니다. 오랜 지속력과 촉촉한 사용감을 제공합니다.',
+ tags: ['BEAUTY'],
+ price: 65000,
+ createdAt: '2023-07-15T15:00:00Z',
+ updatedAt: '2023-07-15T15:00:00Z',
+ },
+ {
+ id: '4be1e6fd-2271-4f15-96e3-577c4a057c8b',
+ name: '올바로 10단 스텐레스 냄비세트',
+ description:
+ '올바로의 10단 스텐레스 냄비세트는 내열성과 내식성이 우수한 제품입니다. 다양한 크기와 기능으로 요리를 즐길 수 있습니다.',
+ tags: ['KITCHENWARE'],
+ price: 179000,
+ createdAt: '2023-07-15T15:30:00Z',
+ updatedAt: '2023-07-15T15:30:00Z',
+ },
+ {
+ id: 'a4468fc1-3cfc-4c46-9ff9-2469b5f9e88a',
+ name: '아디다스 운동바지',
+ description:
+ '아디다스의 운동바지는 신축성과 편안한 착용감으로 활동성을 높여줍니다. 퀄리티한 소재와 다양한 스타일로 선택의 폭이 넓습니다.',
+ tags: ['FASHION'],
+ price: 59000,
+ createdAt: '2023-07-15T16:00:00Z',
+ updatedAt: '2023-07-15T16:00:00Z',
+ },
+ {
+ id: 'be07b27a-93e9-4d1a-8a54-133c0f04541e',
+ name: 'LG 울트라 HD 4K TV',
+ description:
+ 'LG의 울트라 HD 4K TV는 생생하고 선명한 화질로 몰입감 있는 시청 경험을 제공합니다. 스마트 기능과 다양한 연결 옵션을 갖추고 있습니다.',
+ tags: ['ELECTRONICS'],
+ price: 2299000,
+ createdAt: '2023-07-15T16:30:00Z',
+ updatedAt: '2023-07-15T16:30:00Z',
+ },
+ {
+ id: 'de573676-2a27-4cfa-85a1-0e9dd3ed5315',
+ name: '삼성 갤럭시 워치4',
+ description:
+ '삼성의 갤럭시 워치4는 스타일리시한 디자인과 다양한 기능으로 탁월한 착용 경험을 선사합니다. 편리한 스마트 기능과 건강 관리 기능을 갖추고 있습니다.',
+ tags: ['ELECTRONICS'],
+ price: 399000,
+ createdAt: '2023-07-15T17:00:00Z',
+ updatedAt: '2023-07-15T17:00:00Z',
+ },
+ {
+ id: '9c6ebf9b-f666-44da-b79f-5f62517f55fc',
+ name: '나이스컷 고급 칼세트',
+ description:
+ '나이스컷의 고급 칼세트는 품질 높은 강화 스테인리스 블레이드로 효율적인 커팅을 가능하게 합니다. 다양한 칼 종류로 다양한 요리를 할 수 있습니다.',
+ tags: ['KITCHENWARE'],
+ price: 99000,
+ createdAt: '2023-07-15T17:30:00Z',
+ updatedAt: '2023-07-15T17:30:00Z',
+ },
+ {
+ id: '17cc4f9c-1e71-4782-914a-c8a12dc6f94b',
+ name: '아벤크롬비 스니커즈',
+ description:
+ '아벤크롬비의 스니커즈는 스포티한 디자인과 편안한 착용감으로 인기 있는 제품입니다. 고품질 소재와 세련된 스타일로 멋진 캐주얼 룩을 완성할 수 있습니다.',
+ tags: ['FASHION'],
+ price: 89000,
+ createdAt: '2023-07-15T18:00:00Z',
+ updatedAt: '2023-07-15T18:00:00Z',
+ },
+ {
+ id: 'c1b31e2b-6d6a-4b24-b3ea-d0746f9cc6ea',
+ name: '피오라 러브포이즌 향수',
+ description:
+ '피오라의 러브포이즌 향수는 상쾌하고 여성스러운 향기로 매력적인 분위기를 연출합니다. 오래 지속되는 향과 우아한 디자인으로 사랑받는 제품입니다.',
+ tags: ['BEAUTY'],
+ price: 79000,
+ createdAt: '2023-07-15T18:30:00Z',
+ updatedAt: '2023-07-15T18:30:00Z',
+ },
+ {
+ id: '5ae4a823-0076-4ae0-af49-7f3eb0002c0a',
+ name: '니스 퍼퓸 캔들',
+ description:
+ '니스 퍼퓸의 캔들은 아로마 향으로 휴식과 힐링을 선사합니다. 품질 좋은 원료로 만들어진 캔들은 안전하고 오랜 시간 향기를 유지합니다.',
+ tags: ['HOME_INTERIOR'],
+ price: 35000,
+ createdAt: '2023-07-15T19:00:00Z',
+ updatedAt: '2023-07-15T19:00:00Z',
+ },
+ {
+ id: '50ce94ef-cb04-4c2f-bad4-d36b18956b9a',
+ name: '디젤 남성 슬림 핏 청바지',
+ description:
+ '디젤의 남성 슬림 핏 청바지는 모던하고 스타일리시한 디자인으로 인기를 끄는 제품입니다. 퀄리티한 소재와 세련된 실루엣으로 완벽한 핏을 선사합니다.',
+ tags: ['FASHION'],
+ price: 139000,
+ createdAt: '2023-07-15T19:30:00Z',
+ updatedAt: '2023-07-15T19:30:00Z',
+ },
+ {
+ id: 'd6c5e7d5-225f-4f6d-ba17-6f79d32726db',
+ name: '슈퍼디올 여성 선글라스',
+ description:
+ '슈퍼디올의 여성 선글라스는 세련된 디자인과 탁월한 안경 렌즈로 스타일과 보호를 동시에 제공합니다. 여름 휴가나 일상적인 착용에 적합합니다.',
+ tags: ['FASHION'],
+ price: 189000,
+ createdAt: '2023-07-15T20:00:00Z',
+ updatedAt: '2023-07-15T20:00:00Z',
+ },
+ {
+ id: '9c2bc7ad-6f1e-491d-8d81-9ed78a805a20',
+ name: '보타닉 프레쉬 화장품 세트',
+ description:
+ '보타닉 프레쉬의 화장품 세트는 천연 식물성 성분을 사용하여 피부에 영양과 보습을 제공합니다. 다양한 제품으로 피부 관리를 완벽하게 할 수 있습니다.',
+ tags: ['BEAUTY'],
+ price: 99000,
+ createdAt: '2023-07-15T20:30:00Z',
+ updatedAt: '2023-07-15T20:30:00Z',
+ },
+ {
+ id: 'f8c95e63-ba05-4e2d-8a61-9e5b07d4ccdb',
+ name: '프라다 남성 가죽 벨트',
+ description:
+ '프라다의 남성 가죽 벨트는 고품질 가죽 소재와 세련된 디자인으로 멋진 스타일링을 완성할 수 있습니다. 다양한 의상에 어울리는 제품입니다.',
+ tags: ['FASHION'],
+ price: 249000,
+ createdAt: '2023-07-15T21:00:00Z',
+ updatedAt: '2023-07-15T21:00:00Z',
+ },
+ {
+ id: 'e6c6aeed-209d-4f3d-907e-9d208d5bcfd2',
+ name: 'LG 스마트 인버터 에어컨',
+ description:
+ 'LG의 스마트 인버터 에어컨은 효율적인 냉방과 에너지 절약을 위한 기능을 갖추고 있습니다. 스마트한 제어와 시원한 바람으로 쾌적한 환경을 제공합니다.',
+ tags: ['ELECTRONICS'],
+ price: 1399000,
+ createdAt: '2023-07-15T21:30:00Z',
+ updatedAt: '2023-07-15T21:30:00Z',
+ },
+ {
+ id: 'a81dd4df-5bc4-44b5-89a0-769a7b6d3cc0',
+ name: '닌텐도 스위치 라이트',
+ description:
+ '닌텐도 스위치 라이트는 휴대성과 다양한 게임 플레이를 위한 제품입니다. 화질과 음질이 우수하며 다양한 게임을 즐길 수 있습니다.',
+ tags: ['ELECTRONICS'],
+ price: 299000,
+ createdAt: '2023-07-15T22:00:00Z',
+ updatedAt: '2023-07-15T22:00:00Z',
+ },
+ {
+ id: '6e890e6d-df7b-4c50-8d3e-59a32c48ae51',
+ name: '루미에어 프리미엄 베개',
+ description:
+ '루미에어의 프리미엄 베개는 안락한 수면을 위한 편안한 디자인과 고품질 소재로 제작되었습니다. 적절한 지지력과 통기성을 제공합니다.',
+ tags: ['HOME_INTERIOR'],
+ price: 59000,
+ createdAt: '2023-07-15T22:30:00Z',
+ updatedAt: '2023-07-15T22:30:00Z',
+ },
+ {
+ id: '7a14ccf9-8b08-4e9a-8f0f-624dcf7d6d74',
+ name: '메종키츠네 남성 반팔 티셔츠',
+ description:
+ '메종키츠네의 남성 반팔 티셔츠는 고품질 소재와 세련된 디자인으로 스타일리시한 룩을 완성할 수 있습니다. 다양한 컬러와 패턴으로 선택의 폭을 넓힐 수 있습니다.',
+ tags: ['FASHION'],
+ price: 149000,
+ createdAt: '2023-07-15T23:00:00Z',
+ updatedAt: '2023-07-15T23:00:00Z',
+ },
+ {
+ id: 'b5d2d3ad-7d92-4f17-a1e5-2260a2d69d0d',
+ name: '올림포스 포터블 프린터',
+ description:
+ '올림포스의 포터블 프린터는 휴대성과 편리한 사용을 제공하는 제품입니다. 다양한 용지 크기와 고품질 인쇄를 지원합니다.',
+ tags: ['ELECTRONICS'],
+ price: 99000,
+ createdAt: '2023-07-16T00:00:00Z',
+ updatedAt: '2023-07-16T00:00:00Z',
+ },
+
+ {
+ id: '3f0ccf0a-9980-4a55-9ef0-57e929fe813c',
+ name: '디올 애디크트 립스틱',
+ description:
+ '디올의 애디크트 립스틱은 풍부한 컬러와 윤기로운 마무리를 선사합니다. 부드러운 발림성과 오랜 지속력으로 립 메이크업을 완성합니다.',
+ tags: ['BEAUTY'],
+ price: 45000,
+ createdAt: '2023-07-16T00:30:00Z',
+ updatedAt: '2023-07-16T00:30:00Z',
+ },
+ {
+ id: 'f6a63b92-870e-4b54-8f80-7ae5e0b5be78',
+ name: '삼성 갤럭시 버즈 프로',
+ description:
+ '삼성의 갤럭시 버즈 프로는 탁월한 음질과 노이즈 캔슬링 기능을 제공하는 무선 이어폰입니다. 스마트한 기능과 편안한 착용감을 동시에 즐길 수 있습니다.',
+ tags: ['ELECTRONICS'],
+ price: 229000,
+ createdAt: '2023-07-16T01:00:00Z',
+ updatedAt: '2023-07-16T01:00:00Z',
+ },
+ {
+ id: 'd0ccffae-fa61-4e2f-88ff-2253e838bf2d',
+ name: '지오다노 남성 반팔 티셔츠',
+ description:
+ '지오다노의 남성 반팔 티셔츠는 심플하면서도 스타일리시한 디자인으로 인기를 끄는 제품입니다. 퀄리티한 소재와 편안한 착용감을 제공합니다.',
+ tags: ['FASHION'],
+ price: 29000,
+ createdAt: '2023-07-16T01:30:00Z',
+ updatedAt: '2023-07-16T01:30:00Z',
+ },
+ {
+ id: '8a131b86-7b3e-44c2-a02b-b17c69a2c780',
+ name: '필립스 공기청정기',
+ description:
+ '필립스의 공기청정기는 공기 중의 먼지와 유해 물질을 효과적으로 제거하여 깨끗한 공기를 유지합니다. 소음이 적고 사용이 편리합니다.',
+ tags: ['ELECTRONICS'],
+ price: 119000,
+ createdAt: '2023-07-16T02:00:00Z',
+ updatedAt: '2023-07-16T02:00:00Z',
+ },
+ {
+ id: 'f8cb3992-c455-4c9a-9736-1f94a83b3e6f',
+ name: '후아유 촉촉 수분 선크림',
+ description:
+ '후아유의 촉촉 수분 선크림은 가볍고 촉촉한 텍스처로 피부에 쉽게 흡수되며 자외선 차단과 피부 보호를 동시에 제공합니다.',
+ tags: ['BEAUTY'],
+ price: 39000,
+ createdAt: '2023-07-16T02:30:00Z',
+ updatedAt: '2023-07-16T02:30:00Z',
+ },
+ {
+ id: '78e5b047-f28e-4b3e-9c62-54c2d12565cd',
+ name: '조지아 아이스 커피',
+ description:
+ '조지아 아이스 커피는 진하고 풍부한 커피 맛과 시원한 얼음으로 상쾌한 음료를 즐길 수 있습니다. 휴대하기 편리한 캔 형태로 제공됩니다.',
+ tags: ['HOUSEHOLD_SUPPLIES'],
+ price: 2000,
+ createdAt: '2023-07-16T03:00:00Z',
+ updatedAt: '2023-07-16T03:00:00Z',
+ },
+ {
+ id: 'b8e3e8d9-0f39-4f2e-99a0-1c00c8f5d9e1',
+ name: '헤스티아 화장지',
+ description:
+ '헤스티아의 화장지는 부드럽고 흡수력이 뛰어나며 피부 친화적인 소재로 만들어진 제품입니다. 편리한 사용감과 위생성을 제공합니다.',
+ tags: ['HOUSEHOLD_SUPPLIES'],
+ price: 5000,
+ createdAt: '2023-07-16T03:30:00Z',
+ updatedAt: '2023-07-16T03:30:00Z',
+ },
+ {
+ id: '0d8e554a-84c0-4b9c-bdc3-d04b9bbf1344',
+ name: '탐스킨 리뉴얼 페이셜 클렌저',
+ description:
+ '탐스킨의 리뉴얼 페이셜 클렌저는 부드러운 거품과 깨끗한 세정력으로 피부를 깨끗하게 유지합니다. 피부에 자극을 주지 않는 올인원 클렌저입니다.',
+ tags: ['BEAUTY'],
+ price: 15000,
+ createdAt: '2023-07-16T04:00:00Z',
+ updatedAt: '2023-07-16T04:00:00Z',
+ },
+ {
+ id: '1cc006a0-82a3-4e70-8d92-974d1ea9c3af',
+ name: '카시오 베이비-지 시계',
+ description:
+ '카시오 베이비-지 시계는 귀여운 디자인과 실용적인 기능을 가진 어린이용 시계입니다. 내추럴 라이트와 방수 기능을 갖추고 있습니다.',
+ tags: ['ELECTRONICS'],
+ price: 59000,
+ createdAt: '2023-07-16T04:30:00Z',
+ updatedAt: '2023-07-16T04:30:00Z',
+ },
+ {
+ id: '85fc2182-8b7f-47f1-8f3d-911e0a3c0582',
+ name: '홀리바나나 남성 가죽 슬리퍼',
+ description:
+ '홀리바나나 남성 가죽 슬리퍼는 편안한 착용감과 세련된 디자인으로 인기를 끄는 제품입니다. 고품질 가죽 소재와 내구성을 겸비하고 있습니다.',
+ tags: ['FASHION'],
+ price: 79000,
+ createdAt: '2023-07-16T05:00:00Z',
+ updatedAt: '2023-07-16T05:00:00Z',
+ },
+ {
+ id: 'd9e3f6ae-04e5-4a7b-8c7c-2d582af08489',
+ name: '티파니 실버 팔찌',
+ description:
+ '티파니의 실버 팔찌는 우아하고 고급스러운 디자인으로 많은 사랑을 받는 제품입니다. 실버 소재와 섬세한 장식이 특징입니다.',
+ tags: ['FASHION'],
+ price: 289000,
+ createdAt: '2023-07-16T05:30:00Z',
+ updatedAt: '2023-07-16T05:30:00Z',
+ },
+ {
+ id: '10f12b65-20f5-43d9-860e-faf4890e2a9e',
+ name: '조던 남성 농구화',
+ description:
+ '조던의 남성 농구화는 탁월한 풋워크와 스트라이드를 위한 디자인과 탁월한 지지력을 제공합니다. 전문 농구 선수들의 선택입니다.',
+ tags: ['SPORTS'],
+ price: 189000,
+ createdAt: '2023-07-16T06:00:00Z',
+ updatedAt: '2023-07-16T06:00:00Z',
+ },
+ {
+ id: '6922d7f9-72d6-46fe-8b0e-890700aa8f13',
+ name: '유니클로 남성 셔츠',
+ description:
+ '유니클로의 남성 셔츠는 심플하면서도 편안한 디자인으로 인기를 끄는 제품입니다. 다양한 컬러와 스타일로 선택의 폭을 넓힐 수 있습니다.',
+ tags: ['FASHION'],
+ price: 25000,
+ createdAt: '2023-07-16T06:30:00Z',
+ updatedAt: '2023-07-16T06:30:00Z',
+ },
+ {
+ id: 'e5d7a4f3-9e21-4125-9e1c-216d5a226b74',
+ name: '베리알 남성 어깨 가방',
+ description:
+ '베리알의 남성 어깨 가방은 실용적인 디자인과 고품질 소재로 제작되었습니다. 여행이나 일상적인 외출에 편리한 수납 공간을 제공합니다.',
+ tags: ['FASHION'],
+ price: 69000,
+ createdAt: '2023-07-16T07:00:00Z',
+ updatedAt: '2023-07-16T07:00:00Z',
+ },
+ {
+ id: 'f751e63f-686d-42d4-898e-3ef6d4137908',
+ name: '키친아트 커피머신',
+ description:
+ '키친아트의 커피머신은 다양한 커피 메뉴를 즐길 수 있는 다기능 제품입니다. 간편한 조작과 풍부한 맛을 선사합니다.',
+ tags: ['KITCHENWARE'],
+ price: 89000,
+ createdAt: '2023-07-16T07:30:00Z',
+ updatedAt: '2023-07-16T07:30:00Z',
+ },
+ {
+ id: '1e112c02-7c92-4d52-b76e-485e41e3f64d',
+ name: '파나소닉 전기 면도기',
+ description:
+ '파나소닉의 전기 면도기는 부드럽고 깨끗한 면도 경험을 제공합니다. 섬세한 면도와 피부 자극을 최소화합니다.',
+ tags: ['ELECTRONICS'],
+ price: 79000,
+ createdAt: '2023-07-16T08:00:00Z',
+ updatedAt: '2023-07-16T08:00:00Z',
+ },
+ {
+ id: '19a6ce8e-70ed-4e3c-832a-ba3541a7da15',
+ name: '보브의 화장 솔루션 세트',
+ description:
+ '보브의 화장 솔루션 세트는 다양한 솔루션으로 메이크업을 완벽하게 지원합니다. 피부에 자연스러운 톤과 미세한 마무리를 제공합니다.',
+ tags: ['BEAUTY'],
+ price: 39000,
+ createdAt: '2023-07-16T02:15:00Z',
+ updatedAt: '2023-07-16T02:15:00Z',
+ },
+];
diff --git a/sprint-mission-3/prisma/schema.prisma b/sprint-mission-3/prisma/schema.prisma
new file mode 100644
index 00000000..d9f86161
--- /dev/null
+++ b/sprint-mission-3/prisma/schema.prisma
@@ -0,0 +1,78 @@
+// // 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")
+// }
+
+// //--- 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, 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
+// }
+
+// //제품 판매 상태
+// enum ProductStatus {
+// SALE //판매중
+// RESERVED //예약중
+// SOLD //판매완료
+// }
diff --git a/sprint-mission-3/prisma/seed.js b/sprint-mission-3/prisma/seed.js
new file mode 100644
index 00000000..48dcbe21
--- /dev/null
+++ b/sprint-mission-3/prisma/seed.js
@@ -0,0 +1,46 @@
+import prisma from '../src/lib/prismaClient.js';
+import { USERS, PRODUCTS } from './mock.js';
+
+async function main() {
+ //기존 데이터 잔존시 시드를 재실행 했을 때 꼬일 수 있으므로 삭제 코드 추가
+ await prisma.user.deleteMany();
+ await prisma.product.deleteMany();
+
+ //목데이터 삽입
+ await prisma.user.createMany({
+ data: USERS,
+ skipDuplicates: true,
+ });
+
+ //셀러 아이디 부여
+ const productsWithSeller = PRODUCTS.map((product) => {
+ //유저 수 만큼 랜덤인덱스 뽑기
+ const randomIndex = Math.floor(Math.random() * USERS.length);
+ //유저스의 랜덤 인덱스에서의 아이디를 랜덤 셀러아이디에 부여
+ const randomSellerId = USERS[randomIndex].id;
+
+ return {
+ ...product,
+ sellerId: randomSellerId,
+ };
+ });
+
+ await prisma.product.createMany({
+ data: productsWithSeller,
+ skipDuplicates: true,
+ });
+ console.log('시딩 완료!');
+}
+
+//실행기능
+main()
+ .catch((e) => {
+ console.error('에러 캐치!:', e);
+ process.exit(1);
+ })
+ .finally(async () => {
+ await prisma.$disconnect();
+ console.log('데이터베이스 연결 종료');
+ });
+
+//npx prisma db seed
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]
+)
+
+## 멘토에게
+
+- 아직 잘 모르겠습니다....
+-
diff --git a/class/Article.js b/sprint-mission-3/sprint-mission-2/class/Article.js
similarity index 100%
rename from class/Article.js
rename to sprint-mission-3/sprint-mission-2/class/Article.js
diff --git a/class/ElectronicProduct.js b/sprint-mission-3/sprint-mission-2/class/ElectronicProduct.js
similarity index 100%
rename from class/ElectronicProduct.js
rename to sprint-mission-3/sprint-mission-2/class/ElectronicProduct.js
diff --git a/class/Product.js b/sprint-mission-3/sprint-mission-2/class/Product.js
similarity index 100%
rename from class/Product.js
rename to sprint-mission-3/sprint-mission-2/class/Product.js
diff --git a/main/main.js b/sprint-mission-3/sprint-mission-2/main/main.js
similarity index 100%
rename from main/main.js
rename to sprint-mission-3/sprint-mission-2/main/main.js
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/service/ArticleService.js b/sprint-mission-3/sprint-mission-2/service/ArticleService.js
similarity index 100%
rename from service/ArticleService.js
rename to sprint-mission-3/sprint-mission-2/service/ArticleService.js
diff --git a/service/ProductService.js b/sprint-mission-3/sprint-mission-2/service/ProductService.js
similarity index 100%
rename from service/ProductService.js
rename to sprint-mission-3/sprint-mission-2/service/ProductService.js
diff --git a/sprint-mission-3/src/app.js b/sprint-mission-3/src/app.js
new file mode 100644
index 00000000..11efe26e
--- /dev/null
+++ b/sprint-mission-3/src/app.js
@@ -0,0 +1,37 @@
+import express from 'express';
+import cors from 'cors';
+import dotenv from 'dotenv';
+import errorHandler from './middlewares/errorHandler.js';
+import productRouter from './routers/productRouter.js';
+import articleRouter from './routers/articleRouter.js';
+import commentRouter from './routers/commentRouter.js';
+import uploadRouter from './routers/uploadRouter.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/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/sprint-mission-3/src/middlewares/errorHandler.js b/sprint-mission-3/src/middlewares/errorHandler.js
new file mode 100644
index 00000000..8f312d8c
--- /dev/null
+++ b/sprint-mission-3/src/middlewares/errorHandler.js
@@ -0,0 +1,17 @@
+// 'P2025' = not found
+const errorHandler = (err, req, res, next) => {
+ if (err.code === 'P2025') {
+ return res.status(404).send({
+ success: false,
+ message: '요청하신 내용을 찾을 수 없습니다.',
+ });
+ } else {
+ const statusCode = err.status || 500;
+ return res.status(statusCode).send({
+ success: false,
+ message: err.message || '서버 오류가 발생했습니다. 다시 시도해주십시오.',
+ });
+ }
+};
+
+export default errorHandler;
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/sprint-mission-3/src/middlewares/validator.js b/sprint-mission-3/src/middlewares/validator.js
new file mode 100644
index 00000000..763c0ad0
--- /dev/null
+++ b/sprint-mission-3/src/middlewares/validator.js
@@ -0,0 +1,56 @@
+//중고 마켓
+const validateProduct = (req, res, next) => {
+ const { name, description, price } = req.body;
+ if (!name) {
+ const err = new Error('상품 이름(name)은 필수입니다');
+ err.status = 400;
+ return next(err);
+ }
+
+ if (!description) {
+ const err = new Error('상품 설명(description)은 필수입니다.');
+ err.status = 400;
+ return next(err);
+ }
+
+ if (!price) {
+ const err = new Error('상품 가격(price)은 필수입니다.');
+ err.status = 400;
+ return next(err);
+ }
+
+ next();
+};
+
+//자유 게시판
+const validateArticle = (req, res, next) => {
+ const { title, content } = req.body;
+
+ if (!title) {
+ const err = new Error('게시글 제목(title)은 필수입니다.');
+ err.status = 400;
+ return next(err);
+ }
+
+ if (!content) {
+ const err = new Error('게시글 내용(content)은 필수입니다.');
+ err.status = 400;
+ return next(err);
+ }
+
+ next();
+};
+
+//댓글
+const validateComment = (req, res, next) => {
+ const { content } = req.body;
+
+ if (!content) {
+ const err = new Error('댓글 내용(content)은 필수입니다.');
+ err.status = 400;
+ return next(err);
+ }
+ next();
+};
+
+export { validateProduct, validateArticle, validateComment };
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
diff --git a/src/app.js b/src/app.js
index 11efe26e..40d1ab48 100644
--- a/src/app.js
+++ b/src/app.js
@@ -2,10 +2,12 @@ import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import errorHandler from './middlewares/errorHandler.js';
-import productRouter from './routers/productRouter.js';
-import articleRouter from './routers/articleRouter.js';
-import commentRouter from './routers/commentRouter.js';
-import uploadRouter from './routers/uploadRouter.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 commentRouter from './routes/comment.router.js';
+// import uploadRouter from './routes/upload.router.js';
dotenv.config();
const app = express();
@@ -17,17 +19,22 @@ app.get('/', (req, res) => {
res.send('두려워하지 마십시오. 죽음이 끝은 아닙니다.');
});
+app.use('/auth', authRouter);
+
+// 유저 관련 요청
+app.use('/users', userRouter);
+
//중고마켓
app.use('/products', productRouter);
-//자유게시판
+// //자유게시판
app.use('/articles', articleRouter);
-//댓글
+// //댓글
app.use('/comments', commentRouter);
-//이미지
-app.use('/uploads', uploadRouter);
+// //이미지
+// app.use('/uploads', uploadRouter);
//마지막에 실행.
app.use(errorHandler);
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/controllers/auth.controller.js b/src/controllers/auth.controller.js
new file mode 100644
index 00000000..933e81a0
--- /dev/null
+++ b/src/controllers/auth.controller.js
@@ -0,0 +1,63 @@
+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) => {
+ //요청바디 (입력 내용들에서 정보 꺼내기)
+ 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)
+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/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/controllers/product.controller.js b/src/controllers/product.controller.js
new file mode 100644
index 00000000..062ff75c
--- /dev/null
+++ b/src/controllers/product.controller.js
@@ -0,0 +1,177 @@
+import prisma from '../lib/prisma.js';
+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, sellerId },
+ 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 { id: userId } = req.user;
+ const { productId } = req.params;
+ const inputData = req.body;
+
+ // 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 } } },
+ });
+
+ const responseData = {
+ 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 },
+ });
+
+ return res.status(200).json({ message: '제품 삭제 성공' });
+};
+
+export { createProduct, getListProducts, getProductById, patchProductById, deleteProductById };
diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js
new file mode 100644
index 00000000..71ebcb42
--- /dev/null
+++ b/src/controllers/user.controller.js
@@ -0,0 +1,113 @@
+import bcrypt from 'bcrypt';
+import prisma from '../lib/prisma.js';
+import { getOrderBy } from '../lib/utils.js';
+/**
+200 OK: 일반적인 성공 (GET, UPDATE 후)
+201 Created: 새로운 리소스 생성 성공 (POST)
+204 No Content: 성공했지만 돌려줄 데이터가 없음 (DELETE)
+400 Bad Request: 클라이언트 요청 오류 (유효성 검사 실패 등)
+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 확인
+ // 미들웨어 통과 시 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)
+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/constants.js b/src/lib/constants.js
new file mode 100644
index 00000000..2b9de145
--- /dev/null
+++ b/src/lib/constants.js
@@ -0,0 +1,8 @@
+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';
+export const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key';
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..134108d5
--- /dev/null
+++ b/src/lib/structs.js
@@ -0,0 +1,78 @@
+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 / 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.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로 빼기로 함.)
+});
+
+// 비밀번호 변경용
+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),
+ description: s.size(s.string(), 1, 500),
+ price: s.min(s.number(), 0),
+ tags: s.optional(s.array(s.string())),
+});
+
+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),
+});
+
+export const PatchArticleStruct = s.partial(CreateArticleStruct);
+
+//----- comment -----
+export const CreateCommentStruct = s.object({
+ content: s.size(s.string(), 1, 200),
+});
+
+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/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: '유효하지 않은 토큰입니다.' });
+ }
+};
diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.js
index 8f312d8c..d8b3f7e2 100644
--- a/src/middlewares/errorHandler.js
+++ b/src/middlewares/errorHandler.js
@@ -1,13 +1,23 @@
-// 'P2025' = not found
const errorHandler = (err, req, res, next) => {
+ console.error('❌ Error Log:', err);
+
+ //Prisma 에러: P2025 (찾을 수 없음)
if (err.code === 'P2025') {
- return res.status(404).send({
+ 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).send({
+ return res.status(statusCode).json({
success: false,
message: err.message || '서버 오류가 발생했습니다. 다시 시도해주십시오.',
});
diff --git a/src/middlewares/validator.js b/src/middlewares/validator.js
index 763c0ad0..7244d2c7 100644
--- a/src/middlewares/validator.js
+++ b/src/middlewares/validator.js
@@ -1,56 +1,24 @@
-//중고 마켓
-const validateProduct = (req, res, next) => {
- const { name, description, price } = req.body;
- if (!name) {
- const err = new Error('상품 이름(name)은 필수입니다');
- err.status = 400;
- return next(err);
- }
-
- if (!description) {
- const err = new Error('상품 설명(description)은 필수입니다.');
- err.status = 400;
- return next(err);
- }
-
- if (!price) {
- const err = new Error('상품 가격(price)은 필수입니다.');
- err.status = 400;
- return next(err);
- }
-
- next();
-};
-
-//자유 게시판
-const validateArticle = (req, res, next) => {
- const { title, content } = req.body;
-
- if (!title) {
- const err = new Error('게시글 제목(title)은 필수입니다.');
- err.status = 400;
- return next(err);
- }
-
- if (!content) {
- const err = new Error('게시글 내용(content)은 필수입니다.');
- err.status = 400;
- return next(err);
- }
-
- next();
-};
-
-//댓글
-const validateComment = (req, res, next) => {
- const { content } = req.body;
-
- if (!content) {
- const err = new Error('댓글 내용(content)은 필수입니다.');
- err.status = 400;
- return next(err);
- }
- next();
+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);
+ }
+ };
};
-
-export { validateProduct, validateArticle, validateComment };
diff --git a/src/routes/article.router.js b/src/routes/article.router.js
new file mode 100644
index 00000000..dc3eea2b
--- /dev/null
+++ b/src/routes/article.router.js
@@ -0,0 +1,64 @@
+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,
+ 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;
diff --git a/src/routes/auth.router.js b/src/routes/auth.router.js
new file mode 100644
index 00000000..bb4a6ad0
--- /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 { login, signUp } from '../controllers/auth.controller.js';
+import { CreateUserStruct, LoginUserStruct } 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(LoginUserStruct), withAsync(login));
+
+// =========== 토큰 재발급 POST (/auth/refresh)
+// router.route('/refresh').post(validate(), withAsync());
+
+// =========== 로그아웃 POST (/auth/logout)
+// router.route('/logout').post(validate(), withAsync());
+
+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;
diff --git a/src/routes/product.router.js b/src/routes/product.router.js
new file mode 100644
index 00000000..e0913829
--- /dev/null
+++ b/src/routes/product.router.js
@@ -0,0 +1,62 @@
+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,
+ 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';
+
+const router = express.Router();
+
+// 라우트 ===================================================
+router
+ .route('/')
+ //POST, GET
+ .post(authMiddleware, validate(CreateProductStruct, 'body'), withAsync(createProduct))
+ .get(withAsync(getListProducts));
+
+router
+ .route('/:productId')
+ //GET id,
+ .get(validate(ProductIdStruct, 'params'), withAsync(getProductById))
+
+ //PATCH id,
+ .patch(
+ authMiddleware,
+ validate(ProductIdStruct, 'params'),
+ validate(PatchProductStruct, 'body'),
+ withAsync(patchProductById),
+ )
+
+ //DELETE id
+ .delete(authMiddleware, validate(ProductIdStruct, 'params'), withAsync(deleteProductById));
+
+//중고 장터
+router
+ .route('/:productId/comments')
+ //POST
+ .post(
+ authMiddleware,
+ validate(ProductIdStruct, 'params'),
+ validate(CreateCommentStruct, 'body'),
+ withAsync(createCommentForProduct),
+ ) //중고장터 댓글
+
+ //GET
+ .get(validate(ProductIdStruct, 'params'), withAsync(getCommentListProduct)); //중고장터 댓글
+
+export default router;
diff --git a/src/routes/user.router.js b/src/routes/user.router.js
new file mode 100644
index 00000000..a6000ad1
--- /dev/null
+++ b/src/routes/user.router.js
@@ -0,0 +1,40 @@
+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, PatchPasswordStruct } from '../lib/structs.js';
+import {
+ getUserMe,
+ patchUserMe,
+ deleteUserMe,
+ updatePassword,
+ getMyProducts,
+} from '../controllers/user.controller.js';
+
+const router = express.Router();
+
+// (/users -> /me)
+// 내 정보 조회 GET (/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));
+
+//비밀번호 변경 (/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;