From f8faf873fb8e52acaa4a2c54da8fccb0b1471ac1 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Wed, 30 Jul 2025 00:05:55 +0900 Subject: [PATCH 01/63] =?UTF-8?q?[=EC=9D=B4=EC=A0=9C=EC=B0=BD]=20sprint2?= =?UTF-8?q?=20(#43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/PULL_REQUEST_TEMPLATE.md | 21 ------ ArticleService.js | 90 +++++++++++++++++++++++++ ProductService.js | 110 +++++++++++++++++++++++++++++++ README.md | 1 + main.js | 18 +++++ package.json | 13 ++++ 6 files changed, 232 insertions(+), 21 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 ArticleService.js create mode 100644 ProductService.js create mode 100644 README.md create mode 100644 main.js create mode 100644 package.json diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index ec85f6f1a..000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,21 +0,0 @@ - -## 요구사항 - -### 기본 -- [x] 기본 항목 1 -- [ ] 기본 항목 2 - -### 심화 -- [ ] 심화 항목 1 -- [ ] 심화 항목 2 - -## 주요 변경사항 -- -- - -## 스크린샷 -![image](이미지url) - -## 멘토에게 -- 셀프 코드 리뷰를 통해 질문 이어가겠습니다. -- diff --git a/ArticleService.js b/ArticleService.js new file mode 100644 index 000000000..a58aea399 --- /dev/null +++ b/ArticleService.js @@ -0,0 +1,90 @@ +class Article { + constructor(title, content, writer) { + this.title = title; + this.content = content; + this.writer = writer; + this.likeCount = 0; + this.createdAt = new Date(); + } + like() { + this.likeCount++; + } +} + +const BASE_URL = 'https://panda-market-api-crud.vercel.app'; + +function getAarticleList (page = 1, pageSize = 10, keyword = '') { + fetch (`${BASE_URL}/articles?page=${page}&pageSize=${pageSize}&keyword=${keyword}`) + .then (response => { + if (!response.ok) { + throw new Error (`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then (data => console.log (`Article list:`, data)) + .catch (error => console.error ('Error fetching article list:', error)); +} + +function getArticle(articleId) { + fetch (`${BASE_URL}/articles/${articleId}`) + .then (response => { + if (!response.ok) { + throw new Error (`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then (data => console.log(`Article ${articleId}:`, data)) + .catch (error => console.error (`Error fetching article ${articleId}:`, error)); +} + +function createArticle(articleData) { + fetch (`${BASE_URL}/articles`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(articleData), + }) + .then (response => { + if (!response.ok) { + throw new Error (`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then(data => console.log('Created article:', data)) + .catch (error => console.error ('Error creating article:', error)) +} + +function patchArticle(articleId, articleData) { + fetch (`${BASE_URL}/articles/${articleId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(articleData), + }) + .then (response => { + if (!response.ok) { + throw new Error (`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then(data => console.log(`Patched article ${articleId}:`, data)) + .catch (error => console.error (`Error patching article ${articleId}:`, error)) +} + +function deleteArticle(articleId) { + fetch (`${BASE_URL}/articles/${articleId}`, { + method: 'DELETE', + }) + .then (response => { + if (!response.ok) { + throw new Error (`HTTP error! status: ${response.status}`); + } + return response.json(); + }) + .then(data => console.log(`Deleted article ${articleId}:`, data)) + .catch (error => console.error (`Error deleting article ${articleId}:`, error)) +} + +export { Article, getAarticleList, getArticle, createArticle, patchArticle, deleteArticle }; \ No newline at end of file diff --git a/ProductService.js b/ProductService.js new file mode 100644 index 000000000..8d0b5157c --- /dev/null +++ b/ProductService.js @@ -0,0 +1,110 @@ +class Product { + constructor(name, destription, price, tags, images) { + this.name = name; + this.destription = destription; + this.price = price; + this.tags = tags; + this.images = images; + this.favoriteCount = 0; + } + favorite() { + this.favoriteCount++; + } +} + +class ElectronicProduct extends Product { + (name, destription, price, tags, images, manufacturer) { + super (name, destription, price, tags, images); + this.manufacturer = manufacturer; + } +} + +const BASE_URL = 'https://panda-market-api-crud.vercel.app'; + +async function getProductList (page = 1, pageSize = 10, keyword = '') { + try { + const response = await fetch (`${BASE_URL}/products?page=${page}&pageSize= + ${pageSize}&keyword=${keyword}`); + if (!response.ok) { + throw new Error (`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + const products = data.map (item => { + if (item.tag.includes ('전자제품')) { + return new ElectronicProduct(item.name, item.destription, item.price, item.tags, + item.images, item.manufacturer); + } else { + return new Product (item.name, item.destription, item.price, item.tags, item.images) + } + }); + return products; + } catch (error) { + console.error('Error fetching product list:', error); + } +} + +async function getProduct(productId) { + + try { + const response = await fetch(`${BASE_URL}/products/${productId}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error(`Error fetching product ${productId}:`, error); + } +} + +async function createProduct(productData) { + try { + const response = await fetch(`${BASE_URL}/products`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(productData), + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error('Error creating product:', error); + } +} + +async function patchProduct(productId, productData) { + try { + const response = await fetch(`${BASE_URL}/products/${productId}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(productData), + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error(`Error patching product ${productId}:`, error); + } +} + +async function deleteProduct(productId) { + try { + const response = await fetch(`${BASE_URL}/products/${productId}`, { + method: 'DELETE', + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error(`Error deleting product ${productId}:`, error); + } +} + +export { Product, ElectronicProduct, getProductList, getProduct, createProduct, patchProduct, + deleteProduct }; \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..d8fe5a912 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +스프린트 미션2 diff --git a/main.js b/main.js new file mode 100644 index 000000000..bc1536aec --- /dev/null +++ b/main.js @@ -0,0 +1,18 @@ +import { + Product, + ElectronicProduct, + getProductList, + getProduct, + createProduct, + patchProduct, + deleteProduct, +} from "./ProductService.js"; + +import { + Article, + getAarticleList, + getArticle, + createArticle, + patchArticle, + deleteArticle, +} from "./ArticleService.js"; diff --git a/package.json b/package.json new file mode 100644 index 000000000..c517201a7 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "test2", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs" +} From 2564887aa8f22fd290e8f37feefc3967bf26bffc Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 17 Aug 2025 20:20:58 +0900 Subject: [PATCH 02/63] feat: sprintmission3 --- .github/PULL_REQUEST_TEMPLATE.md | 21 - .gitignore | 6 + index.js | 36 + middlewares/validation.middleware.js | 22 + package-lock.json | 1927 +++++++++++++++++ package.json | 35 + prisma/index.js | 31 + .../20250817033539_init/migration.sql | 51 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 50 + prisma/seed.js | 65 + routes/articles.router.js | 157 ++ routes/products.router.js | 160 ++ routes/upload.router.js | 39 + 14 files changed, 2582 insertions(+), 21 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitignore create mode 100644 index.js create mode 100644 middlewares/validation.middleware.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 prisma/index.js create mode 100644 prisma/migrations/20250817033539_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.js create mode 100644 routes/articles.router.js create mode 100644 routes/products.router.js create mode 100644 routes/upload.router.js diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index ec85f6f1a..000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,21 +0,0 @@ - -## 요구사항 - -### 기본 -- [x] 기본 항목 1 -- [ ] 기본 항목 2 - -### 심화 -- [ ] 심화 항목 1 -- [ ] 심화 항목 2 - -## 주요 변경사항 -- -- - -## 스크린샷 -![image](이미지url) - -## 멘토에게 -- 셀프 코드 리뷰를 통해 질문 이어가겠습니다. -- diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..980180f3b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +# Keep environment variables out of version control +.env +node_modules +uploads +/generated/prisma diff --git a/index.js b/index.js new file mode 100644 index 000000000..7395c3275 --- /dev/null +++ b/index.js @@ -0,0 +1,36 @@ +require('dotenv').config(); +const express = require('express'); +const cors = require('cors'); +const path = require('path'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +//import router +const productRouter = require('./rotes/products.router.js'); +const articleRouter = require('./routes/articles.router,js'); +const uploadRouter = require('./routes.upload.router.js'); + +//Middleware +app.use(cors()); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use('/uploads', express.static(path,join(__dirname, 'uploads'))); + + +//route settitng +app.use('/api', [productRouter, articleRouter, uploadRouter]); + +//Error Handler Middleware +app.use((err, req, res, next) => { + console.error(err.stack); + const statusCode = err.statusCode || 500; + const message = err.message || '오류가 발생했습니다.'; + res.status(statusCode).json({ message }); +}); + + +app.listen(PORT, () => { + console.log(`서버가 ${PORT}번에서 실행중입니다.`); +}); + diff --git a/middlewares/validation.middleware.js b/middlewares/validation.middleware.js new file mode 100644 index 000000000..cf82500e6 --- /dev/null +++ b/middlewares/validation.middleware.js @@ -0,0 +1,22 @@ +//validation +const validateProduct = (req, res, next) => { + const { name, description, price } = req.body; + if (!name || !description || price == null) { + return res.status(400).json({ message: '이름, 설명, 가격을 입력해야 합니다.'}); + } + if (typeof price !== 'number' || price <= 0) { + return res.status(400).json({ message: '가격은 0보다 커야합니다.'}); + } + next(); +} + +//article validation +const validateArticle = (req, res, next) => { + const { title, content } = req.body; + if (!title || !content) { + return res.status(400).json({ message: '제목, 내용을 입력해야 합니다.'}); + } + next(); +} + +module.exports = { validateProduct, validateArticle }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..1a5270786 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1927 @@ +{ + "name": "sprintmission3", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sprintmission3", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "^6.14.0", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^5.1.0", + "multer": "^2.0.2", + "pg": "^8.16.3" + }, + "devDependencies": { + "nodemon": "^3.1.10", + "prisma": "^6.14.0" + } + }, + "node_modules/@prisma/client": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.14.0.tgz", + "integrity": "sha512-8E/Nk3eL5g7RQIg/LUj1ICyDmhD053STjxrPxUtCRybs2s/2sOEcx9NpITuAOPn07HEpWBfhAVe1T/HYWXUPOw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.14.0.tgz", + "integrity": "sha512-IwC7o5KNNGhmblLs23swnfBjADkacBb7wvyDXUWLwuvUQciKJZqyecU0jw0d7JRkswrj+XTL8fdr0y2/VerKQQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.16.12", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.14.0.tgz", + "integrity": "sha512-j4Lf+y+5QIJgQD4sJWSbkOD7geKx9CakaLp/TyTy/UDu9Wo0awvWCBH/BAxTHUaCpIl9USA5VS/KJhDqKJSwug==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.14.0.tgz", + "integrity": "sha512-LhJjqsALFEcoAtF07nSaOkVguaxw/ZsgfROIYZ8bAZDobe7y8Wy+PkYQaPOK1iLSsFgV2MhCO/eNrI1gdSOj6w==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.14.0", + "@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49", + "@prisma/fetch-engine": "6.14.0", + "@prisma/get-platform": "6.14.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49.tgz", + "integrity": "sha512-EgN9ODJpiX45yvwcngoStp3uQPJ3l+AEVoQ6dMMO2QvmwIlnxfApzKmJQExzdo7/hqQANrz5txHJdGYHzOnGHA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.14.0.tgz", + "integrity": "sha512-MPzYPOKMENYOaY3AcAbaKrfvXVlvTc6iHmTXsp9RiwCX+bPyfDMqMFVUSVXPYrXnrvEzhGHfyiFy0PRLHPysNg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.14.0", + "@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49", + "@prisma/get-platform": "6.14.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.14.0.tgz", + "integrity": "sha512-7VjuxKNwjnBhKfqPpMeWiHEa2sVjYzmHdl1slW6STuUCe9QnOY0OY1ljGSvz6wpG4U8DfbDqkG1yofd/1GINww==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.14.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/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/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": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "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/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "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/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.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": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "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/effect": { + "version": "3.16.12", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", + "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 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": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/http-errors/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/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.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.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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/multer/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/multer/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/multer/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/multer/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/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "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/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/nypm": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz", + "integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.2.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz", + "integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prisma": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.14.0.tgz", + "integrity": "sha512-QEuCwxu+Uq9BffFw7in8In+WfbSUN0ewnaSUKloLkbJd42w6EyFckux4M0f7VwwHlM3A8ssaz4OyniCXlsn0WA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.14.0", + "@prisma/engines": "6.14.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "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/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "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/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/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "devOptional": true, + "license": "MIT" + }, + "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": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/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/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "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/package.json b/package.json new file mode 100644 index 000000000..b4989b9c3 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "sprintmission3", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "seed": "node prisma/seed.js", + "start": "node index.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Jerang2/4-sprint-mission_fork.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "bugs": { + "url": "https://github.com/Jerang2/4-sprint-mission_fork/issues" + }, + "homepage": "https://github.com/Jerang2/4-sprint-mission_fork#readme", + "dependencies": { + "@prisma/client": "^6.14.0", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^5.1.0", + "multer": "^2.0.2", + "pg": "^8.16.3" + }, + "devDependencies": { + "nodemon": "^3.1.10", + "prisma": "^6.14.0" + } +} diff --git a/prisma/index.js b/prisma/index.js new file mode 100644 index 000000000..bb6ade6f8 --- /dev/null +++ b/prisma/index.js @@ -0,0 +1,31 @@ +require('dotenv').config(); +const express = require('express'); +const cors = require('cors'); +const path = require('path'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +//router import +const productRouter = require('./routes/products.router.js'); +const articleRouter = require('./routes/articles.router.js'); +const uploadRouter = require('./routes/upload.router.js'); + +//middleware +app.use(cors()); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +app.use('/uploads', express.static(path.join(__dirname, 'uploads' +))); + +//Error middleware +app.use((err, req, res, next) => { + console.error(err.stack); + const statusCode = err.statusCode || 500; + const message = err.message || '오류가 발생했습니다.'; + res.status(statusCode).json({ message }); +}); + +app.listen(PORT, () => { + console.log(`서버가 ${PORT}번에서 실행중입니다.`); +}); \ No newline at end of file diff --git a/prisma/migrations/20250817033539_init/migration.sql b/prisma/migrations/20250817033539_init/migration.sql new file mode 100644 index 000000000..6defc84c2 --- /dev/null +++ b/prisma/migrations/20250817033539_init/migration.sql @@ -0,0 +1,51 @@ +-- CreateTable +CREATE TABLE "public"."Product" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "price" INTEGER NOT NULL, + "tags" TEXT[], + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Product_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."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, + + CONSTRAINT "Article_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."ProductComment" ( + "id" SERIAL NOT NULL, + "comment" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "productId" INTEGER NOT NULL, + + CONSTRAINT "ProductComment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."ArticleComment" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "articleId" INTEGER NOT NULL, + + CONSTRAINT "ArticleComment_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "public"."ProductComment" ADD CONSTRAINT "ProductComment_productId_fkey" FOREIGN KEY ("productId") REFERENCES "public"."Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."ArticleComment" ADD CONSTRAINT "ArticleComment_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "public"."Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..044d57cdb --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 000000000..8ceeab4ab --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,50 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Product { + id Int @id @default(autoincrement()) + name String + description String + price Int + tags String[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + comments ProductComment[] +} + +model Article { + id Int @id @default(autoincrement()) + title String + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + comments ArticleComment[] +} + +model ProductComment { + id Int @id @default(autoincrement()) + comment String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + product Product @relation(fields: [productId], references: [id], onDelete: Cascade) + productId Int +} + +model ArticleComment { + id Int @id @default(autoincrement()) + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) + articleId Int +} diff --git a/prisma/seed.js b/prisma/seed.js new file mode 100644 index 000000000..db6e1274f --- /dev/null +++ b/prisma/seed.js @@ -0,0 +1,65 @@ +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); + +async function main() { + // Create Products + const product1 = await prisma.product.create({ + data: { + name: '노트북', + description: '고성능 노트북입니다.', + price: 1500000, + tags: ['electronics', 'computer'], + }, + }); + + const product2 = await prisma.product.create({ + data: { + name: '키보드', + description: '기계식 키보드입니다.', + price: 100000, + tags: ['electronics', 'accessory'], + }, + }); + + // Create Articles + const article1 = await prisma.article.create({ + data: { + title: '첫 번째 게시글', + content: '이것은 첫 번째 게시글의 내용입니다.', + }, + }); + + const article2 = await prisma.article.create({ + data: { + title: '두 번째 게시글', + content: '이것은 두 번째 게시글의 내용입니다.', + }, + }); + + // Create Product Comments + await prisma.productComment.create({ + data: { + comment: '이 노트북 정말 좋네요!', + productId: product1.id, + }, + }); + + // Create Article Comments + await prisma.articleComment.create({ + data: { + content: '좋은 글 감사합니다.', + articleId: article1.id, + }, + }); + + console.log('Seeding finished.'); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/routes/articles.router.js b/routes/articles.router.js new file mode 100644 index 000000000..292059086 --- /dev/null +++ b/routes/articles.router.js @@ -0,0 +1,157 @@ +//adricle router +const express = require('express'); +const router = express.Router(); +const { PrismaClient } =require('@prisma/client'); +const prisma = new PrismaClient(); +const { validateArticle } = require('../middlewares/validation.middleware.js'); + +//article registration +router + .route('/articles') + .post(validateArticle, async (req, res, next) => { + try { + const { title, content } = req.body; + const article = await prisma.article.create({ data: { title, content }}); + res.status(201).json(article); + } catch (error) { + next(error); + } +}) + .get(async (req, res, next) => { + try { + const { sort, search } = req.query; + let page = parseInt(req.query.page) || 1; + let limit = parseInt(req.query.limit) || 10; + let offset = (page - 1) * limit; + + const where = search + ? { + OR: [ + { title: { contains: search, mode: 'insenstive' }}, + { content: { constains: search, mode: 'insenstive' }}, + ], + } + : {}; + + const articles = await prisma.article.findMany({ + where, + select: { id: true, title: true, content: true, createdAt: true }, + orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, + skip: offset, + take: limit, + }); + res.status(200).json(articles); + } catch (error) { + next(error); + } + }); + +//article detail, modify, delete +router + .route('/articles/:articleId') + .get(async (req, res, next) => { + try { + const { articleId } = req.params; + const article = await prisma.article.findUnique({ + where: { id: parseInt(articleId) }, + select: { id: true, title: true, content: true, createdAt: true }, + }); + if (!article) return res.status(404).json({ message: '게시글을 찾을 수 없습니다.'}); + res.status(200).json(article); + } catch (error) { + next(error); + } +}) + .patch(validateArticle, async (req, res, next) => { + try { + const { articleId } = req.params; + const { title, content } = req.body; + const updatedArticle = await prisma.article.update({ + where: { id: parseInt(articleId) }, + data: { title, content }, + }); + res.status(200).json(updatedArticle); + } catch (error) { + next(error); + } +}) + .delete(async (req, res, next) => { + try { + const { articleId } = req.params; + await prisma.article.delete({ where: { id: parseInt(articleId) }}); + res.status(204).send(); + } catch (error) { + next(error); + } + }); + +//article API +router.post('/articles/:articleId/comments', async (req, res, next) => { + try { + const { articleId } = req.params; + const { content } = req.body; + if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.'}); + + const newComment = await prisma.articleComment.create({ + data: { + content, + articleId: parseInt(articleId), + }, + }); + res.status(201).json(newComment); + } catch (error) { + next(error); + } +}); + +// article check +router.get('/articles/:articleId/comments', async (req, res, next) => { + try { + const { articleId } = req.params; + let cursor = req.query.cursor ? parseInt(req.query.cursor): undefined; + let limit = parseInt(req.query.limit) || 10; + + const comments = await prisma.articleComment.findMany({ + where: { articleId: parseInt(articleId) }, + select: { id: true, content: true, createdAt: true }, + orderBy: { createdAt: 'desc' }, + cursor: cursor ? { id: cursor } : undefined, + take: limit, + skip: cursor ? 1 : 0, + }); + res.status(200).json(comments); + } catch(error) { + next(error); + } +}); + +//article modify +router.patcch('/articles/comments/:commentId', async (req, res, next) => { + try { + const { commentId } = req.params; + const { content } = req.body; + if (!content) return res.status(400).json({ message: '수정할 내용을 입력하세요.' }); + + const updatedComment = await prisma.articleComment.update + ({ + where: { id: parseInt(commentId) }, + data: { content }, + }); + res.status(200).json(updatedComment); + } catch (error) { + next(error) + } +}); + +//article delete +router.delete('/articles/comments/:commentId', async (req, res, next) => { + try { + const { commentId } = req.params; + await prisma.articleComment.delete({ where: { id: parseInt(commetId) }}); + res.status(204).send(); + } catch (error) { + next(error); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/products.router.js b/routes/products.router.js new file mode 100644 index 000000000..2e4d45918 --- /dev/null +++ b/routes/products.router.js @@ -0,0 +1,160 @@ +//product router +const express = require('express'); +const router = express.Router(); +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); +const { validationProduct, validateProduct } = require('../middlewares/validation.middleware.js'); + +//registration router +router.post('/products', validateProduct, async (req, res, next) => { + try { + const { name, description, price, tags } = req.body; + const product = await prisma.product.create({ + data: { name, description, price, tags }, + }); + res.status(201).json(product); + } catch(error) { + next(error); + } +}) + +//cherck router +router.ger('/products', async (req, res, next) => { + try { + const { sort, search } = req.query; + let page = parseInt(req.query.page) || 1; + let limit = parseInt(req.query.limit) || 10; + let offset = (page - 1) * limit; + + const where = search + ? { + OR: [ + { name: { contains: search, mode: 'insensitive' }}, + { description: { contains: search, mode: 'insentive' + } }, + ], + } + : {}; + const products = await prisma.product.findMany({ + where, + select: { id: true, name: true, price: true, createdAt: true, imageUrl: true }, + orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, + skip: offset, + take: limit, + }); + res.status(200).json(products); + } catch (error) { + next(error); + } +}); + +//datail, modify, delete +router + .route('/products/:productId') + .get(async (req, res, next) => { + try { + const { productId } = req.params; + const product = await prisma.product.findUnique({ + where: { id: parseInt(productId) }, + select: { id: true, name: true, description: true, price: true, + tags: true, createdAt: true, imageUrl: true }, + }); + if (!product) return res.status(404).json({ message: '상품을 찾을수 없습니다.'}); + res.status(200).json(product); + } catch (error) { + next(error); + } + }) + .patch(validateProduct, async (req, res, next) => { + try { + const { productid } = req.params; + const { name, description, price, tags } = req.body; + const updatedProduct = await prisma.product.update({ + where: { id: parseInt(productID) }, + data: { name, description, price, tags }, + }); + res.status(200).json(updatedProduct); + } catch (error) { + next(error); + } + }) + .delete(async (req, res, next) => { + try { + const { productId } = req.params; + await prisma.product.delete({ where: { id: parseInt(productId) }}); + res.status(204).send(); + } catch (error) { + next(error); + } + }); + +//comment +router.post('/products/:productId/comments', async (req, res, next) => { + try { + const { productId } = req.params; + const { comment } = req.body + if (!comment) return res.status(400).json({ message: '댓글을 입력해주세요.' }); + + const newComment = await prisma.productComment.create({ + data: { + comment, + productId: parseInt(productId), + }, + }); + res.status(201).json(newComment); + } catch (error) { + next(error); + } +}); + +//comment check +router.get('/products/:productId/comments', async (req, res, next) => { + try { + const { productId } = req.params; + let cursor = req.query.cursor ? parseInt(req.query.cursor): undefined; + let limit = parseInt(req.query,limit) || 10; + + const comments = await prisma.productComment.findMany({ + where: { productId: parseInt(productId) }, + select: { id: true, comment: true, createdAt: true }, + orderBy: { createdAt: 'desc' }, + cursor: cursor ? { id: cursor } : undefined, + take: limit, + skip: cursor ? 1 : 0, + }); + res.status(200).json(comments); + } catch (error) { + next(error); + } +}); + +//comment modify +router.patch('/products/comments/:commentId', async (req, res, next) => { + try { + const { commentId } = req.params; + const { comment } = req.body; + if (!comment) return res.status(400).json({ message: '수정할 내용을 입력해주세요.'}); + + const updatedComment = await prisma.productComment.update +({ + where: { id: parseInt(commentId) }, + data: { content }, + }); + res.status(200).json(updatedComment); + } catch (error) { + next(error); + } +}); + +//comment delete +router.delete('/products/comments/:commentId', async (req, res, next) => { + try { + const { commentId } = req.params; + await prisma.productComment.delete({ where: { id: parseInt(commentId) }}); + res.status(204).send(); + } catch(error) { + next(error); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/upload.router.js b/routes/upload.router.js new file mode 100644 index 000000000..a9894a61e --- /dev/null +++ b/routes/upload.router.js @@ -0,0 +1,39 @@ +//upload route +const express = requires('express'); +const router = express.Router(); +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); +const { DESTRUCTION } = require('dns'); + +//uploads 디렉토리가 없을 때 생성 +const uploadDir = 'uploads/'; +if (!fs.existsSync(uploadDir)){ + fs.mkdirSync(uploadDir); +} + +//multer repository +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, 'uploads/'); + }, + filename: function (req, file, cb) { + const ext = path.extname(file.originalname); + cb(null, Date.now() + ext); + }, +}); + +//multer middleware +const upload = multer({ storage: storage }); + +//image api +router.post('/upload', upload.single('image'), (req, res, next) => { + if (!req.file) { + return res.status(400).json({ message: '이미지 파일이 필요합니다.'}); + } + + const imageUrl = `/uploads/${req.file.filename}`; + res.status(201).json({ imageUrl: imageUrl }); +}); + +module.exports = router; From 1439346f3a61fdeb4b519fd63b8041d57d43413f Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 17 Aug 2025 23:14:45 +0900 Subject: [PATCH 03/63] feat: image resize --- package-lock.json | 546 +++++++++++++++++++++++++++++++++++++++- package.json | 3 +- routes/upload.router.js | 34 +-- 3 files changed, 565 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a5270786..a5ea587b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,13 +14,442 @@ "dotenv": "^16.4.5", "express": "^5.1.0", "multer": "^2.0.2", - "pg": "^8.16.3" + "pg": "^8.16.3", + "sharp": "^0.34.3" }, "devDependencies": { "nodemon": "^3.1.10", "prisma": "^6.14.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@prisma/client": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.14.0.tgz", @@ -359,6 +788,47 @@ "consola": "^3.2.3" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -500,6 +970,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -924,6 +1403,12 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1633,7 +2118,6 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1685,6 +2169,48 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -1757,6 +2283,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1857,6 +2392,13 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", diff --git a/package.json b/package.json index b4989b9c3..b7121fea6 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "dotenv": "^16.4.5", "express": "^5.1.0", "multer": "^2.0.2", - "pg": "^8.16.3" + "pg": "^8.16.3", + "sharp": "^0.34.3" }, "devDependencies": { "nodemon": "^3.1.10", diff --git a/routes/upload.router.js b/routes/upload.router.js index a9894a61e..5b8b8c018 100644 --- a/routes/upload.router.js +++ b/routes/upload.router.js @@ -1,9 +1,10 @@ //upload route -const express = requires('express'); +const express = require('express'); const router = express.Router(); const multer = require('multer'); const path = require('path'); const fs = require('fs'); +const sharp = require('sharp'); const { DESTRUCTION } = require('dns'); //uploads 디렉토리가 없을 때 생성 @@ -12,28 +13,31 @@ if (!fs.existsSync(uploadDir)){ fs.mkdirSync(uploadDir); } -//multer repository -const storage = multer.diskStorage({ - destination: function (req, file, cb) { - cb(null, 'uploads/'); - }, - filename: function (req, file, cb) { - const ext = path.extname(file.originalname); - cb(null, Date.now() + ext); - }, -}); +const storage = multer.memoryStorage(); //multer middleware const upload = multer({ storage: storage }); //image api -router.post('/upload', upload.single('image'), (req, res, next) => { +router.post('/upload', upload.single('image'), async(req, res, next) => { if (!req.file) { return res.status(400).json({ message: '이미지 파일이 필요합니다.'}); } - const imageUrl = `/uploads/${req.file.filename}`; - res.status(201).json({ imageUrl: imageUrl }); -}); + try { + const ext = path.extname(req.file.originalname); + const filename = Date.now() + ext; + const imagePath = path.join(uploadDir, filename); + await sharp(req.file.buffer) + .resize({ width: 500 }) + .toFile(imagePath); + + const imageUrl = `/uploads/${filename}`; + res.status(201).json({ imageUrl: imageUrl }); + } catch (error) { + next(error); + } +}); + module.exports = router; From 39cde2838cd82f320c25beda6d277dd934891f66 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 14 Sep 2025 13:57:49 +0900 Subject: [PATCH 04/63] =?UTF-8?q?prisma.schema=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/schema.prisma | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8ceeab4ab..3ed863452 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,6 +17,8 @@ model Product { updatedAt DateTime @updatedAt comments ProductComment[] + User User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int } model Article { @@ -27,6 +29,8 @@ model Article { updatedAt DateTime @updatedAt comments ArticleComment[] + User User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int } model ProductComment { @@ -37,6 +41,8 @@ model ProductComment { product Product @relation(fields: [productId], references: [id], onDelete: Cascade) productId Int + User User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int } model ArticleComment { @@ -47,4 +53,21 @@ model ArticleComment { article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) articleId Int + User User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + nickname String + image String? + password String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + products Product[] + articles Article[] + productComments ProductComment[] + articleComments ArticleComment[] } From ac96417cdfa2f7b5a3b074bc338fde3a6abcf650 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 14 Sep 2025 14:16:22 +0900 Subject: [PATCH 05/63] prisma migrate --- package-lock.json | 78 +++++++++---------- package.json | 4 +- .../20250914051507_modify/migration.sql | 48 ++++++++++++ 3 files changed, 89 insertions(+), 41 deletions(-) create mode 100644 prisma/migrations/20250914051507_modify/migration.sql diff --git a/package-lock.json b/package-lock.json index a5ea587b8..2564c22b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@prisma/client": "^6.14.0", + "@prisma/client": "^6.16.1", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^5.1.0", @@ -19,7 +19,7 @@ }, "devDependencies": { "nodemon": "^3.1.10", - "prisma": "^6.14.0" + "prisma": "^6.16.1" } }, "node_modules/@emnapi/runtime": { @@ -451,9 +451,9 @@ } }, "node_modules/@prisma/client": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.14.0.tgz", - "integrity": "sha512-8E/Nk3eL5g7RQIg/LUj1ICyDmhD053STjxrPxUtCRybs2s/2sOEcx9NpITuAOPn07HEpWBfhAVe1T/HYWXUPOw==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.1.tgz", + "integrity": "sha512-QaBCOY29lLAxEFFJgBPyW3WInCW52fJeQTmWx/h6YsP5u0bwuqP51aP0uhqFvhK9DaZPwvai/M4tSDYLVE9vRg==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { @@ -473,9 +473,9 @@ } }, "node_modules/@prisma/config": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.14.0.tgz", - "integrity": "sha512-IwC7o5KNNGhmblLs23swnfBjADkacBb7wvyDXUWLwuvUQciKJZqyecU0jw0d7JRkswrj+XTL8fdr0y2/VerKQQ==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.1.tgz", + "integrity": "sha512-sz3uxRPNL62QrJ0EYiujCFkIGZ3hg+9hgC1Ae1HjoYuj0BxCqHua4JNijYvYCrh9LlofZDZcRBX3tHBfLvAngA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -486,53 +486,53 @@ } }, "node_modules/@prisma/debug": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.14.0.tgz", - "integrity": "sha512-j4Lf+y+5QIJgQD4sJWSbkOD7geKx9CakaLp/TyTy/UDu9Wo0awvWCBH/BAxTHUaCpIl9USA5VS/KJhDqKJSwug==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.1.tgz", + "integrity": "sha512-RWv/VisW5vJE4cDRTuAHeVedtGoItXTnhuLHsSlJ9202QKz60uiXWywBlVcqXVq8bFeIZoCoWH+R1duZJPwqLw==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.14.0.tgz", - "integrity": "sha512-LhJjqsALFEcoAtF07nSaOkVguaxw/ZsgfROIYZ8bAZDobe7y8Wy+PkYQaPOK1iLSsFgV2MhCO/eNrI1gdSOj6w==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.1.tgz", + "integrity": "sha512-EOnEM5HlosPudBqbI+jipmaW/vQEaF0bKBo4gVkGabasINHR6RpC6h44fKZEqx4GD8CvH+einD2+b49DQrwrAg==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.14.0", - "@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49", - "@prisma/fetch-engine": "6.14.0", - "@prisma/get-platform": "6.14.0" + "@prisma/debug": "6.16.1", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/fetch-engine": "6.16.1", + "@prisma/get-platform": "6.16.1" } }, "node_modules/@prisma/engines-version": { - "version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49.tgz", - "integrity": "sha512-EgN9ODJpiX45yvwcngoStp3uQPJ3l+AEVoQ6dMMO2QvmwIlnxfApzKmJQExzdo7/hqQANrz5txHJdGYHzOnGHA==", + "version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43.tgz", + "integrity": "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.14.0.tgz", - "integrity": "sha512-MPzYPOKMENYOaY3AcAbaKrfvXVlvTc6iHmTXsp9RiwCX+bPyfDMqMFVUSVXPYrXnrvEzhGHfyiFy0PRLHPysNg==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.1.tgz", + "integrity": "sha512-fl/PKQ8da5YTayw86WD3O9OmKJEM43gD3vANy2hS5S1CnfW2oPXk+Q03+gUWqcKK306QqhjjIHRFuTZ31WaosQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.14.0", - "@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49", - "@prisma/get-platform": "6.14.0" + "@prisma/debug": "6.16.1", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/get-platform": "6.16.1" } }, "node_modules/@prisma/get-platform": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.14.0.tgz", - "integrity": "sha512-7VjuxKNwjnBhKfqPpMeWiHEa2sVjYzmHdl1slW6STuUCe9QnOY0OY1ljGSvz6wpG4U8DfbDqkG1yofd/1GINww==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.1.tgz", + "integrity": "sha512-kUfg4vagBG7dnaGRcGd1c0ytQFcDj2SUABiuveIpL3bthFdTLI6PJeLEia6Q8Dgh+WhPdo0N2q0Fzjk63XTyaA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.14.0" + "@prisma/debug": "6.16.1" } }, "node_modules/@standard-schema/spec": { @@ -1882,9 +1882,9 @@ } }, "node_modules/pkg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz", - "integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", "devOptional": true, "license": "MIT", "dependencies": { @@ -1933,15 +1933,15 @@ } }, "node_modules/prisma": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.14.0.tgz", - "integrity": "sha512-QEuCwxu+Uq9BffFw7in8In+WfbSUN0ewnaSUKloLkbJd42w6EyFckux4M0f7VwwHlM3A8ssaz4OyniCXlsn0WA==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.1.tgz", + "integrity": "sha512-MFkMU0eaDDKAT4R/By2IA9oQmwLTxokqv2wegAErr9Rf+oIe7W2sYpE/Uxq0H2DliIR7vnV63PkC1bEwUtl98w==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "6.14.0", - "@prisma/engines": "6.14.0" + "@prisma/config": "6.16.1", + "@prisma/engines": "6.16.1" }, "bin": { "prisma": "build/index.js" diff --git a/package.json b/package.json index b7121fea6..31b7d7b1f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ }, "homepage": "https://github.com/Jerang2/4-sprint-mission_fork#readme", "dependencies": { - "@prisma/client": "^6.14.0", + "@prisma/client": "^6.16.1", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^5.1.0", @@ -31,6 +31,6 @@ }, "devDependencies": { "nodemon": "^3.1.10", - "prisma": "^6.14.0" + "prisma": "^6.16.1" } } diff --git a/prisma/migrations/20250914051507_modify/migration.sql b/prisma/migrations/20250914051507_modify/migration.sql new file mode 100644 index 000000000..aa2f5ad71 --- /dev/null +++ b/prisma/migrations/20250914051507_modify/migration.sql @@ -0,0 +1,48 @@ +/* + Warnings: + + - Added the required column `userId` to the `Article` table without a default value. This is not possible if the table is not empty. + - Added the required column `userId` to the `ArticleComment` table without a default value. This is not possible if the table is not empty. + - Added the required column `userId` to the `Product` table without a default value. This is not possible if the table is not empty. + - Added the required column `userId` to the `ProductComment` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "public"."Article" ADD COLUMN "userId" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "public"."ArticleComment" ADD COLUMN "userId" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "public"."Product" ADD COLUMN "userId" INTEGER NOT NULL; + +-- AlterTable +ALTER TABLE "public"."ProductComment" ADD COLUMN "userId" INTEGER NOT NULL; + +-- CreateTable +CREATE TABLE "public"."User" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "nickname" TEXT NOT NULL, + "image" TEXT, + "password" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email"); + +-- AddForeignKey +ALTER TABLE "public"."Product" ADD CONSTRAINT "Product_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Article" ADD CONSTRAINT "Article_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."ProductComment" ADD CONSTRAINT "ProductComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."ArticleComment" ADD CONSTRAINT "ArticleComment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; From 59e7d84f2f207e92863bc049d5cc6e28d3581d67 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 14 Sep 2025 14:17:07 +0900 Subject: [PATCH 06/63] bcrypt install --- package-lock.json | 35 +++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 36 insertions(+) diff --git a/package-lock.json b/package-lock.json index 2564c22b6..c8ac0674c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@prisma/client": "^6.16.1", + "bcrypt": "^6.0.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^5.1.0", @@ -582,6 +583,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", @@ -1632,6 +1647,15 @@ "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-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", @@ -1639,6 +1663,17 @@ "devOptional": true, "license": "MIT" }, + "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", diff --git a/package.json b/package.json index 31b7d7b1f..6df6d712e 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "homepage": "https://github.com/Jerang2/4-sprint-mission_fork#readme", "dependencies": { "@prisma/client": "^6.16.1", + "bcrypt": "^6.0.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^5.1.0", From ae377b5c56729fb8e951e14594562a47cf656d7f Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 14 Sep 2025 14:26:09 +0900 Subject: [PATCH 07/63] =?UTF-8?q?UserService.js=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UserService.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 UserService.js diff --git a/UserService.js b/UserService.js new file mode 100644 index 000000000..c7d65c8f6 --- /dev/null +++ b/UserService.js @@ -0,0 +1,32 @@ +import { PrismaClient } from '@prisma/client'; +import bcrypt from 'bcrypt'; + +export class UserService { + prisma = new PrismaClient(); + + // 회원가입 로직 + signUp = async (email, nickname, password) => { + + // 이메일 중복 확인 + const existingUser = await this.prisma.user.findUnique({ where: { email } }); + if (existingUser) { + throw new Error('이미 사용중인 이메일입니다.'); + } + + // 비밀번호 해싱 + const hashedPassword = await bcrypt.hash(password, 10); + + // 유저 생성 + const user = await this.prisma.user.create({ + data: { + email, + nickname, + password: hashedPassword, + }, + }); + + // 사용자 정보 반환 + const { password: _, ...userWithoutPassword } = user; + return userWithoutPassword; + }; +} From 3f4b18cad557a78489fba2911e9211ea1fb4a2a6 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 14 Sep 2025 14:32:43 +0900 Subject: [PATCH 08/63] =?UTF-8?q?users.router.js=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/users.router.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 routes/users.router.js diff --git a/routes/users.router.js b/routes/users.router.js new file mode 100644 index 000000000..664be02f9 --- /dev/null +++ b/routes/users.router.js @@ -0,0 +1,28 @@ +import express from 'express'; +import { UserService } from '../UserService.js'; + +const router = express.Router(); +const userService = new UserService(); + +// 회원가입 API +router.post('/sign-up', async (req, res, next) => { + try { + const { email, nickname, password } = req.body; + + // 유효성 검사 + if (!email || !nickname || !password) { + return res.status(400).json({ message: '모든 정보를 입력해주세요'}); + } + + const newUser = await userService.signUp(email, nickname, password); + + return res.status(201).json({ + message: '회원가입이 완료되었습니다.', + data: newUser, + }); + } catch (error) { + return res.status(409).json({ message: error.message }); + } +}); + +export default router; \ No newline at end of file From 37122cfc289a419583a6f9865fb1db272187bc4f Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 14 Sep 2025 14:39:36 +0900 Subject: [PATCH 09/63] =?UTF-8?q?ESM=20=EB=B0=A9=EC=8B=9D=20->=20CommonJs?= =?UTF-8?q?=20=EB=B0=A9=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UserService.js | 8 +++++--- routes/users.router.js | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/UserService.js b/UserService.js index c7d65c8f6..7d74a4b72 100644 --- a/UserService.js +++ b/UserService.js @@ -1,7 +1,7 @@ -import { PrismaClient } from '@prisma/client'; -import bcrypt from 'bcrypt'; +const { PrismaClient } = require('@prisma/client'); +const bcrypt = require('bcrypt'); -export class UserService { +class UserService { prisma = new PrismaClient(); // 회원가입 로직 @@ -30,3 +30,5 @@ export class UserService { return userWithoutPassword; }; } + +module.pxports = UserService; diff --git a/routes/users.router.js b/routes/users.router.js index 664be02f9..c1d20cd8d 100644 --- a/routes/users.router.js +++ b/routes/users.router.js @@ -1,5 +1,5 @@ -import express from 'express'; -import { UserService } from '../UserService.js'; +const express = require('express'); +const UserService = require('../UserService.js'); const router = express.Router(); const userService = new UserService(); @@ -25,4 +25,4 @@ router.post('/sign-up', async (req, res, next) => { } }); -export default router; \ No newline at end of file +module.exports = router; \ No newline at end of file From 33c6e2bd72a6dbcd442ededa1965fb43cc9e1bd2 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 14 Sep 2025 14:42:26 +0900 Subject: [PATCH 10/63] =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20AP?= =?UTF-8?q?I=20=EC=99=84=EB=A3=8C=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 7395c3275..ff6b6342d 100644 --- a/index.js +++ b/index.js @@ -6,22 +6,23 @@ const path = require('path'); const app = express(); const PORT = process.env.PORT || 3000; -//import router +// import router const productRouter = require('./rotes/products.router.js'); -const articleRouter = require('./routes/articles.router,js'); -const uploadRouter = require('./routes.upload.router.js'); +const articleRouter = require('./routes/articles.router.js'); +const uploadRouter = require('./routes/upload.router.js'); +const usersRouter = require('./routes/users.router.js'); -//Middleware +// Middleware app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); -app.use('/uploads', express.static(path,join(__dirname, 'uploads'))); +app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); -//route settitng -app.use('/api', [productRouter, articleRouter, uploadRouter]); +// route settitng +app.use('/api', [productRouter, articleRouter, uploadRouter, usersRouter]); -//Error Handler Middleware +// Error Handler Middleware app.use((err, req, res, next) => { console.error(err.stack); const statusCode = err.statusCode || 500; From dab87316c27fd8ffa5d044c59114b73058a240d9 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 14 Sep 2025 14:48:58 +0900 Subject: [PATCH 11/63] =?UTF-8?q?=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UserService.js | 2 +- index.js | 2 +- routes/articles.router.js | 2 +- routes/products.router.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/UserService.js b/UserService.js index 7d74a4b72..dbe8f6c93 100644 --- a/UserService.js +++ b/UserService.js @@ -31,4 +31,4 @@ class UserService { }; } -module.pxports = UserService; +module.exports = UserService; diff --git a/index.js b/index.js index ff6b6342d..9d517f661 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,7 @@ const app = express(); const PORT = process.env.PORT || 3000; // import router -const productRouter = require('./rotes/products.router.js'); +const productRouter = require('./routes/products.router.js'); const articleRouter = require('./routes/articles.router.js'); const uploadRouter = require('./routes/upload.router.js'); const usersRouter = require('./routes/users.router.js'); diff --git a/routes/articles.router.js b/routes/articles.router.js index 292059086..f6aa5c454 100644 --- a/routes/articles.router.js +++ b/routes/articles.router.js @@ -126,7 +126,7 @@ router.get('/articles/:articleId/comments', async (req, res, next) => { }); //article modify -router.patcch('/articles/comments/:commentId', async (req, res, next) => { +router.patch('/articles/comments/:commentId', async (req, res, next) => { try { const { commentId } = req.params; const { content } = req.body; diff --git a/routes/products.router.js b/routes/products.router.js index 2e4d45918..705ac395d 100644 --- a/routes/products.router.js +++ b/routes/products.router.js @@ -19,7 +19,7 @@ router.post('/products', validateProduct, async (req, res, next) => { }) //cherck router -router.ger('/products', async (req, res, next) => { +router.get('/products', async (req, res, next) => { try { const { sort, search } = req.query; let page = parseInt(req.query.page) || 1; From d19e9bed96a8f07a6522325bf914af89c6af3be8 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 14 Sep 2025 14:58:37 +0900 Subject: [PATCH 12/63] =?UTF-8?q?UserService=EC=97=90=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UserService.js | 24 +++++++++++ package-lock.json | 101 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 126 insertions(+) diff --git a/UserService.js b/UserService.js index dbe8f6c93..eaf9a0c6e 100644 --- a/UserService.js +++ b/UserService.js @@ -1,5 +1,6 @@ const { PrismaClient } = require('@prisma/client'); const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); class UserService { prisma = new PrismaClient(); @@ -29,6 +30,29 @@ class UserService { const { password: _, ...userWithoutPassword } = user; return userWithoutPassword; }; + + signIn = async (email, password) => { + // 이메일로 사용자 조회 + const user = await this.prisma.user.findUnique({ where: { email } }); + if (!user) { + throw new Error('존재하지 않는 이메일입니다.'); + } + + // 비밀번호 확인 + const isPasswordMatched = await bcrypt.compare(password, user.password); + if (!isPasswordMatched) { + throw new Error('비밀번호가 일치하지 않습니다.'); + } + + // 로그인 성공시 JWT 생성 + const token = jwt.sign( + { userId: user.id }, + process.env.JWT_SECRET_KEY, + { expiresIn: '12h' } + ); + + return token; + }; } module.exports = UserService; diff --git a/package-lock.json b/package-lock.json index c8ac0674c..494cbe6be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", "pg": "^8.16.3", "sharp": "^0.34.3" @@ -654,6 +655,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", @@ -1020,6 +1027,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", @@ -1486,6 +1502,91 @@ "jiti": "lib/jiti-cli.mjs" } }, + "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/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": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index 6df6d712e..6bae74509 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", "pg": "^8.16.3", "sharp": "^0.34.3" From 48dceef9fd48efb43fdee556b7496ea58e0a67f8 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 14 Sep 2025 15:03:31 +0900 Subject: [PATCH 13/63] =?UTF-8?q?users.router.js=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/users.router.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/routes/users.router.js b/routes/users.router.js index c1d20cd8d..55ee88f3c 100644 --- a/routes/users.router.js +++ b/routes/users.router.js @@ -25,4 +25,23 @@ router.post('/sign-up', async (req, res, next) => { } }); +// 로그인 API +router.post('/sign-in', async (req, res, next) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ message: '이메일과 비밀번호를 모두 입력해주세요.'}); + } + const token = await userService.signIn(email, password); + + return res.status(200).json({ + message: '로그인에 성공했습니다.', + data: { token }, + }); + } catch (error) { + return res.status(401).json({ message: Error.message }); + } +}); + module.exports = router; \ No newline at end of file From b6b9d100ff6eca0045a2b031e86bc2f527878e6c Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 14 Sep 2025 16:17:52 +0900 Subject: [PATCH 14/63] =?UTF-8?q?articles.router.js=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=EB=AF=B8=EB=93=A4=EC=9B=A8=EC=96=B4,=20=EC=86=8C=EC=9C=A0?= =?UTF-8?q?=EC=9E=90=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/articles.router.js | 54 +++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/routes/articles.router.js b/routes/articles.router.js index f6aa5c454..f3168b247 100644 --- a/routes/articles.router.js +++ b/routes/articles.router.js @@ -35,7 +35,7 @@ router const articles = await prisma.article.findMany({ where, - select: { id: true, title: true, content: true, createdAt: true }, + select: { id: true, title: true, content: true, createdAt: true, userId: true }, orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, skip: offset, take: limit, @@ -54,7 +54,7 @@ router const { articleId } = req.params; const article = await prisma.article.findUnique({ where: { id: parseInt(articleId) }, - select: { id: true, title: true, content: true, createdAt: true }, + select: { id: true, title: true, content: true, createdAt: true, userId: true }, }); if (!article) return res.status(404).json({ message: '게시글을 찾을 수 없습니다.'}); res.status(200).json(article); @@ -62,10 +62,14 @@ router next(error); } }) - .patch(validateArticle, async (req, res, next) => { + .patch(authMiddleware, validateArticle, async (req, res, next) => { try { const { articleId } = req.params; const { title, content } = req.body; + const { user } = req; + + // 게시글 소유 확인 + const article = await prisma.article.findUnique({ where: { id: parseInt(articleId) } }); const updatedArticle = await prisma.article.update({ where: { id: parseInt(articleId) }, data: { title, content }, @@ -75,9 +79,16 @@ router next(error); } }) - .delete(async (req, res, next) => { + .delete(authMiiddleware, async (req, res, next) => { try { const { articleId } = req.params; + const { user } = req; + + const article = await prisma.article.findUnique({ where: { id: parseInt(articleId) } }); + if (!article || article.userId !== user.id) { + return res.status(403).json({ message: '게시글 삭제 권한이 없습니다.' }); + } + await prisma.article.delete({ where: { id: parseInt(articleId) }}); res.status(204).send(); } catch (error) { @@ -85,17 +96,20 @@ router } }); -//article API -router.post('/articles/:articleId/comments', async (req, res, next) => { +// article comment creation +router.post('/articles/:articleId/comments', authMiddleware, async (req, res, next) => { try { const { articleId } = req.params; const { content } = req.body; + const { user } = req; + if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.'}); const newComment = await prisma.articleComment.create({ data: { content, articleId: parseInt(articleId), + userId: user.id, }, }); res.status(201).json(newComment); @@ -104,7 +118,7 @@ router.post('/articles/:articleId/comments', async (req, res, next) => { } }); -// article check +// article comments check router.get('/articles/:articleId/comments', async (req, res, next) => { try { const { articleId } = req.params; @@ -113,7 +127,7 @@ router.get('/articles/:articleId/comments', async (req, res, next) => { const comments = await prisma.articleComment.findMany({ where: { articleId: parseInt(articleId) }, - select: { id: true, content: true, createdAt: true }, + select: { id: true, content: true, createdAt: true, userId: true }, orderBy: { createdAt: 'desc' }, cursor: cursor ? { id: cursor } : undefined, take: limit, @@ -125,13 +139,20 @@ router.get('/articles/:articleId/comments', async (req, res, next) => { } }); -//article modify -router.patch('/articles/comments/:commentId', async (req, res, next) => { +//article comment modify +router.patch('/articles/comments/:commentId', authMiddleware, async (req, res, next) => { try { const { commentId } = req.params; const { content } = req.body; + const { user } = req; + if (!content) return res.status(400).json({ message: '수정할 내용을 입력하세요.' }); + const comment = await prisma.articleComment.findUnique({ where: { id: parseInt(commentId) }}); + if (!comment || comment.userId !== user.id) { + return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); + } + const updatedComment = await prisma.articleComment.update ({ where: { id: parseInt(commentId) }, @@ -143,11 +164,18 @@ router.patch('/articles/comments/:commentId', async (req, res, next) => { } }); -//article delete -router.delete('/articles/comments/:commentId', async (req, res, next) => { +//article comment delete +router.delete('/articles/comments/:commentId', aythMiddleware, async (req, res, next) => { try { const { commentId } = req.params; - await prisma.articleComment.delete({ where: { id: parseInt(commetId) }}); + const { user } = req; + + const comment = await prisma.articleComment.findUnique({ where: { id: parseInt(commentId) } }); + if (!comment || comment.userId !== user.id) { + return res.status(403).json({ message: '댓글 삭제 권환이 없습니다.' }); + } + + await prisma.articleComment.delete({ where: { id: parseInt(commentId) }}); res.status(204).send(); } catch (error) { next(error); From f8b9b45a19b5effaf1da544a2bbf9cfb34950794 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 14 Sep 2025 16:29:48 +0900 Subject: [PATCH 15/63] =?UTF-8?q?products.router.js=20=EC=9D=B8=EC=A6=9D,?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/products.router.js | 68 ++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/routes/products.router.js b/routes/products.router.js index 705ac395d..2e16cd102 100644 --- a/routes/products.router.js +++ b/routes/products.router.js @@ -1,11 +1,11 @@ -//product router +// product router const express = require('express'); const router = express.Router(); const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); const { validationProduct, validateProduct } = require('../middlewares/validation.middleware.js'); -//registration router +// registration router router.post('/products', validateProduct, async (req, res, next) => { try { const { name, description, price, tags } = req.body; @@ -18,7 +18,7 @@ router.post('/products', validateProduct, async (req, res, next) => { } }) -//cherck router +// cherck router router.get('/products', async (req, res, next) => { try { const { sort, search } = req.query; @@ -30,7 +30,7 @@ router.get('/products', async (req, res, next) => { ? { OR: [ { name: { contains: search, mode: 'insensitive' }}, - { description: { contains: search, mode: 'insentive' + { description: { contains: search, mode: 'insensitive' } }, ], } @@ -48,7 +48,7 @@ router.get('/products', async (req, res, next) => { } }); -//datail, modify, delete +// datail, modify, delete router .route('/products/:productId') .get(async (req, res, next) => { @@ -57,7 +57,7 @@ router const product = await prisma.product.findUnique({ where: { id: parseInt(productId) }, select: { id: true, name: true, description: true, price: true, - tags: true, createdAt: true, imageUrl: true }, + tags: true, createdAt: true, imageUrl: true, userId: true }, }); if (!product) return res.status(404).json({ message: '상품을 찾을수 없습니다.'}); res.status(200).json(product); @@ -65,12 +65,20 @@ router next(error); } }) - .patch(validateProduct, async (req, res, next) => { + .patch(validateProduct, authMiddleware, async (req, res, next) => { try { - const { productid } = req.params; + const { productId } = req.params; const { name, description, price, tags } = req.body; + const { user } = req; + + // 상품 소유자 확인 + const product = await prisma.product.findUnique({ where: { id: parseInt(productId) } }); + if (!product || product.userId !== user.id) { + return res.status(403).json({ message: '상품 수정 권한이 없습니다.' }); + } + const updatedProduct = await prisma.product.update({ - where: { id: parseInt(productID) }, + where: { id: parseInt(productId) }, data: { name, description, price, tags }, }); res.status(200).json(updatedProduct); @@ -78,9 +86,16 @@ router next(error); } }) - .delete(async (req, res, next) => { + .delete(authMiddleware, async (req, res, next) => { try { const { productId } = req.params; + const { user } = req; + + const product = await prisma.product.findUnique({ where: { id: parseInt(productId) } }); + if (!product || product.userId !== user.id) { + return res.status(403).json({ message: '상품 삭제 권한이 없습니다.' }); + } + await prisma.product.delete({ where: { id: parseInt(productId) }}); res.status(204).send(); } catch (error) { @@ -88,17 +103,20 @@ router } }); -//comment -router.post('/products/:productId/comments', async (req, res, next) => { +// comment +router.post('/products/:productId/comments', authMiddleware, async (req, res, next) => { try { const { productId } = req.params; const { comment } = req.body + const { user } = req; + if (!comment) return res.status(400).json({ message: '댓글을 입력해주세요.' }); const newComment = await prisma.productComment.create({ data: { comment, productId: parseInt(productId), + userId: user.id, }, }); res.status(201).json(newComment); @@ -112,11 +130,11 @@ router.get('/products/:productId/comments', async (req, res, next) => { try { const { productId } = req.params; let cursor = req.query.cursor ? parseInt(req.query.cursor): undefined; - let limit = parseInt(req.query,limit) || 10; + let limit = parseInt(req.query.limit) || 10; const comments = await prisma.productComment.findMany({ where: { productId: parseInt(productId) }, - select: { id: true, comment: true, createdAt: true }, + select: { id: true, comment: true, createdAt: true, userId: true }, orderBy: { createdAt: 'desc' }, cursor: cursor ? { id: cursor } : undefined, take: limit, @@ -128,13 +146,20 @@ router.get('/products/:productId/comments', async (req, res, next) => { } }); -//comment modify -router.patch('/products/comments/:commentId', async (req, res, next) => { +// comment modify +router.patch('/products/comments/:commentId', authMiddleware, async (req, res, next) => { try { const { commentId } = req.params; const { comment } = req.body; + const { user } = req; + if (!comment) return res.status(400).json({ message: '수정할 내용을 입력해주세요.'}); + const existingComment = await prisma.productComment.findUnique({ where: { id: parseInt(commentId) } }); + if (!existngComment || existingComment.userId !== user.id) { + return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); + } + const updatedComment = await prisma.productComment.update ({ where: { id: parseInt(commentId) }, @@ -146,10 +171,17 @@ router.patch('/products/comments/:commentId', async (req, res, next) => { } }); -//comment delete -router.delete('/products/comments/:commentId', async (req, res, next) => { +// comment delete +router.delete('/products/comments/:commentId', authMiddleware, async (req, res, next) => { try { const { commentId } = req.params; + const { user } = req; + + const existingComment = await prisma.productComment.findUnique({ where: { id: parseInt(commentId) } }); + if (!existingComment || existingComment.userId !== user.id) { + return res.status(403).json({ message: '댓글 삭제 권한이 없습니다.' }); + } + await prisma.productComment.delete({ where: { id: parseInt(commentId) }}); res.status(204).send(); } catch(error) { From e1cf603da4dd6bb84b1230e0bd0e1036c06ffa74 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 14 Sep 2025 23:24:11 +0900 Subject: [PATCH 16/63] prisma.schema modify T.T --- middlewares/auth.middleware.js | 51 +++++++ .../migration.sql | 131 ++++++++++++++++++ prisma/schema.prisma | 84 ++++++----- routes/articles.router.js | 8 +- routes/products.router.js | 1 + 5 files changed, 236 insertions(+), 39 deletions(-) create mode 100644 middlewares/auth.middleware.js create mode 100644 prisma/migrations/20250914141855_prisma_schema/migration.sql diff --git a/middlewares/auth.middleware.js b/middlewares/auth.middleware.js new file mode 100644 index 000000000..38f1075fa --- /dev/null +++ b/middlewares/auth.middleware.js @@ -0,0 +1,51 @@ +const jwt = require('jsonwebtoken'); +const { PrismaClient } = require('@prisma/client'); + +const prisma = new PrismaClient(); + +module.exports = async (req, res, next) => { + try { + // 헤더에서 authorization 값 가져오기 + const { authorization } = req.headers; + if (!authorization) { + return res.status(401).json({ message: '인증 정보가 없습니다.' }); + } + + // authorization 값에서 토큰 추출하기 + const [tokenType, token] = authorization.split(' '); + if (tokenType !== 'Bearer') { + return res.status(401).json({ message: '지원하지 않는 인증 방식입니다.'}) + } + + // 토큰이 없을 경우 + if (!token) { + return res.status(401).json({ message: '인증 정보가 없습니다.' }); + } + + // 토큰 검증 (확인) + const decodedToken = jwt.verify(token, process.env.JWT_SECRET_KEY); + const userId = decodedToken.userId; + + // 토큰 있는 userId로 조회 + const user = await prisma.user.findUnique({ + where: { id: userId}, + }); + if (!user) { + return res.status(401).json({ message: '인증 정보가 유효하지 않습니다.' }); + } + + // 사용자 정보 저장 + req.user = user; + next(); + } catch (error) { + console.error('인증 미들웨어 에러:', error); + switch (error.name) { + case 'TokenExpiredError': + return res.status(401).json({ message: '인증 토큰이 만료되었습니다.' }); + case 'JsonWebTokenError': + return res.status(401).json({ message: '유효하지 않은 인증 토큰입니다.' }); + default: + return res.status(401).json({ message: '인증 정보가 유효하지 않습니다.' }); + } + } +}; \ No newline at end of file diff --git a/prisma/migrations/20250914141855_prisma_schema/migration.sql b/prisma/migrations/20250914141855_prisma_schema/migration.sql new file mode 100644 index 000000000..7e65c4927 --- /dev/null +++ b/prisma/migrations/20250914141855_prisma_schema/migration.sql @@ -0,0 +1,131 @@ +/* + Warnings: + + - You are about to drop the `Article` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `ArticleComment` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Product` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `ProductComment` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "public"."Article" DROP CONSTRAINT "Article_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."ArticleComment" DROP CONSTRAINT "ArticleComment_articleId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."ArticleComment" DROP CONSTRAINT "ArticleComment_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."Product" DROP CONSTRAINT "Product_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."ProductComment" DROP CONSTRAINT "ProductComment_productId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."ProductComment" DROP CONSTRAINT "ProductComment_userId_fkey"; + +-- DropTable +DROP TABLE "public"."Article"; + +-- DropTable +DROP TABLE "public"."ArticleComment"; + +-- DropTable +DROP TABLE "public"."Product"; + +-- DropTable +DROP TABLE "public"."ProductComment"; + +-- DropTable +DROP TABLE "public"."User"; + +-- CreateTable +CREATE TABLE "public"."products" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "content" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'FOR_SALE', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "userId" INTEGER NOT NULL, + + CONSTRAINT "products_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."articles" ( + "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, + "userId" INTEGER NOT NULL, + + CONSTRAINT "articles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."comments" ( + "id" SERIAL NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "userId" INTEGER NOT NULL, + "productId" INTEGER, + "articleId" INTEGER, + + CONSTRAINT "comments_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."likes" ( + "id" SERIAL NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" INTEGER NOT NULL, + "productId" INTEGER, + "articleId" INTEGER, + + CONSTRAINT "likes_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."users" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "nickname" TEXT NOT NULL, + "image" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "public"."users"("email"); + +-- AddForeignKey +ALTER TABLE "public"."products" ADD CONSTRAINT "products_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."articles" ADD CONSTRAINT "articles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."comments" ADD CONSTRAINT "comments_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."comments" ADD CONSTRAINT "comments_productId_fkey" FOREIGN KEY ("productId") REFERENCES "public"."products"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."comments" ADD CONSTRAINT "comments_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "public"."articles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."likes" ADD CONSTRAINT "likes_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."likes" ADD CONSTRAINT "likes_productId_fkey" FOREIGN KEY ("productId") REFERENCES "public"."products"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."likes" ADD CONSTRAINT "likes_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "public"."articles"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3ed863452..7c299e6b8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,66 +8,74 @@ datasource db { } model Product { - id Int @id @default(autoincrement()) - name String - description String - price Int - tags String[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id Int @id @default(autoincrement()) + name String + content String + status String @default("FOR_SALE") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + comments Comment[] + likes Like[] - comments ProductComment[] - User User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int + @@map("products") } model Article { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) title String content String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + comments Comment[] + likes Like[] - comments ArticleComment[] - User User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int + @@map("articles") } -model ProductComment { +model Comment { id Int @id @default(autoincrement()) - comment String + content String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - - product Product @relation(fields: [productId], references: [id], onDelete: Cascade) - productId Int - User User @relation(fields: [userId], references: [id], onDelete: Cascade) userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + productId Int? + product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) + articleId Int? + article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) + + @@map("comments") } -model ArticleComment { +model Like { id Int @id @default(autoincrement()) - content String createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - article Article @relation(fields: [articleId], references: [id], onDelete: Cascade) - articleId Int - User User @relation(fields: [userId], references: [id], onDelete: Cascade) userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + productId Int? + product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) + articleId Int? + article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) + + @@map("likes") } model User { - id Int @id @default(autoincrement()) - email String @unique + id Int @id @default(autoincrement()) + email String @unique + password String nickname String image String? - password String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + products Product[] + articles Article[] + comments Comment[] + likes Like[] - products Product[] - articles Article[] - productComments ProductComment[] - articleComments ArticleComment[] + @@map("users") } diff --git a/routes/articles.router.js b/routes/articles.router.js index f3168b247..6e2c69894 100644 --- a/routes/articles.router.js +++ b/routes/articles.router.js @@ -4,6 +4,7 @@ const router = express.Router(); const { PrismaClient } =require('@prisma/client'); const prisma = new PrismaClient(); const { validateArticle } = require('../middlewares/validation.middleware.js'); +const authMiddleware = require('../middlewares/auth.middleware.js'); //article registration router @@ -11,7 +12,12 @@ router .post(validateArticle, async (req, res, next) => { try { const { title, content } = req.body; - const article = await prisma.article.create({ data: { title, content }}); + const article = await prisma.article.create({ data: { + title, + content, + userId: user.id, + } + }); res.status(201).json(article); } catch (error) { next(error); diff --git a/routes/products.router.js b/routes/products.router.js index 2e16cd102..cb11b3c89 100644 --- a/routes/products.router.js +++ b/routes/products.router.js @@ -4,6 +4,7 @@ const router = express.Router(); const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); const { validationProduct, validateProduct } = require('../middlewares/validation.middleware.js'); +const authMiddleware = require('../middlewares/auth.middleware.js'); // registration router router.post('/products', validateProduct, async (req, res, next) => { From 9b8d94e6a4b5e82c344698471d6ab4e152669698 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 14 Sep 2025 23:35:58 +0900 Subject: [PATCH 17/63] =?UTF-8?q?=EB=B0=94=EB=80=90=20prisma.schema?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=B0=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/articles.router.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/routes/articles.router.js b/routes/articles.router.js index 6e2c69894..9ea9b9afe 100644 --- a/routes/articles.router.js +++ b/routes/articles.router.js @@ -9,9 +9,11 @@ const authMiddleware = require('../middlewares/auth.middleware.js'); //article registration router .route('/articles') - .post(validateArticle, async (req, res, next) => { + .post(authMiddleware, validateArticle, async (req, res, next) => { try { const { title, content } = req.body; + const { user } = req; + const article = await prisma.article.create({ data: { title, content, @@ -23,6 +25,7 @@ router next(error); } }) + // 게시글 목록 조회 .get(async (req, res, next) => { try { const { sort, search } = req.query; @@ -33,8 +36,8 @@ router const where = search ? { OR: [ - { title: { contains: search, mode: 'insenstive' }}, - { content: { constains: search, mode: 'insenstive' }}, + { title: { contains: search, mode: 'insensitive' }}, + { content: { contains: search, mode: 'insensitive' }}, ], } : {}; @@ -52,7 +55,7 @@ router } }); -//article detail, modify, delete +// article detail, modify, delete router .route('/articles/:articleId') .get(async (req, res, next) => { @@ -85,7 +88,7 @@ router next(error); } }) - .delete(authMiiddleware, async (req, res, next) => { + .delete(authMiddleware, async (req, res, next) => { try { const { articleId } = req.params; const { user } = req; @@ -111,7 +114,7 @@ router.post('/articles/:articleId/comments', authMiddleware, async (req, res, ne if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.'}); - const newComment = await prisma.articleComment.create({ + const newComment = await prisma.comment.create({ data: { content, articleId: parseInt(articleId), @@ -131,7 +134,7 @@ router.get('/articles/:articleId/comments', async (req, res, next) => { let cursor = req.query.cursor ? parseInt(req.query.cursor): undefined; let limit = parseInt(req.query.limit) || 10; - const comments = await prisma.articleComment.findMany({ + const comments = await prisma.comment.findMany({ where: { articleId: parseInt(articleId) }, select: { id: true, content: true, createdAt: true, userId: true }, orderBy: { createdAt: 'desc' }, @@ -154,12 +157,12 @@ router.patch('/articles/comments/:commentId', authMiddleware, async (req, res, n if (!content) return res.status(400).json({ message: '수정할 내용을 입력하세요.' }); - const comment = await prisma.articleComment.findUnique({ where: { id: parseInt(commentId) }}); + const comment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) }}); if (!comment || comment.userId !== user.id) { return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); } - const updatedComment = await prisma.articleComment.update + const updatedComment = await prisma.comment.update ({ where: { id: parseInt(commentId) }, data: { content }, @@ -171,17 +174,17 @@ router.patch('/articles/comments/:commentId', authMiddleware, async (req, res, n }); //article comment delete -router.delete('/articles/comments/:commentId', aythMiddleware, async (req, res, next) => { +router.delete('/articles/comments/:commentId', authMiddleware, async (req, res, next) => { try { const { commentId } = req.params; const { user } = req; - const comment = await prisma.articleComment.findUnique({ where: { id: parseInt(commentId) } }); + const comment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) } }); if (!comment || comment.userId !== user.id) { return res.status(403).json({ message: '댓글 삭제 권환이 없습니다.' }); } - await prisma.articleComment.delete({ where: { id: parseInt(commentId) }}); + await prisma.comment.delete({ where: { id: parseInt(commentId) }}); res.status(204).send(); } catch (error) { next(error); From 48fcfe4d174537a4d40de0651e2cdd6b33c97f65 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 14 Sep 2025 23:59:30 +0900 Subject: [PATCH 18/63] =?UTF-8?q?=EB=B3=80=EA=B2=BD=EB=90=9C=20prisma.sche?= =?UTF-8?q?ma=20model=EB=A1=9C=20products.router.js=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/products.router.js | 46 ++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/routes/products.router.js b/routes/products.router.js index cb11b3c89..e8a847139 100644 --- a/routes/products.router.js +++ b/routes/products.router.js @@ -7,11 +7,17 @@ const { validationProduct, validateProduct } = require('../middlewares/validatio const authMiddleware = require('../middlewares/auth.middleware.js'); // registration router -router.post('/products', validateProduct, async (req, res, next) => { +router.post('/products', authMiddleware, validateProduct, async (req, res, next) => { try { - const { name, description, price, tags } = req.body; + const { name, description, price } = req.body; + const { user } = req; const product = await prisma.product.create({ - data: { name, description, price, tags }, + data: { + name, + description, + price, + userId: user.id, + }, }); res.status(201).json(product); } catch(error) { @@ -38,7 +44,7 @@ router.get('/products', async (req, res, next) => { : {}; const products = await prisma.product.findMany({ where, - select: { id: true, name: true, price: true, createdAt: true, imageUrl: true }, + select: { id: true, name: true, price: true, createdAt: true, userId: true }, orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, skip: offset, take: limit, @@ -58,7 +64,7 @@ router const product = await prisma.product.findUnique({ where: { id: parseInt(productId) }, select: { id: true, name: true, description: true, price: true, - tags: true, createdAt: true, imageUrl: true, userId: true }, + createdAt: true, userId: true }, }); if (!product) return res.status(404).json({ message: '상품을 찾을수 없습니다.'}); res.status(200).json(product); @@ -69,7 +75,7 @@ router .patch(validateProduct, authMiddleware, async (req, res, next) => { try { const { productId } = req.params; - const { name, description, price, tags } = req.body; + const { name, description, price } = req.body; const { user } = req; // 상품 소유자 확인 @@ -80,7 +86,7 @@ router const updatedProduct = await prisma.product.update({ where: { id: parseInt(productId) }, - data: { name, description, price, tags }, + data: { name, description, price }, }); res.status(200).json(updatedProduct); } catch (error) { @@ -108,14 +114,14 @@ router router.post('/products/:productId/comments', authMiddleware, async (req, res, next) => { try { const { productId } = req.params; - const { comment } = req.body + const { content } = req.body const { user } = req; - if (!comment) return res.status(400).json({ message: '댓글을 입력해주세요.' }); + if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.' }); - const newComment = await prisma.productComment.create({ + const newComment = await prisma.comment.create({ data: { - comment, + content, productId: parseInt(productId), userId: user.id, }, @@ -133,9 +139,9 @@ router.get('/products/:productId/comments', async (req, res, next) => { let cursor = req.query.cursor ? parseInt(req.query.cursor): undefined; let limit = parseInt(req.query.limit) || 10; - const comments = await prisma.productComment.findMany({ + const comments = await prisma.comment.findMany({ where: { productId: parseInt(productId) }, - select: { id: true, comment: true, createdAt: true, userId: true }, + select: { id: true, content: true, createdAt: true, userId: true }, orderBy: { createdAt: 'desc' }, cursor: cursor ? { id: cursor } : undefined, take: limit, @@ -151,17 +157,17 @@ router.get('/products/:productId/comments', async (req, res, next) => { router.patch('/products/comments/:commentId', authMiddleware, async (req, res, next) => { try { const { commentId } = req.params; - const { comment } = req.body; + const { content } = req.body; const { user } = req; - if (!comment) return res.status(400).json({ message: '수정할 내용을 입력해주세요.'}); + if (!content) return res.status(400).json({ message: '수정할 내용을 입력해주세요.'}); - const existingComment = await prisma.productComment.findUnique({ where: { id: parseInt(commentId) } }); - if (!existngComment || existingComment.userId !== user.id) { + const existingComment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) } }); + if (!existingComment || existingComment.userId !== user.id) { return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); } - const updatedComment = await prisma.productComment.update + const updatedComment = await prisma.comment.update ({ where: { id: parseInt(commentId) }, data: { content }, @@ -178,12 +184,12 @@ router.delete('/products/comments/:commentId', authMiddleware, async (req, res, const { commentId } = req.params; const { user } = req; - const existingComment = await prisma.productComment.findUnique({ where: { id: parseInt(commentId) } }); + const existingComment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) } }); if (!existingComment || existingComment.userId !== user.id) { return res.status(403).json({ message: '댓글 삭제 권한이 없습니다.' }); } - await prisma.productComment.delete({ where: { id: parseInt(commentId) }}); + await prisma.comment.delete({ where: { id: parseInt(commentId) }}); res.status(204).send(); } catch(error) { next(error); From cb17c18a2fa2714d28c074544b281763f2891f79 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Mon, 15 Sep 2025 00:14:00 +0900 Subject: [PATCH 19/63] =?UTF-8?q?user.router.js=20=EB=82=B4=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=88=98=EC=A0=95=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/users.router.js | 61 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/routes/users.router.js b/routes/users.router.js index 55ee88f3c..7fdcaa9bc 100644 --- a/routes/users.router.js +++ b/routes/users.router.js @@ -1,7 +1,10 @@ const express = require('express'); const UserService = require('../UserService.js'); +const authMiddleware = require('../middlewares/auth.middleware.js'); +const { PrismaClient } = require('@prisma/client'); const router = express.Router(); +const prisma = new PrismaClient(); const userService = new UserService(); // 회원가입 API @@ -44,4 +47,62 @@ router.post('/sign-in', async (req, res, next) => { } }); +// 내 정보 조회 API +router.get('/me', authMiddleware, async (req, res, next) => { + try { + const { user } = req; + + res.status(200).json({ + message: '내 정보 조회 성공', + data: { + id: user.id, + email: user.email, + nickname: user.nickname, + image: user.image, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }, + }); + } catch (error) { + next(error); + } +}); + +// 내 정보 수정 API +router.patch('/me', authMiddleware, async (req, res, next) => { + try { + const { nickname, image } = req.body; + const { user } = req; + + if (!nickname && !image) { + return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); + } + + // 수정할 사용자 정보 + const updatedData = { + ...(nickname && { nickname }), + ...(image && { image }), + }; + + const updatedUser = await prisma.user.update({ + where: { id: user.id }, + data: updatedData, + }); + + res.status(200).json({ + message: '내 정보 수정에 성공했습니다.', + data: { + id: updatedUser.id, + email: updatedUser.email, + nickname: updatedUser.nickname, + image: updatedUser.image, + createdAt: updatedUser.createdAt, + updatedAt: updatedUser.updatedAt, + }, + }); + } catch (error) { + next(error); + } +}); + module.exports = router; \ No newline at end of file From 71ca61464f9549eab1d85fb00fd81a40cde6da95 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Mon, 15 Sep 2025 00:22:36 +0900 Subject: [PATCH 20/63] =?UTF-8?q?users.router.js=20=EB=B9=84=EB=B0=80?= =?UTF-8?q?=EB=B2=88=ED=98=B8=20=EB=B3=80=EA=B2=BD=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/users.router.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/routes/users.router.js b/routes/users.router.js index 7fdcaa9bc..6d23c4996 100644 --- a/routes/users.router.js +++ b/routes/users.router.js @@ -2,6 +2,7 @@ const express = require('express'); const UserService = require('../UserService.js'); const authMiddleware = require('../middlewares/auth.middleware.js'); const { PrismaClient } = require('@prisma/client'); +const bcrypt = require('bcrypt'); const router = express.Router(); const prisma = new PrismaClient(); @@ -105,4 +106,38 @@ router.patch('/me', authMiddleware, async (req, res, next) => { } }); +// 비밀번호 변경 API +router.patch('/me/password', authMiddleware, async (req, res, next) => { + try { + const { currentPassword, newPassword, confirmNewPassword } = req.body; + const { user } = req; + + if (!currentPassword || !newPassword || !confirmNewPassword) { + return res.status(400).json({ message: '모든 정보를 입력해주세요.' }); + } + if (newPassword !== confirmNewPassword) { + return res.status(400).json({ message: '새 비밀번호와 확인 비밀번호가 일치하지 않습니다.' }); + } + + + // 현재 비밀번호 확인 + const isPasswordMatched = await bcrypt.compare(currentPassword, user.password); + if (!isPasswordMatched) { + return res.status(401).json({ message: '현재 비밀번호가 일치하지 않습니다.' }); + } + + // 새 비밀번호 해싱 + const hashedNewPassword = await bcrypt.hash(newPassword, 10); + + await prisma.user.update({ + where: { id: user.id }, + data: { password: hashedNewPassword }, + }); + + res.status(200).json({ message: '비밀번호 변경이 완료되었습니다.' }); + } catch (error) { + next(error); + } +}); + module.exports = router; \ No newline at end of file From f8efc37a763409755309a622dfbf02cd3b95643f Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Mon, 15 Sep 2025 00:25:49 +0900 Subject: [PATCH 21/63] =?UTF-8?q?=20users.router.js=20=EC=9E=91=EC=84=B1?= =?UTF-8?q?=20=EC=83=81=ED=92=88=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/users.router.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/routes/users.router.js b/routes/users.router.js index 6d23c4996..02c3f4b3f 100644 --- a/routes/users.router.js +++ b/routes/users.router.js @@ -140,4 +140,23 @@ router.patch('/me/password', authMiddleware, async (req, res, next) => { } }); +// 내가 작성한 상품 목록 조회 API +router.get('/me/products', authMiddleware, async (req, res, next) => { + try { + const { user } = req; + + const products = await prisma.product.findMany({ + where: { userId: user.id }, + orderBy: { createdAt: 'desc' }, + }); + + res.status(200).json({ + message: '내가 작성한 상품 목록 조회에 성공했습니다.', + data: products, + }); + } catch (error) { + next(error); + } +}); + module.exports = router; \ No newline at end of file From df167d38c67f9103615b0d193d66cdc7432e46b3 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Mon, 15 Sep 2025 00:33:54 +0900 Subject: [PATCH 22/63] =?UTF-8?q?products.router.js=EC=97=90=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/products.router.js | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/routes/products.router.js b/routes/products.router.js index e8a847139..f256c37a0 100644 --- a/routes/products.router.js +++ b/routes/products.router.js @@ -196,4 +196,45 @@ router.delete('/products/comments/:commentId', authMiddleware, async (req, res, } }); +// 상품 좋아요 API +router.post('/:priductId/like', authMiddleware, async (req, res, next) => { + try { + const { productId } = req.params; + const { user } = req; + + // 상품 존재 확인 + const product = await prisma.product.findUnique({ where: { id: parseInt(productId) } }); + if (productId) { + return res.status(404).json({ message: '상품을 찾을 수 없습니다.'}); + } + + // 기존 좋아요 확인 + const existingLike = await prisma.like.findFirst({ + where: { + userId: user.id, + productId: parseInt(productId), + }, + }); + + if (existingLike) { + // 좋아요가 이미 있으면 좋아요 취소 + await prisma.like.delete({ + where: { id: existingLike.id }, + }); + res.status(200).json({ message: '상품 좋아요를 취소했습니다.' }); + } else { + // 좋아요가 없으면 좋아요 생성 + await prisma.like.create({ + data: { + userId: user.id, + productId: parseInt(productId), + }, + }); + res.status(201).json({ message: '상품에 좋아요를 눌렀습니다.' }); + } + } catch (error) { + next(error); + } + }); + module.exports = router; \ No newline at end of file From 5a421b079549b1ab09fc467aebaf9bc4b7a8c943 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Mon, 15 Sep 2025 00:36:21 +0900 Subject: [PATCH 23/63] =?UTF-8?q?=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/products.router.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/products.router.js b/routes/products.router.js index f256c37a0..abbf5552a 100644 --- a/routes/products.router.js +++ b/routes/products.router.js @@ -197,14 +197,14 @@ router.delete('/products/comments/:commentId', authMiddleware, async (req, res, }); // 상품 좋아요 API -router.post('/:priductId/like', authMiddleware, async (req, res, next) => { +router.post('/:productId/like', authMiddleware, async (req, res, next) => { try { const { productId } = req.params; const { user } = req; // 상품 존재 확인 const product = await prisma.product.findUnique({ where: { id: parseInt(productId) } }); - if (productId) { + if (!product) { return res.status(404).json({ message: '상품을 찾을 수 없습니다.'}); } From f2f75e72bf54e996c33cedc76c85aa269c043d2f Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Mon, 15 Sep 2025 00:44:44 +0900 Subject: [PATCH 24/63] =?UTF-8?q?articles.router.js=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/articles.router.js | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/routes/articles.router.js b/routes/articles.router.js index 9ea9b9afe..2a2e4c307 100644 --- a/routes/articles.router.js +++ b/routes/articles.router.js @@ -191,4 +191,45 @@ router.delete('/articles/comments/:commentId', authMiddleware, async (req, res, } }); +// 게시글 좋아요 API +router.post('/:articleId/like', authMiddleware, async (req, res, next) => { + try { + const { articleId } = req.params; + const { user } = req; + + // 게시글 존재 확인 + const article = await prisma.article.findUnique({ where: { id: parseInt(articleId) } }); + if (!article) { + return res.status(404).json({ message: '게시글을 찾을 수 없습니다.'}); + } + + // 기존 좋아요 확인 + const existingLike = await prisma.like.findFirst({ + where: { + userId: user.id, + articleId: parseInt(articleId), + }, + }); + + if (existingLike) { + // 좋아요가 이미 존재하면 취소 + await prisma.like.delete({ + where: { id: existingLike.id }, + }); + res.status(200).json({ message: '게시글 좋아요를 취소했습니다.' }); + } else { + // 좋아요가 없으면 좋아요 생성 + await prisma.like.create({ + data: { + userId: user.id, + articleId: parseInt(articleId), + }, + }); + res.status(201).json({ message: '게시글에 좋아요를 눌렀습니다.' }); + } + } catch (error) { + next (error); + } +}); + module.exports = router; \ No newline at end of file From 23a053101bf695a74b6ddf5a88460fc5b8bc8f02 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Mon, 15 Sep 2025 00:52:29 +0900 Subject: [PATCH 25/63] =?UTF-8?q?optionalAuth.middleware.js=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- middlewares/optionalAuth.middleware.js | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 middlewares/optionalAuth.middleware.js diff --git a/middlewares/optionalAuth.middleware.js b/middlewares/optionalAuth.middleware.js new file mode 100644 index 000000000..d6d47e1d1 --- /dev/null +++ b/middlewares/optionalAuth.middleware.js @@ -0,0 +1,31 @@ +const jwt = require('jsonwebtoken'); +const { PrismaClient } = require('@prisma/client'); +const prisma = new PrismaClient(); + +// 선택적 인증 미들웨어 +// 인증에 성공하면 사용자 정보를 추가하고 실패해도 에러없이 다음 미들웨어 진행 +module.exports = async (req, res, next) => { + try { + const { authorization } = req.headers; + if (!authorization) { + return next(); + } + + const [tokenType, token] = authorization.split(' '); + if (tokenType !== 'Bearer' || !token) { + return next(); + } + + const decodedToken = jwt.verify(token, process.env.JWT_SECRET_KEY); + const user = await prisma.user.findUnique({ + where: { id: decodedToken.userId }, + }); + + if (user) { + req.user = user; + } + } catch (error) { + } + + return next(); +}; \ No newline at end of file From 641ca6dfda5df51891311f86784c4669777b3b32 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Mon, 15 Sep 2025 01:06:47 +0900 Subject: [PATCH 26/63] =?UTF-8?q?articles.router.js=EC=97=90=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=EC=A0=81=20=EC=9D=B8=EC=A6=9D=20=EB=AF=B8=EB=93=A4?= =?UTF-8?q?=EC=9B=A8=EC=96=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/articles.router.js | 29 ++++++++++++++++++++++++++--- routes/products.router.js | 28 +++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/routes/articles.router.js b/routes/articles.router.js index 2a2e4c307..96848bef7 100644 --- a/routes/articles.router.js +++ b/routes/articles.router.js @@ -5,6 +5,7 @@ const { PrismaClient } =require('@prisma/client'); const prisma = new PrismaClient(); const { validateArticle } = require('../middlewares/validation.middleware.js'); const authMiddleware = require('../middlewares/auth.middleware.js'); +const optionalAuthMiddleware = require('../middlewares/optionalAuth.middleware.js'); //article registration router @@ -58,19 +59,41 @@ router // article detail, modify, delete router .route('/articles/:articleId') - .get(async (req, res, next) => { + .get(optionalAuthMiddleware, async (req, res, next) => { try { const { articleId } = req.params; + const user = req.user; + const article = await prisma.article.findUnique({ where: { id: parseInt(articleId) }, select: { id: true, title: true, content: true, createdAt: true, userId: true }, }); - if (!article) return res.status(404).json({ message: '게시글을 찾을 수 없습니다.'}); - res.status(200).json(article); + + if (!article) { + return res.status(404).json({ message: '게시글을 찾을 수 없습니다.'}); + } + + let isLiked = false; + if (user) { + const like = await prisma.like.findFirst({ + where: { + articleId: article.id, + userId: user.id, + }, + }); + if (like) { + isLiked = true; + } + } + + const responseArticle = { ...article, isLiked }; + res.status(200).json(responseArticle); + } catch (error) { next(error); } }) + .patch(authMiddleware, validateArticle, async (req, res, next) => { try { const { articleId } = req.params; diff --git a/routes/products.router.js b/routes/products.router.js index abbf5552a..f9ff1af81 100644 --- a/routes/products.router.js +++ b/routes/products.router.js @@ -5,6 +5,7 @@ const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient(); const { validationProduct, validateProduct } = require('../middlewares/validation.middleware.js'); const authMiddleware = require('../middlewares/auth.middleware.js'); +const optionalAuthMiddleware = require('../middlewares/optionalAuth.middleware.js'); // registration router router.post('/products', authMiddleware, validateProduct, async (req, res, next) => { @@ -58,20 +59,41 @@ router.get('/products', async (req, res, next) => { // datail, modify, delete router .route('/products/:productId') - .get(async (req, res, next) => { + .get(optionalAuthMiddleware, async (req, res, next) => { try { const { productId } = req.params; + const user = req.user; + const product = await prisma.product.findUnique({ where: { id: parseInt(productId) }, select: { id: true, name: true, description: true, price: true, createdAt: true, userId: true }, }); + if (!product) return res.status(404).json({ message: '상품을 찾을수 없습니다.'}); - res.status(200).json(product); + + let isLiked = false; + if (user) { + // 로그인한 경우 좋아요 여부 확인 + const like = await prisma.like.findFirst({ + where: { + productId: product.id, + userId: user.id, + }, + }); + if (like) { + isLiked = true; + } + } + + const responseProduct = { ...product, isLiked }; + res.status(200).json(responseProduct); } catch (error) { next(error); } - }) + }) + + .patch(validateProduct, authMiddleware, async (req, res, next) => { try { const { productId } = req.params; From c4996fdd6838c326327b9b63e0107e7bbbe853a6 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Mon, 15 Sep 2025 01:13:49 +0900 Subject: [PATCH 27/63] =?UTF-8?q?products.router.js=EC=97=90=20=EC=83=81?= =?UTF-8?q?=ED=92=88=EB=AA=A9=EB=A1=9D=EC=A1=B0=ED=9A=8C=20isLiked=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/products.router.js | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/routes/products.router.js b/routes/products.router.js index f9ff1af81..83cf3feae 100644 --- a/routes/products.router.js +++ b/routes/products.router.js @@ -27,7 +27,7 @@ router.post('/products', authMiddleware, validateProduct, async (req, res, next) }) // cherck router -router.get('/products', async (req, res, next) => { +router.get('/products', optionalAuthMiddleware, async (req, res, next) => { try { const { sort, search } = req.query; let page = parseInt(req.query.page) || 1; @@ -43,6 +43,8 @@ router.get('/products', async (req, res, next) => { ], } : {}; + + const user = req.user; const products = await prisma.product.findMany({ where, select: { id: true, name: true, price: true, createdAt: true, userId: true }, @@ -50,7 +52,37 @@ router.get('/products', async (req, res, next) => { skip: offset, take: limit, }); - res.status(200).json(products); + + let responseProducts = products; + + if (user) { + // 로그인한 사용자의 좋아요 누른 상품 목록 조회 + const productIds = products.map(product => product.id); + const likes = await prisma.like.findMany({ + where: { + userId: user.id, + productId: { in: productIds }, + }, + }); + + const likedProductIds = new Set(likes.map(like => like.productId)); + + // 각 상품에 isLiked 필드 추가 + responseProducts = products.map(product => ({ + ...product, + isLiked: likedProductIds.has(product.id), + })); + } else { + + // 로그인 하지 않은 사용자의 경우 모든 isLiked = false + responseProducts = products.map(product => ({ + ...product, + isLiked: false, + })); + } + + res.status(200).json(responseProducts); + } catch (error) { next(error); } From a70d66d8953d3e5280790d96e285f378bc05870f Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Mon, 15 Sep 2025 01:19:48 +0900 Subject: [PATCH 28/63] =?UTF-8?q?articles.router.js=EC=97=90=20isLiked=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/articles.router.js | 50 +++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/routes/articles.router.js b/routes/articles.router.js index 96848bef7..b64dbac1e 100644 --- a/routes/articles.router.js +++ b/routes/articles.router.js @@ -27,7 +27,7 @@ router } }) // 게시글 목록 조회 - .get(async (req, res, next) => { + .get(optionalAuthMiddleware, async (req, res, next) => { try { const { sort, search } = req.query; let page = parseInt(req.query.page) || 1; @@ -43,18 +43,44 @@ router } : {}; - const articles = await prisma.article.findMany({ - where, - select: { id: true, title: true, content: true, createdAt: true, userId: true }, - orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, - skip: offset, - take: limit, - }); - res.status(200).json(articles); - } catch (error) { - next(error); + const user = req.user; + const articles = await prisma.article.findMany({ + where, + select: { id: true, title: true, content: true, createdAt: true, userId: true }, + orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, + skip: offset, + take: limit, + }); + + let responseArticle = articles; + + if (user) { + const articleIds = articles.map(article => article.id); + const likes = await prisma.like.findMany({ + where: { + userId: user.id, + articleId: { in: articleIds }, + }, + }); + + const likedArticleIds = new Set(likes.map(like => like.articleId)); + + responseArticles = articles.map(article => ({ + ...article, + isLiked: likedArticleIds.has(article.id), + })); + } else { + responseArticles = articles.map(article => ({ + ...article, + isLiked: false, + })); } - }); + + res.status(200).json(responseArticles); + } catch (error) { + next(error); + } +}); // article detail, modify, delete router From 3cc240061edf9f548408a09e757b0a8f2ea20ace Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Mon, 15 Sep 2025 01:27:52 +0900 Subject: [PATCH 29/63] =?UTF-8?q?UseService.js=EC=9D=98=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81=EC=9D=84=20access,=20re?= =?UTF-8?q?fresh=20token=EB=A5=BC=20=EB=B0=9B=EB=8A=94=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UserService.js | 20 ++++++++++++---- .../migration.sql | 2 ++ prisma/schema.prisma | 23 ++++++++++--------- 3 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 prisma/migrations/20250914162141_add_refresh_token/migration.sql diff --git a/UserService.js b/UserService.js index eaf9a0c6e..5745f27a2 100644 --- a/UserService.js +++ b/UserService.js @@ -44,15 +44,27 @@ class UserService { throw new Error('비밀번호가 일치하지 않습니다.'); } - // 로그인 성공시 JWT 생성 - const token = jwt.sign( + // Access Token 생성 (12시간) + const accessToken = jwt.sign( { userId: user.id }, process.env.JWT_SECRET_KEY, { expiresIn: '12h' } ); - return token; + // Refresh Token 생성 (7일) + const refreshToken = jwt.sign( + {}, + process.env.REFRESH_TOKEN_SECRET_KEY, + { expiresIn: '7d' } + ); + + // Refresh Token을 해싱해서 DB에 저장 + await this.prisma.user.update({ + where: { id: user.id }, + data: { refreshToken: await bcrypt.hash(refreshToken, 10) }, + }); + + return { accessToke, refreshToken }; }; } - module.exports = UserService; diff --git a/prisma/migrations/20250914162141_add_refresh_token/migration.sql b/prisma/migrations/20250914162141_add_refresh_token/migration.sql new file mode 100644 index 000000000..091164c26 --- /dev/null +++ b/prisma/migrations/20250914162141_add_refresh_token/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "public"."users" ADD COLUMN "refreshToken" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7c299e6b8..3d8a36c70 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -65,17 +65,18 @@ model Like { } model User { - id Int @id @default(autoincrement()) - email String @unique - password String - nickname String - image String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - products Product[] - articles Article[] - comments Comment[] - likes Like[] + id Int @id @default(autoincrement()) + email String @unique + password String + nickname String + image String? + refreshToken String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + products Product[] + articles Article[] + comments Comment[] + likes Like[] @@map("users") } From d9b1629a0de65637b66ffdb0fee7bbf4d560fc5c Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Mon, 15 Sep 2025 01:29:27 +0900 Subject: [PATCH 30/63] =?UTF-8?q?index.js=EC=97=90=20cookie=20=EB=AF=B8?= =?UTF-8?q?=EB=93=A4=EC=9B=A8=EC=96=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/prisma/index.js b/prisma/index.js index bb6ade6f8..4e565ca04 100644 --- a/prisma/index.js +++ b/prisma/index.js @@ -2,6 +2,7 @@ require('dotenv').config(); const express = require('express'); const cors = require('cors'); const path = require('path'); +const cookieParser = require('cookie=parser'); const app = express(); const PORT = process.env.PORT || 3000; @@ -14,6 +15,7 @@ const uploadRouter = require('./routes/upload.router.js'); //middleware app.use(cors()); app.use(express.json()); +app.use(cookieParser()); app.use(express.urlencoded({ extended: true })); app.use('/uploads', express.static(path.join(__dirname, 'uploads' ))); From 1d25f6f57eadfb2594a44609736ade6be5bb6222 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Mon, 15 Sep 2025 01:39:37 +0900 Subject: [PATCH 31/63] =?UTF-8?q?=EC=98=A4=ED=83=88=EC=9E=90=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20index.js=20=EB=9D=BC=EC=9A=B0=ED=8A=B8?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prisma/index.js | 12 ++++++++---- routes/users.router.js | 13 ++++++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/prisma/index.js b/prisma/index.js index 4e565ca04..22157413e 100644 --- a/prisma/index.js +++ b/prisma/index.js @@ -2,17 +2,18 @@ require('dotenv').config(); const express = require('express'); const cors = require('cors'); const path = require('path'); -const cookieParser = require('cookie=parser'); +const cookieParser = require('cookie-parser'); const app = express(); const PORT = process.env.PORT || 3000; -//router import +// router import const productRouter = require('./routes/products.router.js'); const articleRouter = require('./routes/articles.router.js'); const uploadRouter = require('./routes/upload.router.js'); +const usersRouter = require('./routes/users.router.js'); -//middleware +// middleware app.use(cors()); app.use(express.json()); app.use(cookieParser()); @@ -20,7 +21,10 @@ app.use(express.urlencoded({ extended: true })); app.use('/uploads', express.static(path.join(__dirname, 'uploads' ))); -//Error middleware +// 라우터 설정 +app.use('/api', [productRouter, articleRouter, uploadRouter, usersRouter]); + +// Error middleware app.use((err, req, res, next) => { console.error(err.stack); const statusCode = err.statusCode || 500; diff --git a/routes/users.router.js b/routes/users.router.js index 02c3f4b3f..ab8af954c 100644 --- a/routes/users.router.js +++ b/routes/users.router.js @@ -37,14 +37,21 @@ router.post('/sign-in', async (req, res, next) => { if (!email || !password) { return res.status(400).json({ message: '이메일과 비밀번호를 모두 입력해주세요.'}); } - const token = await userService.signIn(email, password); + const { accessToken, refreshToken } = await userService.signIn(email, password); + + //refresh token을 쿠키에 설정 + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: false, + maxAge: 1000 * 60 * 60 * 24 * 7, + }); return res.status(200).json({ message: '로그인에 성공했습니다.', - data: { token }, + data: { accessToken }, }); } catch (error) { - return res.status(401).json({ message: Error.message }); + return res.status(401).json({ message: error.message }); } }); From 4dcee1a0529893039f66336de44a7c1ec21ab79d Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Mon, 15 Sep 2025 01:54:00 +0900 Subject: [PATCH 32/63] =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=9E=AC=EB=B0=9C?= =?UTF-8?q?=EA=B8=89=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UserService.js | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/UserService.js b/UserService.js index 5745f27a2..4d15564df 100644 --- a/UserService.js +++ b/UserService.js @@ -1,6 +1,7 @@ const { PrismaClient } = require('@prisma/client'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); +const router = require('./routes/products.router'); class UserService { prisma = new PrismaClient(); @@ -53,7 +54,7 @@ class UserService { // Refresh Token 생성 (7일) const refreshToken = jwt.sign( - {}, + { userId: user.id }, process.env.REFRESH_TOKEN_SECRET_KEY, { expiresIn: '7d' } ); @@ -64,7 +65,47 @@ class UserService { data: { refreshToken: await bcrypt.hash(refreshToken, 10) }, }); + // Token 재발급 API + router.post('/token/refresh', async (req, res, next) => { + try { + const { refreshToken } = req.cookies; + if (!refreshToken) { + return res.status(401).json({ message: 'Refresh Token이 없습니다.' }); + } + + const decodedToken = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET_KEY); + const userId = decodedToken.userId; + + const user = await this.prisma.user.findUnique({ where: { id: userId } }); + if (!user) { + return res.status(401).json({ message: '사용자를 찾을 수 없습니다.' }); + } + + //DB에 저장된 hashed Refresh Token과 비교 + const isRefreshTokenMatched = await bcrypt.compare(refreshToken, user.refreshToken); + if (!isRefreshTokenMatched) { + return res.status(401).json({ message: 'Refresh Token이 유효하지 않습니다.' }); + } + + // 새로운 Access Token 생성 + const newAccessToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET_KEY, { + expiresIn: '12h', + }); + + return res.status(200).json({ + message: 'Access Token이 재발급되었습니다.', + data: { accessToke, newAccessToken }, + }); + } catch (error) { + if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') { + return res.status(401).json({ message: 'Refresh Token이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요 '}); + } + next(error); + } + }); + return { accessToke, refreshToken }; - }; + } } + module.exports = UserService; From f97e999075a341c77f4e1b2edcd8611ec72690ac Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 13:53:09 +0900 Subject: [PATCH 33/63] =?UTF-8?q?=ED=83=80=EC=9E=85=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A6=BD=ED=8A=B8=20js=20->=20ts=20=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ArticleService.js => ArticleService.ts | 0 ProductService.js => ProductService.ts | 0 UserService.js => UserService.ts | 6 +- index.js => index.ts | 4 + main.js => main.ts | 0 ...{auth.middleware.js => auth.middleware.ts} | 0 ...ddleware.js => optionalAuth.middleware.ts} | 0 ...middleware.js => validation.middleware.ts} | 0 package-lock.json | 207 +++++++++++++++++- package.json | 11 +- ...{articles.router.js => articles.router.ts} | 3 +- ...{products.router.js => products.router.ts} | 3 +- routes/{upload.router.js => upload.router.ts} | 0 routes/{users.router.js => users.router.ts} | 4 +- tsconfig.json | 20 ++ 15 files changed, 249 insertions(+), 9 deletions(-) rename ArticleService.js => ArticleService.ts (100%) rename ProductService.js => ProductService.ts (100%) rename UserService.js => UserService.ts (95%) rename index.js => index.ts (90%) rename main.js => main.ts (100%) rename middlewares/{auth.middleware.js => auth.middleware.ts} (100%) rename middlewares/{optionalAuth.middleware.js => optionalAuth.middleware.ts} (100%) rename middlewares/{validation.middleware.js => validation.middleware.ts} (100%) rename routes/{articles.router.js => articles.router.ts} (99%) rename routes/{products.router.js => products.router.ts} (99%) rename routes/{upload.router.js => upload.router.ts} (100%) rename routes/{users.router.js => users.router.ts} (97%) create mode 100644 tsconfig.json diff --git a/ArticleService.js b/ArticleService.ts similarity index 100% rename from ArticleService.js rename to ArticleService.ts diff --git a/ProductService.js b/ProductService.ts similarity index 100% rename from ProductService.js rename to ProductService.ts diff --git a/UserService.js b/UserService.ts similarity index 95% rename from UserService.js rename to UserService.ts index 4d15564df..8e393fd22 100644 --- a/UserService.js +++ b/UserService.ts @@ -2,9 +2,13 @@ const { PrismaClient } = require('@prisma/client'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const router = require('./routes/products.router'); +const prisma = require('./index.js'); class UserService { - prisma = new PrismaClient(); + // prisma = new PrismaClient(); // Remove this line + constructor() { + this.prisma = prisma; // Use the imported prisma instance + } // 회원가입 로직 signUp = async (email, nickname, password) => { diff --git a/index.js b/index.ts similarity index 90% rename from index.js rename to index.ts index 9d517f661..aeed117cd 100644 --- a/index.js +++ b/index.ts @@ -2,9 +2,11 @@ require('dotenv').config(); const express = require('express'); const cors = require('cors'); const path = require('path'); +const { PrismaClient } = require('@prisma/client'); const app = express(); const PORT = process.env.PORT || 3000; +const prisma = new PrismaClient(); // import router const productRouter = require('./routes/products.router.js'); @@ -35,3 +37,5 @@ app.listen(PORT, () => { console.log(`서버가 ${PORT}번에서 실행중입니다.`); }); +module.exports = prisma; + diff --git a/main.js b/main.ts similarity index 100% rename from main.js rename to main.ts diff --git a/middlewares/auth.middleware.js b/middlewares/auth.middleware.ts similarity index 100% rename from middlewares/auth.middleware.js rename to middlewares/auth.middleware.ts diff --git a/middlewares/optionalAuth.middleware.js b/middlewares/optionalAuth.middleware.ts similarity index 100% rename from middlewares/optionalAuth.middleware.js rename to middlewares/optionalAuth.middleware.ts diff --git a/middlewares/validation.middleware.js b/middlewares/validation.middleware.ts similarity index 100% rename from middlewares/validation.middleware.js rename to middlewares/validation.middleware.ts diff --git a/package-lock.json b/package-lock.json index 494cbe6be..f93290636 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,8 +20,17 @@ "sharp": "^0.34.3" }, "devDependencies": { + "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.9", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.0.0", + "@types/node": "^24.5.2", + "@types/sharp": "^0.31.1", "nodemon": "^3.1.10", - "prisma": "^6.16.1" + "prisma": "^6.16.1", + "typescript": "^5.9.2" } }, "node_modules/@emnapi/runtime": { @@ -544,6 +553,181 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", + "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.12.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sharp": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz", + "integrity": "sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -2555,6 +2739,20 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -2562,6 +2760,13 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 6bae74509..b8b380ae8 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,16 @@ "sharp": "^0.34.3" }, "devDependencies": { + "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.9", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.3", + "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.0.0", + "@types/node": "^24.5.2", + "@types/sharp": "^0.31.1", "nodemon": "^3.1.10", - "prisma": "^6.16.1" + "prisma": "^6.16.1", + "typescript": "^5.9.2" } } diff --git a/routes/articles.router.js b/routes/articles.router.ts similarity index 99% rename from routes/articles.router.js rename to routes/articles.router.ts index b64dbac1e..281d2209a 100644 --- a/routes/articles.router.js +++ b/routes/articles.router.ts @@ -1,8 +1,7 @@ //adricle router const express = require('express'); const router = express.Router(); -const { PrismaClient } =require('@prisma/client'); -const prisma = new PrismaClient(); +const prisma = require('../index.js'); // Import prisma from index.js const { validateArticle } = require('../middlewares/validation.middleware.js'); const authMiddleware = require('../middlewares/auth.middleware.js'); const optionalAuthMiddleware = require('../middlewares/optionalAuth.middleware.js'); diff --git a/routes/products.router.js b/routes/products.router.ts similarity index 99% rename from routes/products.router.js rename to routes/products.router.ts index 83cf3feae..9b221a024 100644 --- a/routes/products.router.js +++ b/routes/products.router.ts @@ -1,8 +1,7 @@ // product router const express = require('express'); const router = express.Router(); -const { PrismaClient } = require('@prisma/client'); -const prisma = new PrismaClient(); +const prisma = require('../index.js'); // Import prisma from index.js const { validationProduct, validateProduct } = require('../middlewares/validation.middleware.js'); const authMiddleware = require('../middlewares/auth.middleware.js'); const optionalAuthMiddleware = require('../middlewares/optionalAuth.middleware.js'); diff --git a/routes/upload.router.js b/routes/upload.router.ts similarity index 100% rename from routes/upload.router.js rename to routes/upload.router.ts diff --git a/routes/users.router.js b/routes/users.router.ts similarity index 97% rename from routes/users.router.js rename to routes/users.router.ts index ab8af954c..821b425ab 100644 --- a/routes/users.router.js +++ b/routes/users.router.ts @@ -1,11 +1,11 @@ const express = require('express'); const UserService = require('../UserService.js'); const authMiddleware = require('../middlewares/auth.middleware.js'); -const { PrismaClient } = require('@prisma/client'); const bcrypt = require('bcrypt'); +const prisma = require('../index.js'); // Import prisma from index.js const router = express.Router(); -const prisma = new PrismaClient(); +// const prisma = new PrismaClient(); // Remove this line const userService = new UserService(); // 회원가입 API diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..b799bf95e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": [ + "**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file From f43496245c55057d190945a797708df693a8ade3 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 13:58:30 +0900 Subject: [PATCH 34/63] =?UTF-8?q?import=EB=AC=B8=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ArticleService.ts | 109 ++++++-------------- ProductService.ts | 133 ++++++------------------- UserService.ts | 73 +++----------- index.ts | 25 ++--- main.ts | 4 +- middlewares/auth.middleware.ts | 35 ++++--- middlewares/optionalAuth.middleware.ts | 20 ++-- middlewares/validation.middleware.ts | 8 +- routes/articles.router.ts | 83 ++++++++++----- routes/products.router.ts | 80 ++++++++++----- routes/upload.router.ts | 19 ++-- routes/users.router.ts | 99 ++++++++++++++---- types/express.d.ts | 9 ++ 13 files changed, 342 insertions(+), 355 deletions(-) create mode 100644 types/express.d.ts diff --git a/ArticleService.ts b/ArticleService.ts index a58aea399..08c1939d4 100644 --- a/ArticleService.ts +++ b/ArticleService.ts @@ -1,90 +1,37 @@ -class Article { - constructor(title, content, writer) { - this.title = title; - this.content = content; - this.writer = writer; - this.likeCount = 0; - this.createdAt = new Date(); - } - like() { - this.likeCount++; - } -} - -const BASE_URL = 'https://panda-market-api-crud.vercel.app'; +import prisma from './index'; +import { Article as PrismaArticle, Prisma } from '@prisma/client'; -function getAarticleList (page = 1, pageSize = 10, keyword = '') { - fetch (`${BASE_URL}/articles?page=${page}&pageSize=${pageSize}&keyword=${keyword}`) - .then (response => { - if (!response.ok) { - throw new Error (`HTTP error! status: ${response.status}`); - } - return response.json(); - }) - .then (data => console.log (`Article list:`, data)) - .catch (error => console.error ('Error fetching article list:', error)); +interface ArticleCreateInput { + title: string; + content: string; + userId: number; } -function getArticle(articleId) { - fetch (`${BASE_URL}/articles/${articleId}`) - .then (response => { - if (!response.ok) { - throw new Error (`HTTP error! status: ${response.status}`); - } - return response.json(); - }) - .then (data => console.log(`Article ${articleId}:`, data)) - .catch (error => console.error (`Error fetching article ${articleId}:`, error)); +interface ArticleUpdateInput { + title?: string; + content?: string; } -function createArticle(articleData) { - fetch (`${BASE_URL}/articles`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(articleData), - }) - .then (response => { - if (!response.ok) { - throw new Error (`HTTP error! status: ${response.status}`); - } - return response.json(); - }) - .then(data => console.log('Created article:', data)) - .catch (error => console.error ('Error creating article:', error)) -} +class ArticleService { + async createArticle(data: ArticleCreateInput): Promise { + return prisma.article.create({ data }); + } -function patchArticle(articleId, articleData) { - fetch (`${BASE_URL}/articles/${articleId}`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(articleData), - }) - .then (response => { - if (!response.ok) { - throw new Error (`HTTP error! status: ${response.status}`); - } - return response.json(); - }) - .then(data => console.log(`Patched article ${articleId}:`, data)) - .catch (error => console.error (`Error patching article ${articleId}:`, error)) -} + async getArticleById(id: number): Promise { + return prisma.article.findUnique({ where: { id } }); + } + + async getArticles(options?: { skip?: number; take?: number; where?: Prisma.ArticleWhereInput; orderBy?: Prisma.ArticleOrderByWithRelationInput }): Promise { + return prisma.article.findMany(options); + } + + async updateArticle(id: number, data: ArticleUpdateInput): Promise { + return prisma.article.update({ where: { id }, data }); + } -function deleteArticle(articleId) { - fetch (`${BASE_URL}/articles/${articleId}`, { - method: 'DELETE', - }) - .then (response => { - if (!response.ok) { - throw new Error (`HTTP error! status: ${response.status}`); - } - return response.json(); - }) - .then(data => console.log(`Deleted article ${articleId}:`, data)) - .catch (error => console.error (`Error deleting article ${articleId}:`, error)) + async deleteArticle(id: number): Promise { + return prisma.article.delete({ where: { id } }); + } } -export { Article, getAarticleList, getArticle, createArticle, patchArticle, deleteArticle }; \ No newline at end of file +export default ArticleService; diff --git a/ProductService.ts b/ProductService.ts index 8d0b5157c..8eb4c3a5f 100644 --- a/ProductService.ts +++ b/ProductService.ts @@ -1,110 +1,39 @@ -class Product { - constructor(name, destription, price, tags, images) { - this.name = name; - this.destription = destription; - this.price = price; - this.tags = tags; - this.images = images; - this.favoriteCount = 0; - } - favorite() { - this.favoriteCount++; - } +import prisma from './index'; +import { Product as PrismaProduct, Prisma } from '@prisma/client'; + +interface ProductCreateInput { + name: string; + content: string; + status?: string; + userId: number; } -class ElectronicProduct extends Product { - (name, destription, price, tags, images, manufacturer) { - super (name, destription, price, tags, images); - this.manufacturer = manufacturer; - } +interface ProductUpdateInput { + name?: string; + content?: string; + status?: string; } -const BASE_URL = 'https://panda-market-api-crud.vercel.app'; +class ProductService { + async createProduct(data: ProductCreateInput): Promise { + return prisma.product.create({ data }); + } -async function getProductList (page = 1, pageSize = 10, keyword = '') { - try { - const response = await fetch (`${BASE_URL}/products?page=${page}&pageSize= - ${pageSize}&keyword=${keyword}`); - if (!response.ok) { - throw new Error (`HTTP error! status: ${response.status}`); - } - const data = await response.json(); - const products = data.map (item => { - if (item.tag.includes ('전자제품')) { - return new ElectronicProduct(item.name, item.destription, item.price, item.tags, - item.images, item.manufacturer); - } else { - return new Product (item.name, item.destription, item.price, item.tags, item.images) - } - }); - return products; - } catch (error) { - console.error('Error fetching product list:', error); - } -} + async getProductById(id: number): Promise { + return prisma.product.findUnique({ where: { id } }); + } -async function getProduct(productId) { - - try { - const response = await fetch(`${BASE_URL}/products/${productId}`); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return await response.json(); - } catch (error) { - console.error(`Error fetching product ${productId}:`, error); - } -} - -async function createProduct(productData) { - try { - const response = await fetch(`${BASE_URL}/products`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(productData), - }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return await response.json(); - } catch (error) { - console.error('Error creating product:', error); - } -} - -async function patchProduct(productId, productData) { - try { - const response = await fetch(`${BASE_URL}/products/${productId}`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(productData), - }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return await response.json(); - } catch (error) { - console.error(`Error patching product ${productId}:`, error); - } -} - -async function deleteProduct(productId) { - try { - const response = await fetch(`${BASE_URL}/products/${productId}`, { - method: 'DELETE', - }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - return await response.json(); - } catch (error) { - console.error(`Error deleting product ${productId}:`, error); - } + async getProducts(options?: { skip?: number; take?: number; where?: Prisma.ProductWhereInput; orderBy?: Prisma.ProductOrderByWithRelationInput }): Promise { + return prisma.product.findMany(options); + } + + async updateProduct(id: number, data: ProductUpdateInput): Promise { + return prisma.product.update({ where: { id }, data }); + } + + async deleteProduct(id: number): Promise { + return prisma.product.delete({ where: { id } }); + } } -export { Product, ElectronicProduct, getProductList, getProduct, createProduct, patchProduct, - deleteProduct }; \ No newline at end of file +export default ProductService; diff --git a/UserService.ts b/UserService.ts index 8e393fd22..49022c190 100644 --- a/UserService.ts +++ b/UserService.ts @@ -1,17 +1,15 @@ -const { PrismaClient } = require('@prisma/client'); -const bcrypt = require('bcrypt'); -const jwt = require('jsonwebtoken'); -const router = require('./routes/products.router'); -const prisma = require('./index.js'); +import prisma from './index'; +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; +import { User as PrismaUser } from '@prisma/client'; -class UserService { - // prisma = new PrismaClient(); // Remove this line - constructor() { - this.prisma = prisma; // Use the imported prisma instance - } +interface UserWithoutPassword extends Omit {} + +class UserService { + private prisma = prisma; // 회원가입 로직 - signUp = async (email, nickname, password) => { + public signUp = async (email: string, nickname: string, password: string): Promise => { // 이메일 중복 확인 const existingUser = await this.prisma.user.findUnique({ where: { email } }); @@ -32,11 +30,11 @@ class UserService { }); // 사용자 정보 반환 - const { password: _, ...userWithoutPassword } = user; + const { password: _, refreshToken: __, ...userWithoutPassword } = user; return userWithoutPassword; }; - signIn = async (email, password) => { + public signIn = async (email: string, password: string): Promise<{ accessToken: string; refreshToken: string }> => { // 이메일로 사용자 조회 const user = await this.prisma.user.findUnique({ where: { email } }); if (!user) { @@ -52,14 +50,14 @@ class UserService { // Access Token 생성 (12시간) const accessToken = jwt.sign( { userId: user.id }, - process.env.JWT_SECRET_KEY, + process.env.JWT_SECRET_KEY as string, { expiresIn: '12h' } ); // Refresh Token 생성 (7일) const refreshToken = jwt.sign( { userId: user.id }, - process.env.REFRESH_TOKEN_SECRET_KEY, + process.env.REFRESH_TOKEN_SECRET_KEY as string, { expiresIn: '7d' } ); @@ -69,47 +67,8 @@ class UserService { data: { refreshToken: await bcrypt.hash(refreshToken, 10) }, }); - // Token 재발급 API - router.post('/token/refresh', async (req, res, next) => { - try { - const { refreshToken } = req.cookies; - if (!refreshToken) { - return res.status(401).json({ message: 'Refresh Token이 없습니다.' }); - } - - const decodedToken = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET_KEY); - const userId = decodedToken.userId; - - const user = await this.prisma.user.findUnique({ where: { id: userId } }); - if (!user) { - return res.status(401).json({ message: '사용자를 찾을 수 없습니다.' }); - } - - //DB에 저장된 hashed Refresh Token과 비교 - const isRefreshTokenMatched = await bcrypt.compare(refreshToken, user.refreshToken); - if (!isRefreshTokenMatched) { - return res.status(401).json({ message: 'Refresh Token이 유효하지 않습니다.' }); - } - - // 새로운 Access Token 생성 - const newAccessToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET_KEY, { - expiresIn: '12h', - }); - - return res.status(200).json({ - message: 'Access Token이 재발급되었습니다.', - data: { accessToke, newAccessToken }, - }); - } catch (error) { - if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') { - return res.status(401).json({ message: 'Refresh Token이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요 '}); - } - next(error); - } - }); - - return { accessToke, refreshToken }; - } + return { accessToken, refreshToken }; + }; } -module.exports = UserService; +export default UserService; \ No newline at end of file diff --git a/index.ts b/index.ts index aeed117cd..7660c0b4b 100644 --- a/index.ts +++ b/index.ts @@ -1,18 +1,19 @@ -require('dotenv').config(); -const express = require('express'); -const cors = require('cors'); -const path = require('path'); -const { PrismaClient } = require('@prisma/client'); +import dotenv from 'dotenv'; +dotenv.config(); +import express, { Request, Response, NextFunction } from 'express'; +import cors from 'cors'; +import path from 'path +import { PrismaClient } from '@prisma/client'; const app = express(); const PORT = process.env.PORT || 3000; const prisma = new PrismaClient(); // import router -const productRouter = require('./routes/products.router.js'); -const articleRouter = require('./routes/articles.router.js'); -const uploadRouter = require('./routes/upload.router.js'); -const usersRouter = require('./routes/users.router.js'); +import productRouter from './routes/products.router'; +import articleRouter from './routes/articles.router'; +import uploadRouter from './routes/upload.router'; +import usersRouter from './routes/users.router'; // Middleware app.use(cors()); @@ -25,9 +26,9 @@ app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); app.use('/api', [productRouter, articleRouter, uploadRouter, usersRouter]); // Error Handler Middleware -app.use((err, req, res, next) => { +app.use((err: Error, req: Request, res: Response, next: NextFunction) => { console.error(err.stack); - const statusCode = err.statusCode || 500; + const statusCode = (err as any).statusCode || 500; const message = err.message || '오류가 발생했습니다.'; res.status(statusCode).json({ message }); }); @@ -37,5 +38,5 @@ app.listen(PORT, () => { console.log(`서버가 ${PORT}번에서 실행중입니다.`); }); -module.exports = prisma; +export default prisma; diff --git a/main.ts b/main.ts index bc1536aec..25ab1d0e1 100644 --- a/main.ts +++ b/main.ts @@ -6,7 +6,7 @@ import { createProduct, patchProduct, deleteProduct, -} from "./ProductService.js"; +} from "./ProductService"; import { Article, @@ -15,4 +15,4 @@ import { createArticle, patchArticle, deleteArticle, -} from "./ArticleService.js"; +} from "./ArticleService"; diff --git a/middlewares/auth.middleware.ts b/middlewares/auth.middleware.ts index 38f1075fa..5d4bfe3b9 100644 --- a/middlewares/auth.middleware.ts +++ b/middlewares/auth.middleware.ts @@ -1,9 +1,13 @@ -const jwt = require('jsonwebtoken'); -const { PrismaClient } = require('@prisma/client'); +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; +import prisma from '../index'; // Import prisma from index.ts +import { User as PrismaUser } from '@prisma/client'; -const prisma = new PrismaClient(); +interface DecodedToken { + userId: number; +} -module.exports = async (req, res, next) => { +const authMiddleware = async (req: Request, res: Response, next: NextFunction) => { try { // 헤더에서 authorization 값 가져오기 const { authorization } = req.headers; @@ -23,7 +27,7 @@ module.exports = async (req, res, next) => { } // 토큰 검증 (확인) - const decodedToken = jwt.verify(token, process.env.JWT_SECRET_KEY); + const decodedToken = jwt.verify(token, process.env.JWT_SECRET_KEY as string) as DecodedToken; const userId = decodedToken.userId; // 토큰 있는 userId로 조회 @@ -39,13 +43,18 @@ module.exports = async (req, res, next) => { next(); } catch (error) { console.error('인증 미들웨어 에러:', error); - switch (error.name) { - case 'TokenExpiredError': - return res.status(401).json({ message: '인증 토큰이 만료되었습니다.' }); - case 'JsonWebTokenError': - return res.status(401).json({ message: '유효하지 않은 인증 토큰입니다.' }); - default: - return res.status(401).json({ message: '인증 정보가 유효하지 않습니다.' }); + if (error instanceof Error) { + switch (error.name) { + case 'TokenExpiredError': + return res.status(401).json({ message: '인증 토큰이 만료되었습니다.' }); + case 'JsonWebTokenError': + return res.status(401).json({ message: '유효하지 않은 인증 토큰입니다.' }); + default: + return res.status(401).json({ message: '인증 정보가 유효하지 않습니다.' }); + } } + next(error); } -}; \ No newline at end of file +}; + +export default authMiddleware; \ No newline at end of file diff --git a/middlewares/optionalAuth.middleware.ts b/middlewares/optionalAuth.middleware.ts index d6d47e1d1..6ce49f49e 100644 --- a/middlewares/optionalAuth.middleware.ts +++ b/middlewares/optionalAuth.middleware.ts @@ -1,10 +1,14 @@ -const jwt = require('jsonwebtoken'); -const { PrismaClient } = require('@prisma/client'); -const prisma = new PrismaClient(); +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; +import prisma from '../index'; // Import prisma from index.ts + +interface DecodedToken { + userId: number; +} // 선택적 인증 미들웨어 // 인증에 성공하면 사용자 정보를 추가하고 실패해도 에러없이 다음 미들웨어 진행 -module.exports = async (req, res, next) => { +const optionalAuthMiddleware = async (req: Request, res: Response, next: NextFunction) => { try { const { authorization } = req.headers; if (!authorization) { @@ -16,7 +20,7 @@ module.exports = async (req, res, next) => { return next(); } - const decodedToken = jwt.verify(token, process.env.JWT_SECRET_KEY); + const decodedToken = jwt.verify(token, process.env.JWT_SECRET_KEY as string) as DecodedToken; const user = await prisma.user.findUnique({ where: { id: decodedToken.userId }, }); @@ -25,7 +29,11 @@ module.exports = async (req, res, next) => { req.user = user; } } catch (error) { + // 에러 발생 시에도 다음 미들웨어로 진행 (선택적 인증이므로) + console.error('선택적 인증 미들웨어 에러:', error); } return next(); -}; \ No newline at end of file +}; + +export default optionalAuthMiddleware; \ No newline at end of file diff --git a/middlewares/validation.middleware.ts b/middlewares/validation.middleware.ts index cf82500e6..7c365b1d8 100644 --- a/middlewares/validation.middleware.ts +++ b/middlewares/validation.middleware.ts @@ -1,5 +1,7 @@ +import { Request, Response, NextFunction } from 'express'; + //validation -const validateProduct = (req, res, next) => { +export const validateProduct = (req: Request, res: Response, next: NextFunction) => { const { name, description, price } = req.body; if (!name || !description || price == null) { return res.status(400).json({ message: '이름, 설명, 가격을 입력해야 합니다.'}); @@ -11,12 +13,10 @@ const validateProduct = (req, res, next) => { } //article validation -const validateArticle = (req, res, next) => { +export const validateArticle = (req: Request, res: Response, next: NextFunction) => { const { title, content } = req.body; if (!title || !content) { return res.status(400).json({ message: '제목, 내용을 입력해야 합니다.'}); } next(); } - -module.exports = { validateProduct, validateArticle }; \ No newline at end of file diff --git a/routes/articles.router.ts b/routes/articles.router.ts index 281d2209a..c83453f72 100644 --- a/routes/articles.router.ts +++ b/routes/articles.router.ts @@ -1,19 +1,24 @@ -//adricle router -const express = require('express'); -const router = express.Router(); -const prisma = require('../index.js'); // Import prisma from index.js -const { validateArticle } = require('../middlewares/validation.middleware.js'); -const authMiddleware = require('../middlewares/auth.middleware.js'); -const optionalAuthMiddleware = require('../middlewares/optionalAuth.middleware.js'); +import { Router, Request, Response, NextFunction } from 'express'; +import prisma from '../index'; // Import prisma from index.ts +import { validateArticle } from '../middlewares/validation.middleware'; +import authMiddleware from '../middlewares/auth.middleware'; +import optionalAuthMiddleware from '../middlewares/optionalAuth.middleware'; +import { Article as PrismaArticle, Prisma } from '@prisma/client'; + +const router = Router(); //article registration router .route('/articles') - .post(authMiddleware, validateArticle, async (req, res, next) => { + .post(authMiddleware, validateArticle, async (req: Request, res: Response, next: NextFunction) => { try { const { title, content } = req.body; const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const article = await prisma.article.create({ data: { title, content, @@ -26,14 +31,14 @@ router } }) // 게시글 목록 조회 - .get(optionalAuthMiddleware, async (req, res, next) => { + .get(optionalAuthMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { - const { sort, search } = req.query; - let page = parseInt(req.query.page) || 1; - let limit = parseInt(req.query.limit) || 10; + const { sort, search } = req.query as { sort?: string; search?: string }; + let page = parseInt(req.query.page as string) || 1; + let limit = parseInt(req.query.limit as string) || 10; let offset = (page - 1) * limit; - const where = search + const where: Prisma.ArticleWhereInput = search ? { OR: [ { title: { contains: search, mode: 'insensitive' }}, @@ -51,7 +56,7 @@ router take: limit, }); - let responseArticle = articles; + let responseArticles: (PrismaArticle & { isLiked?: boolean })[] = articles; if (user) { const articleIds = articles.map(article => article.id); @@ -84,7 +89,7 @@ router // article detail, modify, delete router .route('/articles/:articleId') - .get(optionalAuthMiddleware, async (req, res, next) => { + .get(optionalAuthMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { articleId } = req.params; const user = req.user; @@ -119,14 +124,22 @@ router } }) - .patch(authMiddleware, validateArticle, async (req, res, next) => { + .patch(authMiddleware, validateArticle, async (req: Request, res: Response, next: NextFunction) => { try { const { articleId } = req.params; const { title, content } = req.body; const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + // 게시글 소유 확인 const article = await prisma.article.findUnique({ where: { id: parseInt(articleId) } }); + if (!article || article.userId !== user.id) { + return res.status(403).json({ message: '게시글 수정 권한이 없습니다.' }); + } + const updatedArticle = await prisma.article.update({ where: { id: parseInt(articleId) }, data: { title, content }, @@ -136,11 +149,15 @@ router next(error); } }) - .delete(authMiddleware, async (req, res, next) => { + .delete(authMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { articleId } = req.params; const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const article = await prisma.article.findUnique({ where: { id: parseInt(articleId) } }); if (!article || article.userId !== user.id) { return res.status(403).json({ message: '게시글 삭제 권한이 없습니다.' }); @@ -154,12 +171,16 @@ router }); // article comment creation -router.post('/articles/:articleId/comments', authMiddleware, async (req, res, next) => { +router.post('/articles/:articleId/comments', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { articleId } = req.params; const { content } = req.body; const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.'}); const newComment = await prisma.comment.create({ @@ -176,11 +197,11 @@ router.post('/articles/:articleId/comments', authMiddleware, async (req, res, ne }); // article comments check -router.get('/articles/:articleId/comments', async (req, res, next) => { +router.get('/articles/:articleId/comments', async (req: Request, res: Response, next: NextFunction) => { try { const { articleId } = req.params; - let cursor = req.query.cursor ? parseInt(req.query.cursor): undefined; - let limit = parseInt(req.query.limit) || 10; + let cursor = req.query.cursor ? parseInt(req.query.cursor as string): undefined; + let limit = parseInt(req.query.limit as string) || 10; const comments = await prisma.comment.findMany({ where: { articleId: parseInt(articleId) }, @@ -197,12 +218,16 @@ router.get('/articles/:articleId/comments', async (req, res, next) => { }); //article comment modify -router.patch('/articles/comments/:commentId', authMiddleware, async (req, res, next) => { +router.patch('/articles/comments/:commentId', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { commentId } = req.params; const { content } = req.body; const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) return res.status(400).json({ message: '수정할 내용을 입력하세요.' }); const comment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) }}); @@ -222,11 +247,15 @@ router.patch('/articles/comments/:commentId', authMiddleware, async (req, res, n }); //article comment delete -router.delete('/articles/comments/:commentId', authMiddleware, async (req, res, next) => { +router.delete('/articles/comments/:commentId', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { commentId } = req.params; const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const comment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) } }); if (!comment || comment.userId !== user.id) { return res.status(403).json({ message: '댓글 삭제 권환이 없습니다.' }); @@ -240,11 +269,15 @@ router.delete('/articles/comments/:commentId', authMiddleware, async (req, res, }); // 게시글 좋아요 API -router.post('/:articleId/like', authMiddleware, async (req, res, next) => { +router.post('/:articleId/like', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { articleId } = req.params; const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + // 게시글 존재 확인 const article = await prisma.article.findUnique({ where: { id: parseInt(articleId) } }); if (!article) { @@ -280,4 +313,4 @@ router.post('/:articleId/like', authMiddleware, async (req, res, next) => { } }); -module.exports = router; \ No newline at end of file +export default router; \ No newline at end of file diff --git a/routes/products.router.ts b/routes/products.router.ts index 9b221a024..bfd69ab20 100644 --- a/routes/products.router.ts +++ b/routes/products.router.ts @@ -1,16 +1,22 @@ -// product router -const express = require('express'); -const router = express.Router(); -const prisma = require('../index.js'); // Import prisma from index.js -const { validationProduct, validateProduct } = require('../middlewares/validation.middleware.js'); -const authMiddleware = require('../middlewares/auth.middleware.js'); -const optionalAuthMiddleware = require('../middlewares/optionalAuth.middleware.js'); +import { Router, Request, Response, NextFunction } from 'express'; +import prisma from '../index'; // Import prisma from index.ts +import { validationProduct, validateProduct } from '../middlewares/validation.middleware'; +import authMiddleware from '../middlewares/auth.middleware'; +import optionalAuthMiddleware from '../middlewares/optionalAuth.middleware'; +import { Product as PrismaProduct, Prisma } from '@prisma/client'; + +const router = Router(); // registration router -router.post('/products', authMiddleware, validateProduct, async (req, res, next) => { +router.post('/products', authMiddleware, validateProduct, async (req: Request, res: Response, next: NextFunction) => { try { const { name, description, price } = req.body; const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const product = await prisma.product.create({ data: { name, @@ -26,14 +32,14 @@ router.post('/products', authMiddleware, validateProduct, async (req, res, next) }) // cherck router -router.get('/products', optionalAuthMiddleware, async (req, res, next) => { +router.get('/products', optionalAuthMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { - const { sort, search } = req.query; - let page = parseInt(req.query.page) || 1; - let limit = parseInt(req.query.limit) || 10; + const { sort, search } = req.query as { sort?: string; search?: string }; + let page = parseInt(req.query.page as string) || 1; + let limit = parseInt(req.query.limit as string) || 10; let offset = (page - 1) * limit; - const where = search + const where: Prisma.ProductWhereInput = search ? { OR: [ { name: { contains: search, mode: 'insensitive' }}, @@ -52,7 +58,7 @@ router.get('/products', optionalAuthMiddleware, async (req, res, next) => { take: limit, }); - let responseProducts = products; + let responseProducts: (PrismaProduct & { isLiked?: boolean })[] = products; if (user) { // 로그인한 사용자의 좋아요 누른 상품 목록 조회 @@ -90,7 +96,7 @@ router.get('/products', optionalAuthMiddleware, async (req, res, next) => { // datail, modify, delete router .route('/products/:productId') - .get(optionalAuthMiddleware, async (req, res, next) => { + .get(optionalAuthMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { productId } = req.params; const user = req.user; @@ -125,12 +131,16 @@ router }) - .patch(validateProduct, authMiddleware, async (req, res, next) => { + .patch(validateProduct, authMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { productId } = req.params; const { name, description, price } = req.body; const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + // 상품 소유자 확인 const product = await prisma.product.findUnique({ where: { id: parseInt(productId) } }); if (!product || product.userId !== user.id) { @@ -146,11 +156,15 @@ router next(error); } }) - .delete(authMiddleware, async (req, res, next) => { + .delete(authMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { productId } = req.params; const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const product = await prisma.product.findUnique({ where: { id: parseInt(productId) } }); if (!product || product.userId !== user.id) { return res.status(403).json({ message: '상품 삭제 권한이 없습니다.' }); @@ -164,12 +178,16 @@ router }); // comment -router.post('/products/:productId/comments', authMiddleware, async (req, res, next) => { +router.post('/products/:productId/comments', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { productId } = req.params; const { content } = req.body const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.' }); const newComment = await prisma.comment.create({ @@ -186,11 +204,11 @@ router.post('/products/:productId/comments', authMiddleware, async (req, res, n }); //comment check -router.get('/products/:productId/comments', async (req, res, next) => { +router.get('/products/:productId/comments', async (req: Request, res: Response, next: NextFunction) => { try { const { productId } = req.params; - let cursor = req.query.cursor ? parseInt(req.query.cursor): undefined; - let limit = parseInt(req.query.limit) || 10; + let cursor = req.query.cursor ? parseInt(req.query.cursor as string): undefined; + let limit = parseInt(req.query.limit as string) || 10; const comments = await prisma.comment.findMany({ where: { productId: parseInt(productId) }, @@ -207,12 +225,16 @@ router.get('/products/:productId/comments', async (req, res, next) => { }); // comment modify -router.patch('/products/comments/:commentId', authMiddleware, async (req, res, next) => { +router.patch('/products/comments/:commentId', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { commentId } = req.params; const { content } = req.body; const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) return res.status(400).json({ message: '수정할 내용을 입력해주세요.'}); const existingComment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) } }); @@ -232,11 +254,15 @@ router.patch('/products/comments/:commentId', authMiddleware, async (req, res, n }); // comment delete -router.delete('/products/comments/:commentId', authMiddleware, async (req, res, next) => { +router.delete('/products/comments/:commentId', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { commentId } = req.params; const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const existingComment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) } }); if (!existingComment || existingComment.userId !== user.id) { return res.status(403).json({ message: '댓글 삭제 권한이 없습니다.' }); @@ -250,11 +276,15 @@ router.delete('/products/comments/:commentId', authMiddleware, async (req, res, }); // 상품 좋아요 API -router.post('/:productId/like', authMiddleware, async (req, res, next) => { +router.post('/:productId/like', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { productId } = req.params; const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + // 상품 존재 확인 const product = await prisma.product.findUnique({ where: { id: parseInt(productId) } }); if (!product) { @@ -290,4 +320,4 @@ router.post('/:productId/like', authMiddleware, async (req, res, next) => { } }); -module.exports = router; \ No newline at end of file +export default router; \ No newline at end of file diff --git a/routes/upload.router.ts b/routes/upload.router.ts index 5b8b8c018..447efffeb 100644 --- a/routes/upload.router.ts +++ b/routes/upload.router.ts @@ -1,11 +1,10 @@ -//upload route -const express = require('express'); -const router = express.Router(); -const multer = require('multer'); -const path = require('path'); -const fs = require('fs'); -const sharp = require('sharp'); -const { DESTRUCTION } = require('dns'); +import { Router, Request, Response, NextFunction } from 'express'; +import multer from 'multer'; +import path from 'path'; +import fs from 'fs'; +import sharp from 'sharp'; + +const router = Router(); //uploads 디렉토리가 없을 때 생성 const uploadDir = 'uploads/'; @@ -19,7 +18,7 @@ const storage = multer.memoryStorage(); const upload = multer({ storage: storage }); //image api -router.post('/upload', upload.single('image'), async(req, res, next) => { +router.post('/upload', upload.single('image'), async(req: Request, res: Response, next: NextFunction) => { if (!req.file) { return res.status(400).json({ message: '이미지 파일이 필요합니다.'}); } @@ -40,4 +39,4 @@ router.post('/upload', upload.single('image'), async(req, res, next) => { } }); -module.exports = router; +export default router; \ No newline at end of file diff --git a/routes/users.router.ts b/routes/users.router.ts index 821b425ab..314543924 100644 --- a/routes/users.router.ts +++ b/routes/users.router.ts @@ -1,15 +1,15 @@ -const express = require('express'); -const UserService = require('../UserService.js'); -const authMiddleware = require('../middlewares/auth.middleware.js'); -const bcrypt = require('bcrypt'); -const prisma = require('../index.js'); // Import prisma from index.js - -const router = express.Router(); -// const prisma = new PrismaClient(); // Remove this line +import { Router, Request, Response, NextFunction } from 'express'; +import UserService from '../UserService'; +import authMiddleware from '../middlewares/auth.middleware'; +import bcrypt from 'bcrypt'; +import prisma from '../index'; // Import prisma from index.ts +import jwt from 'jsonwebtoken'; + +const router = Router(); const userService = new UserService(); // 회원가입 API -router.post('/sign-up', async (req, res, next) => { +router.post('/sign-up', async (req: Request, res: Response, next: NextFunction) => { try { const { email, nickname, password } = req.body; @@ -25,12 +25,15 @@ router.post('/sign-up', async (req, res, next) => { data: newUser, }); } catch (error) { - return res.status(409).json({ message: error.message }); + if (error instanceof Error) { + return res.status(409).json({ message: error.message }); + } + next(error); } }); // 로그인 API -router.post('/sign-in', async (req, res, next) => { +router.post('/sign-in', async (req: Request, res: Response, next: NextFunction) => { try { const { email, password } = req.body; @@ -51,15 +54,22 @@ router.post('/sign-in', async (req, res, next) => { data: { accessToken }, }); } catch (error) { - return res.status(401).json({ message: error.message }); + if (error instanceof Error) { + return res.status(401).json({ message: error.message }); + } + next(error); } }); // 내 정보 조회 API -router.get('/me', authMiddleware, async (req, res, next) => { +router.get('/me', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + res.status(200).json({ message: '내 정보 조회 성공', data: { @@ -77,17 +87,21 @@ router.get('/me', authMiddleware, async (req, res, next) => { }); // 내 정보 수정 API -router.patch('/me', authMiddleware, async (req, res, next) => { +router.patch('/me', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { nickname, image } = req.body; const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!nickname && !image) { return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); } // 수정할 사용자 정보 - const updatedData = { + const updatedData: { nickname?: string; image?: string } = { ...(nickname && { nickname }), ...(image && { image }), }; @@ -114,11 +128,15 @@ router.patch('/me', authMiddleware, async (req, res, next) => { }); // 비밀번호 변경 API -router.patch('/me/password', authMiddleware, async (req, res, next) => { +router.patch('/me/password', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { currentPassword, newPassword, confirmNewPassword } = req.body; const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!currentPassword || !newPassword || !confirmNewPassword) { return res.status(400).json({ message: '모든 정보를 입력해주세요.' }); } @@ -148,10 +166,14 @@ router.patch('/me/password', authMiddleware, async (req, res, next) => { }); // 내가 작성한 상품 목록 조회 API -router.get('/me/products', authMiddleware, async (req, res, next) => { +router.get('/me/products', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { try { const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const products = await prisma.product.findMany({ where: { userId: user.id }, orderBy: { createdAt: 'desc' }, @@ -166,4 +188,45 @@ router.get('/me/products', authMiddleware, async (req, res, next) => { } }); -module.exports = router; \ No newline at end of file +// Token 재발급 API +router.post('/token/refresh', async (req: Request, res: Response, next: NextFunction) => { + try { + const { refreshToken } = req.cookies; + if (!refreshToken) { + return res.status(401).json({ message: 'Refresh Token이 없습니다.' }); + } + + const decodedToken = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET_KEY as string) as { userId: number }; + const userId = decodedToken.userId; + + const user = await prisma.user.findUnique({ where: { id: userId } }); + if (!user) { + return res.status(401).json({ message: '사용자를 찾을 수 없습니다.' }); + } + + //DB에 저장된 hashed Refresh Token과 비교 + const isRefreshTokenMatched = await bcrypt.compare(refreshToken, user.refreshToken as string); + if (!isRefreshTokenMatched) { + return res.status(401).json({ message: 'Refresh Token이 유효하지 않습니다.' }); + } + + // 새로운 Access Token 생성 + const newAccessToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET_KEY as string, { + expiresIn: '12h', + }); + + return res.status(200).json({ + message: 'Access Token이 재발급되었습니다.', + data: { accessToken: newAccessToken }, + }); + } catch (error) { + if (error instanceof Error) { + if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') { + return res.status(401).json({ message: 'Refresh Token이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요 ' }); + } + } + next(error); + } +}); + +export default router; \ No newline at end of file diff --git a/types/express.d.ts b/types/express.d.ts new file mode 100644 index 000000000..54e9e7830 --- /dev/null +++ b/types/express.d.ts @@ -0,0 +1,9 @@ +import { User as PrismaUser } from '@prisma/client'; + +declare global { + namespace Express { + interface Request { + user?: PrismaUser; // Or a more specific subset of PrismaUser if only certain fields are always present + } + } +} \ No newline at end of file From ac533c882381e821382e0478a612be3865ec17ae Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 13:59:29 +0900 Subject: [PATCH 35/63] =?UTF-8?q?json=EC=88=98=EC=A0=95,=20ts-node?= =?UTF-8?q?=EC=84=A4=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 447 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 8 +- 2 files changed, 452 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f93290636..6480249e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,9 +30,24 @@ "@types/sharp": "^0.31.1", "nodemon": "^3.1.10", "prisma": "^6.16.1", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", "typescript": "^5.9.2" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@emnapi/runtime": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", @@ -461,6 +476,34 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@prisma/client": { "version": "6.16.1", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.16.1.tgz", @@ -553,6 +596,34 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", @@ -728,6 +799,20 @@ "@types/node": "*" } }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -741,6 +826,32 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -761,6 +872,13 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1126,6 +1244,13 @@ "node": ">= 0.10" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -1185,6 +1310,16 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -1211,6 +1346,16 @@ "node": ">= 0.4" } }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1421,6 +1566,13 @@ "node": ">= 0.8" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1500,6 +1652,28 @@ "giget": "dist/cli.mjs" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1603,6 +1777,18 @@ "dev": true, "license": "ISC" }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1637,6 +1823,22 @@ "node": ">=8" } }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1771,6 +1973,13 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2076,6 +2285,23 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -2392,6 +2618,41 @@ "node": ">=8.10.0" } }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -2625,6 +2886,27 @@ "node": ">=10" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2660,6 +2942,26 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -2673,6 +2975,19 @@ "node": ">=4" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tinyexec": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", @@ -2712,6 +3027,121 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -2782,6 +3212,13 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2805,6 +3242,16 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } } } } diff --git a/package.json b/package.json index b8b380ae8..78d7c2afb 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "seed": "node prisma/seed.js", - "start": "node index.js" + "build": "tsc", + "dev": "ts-node-dev --respawn --transpile-only index.ts", + "start": "node dist/index.js" }, "repository": { "type": "git", @@ -42,6 +42,8 @@ "@types/sharp": "^0.31.1", "nodemon": "^3.1.10", "prisma": "^6.16.1", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", "typescript": "^5.9.2" } } From b4e01d91bea7fe0036242cb55f422cbf2d59ab18 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 14:01:12 +0900 Subject: [PATCH 36/63] =?UTF-8?q?main.ts=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/ArticleService.js | 24 ++ dist/ProductService.js | 24 ++ dist/UserService.js | 57 ++++ dist/index.js | 37 +++ dist/main.js | 2 + dist/middlewares/auth.middleware.js | 53 ++++ dist/middlewares/optionalAuth.middleware.js | 34 +++ dist/middlewares/validation.middleware.js | 24 ++ dist/routes/articles.router.js | 288 +++++++++++++++++++ dist/routes/products.router.js | 293 ++++++++++++++++++++ dist/routes/upload.router.js | 39 +++ dist/routes/users.router.js | 207 ++++++++++++++ index.ts | 2 +- main.ts | 20 +- 14 files changed, 1085 insertions(+), 19 deletions(-) create mode 100644 dist/ArticleService.js create mode 100644 dist/ProductService.js create mode 100644 dist/UserService.js create mode 100644 dist/index.js create mode 100644 dist/main.js create mode 100644 dist/middlewares/auth.middleware.js create mode 100644 dist/middlewares/optionalAuth.middleware.js create mode 100644 dist/middlewares/validation.middleware.js create mode 100644 dist/routes/articles.router.js create mode 100644 dist/routes/products.router.js create mode 100644 dist/routes/upload.router.js create mode 100644 dist/routes/users.router.js diff --git a/dist/ArticleService.js b/dist/ArticleService.js new file mode 100644 index 000000000..47aca25a4 --- /dev/null +++ b/dist/ArticleService.js @@ -0,0 +1,24 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const index_1 = __importDefault(require("./index")); +class ArticleService { + async createArticle(data) { + return index_1.default.article.create({ data }); + } + async getArticleById(id) { + return index_1.default.article.findUnique({ where: { id } }); + } + async getArticles(options) { + return index_1.default.article.findMany(options); + } + async updateArticle(id, data) { + return index_1.default.article.update({ where: { id }, data }); + } + async deleteArticle(id) { + return index_1.default.article.delete({ where: { id } }); + } +} +exports.default = ArticleService; diff --git a/dist/ProductService.js b/dist/ProductService.js new file mode 100644 index 000000000..ffd9cdefe --- /dev/null +++ b/dist/ProductService.js @@ -0,0 +1,24 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const index_1 = __importDefault(require("./index")); +class ProductService { + async createProduct(data) { + return index_1.default.product.create({ data }); + } + async getProductById(id) { + return index_1.default.product.findUnique({ where: { id } }); + } + async getProducts(options) { + return index_1.default.product.findMany(options); + } + async updateProduct(id, data) { + return index_1.default.product.update({ where: { id }, data }); + } + async deleteProduct(id) { + return index_1.default.product.delete({ where: { id } }); + } +} +exports.default = ProductService; diff --git a/dist/UserService.js b/dist/UserService.js new file mode 100644 index 000000000..bae2f6987 --- /dev/null +++ b/dist/UserService.js @@ -0,0 +1,57 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const index_1 = __importDefault(require("./index")); +const bcrypt_1 = __importDefault(require("bcrypt")); +const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); +class UserService { + constructor() { + this.prisma = index_1.default; + // 회원가입 로직 + this.signUp = async (email, nickname, password) => { + // 이메일 중복 확인 + const existingUser = await this.prisma.user.findUnique({ where: { email } }); + if (existingUser) { + throw new Error('이미 사용중인 이메일입니다.'); + } + // 비밀번호 해싱 + const hashedPassword = await bcrypt_1.default.hash(password, 10); + // 유저 생성 + const user = await this.prisma.user.create({ + data: { + email, + nickname, + password: hashedPassword, + }, + }); + // 사용자 정보 반환 + const { password: _, refreshToken: __, ...userWithoutPassword } = user; + return userWithoutPassword; + }; + this.signIn = async (email, password) => { + // 이메일로 사용자 조회 + const user = await this.prisma.user.findUnique({ where: { email } }); + if (!user) { + throw new Error('존재하지 않는 이메일입니다.'); + } + // 비밀번호 확인 + const isPasswordMatched = await bcrypt_1.default.compare(password, user.password); + if (!isPasswordMatched) { + throw new Error('비밀번호가 일치하지 않습니다.'); + } + // Access Token 생성 (12시간) + const accessToken = jsonwebtoken_1.default.sign({ userId: user.id }, process.env.JWT_SECRET_KEY, { expiresIn: '12h' }); + // Refresh Token 생성 (7일) + const refreshToken = jsonwebtoken_1.default.sign({ userId: user.id }, process.env.REFRESH_TOKEN_SECRET_KEY, { expiresIn: '7d' }); + // Refresh Token을 해싱해서 DB에 저장 + await this.prisma.user.update({ + where: { id: user.id }, + data: { refreshToken: await bcrypt_1.default.hash(refreshToken, 10) }, + }); + return { accessToken, refreshToken }; + }; + } +} +exports.default = UserService; diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 000000000..e2b107d76 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,37 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const dotenv_1 = __importDefault(require("dotenv")); +dotenv_1.default.config(); +const express_1 = __importDefault(require("express")); +const cors_1 = __importDefault(require("cors")); +const path_1 = __importDefault(require("path")); +const client_1 = require("@prisma/client"); +const app = (0, express_1.default)(); +const PORT = process.env.PORT || 3000; +const prisma = new client_1.PrismaClient(); +// import router +const products_router_1 = __importDefault(require("./routes/products.router")); +const articles_router_1 = __importDefault(require("./routes/articles.router")); +const upload_router_1 = __importDefault(require("./routes/upload.router")); +const users_router_1 = __importDefault(require("./routes/users.router")); +// Middleware +app.use((0, cors_1.default)()); +app.use(express_1.default.json()); +app.use(express_1.default.urlencoded({ extended: true })); +app.use('/uploads', express_1.default.static(path_1.default.join(__dirname, 'uploads'))); +// route settitng +app.use('/api', [products_router_1.default, articles_router_1.default, upload_router_1.default, users_router_1.default]); +// Error Handler Middleware +app.use((err, req, res, next) => { + console.error(err.stack); + const statusCode = err.statusCode || 500; + const message = err.message || '오류가 발생했습니다.'; + res.status(statusCode).json({ message }); +}); +app.listen(PORT, () => { + console.log(`서버가 ${PORT}번에서 실행중입니다.`); +}); +exports.default = prisma; diff --git a/dist/main.js b/dist/main.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/dist/main.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/dist/middlewares/auth.middleware.js b/dist/middlewares/auth.middleware.js new file mode 100644 index 000000000..e8bae92ad --- /dev/null +++ b/dist/middlewares/auth.middleware.js @@ -0,0 +1,53 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); +const index_1 = __importDefault(require("../index")); // Import prisma from index.ts +const authMiddleware = async (req, res, next) => { + try { + // 헤더에서 authorization 값 가져오기 + const { authorization } = req.headers; + if (!authorization) { + return res.status(401).json({ message: '인증 정보가 없습니다.' }); + } + // authorization 값에서 토큰 추출하기 + const [tokenType, token] = authorization.split(' '); + if (tokenType !== 'Bearer') { + return res.status(401).json({ message: '지원하지 않는 인증 방식입니다.' }); + } + // 토큰이 없을 경우 + if (!token) { + return res.status(401).json({ message: '인증 정보가 없습니다.' }); + } + // 토큰 검증 (확인) + const decodedToken = jsonwebtoken_1.default.verify(token, process.env.JWT_SECRET_KEY); + const userId = decodedToken.userId; + // 토큰 있는 userId로 조회 + const user = await index_1.default.user.findUnique({ + where: { id: userId }, + }); + if (!user) { + return res.status(401).json({ message: '인증 정보가 유효하지 않습니다.' }); + } + // 사용자 정보 저장 + req.user = user; + next(); + } + catch (error) { + console.error('인증 미들웨어 에러:', error); + if (error instanceof Error) { + switch (error.name) { + case 'TokenExpiredError': + return res.status(401).json({ message: '인증 토큰이 만료되었습니다.' }); + case 'JsonWebTokenError': + return res.status(401).json({ message: '유효하지 않은 인증 토큰입니다.' }); + default: + return res.status(401).json({ message: '인증 정보가 유효하지 않습니다.' }); + } + } + next(error); + } +}; +exports.default = authMiddleware; diff --git a/dist/middlewares/optionalAuth.middleware.js b/dist/middlewares/optionalAuth.middleware.js new file mode 100644 index 000000000..0eeba9b2b --- /dev/null +++ b/dist/middlewares/optionalAuth.middleware.js @@ -0,0 +1,34 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); +const index_1 = __importDefault(require("../index")); // Import prisma from index.ts +// 선택적 인증 미들웨어 +// 인증에 성공하면 사용자 정보를 추가하고 실패해도 에러없이 다음 미들웨어 진행 +const optionalAuthMiddleware = async (req, res, next) => { + try { + const { authorization } = req.headers; + if (!authorization) { + return next(); + } + const [tokenType, token] = authorization.split(' '); + if (tokenType !== 'Bearer' || !token) { + return next(); + } + const decodedToken = jsonwebtoken_1.default.verify(token, process.env.JWT_SECRET_KEY); + const user = await index_1.default.user.findUnique({ + where: { id: decodedToken.userId }, + }); + if (user) { + req.user = user; + } + } + catch (error) { + // 에러 발생 시에도 다음 미들웨어로 진행 (선택적 인증이므로) + console.error('선택적 인증 미들웨어 에러:', error); + } + return next(); +}; +exports.default = optionalAuthMiddleware; diff --git a/dist/middlewares/validation.middleware.js b/dist/middlewares/validation.middleware.js new file mode 100644 index 000000000..cfbb8d93b --- /dev/null +++ b/dist/middlewares/validation.middleware.js @@ -0,0 +1,24 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validateArticle = exports.validateProduct = void 0; +//validation +const validateProduct = (req, res, next) => { + const { name, description, price } = req.body; + if (!name || !description || price == null) { + return res.status(400).json({ message: '이름, 설명, 가격을 입력해야 합니다.' }); + } + if (typeof price !== 'number' || price <= 0) { + return res.status(400).json({ message: '가격은 0보다 커야합니다.' }); + } + next(); +}; +exports.validateProduct = validateProduct; +//article validation +const validateArticle = (req, res, next) => { + const { title, content } = req.body; + if (!title || !content) { + return res.status(400).json({ message: '제목, 내용을 입력해야 합니다.' }); + } + next(); +}; +exports.validateArticle = validateArticle; diff --git a/dist/routes/articles.router.js b/dist/routes/articles.router.js new file mode 100644 index 000000000..4ad52e73a --- /dev/null +++ b/dist/routes/articles.router.js @@ -0,0 +1,288 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const index_1 = __importDefault(require("../index")); // Import prisma from index.ts +const validation_middleware_1 = require("../middlewares/validation.middleware"); +const auth_middleware_1 = __importDefault(require("../middlewares/auth.middleware")); +const optionalAuth_middleware_1 = __importDefault(require("../middlewares/optionalAuth.middleware")); +const router = (0, express_1.Router)(); +//article registration +router + .route('/articles') + .post(auth_middleware_1.default, validation_middleware_1.validateArticle, async (req, res, next) => { + try { + const { title, content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const article = await index_1.default.article.create({ data: { + title, + content, + userId: user.id, + } + }); + res.status(201).json(article); + } + catch (error) { + next(error); + } +}) + // 게시글 목록 조회 + .get(optionalAuth_middleware_1.default, async (req, res, next) => { + try { + const { sort, search } = req.query; + let page = parseInt(req.query.page) || 1; + let limit = parseInt(req.query.limit) || 10; + let offset = (page - 1) * limit; + const where = search + ? { + OR: [ + { title: { contains: search, mode: 'insensitive' } }, + { content: { contains: search, mode: 'insensitive' } }, + ], + } + : {}; + const user = req.user; + const articles = await index_1.default.article.findMany({ + where, + select: { id: true, title: true, content: true, createdAt: true, userId: true }, + orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, + skip: offset, + take: limit, + }); + let responseArticles = articles; + if (user) { + const articleIds = articles.map(article => article.id); + const likes = await index_1.default.like.findMany({ + where: { + userId: user.id, + articleId: { in: articleIds }, + }, + }); + const likedArticleIds = new Set(likes.map(like => like.articleId)); + responseArticles = articles.map(article => ({ + ...article, + isLiked: likedArticleIds.has(article.id), + })); + } + else { + responseArticles = articles.map(article => ({ + ...article, + isLiked: false, + })); + } + res.status(200).json(responseArticles); + } + catch (error) { + next(error); + } +}); +// article detail, modify, delete +router + .route('/articles/:articleId') + .get(optionalAuth_middleware_1.default, async (req, res, next) => { + try { + const { articleId } = req.params; + const user = req.user; + const article = await index_1.default.article.findUnique({ + where: { id: parseInt(articleId) }, + select: { id: true, title: true, content: true, createdAt: true, userId: true }, + }); + if (!article) { + return res.status(404).json({ message: '게시글을 찾을 수 없습니다.' }); + } + let isLiked = false; + if (user) { + const like = await index_1.default.like.findFirst({ + where: { + articleId: article.id, + userId: user.id, + }, + }); + if (like) { + isLiked = true; + } + } + const responseArticle = { ...article, isLiked }; + res.status(200).json(responseArticle); + } + catch (error) { + next(error); + } +}) + .patch(auth_middleware_1.default, validation_middleware_1.validateArticle, async (req, res, next) => { + try { + const { articleId } = req.params; + const { title, content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + // 게시글 소유 확인 + const article = await index_1.default.article.findUnique({ where: { id: parseInt(articleId) } }); + if (!article || article.userId !== user.id) { + return res.status(403).json({ message: '게시글 수정 권한이 없습니다.' }); + } + const updatedArticle = await index_1.default.article.update({ + where: { id: parseInt(articleId) }, + data: { title, content }, + }); + res.status(200).json(updatedArticle); + } + catch (error) { + next(error); + } +}) + .delete(auth_middleware_1.default, async (req, res, next) => { + try { + const { articleId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const article = await index_1.default.article.findUnique({ where: { id: parseInt(articleId) } }); + if (!article || article.userId !== user.id) { + return res.status(403).json({ message: '게시글 삭제 권한이 없습니다.' }); + } + await index_1.default.article.delete({ where: { id: parseInt(articleId) } }); + res.status(204).send(); + } + catch (error) { + next(error); + } +}); +// article comment creation +router.post('/articles/:articleId/comments', auth_middleware_1.default, async (req, res, next) => { + try { + const { articleId } = req.params; + const { content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) + return res.status(400).json({ message: '댓글을 입력해주세요.' }); + const newComment = await index_1.default.comment.create({ + data: { + content, + articleId: parseInt(articleId), + userId: user.id, + }, + }); + res.status(201).json(newComment); + } + catch (error) { + next(error); + } +}); +// article comments check +router.get('/articles/:articleId/comments', async (req, res, next) => { + try { + const { articleId } = req.params; + let cursor = req.query.cursor ? parseInt(req.query.cursor) : undefined; + let limit = parseInt(req.query.limit) || 10; + const comments = await index_1.default.comment.findMany({ + where: { articleId: parseInt(articleId) }, + select: { id: true, content: true, createdAt: true, userId: true }, + orderBy: { createdAt: 'desc' }, + cursor: cursor ? { id: cursor } : undefined, + take: limit, + skip: cursor ? 1 : 0, + }); + res.status(200).json(comments); + } + catch (error) { + next(error); + } +}); +//article comment modify +router.patch('/articles/comments/:commentId', auth_middleware_1.default, async (req, res, next) => { + try { + const { commentId } = req.params; + const { content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) + return res.status(400).json({ message: '수정할 내용을 입력하세요.' }); + const comment = await index_1.default.comment.findUnique({ where: { id: parseInt(commentId) } }); + if (!comment || comment.userId !== user.id) { + return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); + } + const updatedComment = await index_1.default.comment.update({ + where: { id: parseInt(commentId) }, + data: { content }, + }); + res.status(200).json(updatedComment); + } + catch (error) { + next(error); + } +}); +//article comment delete +router.delete('/articles/comments/:commentId', auth_middleware_1.default, async (req, res, next) => { + try { + const { commentId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const comment = await index_1.default.comment.findUnique({ where: { id: parseInt(commentId) } }); + if (!comment || comment.userId !== user.id) { + return res.status(403).json({ message: '댓글 삭제 권환이 없습니다.' }); + } + await index_1.default.comment.delete({ where: { id: parseInt(commentId) } }); + res.status(204).send(); + } + catch (error) { + next(error); + } +}); +// 게시글 좋아요 API +router.post('/:articleId/like', auth_middleware_1.default, async (req, res, next) => { + try { + const { articleId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + // 게시글 존재 확인 + const article = await index_1.default.article.findUnique({ where: { id: parseInt(articleId) } }); + if (!article) { + return res.status(404).json({ message: '게시글을 찾을 수 없습니다.' }); + } + // 기존 좋아요 확인 + const existingLike = await index_1.default.like.findFirst({ + where: { + userId: user.id, + articleId: parseInt(articleId), + }, + }); + if (existingLike) { + // 좋아요가 이미 존재하면 취소 + await index_1.default.like.delete({ + where: { id: existingLike.id }, + }); + res.status(200).json({ message: '게시글 좋아요를 취소했습니다.' }); + } + else { + // 좋아요가 없으면 좋아요 생성 + await index_1.default.like.create({ + data: { + userId: user.id, + articleId: parseInt(articleId), + }, + }); + res.status(201).json({ message: '게시글에 좋아요를 눌렀습니다.' }); + } + } + catch (error) { + next(error); + } +}); +exports.default = router; diff --git a/dist/routes/products.router.js b/dist/routes/products.router.js new file mode 100644 index 000000000..7889814af --- /dev/null +++ b/dist/routes/products.router.js @@ -0,0 +1,293 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const index_1 = __importDefault(require("../index")); // Import prisma from index.ts +const validation_middleware_1 = require("../middlewares/validation.middleware"); +const auth_middleware_1 = __importDefault(require("../middlewares/auth.middleware")); +const optionalAuth_middleware_1 = __importDefault(require("../middlewares/optionalAuth.middleware")); +const router = (0, express_1.Router)(); +// registration router +router.post('/products', auth_middleware_1.default, validation_middleware_1.validateProduct, async (req, res, next) => { + try { + const { name, description, price } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const product = await index_1.default.product.create({ + data: { + name, + description, + price, + userId: user.id, + }, + }); + res.status(201).json(product); + } + catch (error) { + next(error); + } +}); +// cherck router +router.get('/products', optionalAuth_middleware_1.default, async (req, res, next) => { + try { + const { sort, search } = req.query; + let page = parseInt(req.query.page) || 1; + let limit = parseInt(req.query.limit) || 10; + let offset = (page - 1) * limit; + const where = search + ? { + OR: [ + { name: { contains: search, mode: 'insensitive' } }, + { description: { contains: search, mode: 'insensitive' + } }, + ], + } + : {}; + const user = req.user; + const products = await index_1.default.product.findMany({ + where, + select: { id: true, name: true, price: true, createdAt: true, userId: true }, + orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, + skip: offset, + take: limit, + }); + let responseProducts = products; + if (user) { + // 로그인한 사용자의 좋아요 누른 상품 목록 조회 + const productIds = products.map(product => product.id); + const likes = await index_1.default.like.findMany({ + where: { + userId: user.id, + productId: { in: productIds }, + }, + }); + const likedProductIds = new Set(likes.map(like => like.productId)); + // 각 상품에 isLiked 필드 추가 + responseProducts = products.map(product => ({ + ...product, + isLiked: likedProductIds.has(product.id), + })); + } + else { + // 로그인 하지 않은 사용자의 경우 모든 isLiked = false + responseProducts = products.map(product => ({ + ...product, + isLiked: false, + })); + } + res.status(200).json(responseProducts); + } + catch (error) { + next(error); + } +}); +// datail, modify, delete +router + .route('/products/:productId') + .get(optionalAuth_middleware_1.default, async (req, res, next) => { + try { + const { productId } = req.params; + const user = req.user; + const product = await index_1.default.product.findUnique({ + where: { id: parseInt(productId) }, + select: { id: true, name: true, description: true, price: true, + createdAt: true, userId: true }, + }); + if (!product) + return res.status(404).json({ message: '상품을 찾을수 없습니다.' }); + let isLiked = false; + if (user) { + // 로그인한 경우 좋아요 여부 확인 + const like = await index_1.default.like.findFirst({ + where: { + productId: product.id, + userId: user.id, + }, + }); + if (like) { + isLiked = true; + } + } + const responseProduct = { ...product, isLiked }; + res.status(200).json(responseProduct); + } + catch (error) { + next(error); + } +}) + .patch(validation_middleware_1.validateProduct, auth_middleware_1.default, async (req, res, next) => { + try { + const { productId } = req.params; + const { name, description, price } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + // 상품 소유자 확인 + const product = await index_1.default.product.findUnique({ where: { id: parseInt(productId) } }); + if (!product || product.userId !== user.id) { + return res.status(403).json({ message: '상품 수정 권한이 없습니다.' }); + } + const updatedProduct = await index_1.default.product.update({ + where: { id: parseInt(productId) }, + data: { name, description, price }, + }); + res.status(200).json(updatedProduct); + } + catch (error) { + next(error); + } +}) + .delete(auth_middleware_1.default, async (req, res, next) => { + try { + const { productId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const product = await index_1.default.product.findUnique({ where: { id: parseInt(productId) } }); + if (!product || product.userId !== user.id) { + return res.status(403).json({ message: '상품 삭제 권한이 없습니다.' }); + } + await index_1.default.product.delete({ where: { id: parseInt(productId) } }); + res.status(204).send(); + } + catch (error) { + next(error); + } +}); +// comment +router.post('/products/:productId/comments', auth_middleware_1.default, async (req, res, next) => { + try { + const { productId } = req.params; + const { content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) + return res.status(400).json({ message: '댓글을 입력해주세요.' }); + const newComment = await index_1.default.comment.create({ + data: { + content, + productId: parseInt(productId), + userId: user.id, + }, + }); + res.status(201).json(newComment); + } + catch (error) { + next(error); + } +}); +//comment check +router.get('/products/:productId/comments', async (req, res, next) => { + try { + const { productId } = req.params; + let cursor = req.query.cursor ? parseInt(req.query.cursor) : undefined; + let limit = parseInt(req.query.limit) || 10; + const comments = await index_1.default.comment.findMany({ + where: { productId: parseInt(productId) }, + select: { id: true, content: true, createdAt: true, userId: true }, + orderBy: { createdAt: 'desc' }, + cursor: cursor ? { id: cursor } : undefined, + take: limit, + skip: cursor ? 1 : 0, + }); + res.status(200).json(comments); + } + catch (error) { + next(error); + } +}); +// comment modify +router.patch('/products/comments/:commentId', auth_middleware_1.default, async (req, res, next) => { + try { + const { commentId } = req.params; + const { content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) + return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); + const existingComment = await index_1.default.comment.findUnique({ where: { id: parseInt(commentId) } }); + if (!existingComment || existingComment.userId !== user.id) { + return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); + } + const updatedComment = await index_1.default.comment.update({ + where: { id: parseInt(commentId) }, + data: { content }, + }); + res.status(200).json(updatedComment); + } + catch (error) { + next(error); + } +}); +// comment delete +router.delete('/products/comments/:commentId', auth_middleware_1.default, async (req, res, next) => { + try { + const { commentId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const existingComment = await index_1.default.comment.findUnique({ where: { id: parseInt(commentId) } }); + if (!existingComment || existingComment.userId !== user.id) { + return res.status(403).json({ message: '댓글 삭제 권한이 없습니다.' }); + } + await index_1.default.comment.delete({ where: { id: parseInt(commentId) } }); + res.status(204).send(); + } + catch (error) { + next(error); + } +}); +// 상품 좋아요 API +router.post('/:productId/like', auth_middleware_1.default, async (req, res, next) => { + try { + const { productId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + // 상품 존재 확인 + const product = await index_1.default.product.findUnique({ where: { id: parseInt(productId) } }); + if (!product) { + return res.status(404).json({ message: '상품을 찾을 수 없습니다.' }); + } + // 기존 좋아요 확인 + const existingLike = await index_1.default.like.findFirst({ + where: { + userId: user.id, + productId: parseInt(productId), + }, + }); + if (existingLike) { + // 좋아요가 이미 있으면 좋아요 취소 + await index_1.default.like.delete({ + where: { id: existingLike.id }, + }); + res.status(200).json({ message: '상품 좋아요를 취소했습니다.' }); + } + else { + // 좋아요가 없으면 좋아요 생성 + await index_1.default.like.create({ + data: { + userId: user.id, + productId: parseInt(productId), + }, + }); + res.status(201).json({ message: '상품에 좋아요를 눌렀습니다.' }); + } + } + catch (error) { + next(error); + } +}); +exports.default = router; diff --git a/dist/routes/upload.router.js b/dist/routes/upload.router.js new file mode 100644 index 000000000..230d95cc7 --- /dev/null +++ b/dist/routes/upload.router.js @@ -0,0 +1,39 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const multer_1 = __importDefault(require("multer")); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); +const sharp_1 = __importDefault(require("sharp")); +const router = (0, express_1.Router)(); +//uploads 디렉토리가 없을 때 생성 +const uploadDir = 'uploads/'; +if (!fs_1.default.existsSync(uploadDir)) { + fs_1.default.mkdirSync(uploadDir); +} +const storage = multer_1.default.memoryStorage(); +//multer middleware +const upload = (0, multer_1.default)({ storage: storage }); +//image api +router.post('/upload', upload.single('image'), async (req, res, next) => { + if (!req.file) { + return res.status(400).json({ message: '이미지 파일이 필요합니다.' }); + } + try { + const ext = path_1.default.extname(req.file.originalname); + const filename = Date.now() + ext; + const imagePath = path_1.default.join(uploadDir, filename); + await (0, sharp_1.default)(req.file.buffer) + .resize({ width: 500 }) + .toFile(imagePath); + const imageUrl = `/uploads/${filename}`; + res.status(201).json({ imageUrl: imageUrl }); + } + catch (error) { + next(error); + } +}); +exports.default = router; diff --git a/dist/routes/users.router.js b/dist/routes/users.router.js new file mode 100644 index 000000000..667156b7f --- /dev/null +++ b/dist/routes/users.router.js @@ -0,0 +1,207 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const express_1 = require("express"); +const UserService_1 = __importDefault(require("../UserService")); +const auth_middleware_1 = __importDefault(require("../middlewares/auth.middleware")); +const bcrypt_1 = __importDefault(require("bcrypt")); +const index_1 = __importDefault(require("../index")); // Import prisma from index.ts +const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); +const router = (0, express_1.Router)(); +const userService = new UserService_1.default(); +// 회원가입 API +router.post('/sign-up', async (req, res, next) => { + try { + const { email, nickname, password } = req.body; + // 유효성 검사 + if (!email || !nickname || !password) { + return res.status(400).json({ message: '모든 정보를 입력해주세요' }); + } + const newUser = await userService.signUp(email, nickname, password); + return res.status(201).json({ + message: '회원가입이 완료되었습니다.', + data: newUser, + }); + } + catch (error) { + if (error instanceof Error) { + return res.status(409).json({ message: error.message }); + } + next(error); + } +}); +// 로그인 API +router.post('/sign-in', async (req, res, next) => { + try { + const { email, password } = req.body; + if (!email || !password) { + return res.status(400).json({ message: '이메일과 비밀번호를 모두 입력해주세요.' }); + } + const { accessToken, refreshToken } = await userService.signIn(email, password); + //refresh token을 쿠키에 설정 + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: false, + maxAge: 1000 * 60 * 60 * 24 * 7, + }); + return res.status(200).json({ + message: '로그인에 성공했습니다.', + data: { accessToken }, + }); + } + catch (error) { + if (error instanceof Error) { + return res.status(401).json({ message: error.message }); + } + next(error); + } +}); +// 내 정보 조회 API +router.get('/me', auth_middleware_1.default, async (req, res, next) => { + try { + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + res.status(200).json({ + message: '내 정보 조회 성공', + data: { + id: user.id, + email: user.email, + nickname: user.nickname, + image: user.image, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }, + }); + } + catch (error) { + next(error); + } +}); +// 내 정보 수정 API +router.patch('/me', auth_middleware_1.default, async (req, res, next) => { + try { + const { nickname, image } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!nickname && !image) { + return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); + } + // 수정할 사용자 정보 + const updatedData = { + ...(nickname && { nickname }), + ...(image && { image }), + }; + const updatedUser = await index_1.default.user.update({ + where: { id: user.id }, + data: updatedData, + }); + res.status(200).json({ + message: '내 정보 수정에 성공했습니다.', + data: { + id: updatedUser.id, + email: updatedUser.email, + nickname: updatedUser.nickname, + image: updatedUser.image, + createdAt: updatedUser.createdAt, + updatedAt: updatedUser.updatedAt, + }, + }); + } + catch (error) { + next(error); + } +}); +// 비밀번호 변경 API +router.patch('/me/password', auth_middleware_1.default, async (req, res, next) => { + try { + const { currentPassword, newPassword, confirmNewPassword } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!currentPassword || !newPassword || !confirmNewPassword) { + return res.status(400).json({ message: '모든 정보를 입력해주세요.' }); + } + if (newPassword !== confirmNewPassword) { + return res.status(400).json({ message: '새 비밀번호와 확인 비밀번호가 일치하지 않습니다.' }); + } + // 현재 비밀번호 확인 + const isPasswordMatched = await bcrypt_1.default.compare(currentPassword, user.password); + if (!isPasswordMatched) { + return res.status(401).json({ message: '현재 비밀번호가 일치하지 않습니다.' }); + } + // 새 비밀번호 해싱 + const hashedNewPassword = await bcrypt_1.default.hash(newPassword, 10); + await index_1.default.user.update({ + where: { id: user.id }, + data: { password: hashedNewPassword }, + }); + res.status(200).json({ message: '비밀번호 변경이 완료되었습니다.' }); + } + catch (error) { + next(error); + } +}); +// 내가 작성한 상품 목록 조회 API +router.get('/me/products', auth_middleware_1.default, async (req, res, next) => { + try { + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const products = await index_1.default.product.findMany({ + where: { userId: user.id }, + orderBy: { createdAt: 'desc' }, + }); + res.status(200).json({ + message: '내가 작성한 상품 목록 조회에 성공했습니다.', + data: products, + }); + } + catch (error) { + next(error); + } +}); +// Token 재발급 API +router.post('/token/refresh', async (req, res, next) => { + try { + const { refreshToken } = req.cookies; + if (!refreshToken) { + return res.status(401).json({ message: 'Refresh Token이 없습니다.' }); + } + const decodedToken = jsonwebtoken_1.default.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET_KEY); + const userId = decodedToken.userId; + const user = await index_1.default.user.findUnique({ where: { id: userId } }); + if (!user) { + return res.status(401).json({ message: '사용자를 찾을 수 없습니다.' }); + } + //DB에 저장된 hashed Refresh Token과 비교 + const isRefreshTokenMatched = await bcrypt_1.default.compare(refreshToken, user.refreshToken); + if (!isRefreshTokenMatched) { + return res.status(401).json({ message: 'Refresh Token이 유효하지 않습니다.' }); + } + // 새로운 Access Token 생성 + const newAccessToken = jsonwebtoken_1.default.sign({ userId: user.id }, process.env.JWT_SECRET_KEY, { + expiresIn: '12h', + }); + return res.status(200).json({ + message: 'Access Token이 재발급되었습니다.', + data: { accessToken: newAccessToken }, + }); + } + catch (error) { + if (error instanceof Error) { + if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') { + return res.status(401).json({ message: 'Refresh Token이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요 ' }); + } + } + next(error); + } +}); +exports.default = router; diff --git a/index.ts b/index.ts index 7660c0b4b..14f2bc222 100644 --- a/index.ts +++ b/index.ts @@ -2,7 +2,7 @@ import dotenv from 'dotenv'; dotenv.config(); import express, { Request, Response, NextFunction } from 'express'; import cors from 'cors'; -import path from 'path +import path from 'path'; import { PrismaClient } from '@prisma/client'; const app = express(); diff --git a/main.ts b/main.ts index 25ab1d0e1..1ac03b19d 100644 --- a/main.ts +++ b/main.ts @@ -1,18 +1,2 @@ -import { - Product, - ElectronicProduct, - getProductList, - getProduct, - createProduct, - patchProduct, - deleteProduct, -} from "./ProductService"; - -import { - Article, - getAarticleList, - getArticle, - createArticle, - patchArticle, - deleteArticle, -} from "./ArticleService"; +import ProductService from "./ProductService"; +import ArticleService from "./ArticleService"; From b6652067fb9536c9ce0cb0111d649084049d2fab Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 14:01:53 +0900 Subject: [PATCH 37/63] =?UTF-8?q?articels.router=20updateAt=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/articles.router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/articles.router.ts b/routes/articles.router.ts index c83453f72..f7d7b3985 100644 --- a/routes/articles.router.ts +++ b/routes/articles.router.ts @@ -50,7 +50,7 @@ router const user = req.user; const articles = await prisma.article.findMany({ where, - select: { id: true, title: true, content: true, createdAt: true, userId: true }, + select: { id: true, title: true, content: true, createdAt: true, userId: true, updatedAt: true }, orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, skip: offset, take: limit, @@ -96,7 +96,7 @@ router const article = await prisma.article.findUnique({ where: { id: parseInt(articleId) }, - select: { id: true, title: true, content: true, createdAt: true, userId: true }, + select: { id: true, title: true, content: true, createdAt: true, userId: true, updatedAt: true }, }); if (!article) { From 57dfa677a1b762f4ea0baf902cb96fc42c793742 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 14:02:29 +0900 Subject: [PATCH 38/63] =?UTF-8?q?products.router=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/products.router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/products.router.ts b/routes/products.router.ts index bfd69ab20..60e493afb 100644 --- a/routes/products.router.ts +++ b/routes/products.router.ts @@ -1,6 +1,6 @@ import { Router, Request, Response, NextFunction } from 'express'; import prisma from '../index'; // Import prisma from index.ts -import { validationProduct, validateProduct } from '../middlewares/validation.middleware'; +import { validateProduct } from '../middlewares/validation.middleware'; import authMiddleware from '../middlewares/auth.middleware'; import optionalAuthMiddleware from '../middlewares/optionalAuth.middleware'; import { Product as PrismaProduct, Prisma } from '@prisma/client'; From ae34280a5d2116bc30fa9a6f8dfa93ef04813735 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 15:42:03 +0900 Subject: [PATCH 39/63] =?UTF-8?q?select=EB=AC=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ProductService.ts | 6 ++++-- dist/routes/articles.router.js | 4 ++-- dist/routes/products.router.js | 10 +++++----- routes/products.router.ts | 14 +++++++------- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/ProductService.ts b/ProductService.ts index 8eb4c3a5f..5e51c67e0 100644 --- a/ProductService.ts +++ b/ProductService.ts @@ -3,14 +3,16 @@ import { Product as PrismaProduct, Prisma } from '@prisma/client'; interface ProductCreateInput { name: string; - content: string; + content: string; // Changed from description to content + price: number; status?: string; userId: number; } interface ProductUpdateInput { name?: string; - content?: string; + content?: string; // Changed from description to content + price?: number; status?: string; } diff --git a/dist/routes/articles.router.js b/dist/routes/articles.router.js index 4ad52e73a..bf19ccf20 100644 --- a/dist/routes/articles.router.js +++ b/dist/routes/articles.router.js @@ -49,7 +49,7 @@ router const user = req.user; const articles = await index_1.default.article.findMany({ where, - select: { id: true, title: true, content: true, createdAt: true, userId: true }, + select: { id: true, title: true, content: true, createdAt: true, userId: true, updatedAt: true }, orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, skip: offset, take: limit, @@ -90,7 +90,7 @@ router const user = req.user; const article = await index_1.default.article.findUnique({ where: { id: parseInt(articleId) }, - select: { id: true, title: true, content: true, createdAt: true, userId: true }, + select: { id: true, title: true, content: true, createdAt: true, userId: true, updatedAt: true }, }); if (!article) { return res.status(404).json({ message: '게시글을 찾을 수 없습니다.' }); diff --git a/dist/routes/products.router.js b/dist/routes/products.router.js index 7889814af..17dea7450 100644 --- a/dist/routes/products.router.js +++ b/dist/routes/products.router.js @@ -20,7 +20,7 @@ router.post('/products', auth_middleware_1.default, validation_middleware_1.vali const product = await index_1.default.product.create({ data: { name, - description, + content: description, // Map description to content price, userId: user.id, }, @@ -42,7 +42,7 @@ router.get('/products', optionalAuth_middleware_1.default, async (req, res, next ? { OR: [ { name: { contains: search, mode: 'insensitive' } }, - { description: { contains: search, mode: 'insensitive' + { content: { contains: search, mode: 'insensitive' } }, ], } @@ -50,7 +50,7 @@ router.get('/products', optionalAuth_middleware_1.default, async (req, res, next const user = req.user; const products = await index_1.default.product.findMany({ where, - select: { id: true, name: true, price: true, createdAt: true, userId: true }, + select: { id: true, name: true, price: true, createdAt: true, userId: true, updatedAt: true }, orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, skip: offset, take: limit, @@ -95,7 +95,7 @@ router const product = await index_1.default.product.findUnique({ where: { id: parseInt(productId) }, select: { id: true, name: true, description: true, price: true, - createdAt: true, userId: true }, + createdAt: true, userId: true, updatedAt: true }, }); if (!product) return res.status(404).json({ message: '상품을 찾을수 없습니다.' }); @@ -134,7 +134,7 @@ router } const updatedProduct = await index_1.default.product.update({ where: { id: parseInt(productId) }, - data: { name, description, price }, + data: { name, content: description, price }, }); res.status(200).json(updatedProduct); } diff --git a/routes/products.router.ts b/routes/products.router.ts index 60e493afb..53a406d99 100644 --- a/routes/products.router.ts +++ b/routes/products.router.ts @@ -20,10 +20,10 @@ router.post('/products', authMiddleware, validateProduct, async (req: Request, r const product = await prisma.product.create({ data: { name, - description, + content: description, // Map description to content price, userId: user.id, - }, + } as Prisma.ProductCreateInput, }); res.status(201).json(product); } catch(error) { @@ -43,7 +43,7 @@ router.get('/products', optionalAuthMiddleware, async (req: Request, res: Respon ? { OR: [ { name: { contains: search, mode: 'insensitive' }}, - { description: { contains: search, mode: 'insensitive' + { content: { contains: search, mode: 'insensitive' } }, ], } @@ -52,7 +52,7 @@ router.get('/products', optionalAuthMiddleware, async (req: Request, res: Respon const user = req.user; const products = await prisma.product.findMany({ where, - select: { id: true, name: true, price: true, createdAt: true, userId: true }, + select: { id: true, name: true, content: true, price: true, createdAt: true, userId: true, updatedAt: true }, orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, skip: offset, take: limit, @@ -103,8 +103,8 @@ router const product = await prisma.product.findUnique({ where: { id: parseInt(productId) }, - select: { id: true, name: true, description: true, price: true, - createdAt: true, userId: true }, + select: { id: true, name: true, content: true, price: true, + createdAt: true, userId: true, updatedAt: true }, }); if (!product) return res.status(404).json({ message: '상품을 찾을수 없습니다.'}); @@ -149,7 +149,7 @@ router const updatedProduct = await prisma.product.update({ where: { id: parseInt(productId) }, - data: { name, description, price }, + data: { name, content: description, price }, }); res.status(200).json(updatedProduct); } catch (error) { From d1bff8ffeeb2f545d7a689b2b8357b3a02e081f7 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 15:47:55 +0900 Subject: [PATCH 40/63] =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/routes/products.router.js | 5 ++--- routes/products.router.ts | 9 ++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/dist/routes/products.router.js b/dist/routes/products.router.js index 17dea7450..bdd3ff388 100644 --- a/dist/routes/products.router.js +++ b/dist/routes/products.router.js @@ -50,7 +50,7 @@ router.get('/products', optionalAuth_middleware_1.default, async (req, res, next const user = req.user; const products = await index_1.default.product.findMany({ where, - select: { id: true, name: true, price: true, createdAt: true, userId: true, updatedAt: true }, + select: { id: true, name: true, content: true, createdAt: true, userId: true, updatedAt: true, status: true }, orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, skip: offset, take: limit, @@ -94,8 +94,7 @@ router const user = req.user; const product = await index_1.default.product.findUnique({ where: { id: parseInt(productId) }, - select: { id: true, name: true, description: true, price: true, - createdAt: true, userId: true, updatedAt: true }, + select: { id: true, name: true, content: true, createdAt: true, userId: true, updatedAt: true, status: true }, }); if (!product) return res.status(404).json({ message: '상품을 찾을수 없습니다.' }); diff --git a/routes/products.router.ts b/routes/products.router.ts index 53a406d99..71259282c 100644 --- a/routes/products.router.ts +++ b/routes/products.router.ts @@ -23,7 +23,7 @@ router.post('/products', authMiddleware, validateProduct, async (req: Request, r content: description, // Map description to content price, userId: user.id, - } as Prisma.ProductCreateInput, + } as Prisma.ProductUncheckedCreateInput, }); res.status(201).json(product); } catch(error) { @@ -52,7 +52,7 @@ router.get('/products', optionalAuthMiddleware, async (req: Request, res: Respon const user = req.user; const products = await prisma.product.findMany({ where, - select: { id: true, name: true, content: true, price: true, createdAt: true, userId: true, updatedAt: true }, + select: { id: true, name: true, content: true, createdAt: true, userId: true, updatedAt: true, status: true }, orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, skip: offset, take: limit, @@ -103,8 +103,7 @@ router const product = await prisma.product.findUnique({ where: { id: parseInt(productId) }, - select: { id: true, name: true, content: true, price: true, - createdAt: true, userId: true, updatedAt: true }, + select: { id: true, name: true, content: true, createdAt: true, userId: true, updatedAt: true, status: true }, }); if (!product) return res.status(404).json({ message: '상품을 찾을수 없습니다.'}); @@ -149,7 +148,7 @@ router const updatedProduct = await prisma.product.update({ where: { id: parseInt(productId) }, - data: { name, content: description, price }, + data: { name, content: description, price } as Prisma.ProductUpdateInput, }); res.status(200).json(updatedProduct); } catch (error) { From 2032cc22d0a75e745fea571fb0fa92a74df017a7 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 15:50:38 +0900 Subject: [PATCH 41/63] =?UTF-8?q?src=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=ED=9B=84=20repository=20add?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +-- ArticleService.ts => src/ArticleService.ts | 0 ProductService.ts => src/ProductService.ts | 0 UserService.ts => src/UserService.ts | 3 ++ index.ts => src/index.ts | 0 main.ts => src/main.ts | 0 .../middlewares}/auth.middleware.ts | 0 .../middlewares}/optionalAuth.middleware.ts | 0 .../middlewares}/validation.middleware.ts | 0 src/repositories/ArticleRepository.ts | 26 ++++++++++++++ src/repositories/CommentRepository.ts | 26 ++++++++++++++ src/repositories/LikeRepository.ts | 34 +++++++++++++++++++ src/repositories/ProductRepository.ts | 26 ++++++++++++++ src/repositories/UserRepository.ts | 26 ++++++++++++++ {routes => src/routes}/articles.router.ts | 0 {routes => src/routes}/products.router.ts | 0 {routes => src/routes}/upload.router.ts | 0 {routes => src/routes}/users.router.ts | 0 {types => src/types}/express.d.ts | 0 tsconfig.json | 2 +- 20 files changed, 144 insertions(+), 3 deletions(-) rename ArticleService.ts => src/ArticleService.ts (100%) rename ProductService.ts => src/ProductService.ts (100%) rename UserService.ts => src/UserService.ts (93%) rename index.ts => src/index.ts (100%) rename main.ts => src/main.ts (100%) rename {middlewares => src/middlewares}/auth.middleware.ts (100%) rename {middlewares => src/middlewares}/optionalAuth.middleware.ts (100%) rename {middlewares => src/middlewares}/validation.middleware.ts (100%) create mode 100644 src/repositories/ArticleRepository.ts create mode 100644 src/repositories/CommentRepository.ts create mode 100644 src/repositories/LikeRepository.ts create mode 100644 src/repositories/ProductRepository.ts create mode 100644 src/repositories/UserRepository.ts rename {routes => src/routes}/articles.router.ts (100%) rename {routes => src/routes}/products.router.ts (100%) rename {routes => src/routes}/upload.router.ts (100%) rename {routes => src/routes}/users.router.ts (100%) rename {types => src/types}/express.d.ts (100%) diff --git a/package.json b/package.json index 78d7c2afb..c4e2cf21a 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "main": "index.js", "scripts": { "build": "tsc", - "dev": "ts-node-dev --respawn --transpile-only index.ts", - "start": "node dist/index.js" + "dev": "ts-node-dev --respawn --transpile-only src/index.ts", + "start": "node dist/src/index.js" }, "repository": { "type": "git", diff --git a/ArticleService.ts b/src/ArticleService.ts similarity index 100% rename from ArticleService.ts rename to src/ArticleService.ts diff --git a/ProductService.ts b/src/ProductService.ts similarity index 100% rename from ProductService.ts rename to src/ProductService.ts diff --git a/UserService.ts b/src/UserService.ts similarity index 93% rename from UserService.ts rename to src/UserService.ts index 49022c190..880a7b6b1 100644 --- a/UserService.ts +++ b/src/UserService.ts @@ -2,6 +2,9 @@ import prisma from './index'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import { User as PrismaUser } from '@prisma/client'; +import { Request, Response, NextFunction } from 'express'; +import authMiddleware from './middlewares/auth.middleware'; +import cookieParser from 'cookie-parser'; interface UserWithoutPassword extends Omit {} diff --git a/index.ts b/src/index.ts similarity index 100% rename from index.ts rename to src/index.ts diff --git a/main.ts b/src/main.ts similarity index 100% rename from main.ts rename to src/main.ts diff --git a/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts similarity index 100% rename from middlewares/auth.middleware.ts rename to src/middlewares/auth.middleware.ts diff --git a/middlewares/optionalAuth.middleware.ts b/src/middlewares/optionalAuth.middleware.ts similarity index 100% rename from middlewares/optionalAuth.middleware.ts rename to src/middlewares/optionalAuth.middleware.ts diff --git a/middlewares/validation.middleware.ts b/src/middlewares/validation.middleware.ts similarity index 100% rename from middlewares/validation.middleware.ts rename to src/middlewares/validation.middleware.ts diff --git a/src/repositories/ArticleRepository.ts b/src/repositories/ArticleRepository.ts new file mode 100644 index 000000000..c535f3f66 --- /dev/null +++ b/src/repositories/ArticleRepository.ts @@ -0,0 +1,26 @@ +import prisma from '../index'; +import { Article as PrismaArticle, Prisma } from '@prisma/client'; + +class ArticleRepository { + async findArticleById(id: number): Promise { + return prisma.article.findUnique({ where: { id } }); + } + + async findArticles(options?: { skip?: number; take?: number; where?: Prisma.ArticleWhereInput; orderBy?: Prisma.ArticleOrderByWithRelationInput }): Promise { + return prisma.article.findMany(options); + } + + async createArticle(data: Prisma.ArticleCreateInput): Promise { + return prisma.article.create({ data }); + } + + async updateArticle(id: number, data: Prisma.ArticleUpdateInput): Promise { + return prisma.article.update({ where: { id }, data }); + } + + async deleteArticle(id: number): Promise { + return prisma.article.delete({ where: { id } }); + } +} + +export default ArticleRepository; diff --git a/src/repositories/CommentRepository.ts b/src/repositories/CommentRepository.ts new file mode 100644 index 000000000..5ac9cf757 --- /dev/null +++ b/src/repositories/CommentRepository.ts @@ -0,0 +1,26 @@ +import prisma from '../index'; +import { Comment as PrismaComment, Prisma } from '@prisma/client'; + +class CommentRepository { + async findCommentById(id: number): Promise { + return prisma.comment.findUnique({ where: { id } }); + } + + async findComments(options?: { skip?: number; take?: number; where?: Prisma.CommentWhereInput; orderBy?: Prisma.CommentOrderByWithRelationInput }): Promise { + return prisma.comment.findMany(options); + } + + async createComment(data: Prisma.CommentCreateInput): Promise { + return prisma.comment.create({ data }); + } + + async updateComment(id: number, data: Prisma.CommentUpdateInput): Promise { + return prisma.comment.update({ where: { id }, data }); + } + + async deleteComment(id: number): Promise { + return prisma.comment.delete({ where: { id } }); + } +} + +export default CommentRepository; diff --git a/src/repositories/LikeRepository.ts b/src/repositories/LikeRepository.ts new file mode 100644 index 000000000..6698cee11 --- /dev/null +++ b/src/repositories/LikeRepository.ts @@ -0,0 +1,34 @@ +import prisma from '../index'; +import { Like as PrismaLike, Prisma } from '@prisma/client'; + +class LikeRepository { + async findLikeById(id: number): Promise { + return prisma.like.findUnique({ where: { id } }); + } + + async findLikes(options?: { skip?: number; take?: number; where?: Prisma.LikeWhereInput; orderBy?: Prisma.LikeOrderByWithRelationInput }): Promise { + return prisma.like.findMany(options); + } + + async createLike(data: Prisma.LikeCreateInput): Promise { + return prisma.like.create({ data }); + } + + async deleteLike(id: number): Promise { + return prisma.like.delete({ where: { id } }); + } + + async findLikeByUserIdAndProductId(userId: number, productId: number): Promise { + return prisma.like.findFirst({ + where: { userId, productId }, + }); + } + + async findLikeByUserIdAndArticleId(userId: number, articleId: number): Promise { + return prisma.like.findFirst({ + where: { userId, articleId }, + }); + } +} + +export default LikeRepository; diff --git a/src/repositories/ProductRepository.ts b/src/repositories/ProductRepository.ts new file mode 100644 index 000000000..e4b7bee7c --- /dev/null +++ b/src/repositories/ProductRepository.ts @@ -0,0 +1,26 @@ +import prisma from '../index'; +import { Product as PrismaProduct, Prisma } from '@prisma/client'; + +class ProductRepository { + async findProductById(id: number): Promise { + return prisma.product.findUnique({ where: { id } }); + } + + async findProducts(options?: { skip?: number; take?: number; where?: Prisma.ProductWhereInput; orderBy?: Prisma.ProductOrderByWithRelationInput }): Promise { + return prisma.product.findMany(options); + } + + async createProduct(data: Prisma.ProductCreateInput): Promise { + return prisma.product.create({ data }); + } + + async updateProduct(id: number, data: Prisma.ProductUpdateInput): Promise { + return prisma.product.update({ where: { id }, data }); + } + + async deleteProduct(id: number): Promise { + return prisma.product.delete({ where: { id } }); + } +} + +export default ProductRepository; diff --git a/src/repositories/UserRepository.ts b/src/repositories/UserRepository.ts new file mode 100644 index 000000000..f69b6eafb --- /dev/null +++ b/src/repositories/UserRepository.ts @@ -0,0 +1,26 @@ +import prisma from '../index'; +import { User as PrismaUser, Prisma } from '@prisma/client'; + +class UserRepository { + async findUserById(id: number): Promise { + return prisma.user.findUnique({ where: { id } }); + } + + async findUserByEmail(email: string): Promise { + return prisma.user.findUnique({ where: { email } }); + } + + async createUser(data: Prisma.UserCreateInput): Promise { + return prisma.user.create({ data }); + } + + async updateUser(id: number, data: Prisma.UserUpdateInput): Promise { + return prisma.user.update({ where: { id }, data }); + } + + async deleteUser(id: number): Promise { + return prisma.user.delete({ where: { id } }); + } +} + +export default UserRepository; diff --git a/routes/articles.router.ts b/src/routes/articles.router.ts similarity index 100% rename from routes/articles.router.ts rename to src/routes/articles.router.ts diff --git a/routes/products.router.ts b/src/routes/products.router.ts similarity index 100% rename from routes/products.router.ts rename to src/routes/products.router.ts diff --git a/routes/upload.router.ts b/src/routes/upload.router.ts similarity index 100% rename from routes/upload.router.ts rename to src/routes/upload.router.ts diff --git a/routes/users.router.ts b/src/routes/users.router.ts similarity index 100% rename from routes/users.router.ts rename to src/routes/users.router.ts diff --git a/types/express.d.ts b/src/types/express.d.ts similarity index 100% rename from types/express.d.ts rename to src/types/express.d.ts diff --git a/tsconfig.json b/tsconfig.json index b799bf95e..3ba669922 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "target": "es2018", "module": "commonjs", "outDir": "./dist", - "rootDir": "./", + "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, From 29c9305468ebff7cce99e6be63b3c9bd5f2ca75a Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 15:52:37 +0900 Subject: [PATCH 42/63] =?UTF-8?q?Service=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ArticleService.ts | 20 ++++++++++------ src/ProductService.ts | 24 ++++++++++++------- src/UserService.ts | 56 +++++++++++++++++++++++++++++-------------- 3 files changed, 66 insertions(+), 34 deletions(-) diff --git a/src/ArticleService.ts b/src/ArticleService.ts index 08c1939d4..2616fdab1 100644 --- a/src/ArticleService.ts +++ b/src/ArticleService.ts @@ -1,5 +1,5 @@ -import prisma from './index'; import { Article as PrismaArticle, Prisma } from '@prisma/client'; +import ArticleRepository from './repositories/ArticleRepository'; interface ArticleCreateInput { title: string; @@ -13,25 +13,31 @@ interface ArticleUpdateInput { } class ArticleService { + private articleRepository: ArticleRepository; + + constructor(articleRepository: ArticleRepository) { + this.articleRepository = articleRepository; + } + async createArticle(data: ArticleCreateInput): Promise { - return prisma.article.create({ data }); + return this.articleRepository.createArticle(data); } async getArticleById(id: number): Promise { - return prisma.article.findUnique({ where: { id } }); + return this.articleRepository.findArticleById(id); } async getArticles(options?: { skip?: number; take?: number; where?: Prisma.ArticleWhereInput; orderBy?: Prisma.ArticleOrderByWithRelationInput }): Promise { - return prisma.article.findMany(options); + return this.articleRepository.findArticles(options); } async updateArticle(id: number, data: ArticleUpdateInput): Promise { - return prisma.article.update({ where: { id }, data }); + return this.articleRepository.updateArticle(id, data); } async deleteArticle(id: number): Promise { - return prisma.article.delete({ where: { id } }); + return this.articleRepository.deleteArticle(id); } } -export default ArticleService; +export default ArticleService; \ No newline at end of file diff --git a/src/ProductService.ts b/src/ProductService.ts index 5e51c67e0..5e80f5a4f 100644 --- a/src/ProductService.ts +++ b/src/ProductService.ts @@ -1,9 +1,9 @@ -import prisma from './index'; import { Product as PrismaProduct, Prisma } from '@prisma/client'; +import ProductRepository from './repositories/ProductRepository'; interface ProductCreateInput { name: string; - content: string; // Changed from description to content + content: string; price: number; status?: string; userId: number; @@ -11,31 +11,37 @@ interface ProductCreateInput { interface ProductUpdateInput { name?: string; - content?: string; // Changed from description to content + content?: string; price?: number; status?: string; } class ProductService { + private productRepository: ProductRepository; + + constructor(productRepository: ProductRepository) { + this.productRepository = productRepository; + } + async createProduct(data: ProductCreateInput): Promise { - return prisma.product.create({ data }); + return this.productRepository.createProduct(data); } async getProductById(id: number): Promise { - return prisma.product.findUnique({ where: { id } }); + return this.productRepository.findProductById(id); } async getProducts(options?: { skip?: number; take?: number; where?: Prisma.ProductWhereInput; orderBy?: Prisma.ProductOrderByWithRelationInput }): Promise { - return prisma.product.findMany(options); + return this.productRepository.findProducts(options); } async updateProduct(id: number, data: ProductUpdateInput): Promise { - return prisma.product.update({ where: { id }, data }); + return this.productRepository.updateProduct(id, data); } async deleteProduct(id: number): Promise { - return prisma.product.delete({ where: { id } }); + return this.productRepository.deleteProduct(id); } } -export default ProductService; +export default ProductService; \ No newline at end of file diff --git a/src/UserService.ts b/src/UserService.ts index 880a7b6b1..8af506eee 100644 --- a/src/UserService.ts +++ b/src/UserService.ts @@ -1,21 +1,22 @@ -import prisma from './index'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; -import { User as PrismaUser } from '@prisma/client'; -import { Request, Response, NextFunction } from 'express'; -import authMiddleware from './middlewares/auth.middleware'; -import cookieParser from 'cookie-parser'; +import { User as PrismaUser, Prisma } from '@prisma/client'; +import UserRepository from './repositories/UserRepository'; interface UserWithoutPassword extends Omit {} class UserService { - private prisma = prisma; + private userRepository: UserRepository; + + constructor(userRepository: UserRepository) { + this.userRepository = userRepository; + } // 회원가입 로직 public signUp = async (email: string, nickname: string, password: string): Promise => { // 이메일 중복 확인 - const existingUser = await this.prisma.user.findUnique({ where: { email } }); + const existingUser = await this.userRepository.findUserByEmail(email); if (existingUser) { throw new Error('이미 사용중인 이메일입니다.'); } @@ -24,12 +25,10 @@ class UserService { const hashedPassword = await bcrypt.hash(password, 10); // 유저 생성 - const user = await this.prisma.user.create({ - data: { - email, - nickname, - password: hashedPassword, - }, + const user = await this.userRepository.createUser({ + email, + nickname, + password: hashedPassword, }); // 사용자 정보 반환 @@ -39,7 +38,7 @@ class UserService { public signIn = async (email: string, password: string): Promise<{ accessToken: string; refreshToken: string }> => { // 이메일로 사용자 조회 - const user = await this.prisma.user.findUnique({ where: { email } }); + const user = await this.userRepository.findUserByEmail(email); if (!user) { throw new Error('존재하지 않는 이메일입니다.'); } @@ -65,13 +64,34 @@ class UserService { ); // Refresh Token을 해싱해서 DB에 저장 - await this.prisma.user.update({ - where: { id: user.id }, - data: { refreshToken: await bcrypt.hash(refreshToken, 10) }, + await this.userRepository.updateUser(user.id, { + refreshToken: await bcrypt.hash(refreshToken, 10), }); return { accessToken, refreshToken }; }; + + public getUserById = async (id: number): Promise => { + return this.userRepository.findUserById(id); + }; + + public updateUser = async (id: number, data: Prisma.UserUpdateInput): Promise => { + return this.userRepository.updateUser(id, data); + }; + + public updatePassword = async (id: number, newPasswordHash: string): Promise => { + return this.userRepository.updateUser(id, { password: newPasswordHash }); + }; + + public getProductsByUserId = async (userId: number): Promise => { + // This method will need to be implemented in UserRepository or a new ProductRepository method + // For now, directly using prisma for product related queries from user service + const userWithProducts = await prisma.user.findUnique({ + where: { id: userId }, + include: { products: true }, + }); + return userWithProducts ? userWithProducts.products : null; + }; } -export default UserService; \ No newline at end of file +export default UserService; From 60f5d7fd6da79d3647b5cb7009331a2941d443a8 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 15:54:09 +0900 Subject: [PATCH 43/63] =?UTF-8?q?Controller=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ArticlesController.ts | 301 +++++++++++++++++++++++++ src/controllers/ProductsController.ts | 302 ++++++++++++++++++++++++++ src/controllers/UploadController.ts | 38 ++++ src/controllers/UsersController.ts | 217 ++++++++++++++++++ 4 files changed, 858 insertions(+) create mode 100644 src/controllers/ArticlesController.ts create mode 100644 src/controllers/ProductsController.ts create mode 100644 src/controllers/UploadController.ts create mode 100644 src/controllers/UsersController.ts diff --git a/src/controllers/ArticlesController.ts b/src/controllers/ArticlesController.ts new file mode 100644 index 000000000..ff99c4cb3 --- /dev/null +++ b/src/controllers/ArticlesController.ts @@ -0,0 +1,301 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import ArticleService from '../ArticleService'; +import { Article as PrismaArticle, Prisma } from '@prisma/client'; +import prisma from '../index'; + +class ArticlesController { + private articleService: ArticleService; + + constructor(articleService: ArticleService) { + this.articleService = articleService; + } + + createArticle = async (req: Request, res: Response, next: NextFunction) => { + try { + const { title, content } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const article = await this.articleService.createArticle({ + title, + content, + userId: user.id, + }); + res.status(201).json(article); + } catch (error) { + next(error); + } + }; + + getArticles = async (req: Request, res: Response, next: NextFunction) => { + try { + const { sort, search } = req.query as { sort?: string; search?: string }; + let page = parseInt(req.query.page as string) || 1; + let limit = parseInt(req.query.limit as string) || 10; + let offset = (page - 1) * limit; + + const where: Prisma.ArticleWhereInput = search + ? { + OR: [ + { title: { contains: search, mode: 'insensitive' }}, + { content: { contains: search, mode: 'insensitive' }}, + ], + } + : {}; + + const user = req.user; + const articles = await this.articleService.getArticles({ + where, + select: { id: true, title: true, content: true, createdAt: true, userId: true, updatedAt: true }, + orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, + skip: offset, + take: limit, + }); + + let responseArticles: (PrismaArticle & { isLiked?: boolean })[] = articles; + + if (user) { + const articleIds = articles.map(article => article.id); + const likes = await prisma.like.findMany({ + where: { + userId: user.id, + articleId: { in: articleIds }, + }, + }); + + const likedArticleIds = new Set(likes.map(like => like.articleId)); + + responseArticles = articles.map(article => ({ + ...article, + isLiked: likedArticleIds.has(article.id), + })); + } else { + responseArticles = articles.map(article => ({ + ...article, + isLiked: false, + })); + } + + res.status(200).json(responseArticles); + } catch (error) { + next(error); + } + }; + + getArticleById = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + const user = req.user; + + const article = await this.articleService.getArticleById(parseInt(articleId)); + + if (!article) { + return res.status(404).json({ message: '게시글을 찾을 수 없습니다.'}); + } + + let isLiked = false; + if (user) { + const like = await prisma.like.findFirst({ + where: { + articleId: article.id, + userId: user.id, + }, + }); + if (like) { + isLiked = true; + } + } + + const responseArticle = { ...article, isLiked }; + res.status(200).json(responseArticle); + + } catch (error) { + next(error); + } + }; + + updateArticle = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + const { title, content } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article || article.userId !== user.id) { + return res.status(403).json({ message: '게시글 수정 권한이 없습니다.' }); + } + + const updatedArticle = await this.articleService.updateArticle(parseInt(articleId), { + title, + content, + }); + res.status(200).json(updatedArticle); + } catch (error) { + next(error); + } + }; + + deleteArticle = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article || article.userId !== user.id) { + return res.status(403).json({ message: '게시글 삭제 권한이 없습니다.' }); + } + + await this.articleService.deleteArticle(parseInt(articleId)); + res.status(204).send(); + } catch (error) { + next(error); + } + }; + + createComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + const { content } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.'}); + + const newComment = await prisma.comment.create({ + data: { + content, + articleId: parseInt(articleId), + userId: user.id, + }, + }); + res.status(201).json(newComment); + } catch (error) { + next(error); + } + }; + + getComments = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + let cursor = req.query.cursor ? parseInt(req.query.cursor as string): undefined; + let limit = parseInt(req.query.limit as string) || 10; + + const comments = await prisma.comment.findMany({ + where: { articleId: parseInt(articleId) }, + select: { id: true, content: true, createdAt: true, userId: true }, + orderBy: { createdAt: 'desc' }, + cursor: cursor ? { id: cursor } : undefined, + take: limit, + skip: cursor ? 1 : 0, + }); + res.status(200).json(comments); + } catch (error) { + next(error); + } + }; + + updateComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { commentId } = req.params; + const { content } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!content) return res.status(400).json({ message: '수정할 내용을 입력하세요.' }); + + const comment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) }}); + if (!comment || comment.userId !== user.id) { + return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); + } + + const updatedComment = await prisma.comment.update + ({ + where: { id: parseInt(commentId) }, + data: { content }, + }); + res.status(200).json(updatedComment); + } catch (error) { + next(error) + } + }; + + deleteComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { commentId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const comment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) } }); + if (!comment || comment.userId !== user.id) { + return res.status(403).json({ message: '댓글 삭제 권환이 없습니다.' }); + } + + await prisma.comment.delete({ where: { id: parseInt(commentId) }}); + res.status(204).send(); + } catch (error) { + next(error); + } + }; + + toggleLike = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const article = await prisma.article.findUnique({ where: { id: parseInt(articleId) } }); + if (!article) { + return res.status(404).json({ message: '게시글을 찾을 수 없습니다.'}); + } + + const existingLike = await prisma.like.findFirst({ + where: { + userId: user.id, + articleId: parseInt(articleId), + }, + }); + + if (existingLike) { + await prisma.like.delete({ + where: { id: existingLike.id }, + }); + res.status(200).json({ message: '게시글 좋아요를 취소했습니다.' }); + } else { + await prisma.like.create({ + data: { + userId: user.id, + articleId: parseInt(articleId), + }, + }); + res.status(201).json({ message: '게시글에 좋아요를 눌렀습니다.' }); + } + } catch (error) { + next (error); + } + }; +} + +export default ArticlesController; diff --git a/src/controllers/ProductsController.ts b/src/controllers/ProductsController.ts new file mode 100644 index 000000000..5def517d4 --- /dev/null +++ b/src/controllers/ProductsController.ts @@ -0,0 +1,302 @@ +import { Request, Response, NextFunction } from 'express'; +import ProductService from '../ProductService'; +import { Product as PrismaProduct, Prisma } from '@prisma/client'; + +class ProductsController { + private productService: ProductService; + + constructor(productService: ProductService) { + this.productService = productService; + } + + createProduct = async (req: Request, res: Response, next: NextFunction) => { + try { + const { name, description, price } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const product = await this.productService.createProduct({ + name, + content: description, // Map description to content + price, + userId: user.id, + }); + res.status(201).json(product); + } catch(error) { + next(error); + } + }; + + getProducts = async (req: Request, res: Response, next: NextFunction) => { + try { + const { sort, search } = req.query as { sort?: string; search?: string }; + let page = parseInt(req.query.page as string) || 1; + let limit = parseInt(req.query.limit as string) || 10; + let offset = (page - 1) * limit; + + const where: Prisma.ProductWhereInput = search + ? { + OR: [ + { name: { contains: search, mode: 'insensitive' }}, + { content: { contains: search, mode: 'insensitive' + } }, + ], + } + : {}; + + const user = req.user; + const products = await this.productService.getProducts({ + where, + select: { id: true, name: true, content: true, price: true, createdAt: true, userId: true, updatedAt: true, status: true }, + orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, + skip: offset, + take: limit, + }); + + let responseProducts: (PrismaProduct & { isLiked?: boolean })[] = products; + + if (user) { + const productIds = products.map(product => product.id); + const likes = await prisma.like.findMany({ + where: { + userId: user.id, + productId: { in: productIds }, + }, + }); + + const likedProductIds = new Set(likes.map(like => like.productId)); + + responseProducts = products.map(product => ({ + ...product, + isLiked: likedProductIds.has(product.id), + })); + } else { + + responseProducts = products.map(product => ({ + ...product, + isLiked: false, + })); + } + + res.status(200).json(responseProducts); + + } catch (error) { + next(error); + } + }; + + getProductById = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + const user = req.user; + + const product = await this.productService.getProductById(parseInt(productId)); + + if (!product) return res.status(404).json({ message: '상품을 찾을수 없습니다.'}); + + let isLiked = false; + if (user) { + const like = await prisma.like.findFirst({ + where: { + productId: product.id, + userId: user.id, + }, + }); + if (like) { + isLiked = true; + } + } + + const responseProduct = { ...product, isLiked }; + res.status(200).json(responseProduct); + } catch (error) { + next(error); + } + }; + + updateProduct = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + const { name, description, price } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const product = await this.productService.getProductById(parseInt(productId)); + if (!product || product.userId !== user.id) { + return res.status(403).json({ message: '상품 수정 권한이 없습니다.' }); + } + + const updatedProduct = await this.productService.updateProduct(parseInt(productId), { + name, + content: description, + price, + }); + res.status(200).json(updatedProduct); + } catch (error) { + next(error); + } + }; + + deleteProduct = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const product = await this.productService.getProductById(parseInt(productId)); + if (!product || product.userId !== user.id) { + return res.status(403).json({ message: '상품 삭제 권한이 없습니다.' }); + } + + await this.productService.deleteProduct(parseInt(productId)); + res.status(204).send(); + } catch (error) { + next(error); + } + }; + + createComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + const { content } = req.body + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.' }); + + const newComment = await prisma.comment.create({ + data: { + content, + productId: parseInt(productId), + userId: user.id, + }, + }); + res.status(201).json(newComment); + } catch (error) { + next(error); + } + }; + + getComments = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + let cursor = req.query.cursor ? parseInt(req.query.cursor as string): undefined; + let limit = parseInt(req.query.limit as string) || 10; + + const comments = await prisma.comment.findMany({ + where: { productId: parseInt(productId) }, + select: { id: true, content: true, createdAt: true, userId: true }, + orderBy: { createdAt: 'desc' }, + cursor: cursor ? { id: cursor } : undefined, + take: limit, + skip: cursor ? 1 : 0, + }); + res.status(200).json(comments); + } catch (error) { + next(error); + } + }; + + updateComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { commentId } = req.params; + const { content } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!content) return res.status(400).json({ message: '수정할 내용을 입력해주세요.'}); + + const existingComment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) } }); + if (!existingComment || existingComment.userId !== user.id) { + return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); + } + + const updatedComment = await prisma.comment.update +({ + where: { id: parseInt(commentId) }, + data: { content }, + }); + res.status(200).json(updatedComment); + } catch (error) { + next(error); + } + }; + + deleteComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { commentId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const existingComment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) } }); + if (!existingComment || existingComment.userId !== user.id) { + return res.status(403).json({ message: '댓글 삭제 권한이 없습니다.' }); + } + + await prisma.comment.delete({ where: { id: parseInt(commentId) }}); + res.status(204).send(); + } catch(error) { + next(error); + } + }; + + toggleLike = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const product = await prisma.product.findUnique({ where: { id: parseInt(productId) } }); + if (!product) { + return res.status(404).json({ message: '상품을 찾을 수 없습니다.'}); + } + + const existingLike = await prisma.like.findFirst({ + where: { + userId: user.id, + productId: parseInt(productId), + }, + }); + + if (existingLike) { + await prisma.like.delete({ + where: { id: existingLike.id }, + }); + res.status(200).json({ message: '상품 좋아요를 취소했습니다.' }); + } else { + await prisma.like.create({ + data: { + userId: user.id, + productId: parseInt(productId), + }, + }); + res.status(201).json({ message: '상품에 좋아요를 눌렀습니다.' }); + } + } catch (error) { + next(error); + } + }; +} + +export default ProductsController; diff --git a/src/controllers/UploadController.ts b/src/controllers/UploadController.ts new file mode 100644 index 000000000..1db01657b --- /dev/null +++ b/src/controllers/UploadController.ts @@ -0,0 +1,38 @@ +import { Request, Response, NextFunction } from 'express'; +import multer from 'multer'; +import path from 'path'; +import fs from 'fs'; +import sharp from 'sharp'; + +const uploadDir = 'uploads/'; +if (!fs.existsSync(uploadDir)){ + fs.mkdirSync(uploadDir); +} + +const storage = multer.memoryStorage(); +const upload = multer({ storage: storage }); + +class UploadController { + uploadImage = async (req: Request, res: Response, next: NextFunction) => { + if (!req.file) { + return res.status(400).json({ message: '이미지 파일이 필요합니다.'}); + } + + try { + const ext = path.extname(req.file.originalname); + const filename = Date.now() + ext; + const imagePath = path.join(uploadDir, filename); + + await sharp(req.file.buffer) + .resize({ width: 500 }) + .toFile(imagePath); + + const imageUrl = `/uploads/${filename}`; + res.status(201).json({ imageUrl: imageUrl }); + } catch (error) { + next(error); + } + }; +} + +export default UploadController; diff --git a/src/controllers/UsersController.ts b/src/controllers/UsersController.ts new file mode 100644 index 000000000..4195beb85 --- /dev/null +++ b/src/controllers/UsersController.ts @@ -0,0 +1,217 @@ +import { Request, Response, NextFunction } from 'express'; +import UserService from '../UserService'; +import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; +import prisma from '../index'; + +class UsersController { + private userService: UserService; + + constructor(userService: UserService) { + this.userService = userService; + } + + signUp = async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, nickname, password } = req.body; + + if (!email || !nickname || !password) { + return res.status(400).json({ message: '모든 정보를 입력해주세요'}); + } + + const newUser = await this.userService.signUp(email, nickname, password); + + return res.status(201).json({ + message: '회원가입이 완료되었습니다.', + data: newUser, + }); + } catch (error) { + if (error instanceof Error) { + return res.status(409).json({ message: error.message }); + } + next(error); + } + }; + + signIn = async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ message: '이메일과 비밀번호를 모두 입력해주세요.'}); + } + const { accessToken, refreshToken } = await this.userService.signIn(email, password); + + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: false, + maxAge: 1000 * 60 * 60 * 24 * 7, + }); + + return res.status(200).json({ + message: '로그인에 성공했습니다.', + data: { accessToken }, + }); + } catch (error) { + if (error instanceof Error) { + return res.status(401).json({ message: error.message }); + } + next(error); + } + }; + + getMe = async (req: Request, res: Response, next: NextFunction) => { + try { + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + res.status(200).json({ + message: '내 정보 조회 성공', + data: { + id: user.id, + email: user.email, + nickname: user.nickname, + image: user.image, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }, + }); + } catch (error) { + next(error); + } + }; + + updateMe = async (req: Request, res: Response, next: NextFunction) => { + try { + const { nickname, image } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!nickname && !image) { + return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); + } + + const updatedData: { nickname?: string; image?: string } = { + ...(nickname && { nickname }), + ...(image && { image }), + }; + + const updatedUser = await this.userService.updateUser(user.id, updatedData); + + res.status(200).json({ + message: '내 정보 수정에 성공했습니다.', + data: { + id: updatedUser.id, + email: updatedUser.email, + nickname: updatedUser.nickname, + image: updatedUser.image, + createdAt: updatedUser.createdAt, + updatedAt: updatedUser.updatedAt, + }, + }); + } catch (error) { + next(error); + } + }; + + changePassword = async (req: Request, res: Response, next: NextFunction) => { + try { + const { currentPassword, newPassword, confirmNewPassword } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!currentPassword || !newPassword || !confirmNewPassword) { + return res.status(400).json({ message: '모든 정보를 입력해주세요.' }); + } + if (newPassword !== confirmNewPassword) { + return res.status(400).json({ message: '새 비밀번호와 확인 비밀번호가 일치하지 않습니다.' }); + } + + const existingUser = await this.userService.getUserById(user.id); + if (!existingUser) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const isPasswordMatched = await bcrypt.compare(currentPassword, existingUser.password); + if (!isPasswordMatched) { + return res.status(401).json({ message: '현재 비밀번호가 일치하지 않습니다.' }); + } + + const hashedNewPassword = await bcrypt.hash(newPassword, 10); + + await this.userService.updatePassword(user.id, hashedNewPassword); + + res.status(200).json({ message: '비밀번호 변경이 완료되었습니다.' }); + } catch (error) { + next(error); + } + }; + + getMyProducts = async (req: Request, res: Response, next: NextFunction) => { + try { + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const products = await this.userService.getProductsByUserId(user.id); + + res.status(200).json({ + message: '내가 작성한 상품 목록 조회에 성공했습니다.', + data: products, + }); + } catch (error) { + next(error); + } + }; + + refreshToken = async (req: Request, res: Response, next: NextFunction) => { + try { + const { refreshToken } = req.cookies; + if (!refreshToken) { + return res.status(401).json({ message: 'Refresh Token이 없습니다.' }); + } + + const decodedToken = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET_KEY as string) as { userId: number }; + const userId = decodedToken.userId; + + const user = await this.userService.getUserById(userId); + if (!user) { + return res.status(401).json({ message: '사용자를 찾을 수 없습니다.' }); + } + + const isRefreshTokenMatched = await bcrypt.compare(refreshToken, user.refreshToken as string); + if (!isRefreshToken.Matched) { + return res.status(401).json({ message: 'Refresh Token이 유효하지 않습니다.' }); + } + + const newAccessToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET_KEY as string, { + expiresIn: '12h', + }); + + return res.status(200).json({ + message: 'Access Token이 재발급되었습니다.', + data: { accessToken: newAccessToken }, + }); + } catch (error) { + if (error instanceof Error) { + if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') { + return res.status(401).json({ message: 'Refresh Token이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요 ' }); + } + } + next(error); + } + }; +} + +export default UsersController; From b29cb20d858ed75a69650c271c017b289e5cd73c Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 15:55:57 +0900 Subject: [PATCH 44/63] =?UTF-8?q?roter=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/articles.router.ts | 313 +++------------------------------ src/routes/products.router.ts | 320 +++------------------------------- src/routes/upload.router.ts | 30 +--- src/routes/users.router.ts | 229 ++---------------------- 4 files changed, 64 insertions(+), 828 deletions(-) diff --git a/src/routes/articles.router.ts b/src/routes/articles.router.ts index f7d7b3985..598015af3 100644 --- a/src/routes/articles.router.ts +++ b/src/routes/articles.router.ts @@ -1,316 +1,45 @@ -import { Router, Request, Response, NextFunction } from 'express'; -import prisma from '../index'; // Import prisma from index.ts -import { validateArticle } from '../middlewares/validation.middleware'; +import { Router } from 'express'; +import ArticlesController from '../controllers/ArticlesController'; +import ArticleService from '../ArticleService'; +import ArticleRepository from '../repositories/ArticleRepository'; import authMiddleware from '../middlewares/auth.middleware'; import optionalAuthMiddleware from '../middlewares/optionalAuth.middleware'; -import { Article as PrismaArticle, Prisma } from '@prisma/client'; +import { validateArticle } from '../middlewares/validation.middleware'; const router = Router(); +// Initialize repositories and services +const articleRepository = new ArticleRepository(); +const articleService = new ArticleService(articleRepository); +const articlesController = new ArticlesController(articleService); + //article registration router .route('/articles') - .post(authMiddleware, validateArticle, async (req: Request, res: Response, next: NextFunction) => { - try { - const { title, content } = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const article = await prisma.article.create({ data: { - title, - content, - userId: user.id, - } - }); - res.status(201).json(article); - } catch (error) { - next(error); - } -}) + .post(authMiddleware, validateArticle, articlesController.createArticle) // 게시글 목록 조회 - .get(optionalAuthMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { sort, search } = req.query as { sort?: string; search?: string }; - let page = parseInt(req.query.page as string) || 1; - let limit = parseInt(req.query.limit as string) || 10; - let offset = (page - 1) * limit; - - const where: Prisma.ArticleWhereInput = search - ? { - OR: [ - { title: { contains: search, mode: 'insensitive' }}, - { content: { contains: search, mode: 'insensitive' }}, - ], - } - : {}; - - const user = req.user; - const articles = await prisma.article.findMany({ - where, - select: { id: true, title: true, content: true, createdAt: true, userId: true, updatedAt: true }, - orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, - skip: offset, - take: limit, - }); - - let responseArticles: (PrismaArticle & { isLiked?: boolean })[] = articles; - - if (user) { - const articleIds = articles.map(article => article.id); - const likes = await prisma.like.findMany({ - where: { - userId: user.id, - articleId: { in: articleIds }, - }, - }); - - const likedArticleIds = new Set(likes.map(like => like.articleId)); - - responseArticles = articles.map(article => ({ - ...article, - isLiked: likedArticleIds.has(article.id), - })); - } else { - responseArticles = articles.map(article => ({ - ...article, - isLiked: false, - })); - } - - res.status(200).json(responseArticles); - } catch (error) { - next(error); - } -}); + .get(optionalAuthMiddleware, articlesController.getArticles); // article detail, modify, delete router .route('/articles/:articleId') - .get(optionalAuthMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { articleId } = req.params; - const user = req.user; - - const article = await prisma.article.findUnique({ - where: { id: parseInt(articleId) }, - select: { id: true, title: true, content: true, createdAt: true, userId: true, updatedAt: true }, - }); - - if (!article) { - return res.status(404).json({ message: '게시글을 찾을 수 없습니다.'}); - } - - let isLiked = false; - if (user) { - const like = await prisma.like.findFirst({ - where: { - articleId: article.id, - userId: user.id, - }, - }); - if (like) { - isLiked = true; - } - } - - const responseArticle = { ...article, isLiked }; - res.status(200).json(responseArticle); - - } catch (error) { - next(error); - } -}) - - .patch(authMiddleware, validateArticle, async (req: Request, res: Response, next: NextFunction) => { - try { - const { articleId } = req.params; - const { title, content } = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - // 게시글 소유 확인 - const article = await prisma.article.findUnique({ where: { id: parseInt(articleId) } }); - if (!article || article.userId !== user.id) { - return res.status(403).json({ message: '게시글 수정 권한이 없습니다.' }); - } - - const updatedArticle = await prisma.article.update({ - where: { id: parseInt(articleId) }, - data: { title, content }, - }); - res.status(200).json(updatedArticle); - } catch (error) { - next(error); - } -}) - .delete(authMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { articleId } = req.params; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const article = await prisma.article.findUnique({ where: { id: parseInt(articleId) } }); - if (!article || article.userId !== user.id) { - return res.status(403).json({ message: '게시글 삭제 권한이 없습니다.' }); - } - - await prisma.article.delete({ where: { id: parseInt(articleId) }}); - res.status(204).send(); - } catch (error) { - next(error); - } - }); + .get(optionalAuthMiddleware, articlesController.getArticleById) + .patch(authMiddleware, validateArticle, articlesController.updateArticle) + .delete(authMiddleware, articlesController.deleteArticle); // article comment creation -router.post('/articles/:articleId/comments', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { articleId } = req.params; - const { content } = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.'}); - - const newComment = await prisma.comment.create({ - data: { - content, - articleId: parseInt(articleId), - userId: user.id, - }, - }); - res.status(201).json(newComment); - } catch (error) { - next(error); - } -}); +router.post('/articles/:articleId/comments', authMiddleware, articlesController.createComment); // article comments check -router.get('/articles/:articleId/comments', async (req: Request, res: Response, next: NextFunction) => { - try { - const { articleId } = req.params; - let cursor = req.query.cursor ? parseInt(req.query.cursor as string): undefined; - let limit = parseInt(req.query.limit as string) || 10; - - const comments = await prisma.comment.findMany({ - where: { articleId: parseInt(articleId) }, - select: { id: true, content: true, createdAt: true, userId: true }, - orderBy: { createdAt: 'desc' }, - cursor: cursor ? { id: cursor } : undefined, - take: limit, - skip: cursor ? 1 : 0, - }); - res.status(200).json(comments); - } catch(error) { - next(error); - } -}); +router.get('/articles/:articleId/comments', articlesController.getComments); //article comment modify -router.patch('/articles/comments/:commentId', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { commentId } = req.params; - const { content } = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - if (!content) return res.status(400).json({ message: '수정할 내용을 입력하세요.' }); - - const comment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) }}); - if (!comment || comment.userId !== user.id) { - return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); - } - - const updatedComment = await prisma.comment.update - ({ - where: { id: parseInt(commentId) }, - data: { content }, - }); - res.status(200).json(updatedComment); - } catch (error) { - next(error) - } -}); +router.patch('/articles/comments/:commentId', authMiddleware, articlesController.updateComment); //article comment delete -router.delete('/articles/comments/:commentId', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { commentId } = req.params; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const comment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) } }); - if (!comment || comment.userId !== user.id) { - return res.status(403).json({ message: '댓글 삭제 권환이 없습니다.' }); - } - - await prisma.comment.delete({ where: { id: parseInt(commentId) }}); - res.status(204).send(); - } catch (error) { - next(error); - } -}); +router.delete('/articles/comments/:commentId', authMiddleware, articlesController.deleteComment); // 게시글 좋아요 API -router.post('/:articleId/like', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { articleId } = req.params; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - // 게시글 존재 확인 - const article = await prisma.article.findUnique({ where: { id: parseInt(articleId) } }); - if (!article) { - return res.status(404).json({ message: '게시글을 찾을 수 없습니다.'}); - } - - // 기존 좋아요 확인 - const existingLike = await prisma.like.findFirst({ - where: { - userId: user.id, - articleId: parseInt(articleId), - }, - }); - - if (existingLike) { - // 좋아요가 이미 존재하면 취소 - await prisma.like.delete({ - where: { id: existingLike.id }, - }); - res.status(200).json({ message: '게시글 좋아요를 취소했습니다.' }); - } else { - // 좋아요가 없으면 좋아요 생성 - await prisma.like.create({ - data: { - userId: user.id, - articleId: parseInt(articleId), - }, - }); - res.status(201).json({ message: '게시글에 좋아요를 눌렀습니다.' }); - } - } catch (error) { - next (error); - } -}); +router.post('/:articleId/like', authMiddleware, articlesController.toggleLike); -export default router; \ No newline at end of file +export default router; diff --git a/src/routes/products.router.ts b/src/routes/products.router.ts index 71259282c..6f1f69df3 100644 --- a/src/routes/products.router.ts +++ b/src/routes/products.router.ts @@ -1,322 +1,44 @@ -import { Router, Request, Response, NextFunction } from 'express'; -import prisma from '../index'; // Import prisma from index.ts -import { validateProduct } from '../middlewares/validation.middleware'; +import { Router } from 'express'; +import ProductsController from '../controllers/ProductsController'; +import ProductService from '../ProductService'; +import ProductRepository from '../repositories/ProductRepository'; import authMiddleware from '../middlewares/auth.middleware'; import optionalAuthMiddleware from '../middlewares/optionalAuth.middleware'; -import { Product as PrismaProduct, Prisma } from '@prisma/client'; +import { validateProduct } from '../middlewares/validation.middleware'; const router = Router(); -// registration router -router.post('/products', authMiddleware, validateProduct, async (req: Request, res: Response, next: NextFunction) => { - try { - const { name, description, price } = req.body; - const { user } = req; +// Initialize repositories and services +const productRepository = new ProductRepository(); +const productService = new ProductService(productRepository); +const productsController = new ProductsController(productService); - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const product = await prisma.product.create({ - data: { - name, - content: description, // Map description to content - price, - userId: user.id, - } as Prisma.ProductUncheckedCreateInput, - }); - res.status(201).json(product); - } catch(error) { - next(error); - } -}) +// registration router +router.post('/products', authMiddleware, validateProduct, productsController.createProduct); // cherck router -router.get('/products', optionalAuthMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { sort, search } = req.query as { sort?: string; search?: string }; - let page = parseInt(req.query.page as string) || 1; - let limit = parseInt(req.query.limit as string) || 10; - let offset = (page - 1) * limit; - - const where: Prisma.ProductWhereInput = search - ? { - OR: [ - { name: { contains: search, mode: 'insensitive' }}, - { content: { contains: search, mode: 'insensitive' - } }, - ], - } - : {}; - - const user = req.user; - const products = await prisma.product.findMany({ - where, - select: { id: true, name: true, content: true, createdAt: true, userId: true, updatedAt: true, status: true }, - orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, - skip: offset, - take: limit, - }); - - let responseProducts: (PrismaProduct & { isLiked?: boolean })[] = products; - - if (user) { - // 로그인한 사용자의 좋아요 누른 상품 목록 조회 - const productIds = products.map(product => product.id); - const likes = await prisma.like.findMany({ - where: { - userId: user.id, - productId: { in: productIds }, - }, - }); - - const likedProductIds = new Set(likes.map(like => like.productId)); - - // 각 상품에 isLiked 필드 추가 - responseProducts = products.map(product => ({ - ...product, - isLiked: likedProductIds.has(product.id), - })); - } else { - - // 로그인 하지 않은 사용자의 경우 모든 isLiked = false - responseProducts = products.map(product => ({ - ...product, - isLiked: false, - })); - } - - res.status(200).json(responseProducts); - - } catch (error) { - next(error); - } -}); +router.get('/products', optionalAuthMiddleware, productsController.getProducts); // datail, modify, delete router .route('/products/:productId') - .get(optionalAuthMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { productId } = req.params; - const user = req.user; - - const product = await prisma.product.findUnique({ - where: { id: parseInt(productId) }, - select: { id: true, name: true, content: true, createdAt: true, userId: true, updatedAt: true, status: true }, - }); - - if (!product) return res.status(404).json({ message: '상품을 찾을수 없습니다.'}); - - let isLiked = false; - if (user) { - // 로그인한 경우 좋아요 여부 확인 - const like = await prisma.like.findFirst({ - where: { - productId: product.id, - userId: user.id, - }, - }); - if (like) { - isLiked = true; - } - } - - const responseProduct = { ...product, isLiked }; - res.status(200).json(responseProduct); - } catch (error) { - next(error); - } - }) - - - .patch(validateProduct, authMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { productId } = req.params; - const { name, description, price } = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - // 상품 소유자 확인 - const product = await prisma.product.findUnique({ where: { id: parseInt(productId) } }); - if (!product || product.userId !== user.id) { - return res.status(403).json({ message: '상품 수정 권한이 없습니다.' }); - } - - const updatedProduct = await prisma.product.update({ - where: { id: parseInt(productId) }, - data: { name, content: description, price } as Prisma.ProductUpdateInput, - }); - res.status(200).json(updatedProduct); - } catch (error) { - next(error); - } - }) - .delete(authMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { productId } = req.params; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const product = await prisma.product.findUnique({ where: { id: parseInt(productId) } }); - if (!product || product.userId !== user.id) { - return res.status(403).json({ message: '상품 삭제 권한이 없습니다.' }); - } - - await prisma.product.delete({ where: { id: parseInt(productId) }}); - res.status(204).send(); - } catch (error) { - next(error); - } - }); + .get(optionalAuthMiddleware, productsController.getProductById) + .patch(validateProduct, authMiddleware, productsController.updateProduct) + .delete(authMiddleware, productsController.deleteProduct); // comment -router.post('/products/:productId/comments', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { productId } = req.params; - const { content } = req.body - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.' }); - - const newComment = await prisma.comment.create({ - data: { - content, - productId: parseInt(productId), - userId: user.id, - }, - }); - res.status(201).json(newComment); - } catch (error) { - next(error); - } -}); +router.post('/products/:productId/comments', authMiddleware, productsController.createComment); //comment check -router.get('/products/:productId/comments', async (req: Request, res: Response, next: NextFunction) => { - try { - const { productId } = req.params; - let cursor = req.query.cursor ? parseInt(req.query.cursor as string): undefined; - let limit = parseInt(req.query.limit as string) || 10; - - const comments = await prisma.comment.findMany({ - where: { productId: parseInt(productId) }, - select: { id: true, content: true, createdAt: true, userId: true }, - orderBy: { createdAt: 'desc' }, - cursor: cursor ? { id: cursor } : undefined, - take: limit, - skip: cursor ? 1 : 0, - }); - res.status(200).json(comments); - } catch (error) { - next(error); - } -}); +router.get('/products/:productId/comments', productsController.getComments); // comment modify -router.patch('/products/comments/:commentId', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { commentId } = req.params; - const { content } = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - if (!content) return res.status(400).json({ message: '수정할 내용을 입력해주세요.'}); - - const existingComment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) } }); - if (!existingComment || existingComment.userId !== user.id) { - return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); - } - - const updatedComment = await prisma.comment.update -({ - where: { id: parseInt(commentId) }, - data: { content }, - }); - res.status(200).json(updatedComment); - } catch (error) { - next(error); - } -}); +router.patch('/products/comments/:commentId', authMiddleware, productsController.updateComment); // comment delete -router.delete('/products/comments/:commentId', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { commentId } = req.params; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const existingComment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) } }); - if (!existingComment || existingComment.userId !== user.id) { - return res.status(403).json({ message: '댓글 삭제 권한이 없습니다.' }); - } - - await prisma.comment.delete({ where: { id: parseInt(commentId) }}); - res.status(204).send(); - } catch(error) { - next(error); - } -}); +router.delete('/products/comments/:commentId', authMiddleware, productsController.deleteComment); // 상품 좋아요 API -router.post('/:productId/like', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { productId } = req.params; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - // 상품 존재 확인 - const product = await prisma.product.findUnique({ where: { id: parseInt(productId) } }); - if (!product) { - return res.status(404).json({ message: '상품을 찾을 수 없습니다.'}); - } - - // 기존 좋아요 확인 - const existingLike = await prisma.like.findFirst({ - where: { - userId: user.id, - productId: parseInt(productId), - }, - }); - - if (existingLike) { - // 좋아요가 이미 있으면 좋아요 취소 - await prisma.like.delete({ - where: { id: existingLike.id }, - }); - res.status(200).json({ message: '상품 좋아요를 취소했습니다.' }); - } else { - // 좋아요가 없으면 좋아요 생성 - await prisma.like.create({ - data: { - userId: user.id, - productId: parseInt(productId), - }, - }); - res.status(201).json({ message: '상품에 좋아요를 눌렀습니다.' }); - } - } catch (error) { - next(error); - } - }); +router.post('/:productId/like', authMiddleware, productsController.toggleLike); -export default router; \ No newline at end of file +export default router; diff --git a/src/routes/upload.router.ts b/src/routes/upload.router.ts index 447efffeb..a1b01367a 100644 --- a/src/routes/upload.router.ts +++ b/src/routes/upload.router.ts @@ -1,4 +1,5 @@ -import { Router, Request, Response, NextFunction } from 'express'; +import { Router } from 'express'; +import UploadController from '../controllers/UploadController'; import multer from 'multer'; import path from 'path'; import fs from 'fs'; @@ -13,30 +14,11 @@ if (!fs.existsSync(uploadDir)){ } const storage = multer.memoryStorage(); - -//multer middleware const upload = multer({ storage: storage }); -//image api -router.post('/upload', upload.single('image'), async(req: Request, res: Response, next: NextFunction) => { - if (!req.file) { - return res.status(400).json({ message: '이미지 파일이 필요합니다.'}); - } - - try { - const ext = path.extname(req.file.originalname); - const filename = Date.now() + ext; - const imagePath = path.join(uploadDir, filename); +const uploadController = new UploadController(); - await sharp(req.file.buffer) - .resize({ width: 500 }) - .toFile(imagePath); - - const imageUrl = `/uploads/${filename}`; - res.status(201).json({ imageUrl: imageUrl }); - } catch (error) { - next(error); - } -}); +//image api +router.post('/upload', upload.single('image'), uploadController.uploadImage); -export default router; \ No newline at end of file +export default router; diff --git a/src/routes/users.router.ts b/src/routes/users.router.ts index 314543924..cf2f54f96 100644 --- a/src/routes/users.router.ts +++ b/src/routes/users.router.ts @@ -1,232 +1,35 @@ -import { Router, Request, Response, NextFunction } from 'express'; +import { Router } from 'express'; +import UsersController from '../controllers/UsersController'; import UserService from '../UserService'; +import UserRepository from '../repositories/UserRepository'; import authMiddleware from '../middlewares/auth.middleware'; -import bcrypt from 'bcrypt'; -import prisma from '../index'; // Import prisma from index.ts -import jwt from 'jsonwebtoken'; const router = Router(); -const userService = new UserService(); -// 회원가입 API -router.post('/sign-up', async (req: Request, res: Response, next: NextFunction) => { - try { - const { email, nickname, password } = req.body; - - // 유효성 검사 - if (!email || !nickname || !password) { - return res.status(400).json({ message: '모든 정보를 입력해주세요'}); - } +// Initialize repositories and services +const userRepository = new UserRepository(); +const userService = new UserService(userRepository); +const usersController = new UsersController(userService); - const newUser = await userService.signUp(email, nickname, password); - - return res.status(201).json({ - message: '회원가입이 완료되었습니다.', - data: newUser, - }); - } catch (error) { - if (error instanceof Error) { - return res.status(409).json({ message: error.message }); - } - next(error); - } -}); +// 회원가입 API +router.post('/sign-up', usersController.signUp); // 로그인 API -router.post('/sign-in', async (req: Request, res: Response, next: NextFunction) => { - try { - const { email, password } = req.body; - - if (!email || !password) { - return res.status(400).json({ message: '이메일과 비밀번호를 모두 입력해주세요.'}); - } - const { accessToken, refreshToken } = await userService.signIn(email, password); - - //refresh token을 쿠키에 설정 - res.cookie('refreshToken', refreshToken, { - httpOnly: true, - secure: false, - maxAge: 1000 * 60 * 60 * 24 * 7, - }); - - return res.status(200).json({ - message: '로그인에 성공했습니다.', - data: { accessToken }, - }); - } catch (error) { - if (error instanceof Error) { - return res.status(401).json({ message: error.message }); - } - next(error); - } -}); +router.post('/sign-in', usersController.signIn); // 내 정보 조회 API -router.get('/me', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - res.status(200).json({ - message: '내 정보 조회 성공', - data: { - id: user.id, - email: user.email, - nickname: user.nickname, - image: user.image, - createdAt: user.createdAt, - updatedAt: user.updatedAt, - }, - }); - } catch (error) { - next(error); - } -}); +router.get('/me', authMiddleware, usersController.getMe); // 내 정보 수정 API -router.patch('/me', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { nickname, image } = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - if (!nickname && !image) { - return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); - } - - // 수정할 사용자 정보 - const updatedData: { nickname?: string; image?: string } = { - ...(nickname && { nickname }), - ...(image && { image }), - }; - - const updatedUser = await prisma.user.update({ - where: { id: user.id }, - data: updatedData, - }); - - res.status(200).json({ - message: '내 정보 수정에 성공했습니다.', - data: { - id: updatedUser.id, - email: updatedUser.email, - nickname: updatedUser.nickname, - image: updatedUser.image, - createdAt: updatedUser.createdAt, - updatedAt: updatedUser.updatedAt, - }, - }); - } catch (error) { - next(error); - } -}); +router.patch('/me', authMiddleware, usersController.updateMe); // 비밀번호 변경 API -router.patch('/me/password', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { currentPassword, newPassword, confirmNewPassword } = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - if (!currentPassword || !newPassword || !confirmNewPassword) { - return res.status(400).json({ message: '모든 정보를 입력해주세요.' }); - } - if (newPassword !== confirmNewPassword) { - return res.status(400).json({ message: '새 비밀번호와 확인 비밀번호가 일치하지 않습니다.' }); - } - - - // 현재 비밀번호 확인 - const isPasswordMatched = await bcrypt.compare(currentPassword, user.password); - if (!isPasswordMatched) { - return res.status(401).json({ message: '현재 비밀번호가 일치하지 않습니다.' }); - } - - // 새 비밀번호 해싱 - const hashedNewPassword = await bcrypt.hash(newPassword, 10); - - await prisma.user.update({ - where: { id: user.id }, - data: { password: hashedNewPassword }, - }); - - res.status(200).json({ message: '비밀번호 변경이 완료되었습니다.' }); - } catch (error) { - next(error); - } -}); +router.patch('/me/password', authMiddleware, usersController.changePassword); // 내가 작성한 상품 목록 조회 API -router.get('/me/products', authMiddleware, async (req: Request, res: Response, next: NextFunction) => { - try { - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const products = await prisma.product.findMany({ - where: { userId: user.id }, - orderBy: { createdAt: 'desc' }, - }); - - res.status(200).json({ - message: '내가 작성한 상품 목록 조회에 성공했습니다.', - data: products, - }); - } catch (error) { - next(error); - } -}); +router.get('/me/products', authMiddleware, usersController.getMyProducts); // Token 재발급 API -router.post('/token/refresh', async (req: Request, res: Response, next: NextFunction) => { - try { - const { refreshToken } = req.cookies; - if (!refreshToken) { - return res.status(401).json({ message: 'Refresh Token이 없습니다.' }); - } - - const decodedToken = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET_KEY as string) as { userId: number }; - const userId = decodedToken.userId; - - const user = await prisma.user.findUnique({ where: { id: userId } }); - if (!user) { - return res.status(401).json({ message: '사용자를 찾을 수 없습니다.' }); - } - - //DB에 저장된 hashed Refresh Token과 비교 - const isRefreshTokenMatched = await bcrypt.compare(refreshToken, user.refreshToken as string); - if (!isRefreshTokenMatched) { - return res.status(401).json({ message: 'Refresh Token이 유효하지 않습니다.' }); - } - - // 새로운 Access Token 생성 - const newAccessToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET_KEY as string, { - expiresIn: '12h', - }); - - return res.status(200).json({ - message: 'Access Token이 재발급되었습니다.', - data: { accessToken: newAccessToken }, - }); - } catch (error) { - if (error instanceof Error) { - if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') { - return res.status(401).json({ message: 'Refresh Token이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요 ' }); - } - } - next(error); - } -}); +router.post('/token/refresh', usersController.refreshToken); -export default router; \ No newline at end of file +export default router; From fa869622a98f68829c6e6041bac44549c87396c5 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 15:56:38 +0900 Subject: [PATCH 45/63] =?UTF-8?q?DTO=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/dtos/ProductDto.ts | 11 +++++++++++ src/dtos/UserDto.ts | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/dtos/ProductDto.ts create mode 100644 src/dtos/UserDto.ts diff --git a/src/dtos/ProductDto.ts b/src/dtos/ProductDto.ts new file mode 100644 index 000000000..576e1e38a --- /dev/null +++ b/src/dtos/ProductDto.ts @@ -0,0 +1,11 @@ +export interface ProductCreateDto { + name: string; + description: string; + price: number; +} + +export interface ProductUpdateDto { + name?: string; + description?: string; + price?: number; +} diff --git a/src/dtos/UserDto.ts b/src/dtos/UserDto.ts new file mode 100644 index 000000000..9ff995aef --- /dev/null +++ b/src/dtos/UserDto.ts @@ -0,0 +1,21 @@ +export interface UserCreateDto { + email: string; + nickname: string; + password: string; +} + +export interface UserSignInDto { + email: string; + password: string; +} + +export interface UserUpdateDto { + nickname?: string; + image?: string; +} + +export interface UserChangePasswordDto { + currentPassword: string; + newPassword: string; + confirmNewPassword: string; +} From b7f019bb2f2412ed0f0eea5a44223ff5a907464e Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:00:10 +0900 Subject: [PATCH 46/63] =?UTF-8?q?dto=20=EC=B6=94=EA=B0=80,=20=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ArticlesController.ts | 7 ++++--- src/controllers/ProductsController.ts | 6 ++++-- src/controllers/UsersController.ts | 13 +++++++------ src/dtos/ArticleDto.ts | 9 +++++++++ 4 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 src/dtos/ArticleDto.ts diff --git a/src/controllers/ArticlesController.ts b/src/controllers/ArticlesController.ts index ff99c4cb3..488c86839 100644 --- a/src/controllers/ArticlesController.ts +++ b/src/controllers/ArticlesController.ts @@ -2,6 +2,7 @@ import { Router, Request, Response, NextFunction } from 'express'; import ArticleService from '../ArticleService'; import { Article as PrismaArticle, Prisma } from '@prisma/client'; import prisma from '../index'; +import { ArticleCreateDto, ArticleUpdateDto } from '../dtos/ArticleDto'; class ArticlesController { private articleService: ArticleService; @@ -12,7 +13,7 @@ class ArticlesController { createArticle = async (req: Request, res: Response, next: NextFunction) => { try { - const { title, content } = req.body; + const { title, content }: ArticleCreateDto = req.body; const { user } = req; if (!user) { @@ -120,7 +121,7 @@ class ArticlesController { updateArticle = async (req: Request, res: Response, next: NextFunction) => { try { const { articleId } = req.params; - const { title, content } = req.body; + const { title, content }: ArticleUpdateDto = req.body; const { user } = req; if (!user) { @@ -298,4 +299,4 @@ class ArticlesController { }; } -export default ArticlesController; +export default ArticlesController; \ No newline at end of file diff --git a/src/controllers/ProductsController.ts b/src/controllers/ProductsController.ts index 5def517d4..eff73d52f 100644 --- a/src/controllers/ProductsController.ts +++ b/src/controllers/ProductsController.ts @@ -1,6 +1,8 @@ import { Request, Response, NextFunction } from 'express'; import ProductService from '../ProductService'; import { Product as PrismaProduct, Prisma } from '@prisma/client'; +import prisma from '../index'; +import { ProductCreateDto, ProductUpdateDto } from '../dtos/ProductDto'; class ProductsController { private productService: ProductService; @@ -11,7 +13,7 @@ class ProductsController { createProduct = async (req: Request, res: Response, next: NextFunction) => { try { - const { name, description, price } = req.body; + const { name, description, price }: ProductCreateDto = req.body; const { user } = req; if (!user) { @@ -120,7 +122,7 @@ class ProductsController { updateProduct = async (req: Request, res: Response, next: NextFunction) => { try { const { productId } = req.params; - const { name, description, price } = req.body; + const { name, description, price }: ProductUpdateDto = req.body; const { user } = req; if (!user) { diff --git a/src/controllers/UsersController.ts b/src/controllers/UsersController.ts index 4195beb85..ee17d068b 100644 --- a/src/controllers/UsersController.ts +++ b/src/controllers/UsersController.ts @@ -3,6 +3,7 @@ import UserService from '../UserService'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import prisma from '../index'; +import { UserCreateDto, UserSignInDto, UserUpdateDto, UserChangePasswordDto } from '../dtos/UserDto'; class UsersController { private userService: UserService; @@ -13,7 +14,7 @@ class UsersController { signUp = async (req: Request, res: Response, next: NextFunction) => { try { - const { email, nickname, password } = req.body; + const { email, nickname, password }: UserCreateDto = req.body; if (!email || !nickname || !password) { return res.status(400).json({ message: '모든 정보를 입력해주세요'}); @@ -35,7 +36,7 @@ class UsersController { signIn = async (req: Request, res: Response, next: NextFunction) => { try { - const { email, password } = req.body; + const { email, password }: UserSignInDto = req.body; if (!email || !password) { return res.status(400).json({ message: '이메일과 비밀번호를 모두 입력해주세요.'}); @@ -86,7 +87,7 @@ class UsersController { updateMe = async (req: Request, res: Response, next: NextFunction) => { try { - const { nickname, image } = req.body; + const { nickname, image }: UserUpdateDto = req.body; const { user } = req; if (!user) { @@ -122,7 +123,7 @@ class UsersController { changePassword = async (req: Request, res: Response, next: NextFunction) => { try { - const { currentPassword, newPassword, confirmNewPassword } = req.body; + const { currentPassword, newPassword, confirmNewPassword }: UserChangePasswordDto = req.body; const { user } = req; if (!user) { @@ -191,7 +192,7 @@ class UsersController { } const isRefreshTokenMatched = await bcrypt.compare(refreshToken, user.refreshToken as string); - if (!isRefreshToken.Matched) { + if (!isRefreshTokenMatched) { return res.status(401).json({ message: 'Refresh Token이 유효하지 않습니다.' }); } @@ -214,4 +215,4 @@ class UsersController { }; } -export default UsersController; +export default UsersController; \ No newline at end of file diff --git a/src/dtos/ArticleDto.ts b/src/dtos/ArticleDto.ts new file mode 100644 index 000000000..5cbaeb41c --- /dev/null +++ b/src/dtos/ArticleDto.ts @@ -0,0 +1,9 @@ +export interface ArticleCreateDto { + title: string; + content: string; +} + +export interface ArticleUpdateDto { + title?: string; + content?: string; +} From 9bb1d8dd67a437821fda053636556321a20c2705 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:03:05 +0900 Subject: [PATCH 47/63] =?UTF-8?q?Service=20DTO=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ArticleService.ts | 16 +++++----------- src/ProductService.ts | 19 +++++-------------- src/UserService.ts | 16 ++++++---------- src/repositories/UserRepository.ts | 10 +++++++++- 4 files changed, 25 insertions(+), 36 deletions(-) diff --git a/src/ArticleService.ts b/src/ArticleService.ts index 2616fdab1..8cef62625 100644 --- a/src/ArticleService.ts +++ b/src/ArticleService.ts @@ -1,17 +1,11 @@ import { Article as PrismaArticle, Prisma } from '@prisma/client'; import ArticleRepository from './repositories/ArticleRepository'; +import { ArticleCreateDto, ArticleUpdateDto } from './dtos/ArticleDto'; -interface ArticleCreateInput { - title: string; - content: string; +interface ArticleCreateServiceInput extends ArticleCreateDto { userId: number; } -interface ArticleUpdateInput { - title?: string; - content?: string; -} - class ArticleService { private articleRepository: ArticleRepository; @@ -19,7 +13,7 @@ class ArticleService { this.articleRepository = articleRepository; } - async createArticle(data: ArticleCreateInput): Promise { + async createArticle(data: ArticleCreateServiceInput): Promise { return this.articleRepository.createArticle(data); } @@ -31,7 +25,7 @@ class ArticleService { return this.articleRepository.findArticles(options); } - async updateArticle(id: number, data: ArticleUpdateInput): Promise { + async updateArticle(id: number, data: ArticleUpdateDto): Promise { return this.articleRepository.updateArticle(id, data); } @@ -40,4 +34,4 @@ class ArticleService { } } -export default ArticleService; \ No newline at end of file +export default ArticleService; diff --git a/src/ProductService.ts b/src/ProductService.ts index 5e80f5a4f..0ac4fcc9a 100644 --- a/src/ProductService.ts +++ b/src/ProductService.ts @@ -1,18 +1,9 @@ import { Product as PrismaProduct, Prisma } from '@prisma/client'; import ProductRepository from './repositories/ProductRepository'; +import { ProductCreateDto, ProductUpdateDto } from './dtos/ProductDto'; -interface ProductCreateInput { - name: string; - content: string; - price: number; - status?: string; +interface ProductCreateServiceInput extends ProductCreateDto { userId: number; -} - -interface ProductUpdateInput { - name?: string; - content?: string; - price?: number; status?: string; } @@ -23,7 +14,7 @@ class ProductService { this.productRepository = productRepository; } - async createProduct(data: ProductCreateInput): Promise { + async createProduct(data: ProductCreateServiceInput): Promise { return this.productRepository.createProduct(data); } @@ -35,7 +26,7 @@ class ProductService { return this.productRepository.findProducts(options); } - async updateProduct(id: number, data: ProductUpdateInput): Promise { + async updateProduct(id: number, data: ProductUpdateDto): Promise { return this.productRepository.updateProduct(id, data); } @@ -44,4 +35,4 @@ class ProductService { } } -export default ProductService; \ No newline at end of file +export default ProductService; diff --git a/src/UserService.ts b/src/UserService.ts index 8af506eee..da748b9be 100644 --- a/src/UserService.ts +++ b/src/UserService.ts @@ -2,6 +2,7 @@ import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import { User as PrismaUser, Prisma } from '@prisma/client'; import UserRepository from './repositories/UserRepository'; +import { UserCreateDto, UserUpdateDto } from './dtos/UserDto'; interface UserWithoutPassword extends Omit {} @@ -13,7 +14,8 @@ class UserService { } // 회원가입 로직 - public signUp = async (email: string, nickname: string, password: string): Promise => { + public signUp = async (userData: UserCreateDto): Promise => { + const { email, nickname, password } = userData; // 이메일 중복 확인 const existingUser = await this.userRepository.findUserByEmail(email); @@ -75,7 +77,7 @@ class UserService { return this.userRepository.findUserById(id); }; - public updateUser = async (id: number, data: Prisma.UserUpdateInput): Promise => { + public updateUser = async (id: number, data: UserUpdateDto): Promise => { return this.userRepository.updateUser(id, data); }; @@ -84,14 +86,8 @@ class UserService { }; public getProductsByUserId = async (userId: number): Promise => { - // This method will need to be implemented in UserRepository or a new ProductRepository method - // For now, directly using prisma for product related queries from user service - const userWithProducts = await prisma.user.findUnique({ - where: { id: userId }, - include: { products: true }, - }); - return userWithProducts ? userWithProducts.products : null; + return this.userRepository.findProductsByUserId(userId); }; } -export default UserService; +export default UserService; \ No newline at end of file diff --git a/src/repositories/UserRepository.ts b/src/repositories/UserRepository.ts index f69b6eafb..a6ce7a1be 100644 --- a/src/repositories/UserRepository.ts +++ b/src/repositories/UserRepository.ts @@ -21,6 +21,14 @@ class UserRepository { async deleteUser(id: number): Promise { return prisma.user.delete({ where: { id } }); } + + async findProductsByUserId(userId: number): Promise { + const userWithProducts = await prisma.user.findUnique({ + where: { id: userId }, + include: { products: true }, + }); + return userWithProducts ? userWithProducts.products : null; + } } -export default UserRepository; +export default UserRepository; \ No newline at end of file From 4555a1214ae730a7c91afb58c2df3cbb2dc63010 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:04:33 +0900 Subject: [PATCH 48/63] =?UTF-8?q?CommentService,=20LikeService=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/CommentService.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ src/LikeService.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 src/CommentService.ts create mode 100644 src/LikeService.ts diff --git a/src/CommentService.ts b/src/CommentService.ts new file mode 100644 index 000000000..e7d3db1d6 --- /dev/null +++ b/src/CommentService.ts @@ -0,0 +1,43 @@ +import { Comment as PrismaComment, Prisma } from '@prisma/client'; +import CommentRepository from './repositories/CommentRepository'; + +interface CommentCreateInput { + content: string; + userId: number; + productId?: number; + articleId?: number; +} + +interface CommentUpdateInput { + content?: string; +} + +class CommentService { + private commentRepository: CommentRepository; + + constructor(commentRepository: CommentRepository) { + this.commentRepository = commentRepository; + } + + async createComment(data: CommentCreateInput): Promise { + return this.commentRepository.createComment(data); + } + + async getCommentById(id: number): Promise { + return this.commentRepository.findCommentById(id); + } + + async getComments(options?: { skip?: number; take?: number; where?: Prisma.CommentWhereInput; orderBy?: Prisma.CommentOrderByWithRelationInput }): Promise { + return this.commentRepository.findComments(options); + } + + async updateComment(id: number, data: CommentUpdateInput): Promise { + return this.commentRepository.updateComment(id, data); + } + + async deleteComment(id: number): Promise { + return this.commentRepository.deleteComment(id); + } +} + +export default CommentService; diff --git a/src/LikeService.ts b/src/LikeService.ts new file mode 100644 index 000000000..65590bbe4 --- /dev/null +++ b/src/LikeService.ts @@ -0,0 +1,34 @@ +import { Like as PrismaLike, Prisma } from '@prisma/client'; +import LikeRepository from './repositories/LikeRepository'; + +interface LikeCreateInput { + userId: number; + productId?: number; + articleId?: number; +} + +class LikeService { + private likeRepository: LikeRepository; + + constructor(likeRepository: LikeRepository) { + this.likeRepository = likeRepository; + } + + async createLike(data: LikeCreateInput): Promise { + return this.likeRepository.createLike(data); + } + + async deleteLike(id: number): Promise { + return this.likeRepository.deleteLike(id); + } + + async findLikeByUserIdAndProductId(userId: number, productId: number): Promise { + return this.likeRepository.findLikeByUserIdAndProductId(userId, productId); + } + + async findLikeByUserIdAndArticleId(userId: number, articleId: number): Promise { + return this.likeRepository.findLikeByUserIdAndArticleId(userId, articleId); + } +} + +export default LikeService; From 6d7c540e8e2153371b32957acf48a87e26e821a2 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:05:58 +0900 Subject: [PATCH 49/63] =?UTF-8?q?Comment,=20LikeService=20=EB=A1=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ArticlesController.ts | 66 ++++++++++++--------------- src/controllers/ProductsController.ts | 66 ++++++++++++--------------- 2 files changed, 58 insertions(+), 74 deletions(-) diff --git a/src/controllers/ArticlesController.ts b/src/controllers/ArticlesController.ts index 488c86839..a9be0d2aa 100644 --- a/src/controllers/ArticlesController.ts +++ b/src/controllers/ArticlesController.ts @@ -3,12 +3,22 @@ import ArticleService from '../ArticleService'; import { Article as PrismaArticle, Prisma } from '@prisma/client'; import prisma from '../index'; import { ArticleCreateDto, ArticleUpdateDto } from '../dtos/ArticleDto'; +import CommentService from '../CommentService'; +import LikeService from '../LikeService'; +import CommentRepository from '../repositories/CommentRepository'; +import LikeRepository from '../repositories/LikeRepository'; class ArticlesController { private articleService: ArticleService; + private commentService: CommentService; + private likeService: LikeService; constructor(articleService: ArticleService) { this.articleService = articleService; + const commentRepository = new CommentRepository(); + this.commentService = new CommentService(commentRepository); + const likeRepository = new LikeRepository(); + this.likeService = new LikeService(likeRepository); } createArticle = async (req: Request, res: Response, next: NextFunction) => { @@ -60,7 +70,7 @@ class ArticlesController { if (user) { const articleIds = articles.map(article => article.id); - const likes = await prisma.like.findMany({ + const likes = await this.likeService.findLikes({ where: { userId: user.id, articleId: { in: articleIds }, @@ -99,12 +109,7 @@ class ArticlesController { let isLiked = false; if (user) { - const like = await prisma.like.findFirst({ - where: { - articleId: article.id, - userId: user.id, - }, - }); + const like = await this.likeService.findLikeByUserIdAndArticleId(user.id, parseInt(articleId)); if (like) { isLiked = true; } @@ -176,12 +181,10 @@ class ArticlesController { if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.'}); - const newComment = await prisma.comment.create({ - data: { - content, - articleId: parseInt(articleId), - userId: user.id, - }, + const newComment = await this.commentService.createComment({ + content, + articleId: parseInt(articleId), + userId: user.id, }); res.status(201).json(newComment); } catch (error) { @@ -195,7 +198,7 @@ class ArticlesController { let cursor = req.query.cursor ? parseInt(req.query.cursor as string): undefined; let limit = parseInt(req.query.limit as string) || 10; - const comments = await prisma.comment.findMany({ + const comments = await this.commentService.getComments({ where: { articleId: parseInt(articleId) }, select: { id: true, content: true, createdAt: true, userId: true }, orderBy: { createdAt: 'desc' }, @@ -221,15 +224,13 @@ class ArticlesController { if (!content) return res.status(400).json({ message: '수정할 내용을 입력하세요.' }); - const comment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) }}); + const comment = await this.commentService.getCommentById(parseInt(commentId)); if (!comment || comment.userId !== user.id) { return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); } - const updatedComment = await prisma.comment.update - ({ - where: { id: parseInt(commentId) }, - data: { content }, + const updatedComment = await this.commentService.updateComment(parseInt(commentId), { + content, }); res.status(200).json(updatedComment); } catch (error) { @@ -246,12 +247,12 @@ class ArticlesController { return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); } - const comment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) } }); + const comment = await this.commentService.getCommentById(parseInt(commentId) ); if (!comment || comment.userId !== user.id) { return res.status(403).json({ message: '댓글 삭제 권환이 없습니다.' }); } - await prisma.comment.delete({ where: { id: parseInt(commentId) }}); + await this.commentService.deleteComment(parseInt(commentId)); res.status(204).send(); } catch (error) { next(error); @@ -267,29 +268,20 @@ class ArticlesController { return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); } - const article = await prisma.article.findUnique({ where: { id: parseInt(articleId) } }); + const article = await this.articleService.getArticleById(parseInt(articleId)); if (!article) { return res.status(404).json({ message: '게시글을 찾을 수 없습니다.'}); } - const existingLike = await prisma.like.findFirst({ - where: { - userId: user.id, - articleId: parseInt(articleId), - }, - }); + const existingLike = await this.likeService.findLikeByUserIdAndArticleId(user.id, parseInt(articleId)); if (existingLike) { - await prisma.like.delete({ - where: { id: existingLike.id }, - }); + await this.likeService.deleteLike(existingLike.id); res.status(200).json({ message: '게시글 좋아요를 취소했습니다.' }); } else { - await prisma.like.create({ - data: { - userId: user.id, - articleId: parseInt(articleId), - }, + await this.likeService.createLike({ + userId: user.id, + articleId: parseInt(articleId), }); res.status(201).json({ message: '게시글에 좋아요를 눌렀습니다.' }); } @@ -299,4 +291,4 @@ class ArticlesController { }; } -export default ArticlesController; \ No newline at end of file +export default ArticlesController; diff --git a/src/controllers/ProductsController.ts b/src/controllers/ProductsController.ts index eff73d52f..7d54c669b 100644 --- a/src/controllers/ProductsController.ts +++ b/src/controllers/ProductsController.ts @@ -3,12 +3,22 @@ import ProductService from '../ProductService'; import { Product as PrismaProduct, Prisma } from '@prisma/client'; import prisma from '../index'; import { ProductCreateDto, ProductUpdateDto } from '../dtos/ProductDto'; +import CommentService from '../CommentService'; +import LikeService from '../LikeService'; +import CommentRepository from '../repositories/CommentRepository'; +import LikeRepository from '../repositories/LikeRepository'; class ProductsController { private productService: ProductService; + private commentService: CommentService; + private likeService: LikeService; constructor(productService: ProductService) { this.productService = productService; + const commentRepository = new CommentRepository(); + this.commentService = new CommentService(commentRepository); + const likeRepository = new LikeRepository(); + this.likeService = new LikeService(likeRepository); } createProduct = async (req: Request, res: Response, next: NextFunction) => { @@ -62,7 +72,7 @@ class ProductsController { if (user) { const productIds = products.map(product => product.id); - const likes = await prisma.like.findMany({ + const likes = await this.likeService.findLikes({ where: { userId: user.id, productId: { in: productIds }, @@ -101,12 +111,7 @@ class ProductsController { let isLiked = false; if (user) { - const like = await prisma.like.findFirst({ - where: { - productId: product.id, - userId: user.id, - }, - }); + const like = await this.likeService.findLikeByUserIdAndProductId(user.id, parseInt(productId)); if (like) { isLiked = true; } @@ -178,12 +183,10 @@ class ProductsController { if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.' }); - const newComment = await prisma.comment.create({ - data: { - content, - productId: parseInt(productId), - userId: user.id, - }, + const newComment = await this.commentService.createComment({ + content, + productId: parseInt(productId), + userId: user.id, }); res.status(201).json(newComment); } catch (error) { @@ -197,7 +200,7 @@ class ProductsController { let cursor = req.query.cursor ? parseInt(req.query.cursor as string): undefined; let limit = parseInt(req.query.limit as string) || 10; - const comments = await prisma.comment.findMany({ + const comments = await this.commentService.getComments({ where: { productId: parseInt(productId) }, select: { id: true, content: true, createdAt: true, userId: true }, orderBy: { createdAt: 'desc' }, @@ -223,15 +226,13 @@ class ProductsController { if (!content) return res.status(400).json({ message: '수정할 내용을 입력해주세요.'}); - const existingComment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) } }); + const existingComment = await this.commentService.getCommentById(parseInt(commentId)); if (!existingComment || existingComment.userId !== user.id) { return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); } - const updatedComment = await prisma.comment.update -({ - where: { id: parseInt(commentId) }, - data: { content }, + const updatedComment = await this.commentService.updateComment(parseInt(commentId), { + content, }); res.status(200).json(updatedComment); } catch (error) { @@ -248,12 +249,12 @@ class ProductsController { return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); } - const existingComment = await prisma.comment.findUnique({ where: { id: parseInt(commentId) } }); + const existingComment = await this.commentService.getCommentById(parseInt(commentId)); if (!existingComment || existingComment.userId !== user.id) { return res.status(403).json({ message: '댓글 삭제 권한이 없습니다.' }); } - await prisma.comment.delete({ where: { id: parseInt(commentId) }}); + await this.commentService.deleteComment(parseInt(commentId)); res.status(204).send(); } catch(error) { next(error); @@ -269,29 +270,20 @@ class ProductsController { return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); } - const product = await prisma.product.findUnique({ where: { id: parseInt(productId) } }); + const product = await this.productService.getProductById(parseInt(productId)); if (!product) { return res.status(404).json({ message: '상품을 찾을 수 없습니다.'}); } - const existingLike = await prisma.like.findFirst({ - where: { - userId: user.id, - productId: parseInt(productId), - }, - }); + const existingLike = await this.likeService.findLikeByUserIdAndProductId(user.id, parseInt(productId)); if (existingLike) { - await prisma.like.delete({ - where: { id: existingLike.id }, - }); + await this.likeService.deleteLike(existingLike.id); res.status(200).json({ message: '상품 좋아요를 취소했습니다.' }); } else { - await prisma.like.create({ - data: { - userId: user.id, - productId: parseInt(productId), - }, + await this.likeService.createLike({ + userId: user.id, + productId: parseInt(productId), }); res.status(201).json({ message: '상품에 좋아요를 눌렀습니다.' }); } @@ -301,4 +293,4 @@ class ProductsController { }; } -export default ProductsController; +export default ProductsController; \ No newline at end of file From 9c3506a610fbf7aaeb83858f6dd185c8633225c3 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:09:27 +0900 Subject: [PATCH 50/63] =?UTF-8?q?prisma=EC=99=80=20=EA=B4=80=EA=B3=84=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/ArticleService.js | 17 +- dist/CommentService.js | 23 ++ dist/LikeService.js | 20 ++ dist/ProductService.js | 17 +- dist/UserService.js | 39 ++-- dist/controllers/ArticlesController.js | 257 ++++++++++++++++++++++ dist/controllers/ProductsController.js | 259 ++++++++++++++++++++++ dist/controllers/UploadController.js | 38 ++++ dist/controllers/UsersController.js | 187 ++++++++++++++++ dist/dtos/ArticleDto.js | 2 + dist/dtos/ProductDto.js | 2 + dist/dtos/UserDto.js | 2 + dist/repositories/ArticleRepository.js | 24 ++ dist/repositories/CommentRepository.js | 24 ++ dist/repositories/LikeRepository.js | 31 +++ dist/repositories/ProductRepository.js | 24 ++ dist/repositories/UserRepository.js | 31 +++ dist/routes/articles.router.js | 284 ++---------------------- dist/routes/products.router.js | 290 ++----------------------- dist/routes/upload.router.js | 24 +- dist/routes/users.router.js | 203 ++--------------- src/ArticleService.ts | 12 +- src/CommentService.ts | 10 +- src/LikeService.ts | 10 +- src/ProductService.ts | 13 +- 25 files changed, 1051 insertions(+), 792 deletions(-) create mode 100644 dist/CommentService.js create mode 100644 dist/LikeService.js create mode 100644 dist/controllers/ArticlesController.js create mode 100644 dist/controllers/ProductsController.js create mode 100644 dist/controllers/UploadController.js create mode 100644 dist/controllers/UsersController.js create mode 100644 dist/dtos/ArticleDto.js create mode 100644 dist/dtos/ProductDto.js create mode 100644 dist/dtos/UserDto.js create mode 100644 dist/repositories/ArticleRepository.js create mode 100644 dist/repositories/CommentRepository.js create mode 100644 dist/repositories/LikeRepository.js create mode 100644 dist/repositories/ProductRepository.js create mode 100644 dist/repositories/UserRepository.js diff --git a/dist/ArticleService.js b/dist/ArticleService.js index 47aca25a4..ce82e33e7 100644 --- a/dist/ArticleService.js +++ b/dist/ArticleService.js @@ -1,24 +1,23 @@ "use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; Object.defineProperty(exports, "__esModule", { value: true }); -const index_1 = __importDefault(require("./index")); class ArticleService { + constructor(articleRepository) { + this.articleRepository = articleRepository; + } async createArticle(data) { - return index_1.default.article.create({ data }); + return this.articleRepository.createArticle(data); } async getArticleById(id) { - return index_1.default.article.findUnique({ where: { id } }); + return this.articleRepository.findArticleById(id); } async getArticles(options) { - return index_1.default.article.findMany(options); + return this.articleRepository.findArticles(options); } async updateArticle(id, data) { - return index_1.default.article.update({ where: { id }, data }); + return this.articleRepository.updateArticle(id, data); } async deleteArticle(id) { - return index_1.default.article.delete({ where: { id } }); + return this.articleRepository.deleteArticle(id); } } exports.default = ArticleService; diff --git a/dist/CommentService.js b/dist/CommentService.js new file mode 100644 index 000000000..68f616a11 --- /dev/null +++ b/dist/CommentService.js @@ -0,0 +1,23 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +class CommentService { + constructor(commentRepository) { + this.commentRepository = commentRepository; + } + async createComment(data) { + return this.commentRepository.createComment(data); + } + async getCommentById(id) { + return this.commentRepository.findCommentById(id); + } + async getComments(options) { + return this.commentRepository.findComments(options); + } + async updateComment(id, data) { + return this.commentRepository.updateComment(id, data); + } + async deleteComment(id) { + return this.commentRepository.deleteComment(id); + } +} +exports.default = CommentService; diff --git a/dist/LikeService.js b/dist/LikeService.js new file mode 100644 index 000000000..00a8690c4 --- /dev/null +++ b/dist/LikeService.js @@ -0,0 +1,20 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +class LikeService { + constructor(likeRepository) { + this.likeRepository = likeRepository; + } + async createLike(data) { + return this.likeRepository.createLike(data); + } + async deleteLike(id) { + return this.likeRepository.deleteLike(id); + } + async findLikeByUserIdAndProductId(userId, productId) { + return this.likeRepository.findLikeByUserIdAndProductId(userId, productId); + } + async findLikeByUserIdAndArticleId(userId, articleId) { + return this.likeRepository.findLikeByUserIdAndArticleId(userId, articleId); + } +} +exports.default = LikeService; diff --git a/dist/ProductService.js b/dist/ProductService.js index ffd9cdefe..fa953cfff 100644 --- a/dist/ProductService.js +++ b/dist/ProductService.js @@ -1,24 +1,23 @@ "use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; Object.defineProperty(exports, "__esModule", { value: true }); -const index_1 = __importDefault(require("./index")); class ProductService { + constructor(productRepository) { + this.productRepository = productRepository; + } async createProduct(data) { - return index_1.default.product.create({ data }); + return this.productRepository.createProduct(data); } async getProductById(id) { - return index_1.default.product.findUnique({ where: { id } }); + return this.productRepository.findProductById(id); } async getProducts(options) { - return index_1.default.product.findMany(options); + return this.productRepository.findProducts(options); } async updateProduct(id, data) { - return index_1.default.product.update({ where: { id }, data }); + return this.productRepository.updateProduct(id, data); } async deleteProduct(id) { - return index_1.default.product.delete({ where: { id } }); + return this.productRepository.deleteProduct(id); } } exports.default = ProductService; diff --git a/dist/UserService.js b/dist/UserService.js index bae2f6987..6f367803d 100644 --- a/dist/UserService.js +++ b/dist/UserService.js @@ -3,28 +3,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -const index_1 = __importDefault(require("./index")); const bcrypt_1 = __importDefault(require("bcrypt")); const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); class UserService { - constructor() { - this.prisma = index_1.default; + constructor(userRepository) { // 회원가입 로직 - this.signUp = async (email, nickname, password) => { + this.signUp = async (userData) => { + const { email, nickname, password } = userData; // 이메일 중복 확인 - const existingUser = await this.prisma.user.findUnique({ where: { email } }); + const existingUser = await this.userRepository.findUserByEmail(email); if (existingUser) { throw new Error('이미 사용중인 이메일입니다.'); } // 비밀번호 해싱 const hashedPassword = await bcrypt_1.default.hash(password, 10); // 유저 생성 - const user = await this.prisma.user.create({ - data: { - email, - nickname, - password: hashedPassword, - }, + const user = await this.userRepository.createUser({ + email, + nickname, + password: hashedPassword, }); // 사용자 정보 반환 const { password: _, refreshToken: __, ...userWithoutPassword } = user; @@ -32,7 +29,7 @@ class UserService { }; this.signIn = async (email, password) => { // 이메일로 사용자 조회 - const user = await this.prisma.user.findUnique({ where: { email } }); + const user = await this.userRepository.findUserByEmail(email); if (!user) { throw new Error('존재하지 않는 이메일입니다.'); } @@ -46,12 +43,24 @@ class UserService { // Refresh Token 생성 (7일) const refreshToken = jsonwebtoken_1.default.sign({ userId: user.id }, process.env.REFRESH_TOKEN_SECRET_KEY, { expiresIn: '7d' }); // Refresh Token을 해싱해서 DB에 저장 - await this.prisma.user.update({ - where: { id: user.id }, - data: { refreshToken: await bcrypt_1.default.hash(refreshToken, 10) }, + await this.userRepository.updateUser(user.id, { + refreshToken: await bcrypt_1.default.hash(refreshToken, 10), }); return { accessToken, refreshToken }; }; + this.getUserById = async (id) => { + return this.userRepository.findUserById(id); + }; + this.updateUser = async (id, data) => { + return this.userRepository.updateUser(id, data); + }; + this.updatePassword = async (id, newPasswordHash) => { + return this.userRepository.updateUser(id, { password: newPasswordHash }); + }; + this.getProductsByUserId = async (userId) => { + return this.userRepository.findProductsByUserId(userId); + }; + this.userRepository = userRepository; } } exports.default = UserService; diff --git a/dist/controllers/ArticlesController.js b/dist/controllers/ArticlesController.js new file mode 100644 index 000000000..d2bcf3191 --- /dev/null +++ b/dist/controllers/ArticlesController.js @@ -0,0 +1,257 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const CommentService_1 = __importDefault(require("../CommentService")); +const LikeService_1 = __importDefault(require("../LikeService")); +const CommentRepository_1 = __importDefault(require("../repositories/CommentRepository")); +const LikeRepository_1 = __importDefault(require("../repositories/LikeRepository")); +class ArticlesController { + constructor(articleService) { + this.createArticle = async (req, res, next) => { + try { + const { title, content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const article = await this.articleService.createArticle({ + title, + content, + userId: user.id, + }); + res.status(201).json(article); + } + catch (error) { + next(error); + } + }; + this.getArticles = async (req, res, next) => { + try { + const { sort, search } = req.query; + let page = parseInt(req.query.page) || 1; + let limit = parseInt(req.query.limit) || 10; + let offset = (page - 1) * limit; + const where = search + ? { + OR: [ + { title: { contains: search, mode: 'insensitive' } }, + { content: { contains: search, mode: 'insensitive' } }, + ], + } + : {}; + const user = req.user; + const articles = await this.articleService.getArticles({ + where, + select: { id: true, title: true, content: true, createdAt: true, userId: true, updatedAt: true }, + orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, + skip: offset, + take: limit, + }); + let responseArticles = articles; + if (user) { + const articleIds = articles.map(article => article.id); + const likes = await this.likeService.findLikes({ + where: { + userId: user.id, + articleId: { in: articleIds }, + }, + }); + const likedArticleIds = new Set(likes.map(like => like.articleId)); + responseArticles = articles.map(article => ({ + ...article, + isLiked: likedArticleIds.has(article.id), + })); + } + else { + responseArticles = articles.map(article => ({ + ...article, + isLiked: false, + })); + } + res.status(200).json(responseArticles); + } + catch (error) { + next(error); + } + }; + this.getArticleById = async (req, res, next) => { + try { + const { articleId } = req.params; + const user = req.user; + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article) { + return res.status(404).json({ message: '게시글을 찾을 수 없습니다.' }); + } + let isLiked = false; + if (user) { + const like = await this.likeService.findLikeByUserIdAndArticleId(user.id, parseInt(articleId)); + if (like) { + isLiked = true; + } + } + const responseArticle = { ...article, isLiked }; + res.status(200).json(responseArticle); + } + catch (error) { + next(error); + } + }; + this.updateArticle = async (req, res, next) => { + try { + const { articleId } = req.params; + const { title, content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article || article.userId !== user.id) { + return res.status(403).json({ message: '게시글 수정 권한이 없습니다.' }); + } + const updatedArticle = await this.articleService.updateArticle(parseInt(articleId), { + title, + content, + }); + res.status(200).json(updatedArticle); + } + catch (error) { + next(error); + } + }; + this.deleteArticle = async (req, res, next) => { + try { + const { articleId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article || article.userId !== user.id) { + return res.status(403).json({ message: '게시글 삭제 권한이 없습니다.' }); + } + await this.articleService.deleteArticle(parseInt(articleId)); + res.status(204).send(); + } + catch (error) { + next(error); + } + }; + this.createComment = async (req, res, next) => { + try { + const { articleId } = req.params; + const { content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) + return res.status(400).json({ message: '댓글을 입력해주세요.' }); + const newComment = await this.commentService.createComment({ + content, + articleId: parseInt(articleId), + userId: user.id, + }); + res.status(201).json(newComment); + } + catch (error) { + next(error); + } + }; + this.getComments = async (req, res, next) => { + try { + const { articleId } = req.params; + let cursor = req.query.cursor ? parseInt(req.query.cursor) : undefined; + let limit = parseInt(req.query.limit) || 10; + const comments = await this.commentService.getComments({ + where: { articleId: parseInt(articleId) }, + select: { id: true, content: true, createdAt: true, userId: true }, + orderBy: { createdAt: 'desc' }, + cursor: cursor ? { id: cursor } : undefined, + take: limit, + skip: cursor ? 1 : 0, + }); + res.status(200).json(comments); + } + catch (error) { + next(error); + } + }; + this.updateComment = async (req, res, next) => { + try { + const { commentId } = req.params; + const { content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) + return res.status(400).json({ message: '수정할 내용을 입력하세요.' }); + const comment = await this.commentService.getCommentById(parseInt(commentId)); + if (!comment || comment.userId !== user.id) { + return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); + } + const updatedComment = await this.commentService.updateComment(parseInt(commentId), { + content, + }); + res.status(200).json(updatedComment); + } + catch (error) { + next(error); + } + }; + this.deleteComment = async (req, res, next) => { + try { + const { commentId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const comment = await this.commentService.getCommentById(parseInt(commentId)); + if (!comment || comment.userId !== user.id) { + return res.status(403).json({ message: '댓글 삭제 권환이 없습니다.' }); + } + await this.commentService.deleteComment(parseInt(commentId)); + res.status(204).send(); + } + catch (error) { + next(error); + } + }; + this.toggleLike = async (req, res, next) => { + try { + const { articleId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article) { + return res.status(404).json({ message: '게시글을 찾을 수 없습니다.' }); + } + const existingLike = await this.likeService.findLikeByUserIdAndArticleId(user.id, parseInt(articleId)); + if (existingLike) { + await this.likeService.deleteLike(existingLike.id); + res.status(200).json({ message: '게시글 좋아요를 취소했습니다.' }); + } + else { + await this.likeService.createLike({ + userId: user.id, + articleId: parseInt(articleId), + }); + res.status(201).json({ message: '게시글에 좋아요를 눌렀습니다.' }); + } + } + catch (error) { + next(error); + } + }; + this.articleService = articleService; + const commentRepository = new CommentRepository_1.default(); + this.commentService = new CommentService_1.default(commentRepository); + const likeRepository = new LikeRepository_1.default(); + this.likeService = new LikeService_1.default(likeRepository); + } +} +exports.default = ArticlesController; diff --git a/dist/controllers/ProductsController.js b/dist/controllers/ProductsController.js new file mode 100644 index 000000000..3f38af87e --- /dev/null +++ b/dist/controllers/ProductsController.js @@ -0,0 +1,259 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const CommentService_1 = __importDefault(require("../CommentService")); +const LikeService_1 = __importDefault(require("../LikeService")); +const CommentRepository_1 = __importDefault(require("../repositories/CommentRepository")); +const LikeRepository_1 = __importDefault(require("../repositories/LikeRepository")); +class ProductsController { + constructor(productService) { + this.createProduct = async (req, res, next) => { + try { + const { name, description, price } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const product = await this.productService.createProduct({ + name, + content: description, // Map description to content + price, + userId: user.id, + }); + res.status(201).json(product); + } + catch (error) { + next(error); + } + }; + this.getProducts = async (req, res, next) => { + try { + const { sort, search } = req.query; + let page = parseInt(req.query.page) || 1; + let limit = parseInt(req.query.limit) || 10; + let offset = (page - 1) * limit; + const where = search + ? { + OR: [ + { name: { contains: search, mode: 'insensitive' } }, + { content: { contains: search, mode: 'insensitive' + } }, + ], + } + : {}; + const user = req.user; + const products = await this.productService.getProducts({ + where, + select: { id: true, name: true, content: true, price: true, createdAt: true, userId: true, updatedAt: true, status: true }, + orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, + skip: offset, + take: limit, + }); + let responseProducts = products; + if (user) { + const productIds = products.map(product => product.id); + const likes = await this.likeService.findLikes({ + where: { + userId: user.id, + productId: { in: productIds }, + }, + }); + const likedProductIds = new Set(likes.map(like => like.productId)); + responseProducts = products.map(product => ({ + ...product, + isLiked: likedProductIds.has(product.id), + })); + } + else { + responseProducts = products.map(product => ({ + ...product, + isLiked: false, + })); + } + res.status(200).json(responseProducts); + } + catch (error) { + next(error); + } + }; + this.getProductById = async (req, res, next) => { + try { + const { productId } = req.params; + const user = req.user; + const product = await this.productService.getProductById(parseInt(productId)); + if (!product) + return res.status(404).json({ message: '상품을 찾을수 없습니다.' }); + let isLiked = false; + if (user) { + const like = await this.likeService.findLikeByUserIdAndProductId(user.id, parseInt(productId)); + if (like) { + isLiked = true; + } + } + const responseProduct = { ...product, isLiked }; + res.status(200).json(responseProduct); + } + catch (error) { + next(error); + } + }; + this.updateProduct = async (req, res, next) => { + try { + const { productId } = req.params; + const { name, description, price } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const product = await this.productService.getProductById(parseInt(productId)); + if (!product || product.userId !== user.id) { + return res.status(403).json({ message: '상품 수정 권한이 없습니다.' }); + } + const updatedProduct = await this.productService.updateProduct(parseInt(productId), { + name, + content: description, + price, + }); + res.status(200).json(updatedProduct); + } + catch (error) { + next(error); + } + }; + this.deleteProduct = async (req, res, next) => { + try { + const { productId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const product = await this.productService.getProductById(parseInt(productId)); + if (!product || product.userId !== user.id) { + return res.status(403).json({ message: '상품 삭제 권한이 없습니다.' }); + } + await this.productService.deleteProduct(parseInt(productId)); + res.status(204).send(); + } + catch (error) { + next(error); + } + }; + this.createComment = async (req, res, next) => { + try { + const { productId } = req.params; + const { content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) + return res.status(400).json({ message: '댓글을 입력해주세요.' }); + const newComment = await this.commentService.createComment({ + content, + productId: parseInt(productId), + userId: user.id, + }); + res.status(201).json(newComment); + } + catch (error) { + next(error); + } + }; + this.getComments = async (req, res, next) => { + try { + const { productId } = req.params; + let cursor = req.query.cursor ? parseInt(req.query.cursor) : undefined; + let limit = parseInt(req.query.limit) || 10; + const comments = await this.commentService.getComments({ + where: { productId: parseInt(productId) }, + select: { id: true, content: true, createdAt: true, userId: true }, + orderBy: { createdAt: 'desc' }, + cursor: cursor ? { id: cursor } : undefined, + take: limit, + skip: cursor ? 1 : 0, + }); + res.status(200).json(comments); + } + catch (error) { + next(error); + } + }; + this.updateComment = async (req, res, next) => { + try { + const { commentId } = req.params; + const { content } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!content) + return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); + const existingComment = await this.commentService.getCommentById(parseInt(commentId)); + if (!existingComment || existingComment.userId !== user.id) { + return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); + } + const updatedComment = await this.commentService.updateComment(parseInt(commentId), { + content, + }); + res.status(200).json(updatedComment); + } + catch (error) { + next(error); + } + }; + this.deleteComment = async (req, res, next) => { + try { + const { commentId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const existingComment = await this.commentService.getCommentById(parseInt(commentId)); + if (!existingComment || existingComment.userId !== user.id) { + return res.status(403).json({ message: '댓글 삭제 권한이 없습니다.' }); + } + await this.commentService.deleteComment(parseInt(commentId)); + res.status(204).send(); + } + catch (error) { + next(error); + } + }; + this.toggleLike = async (req, res, next) => { + try { + const { productId } = req.params; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const product = await this.productService.getProductById(parseInt(productId)); + if (!product) { + return res.status(404).json({ message: '상품을 찾을 수 없습니다.' }); + } + const existingLike = await this.likeService.findLikeByUserIdAndProductId(user.id, parseInt(productId)); + if (existingLike) { + await this.likeService.deleteLike(existingLike.id); + res.status(200).json({ message: '상품 좋아요를 취소했습니다.' }); + } + else { + await this.likeService.createLike({ + userId: user.id, + productId: parseInt(productId), + }); + res.status(201).json({ message: '상품에 좋아요를 눌렀습니다.' }); + } + } + catch (error) { + next(error); + } + }; + this.productService = productService; + const commentRepository = new CommentRepository_1.default(); + this.commentService = new CommentService_1.default(commentRepository); + const likeRepository = new LikeRepository_1.default(); + this.likeService = new LikeService_1.default(likeRepository); + } +} +exports.default = ProductsController; diff --git a/dist/controllers/UploadController.js b/dist/controllers/UploadController.js new file mode 100644 index 000000000..101e61eaf --- /dev/null +++ b/dist/controllers/UploadController.js @@ -0,0 +1,38 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const multer_1 = __importDefault(require("multer")); +const path_1 = __importDefault(require("path")); +const fs_1 = __importDefault(require("fs")); +const sharp_1 = __importDefault(require("sharp")); +const uploadDir = 'uploads/'; +if (!fs_1.default.existsSync(uploadDir)) { + fs_1.default.mkdirSync(uploadDir); +} +const storage = multer_1.default.memoryStorage(); +const upload = (0, multer_1.default)({ storage: storage }); +class UploadController { + constructor() { + this.uploadImage = async (req, res, next) => { + if (!req.file) { + return res.status(400).json({ message: '이미지 파일이 필요합니다.' }); + } + try { + const ext = path_1.default.extname(req.file.originalname); + const filename = Date.now() + ext; + const imagePath = path_1.default.join(uploadDir, filename); + await (0, sharp_1.default)(req.file.buffer) + .resize({ width: 500 }) + .toFile(imagePath); + const imageUrl = `/uploads/${filename}`; + res.status(201).json({ imageUrl: imageUrl }); + } + catch (error) { + next(error); + } + }; + } +} +exports.default = UploadController; diff --git a/dist/controllers/UsersController.js b/dist/controllers/UsersController.js new file mode 100644 index 000000000..ed1d04e98 --- /dev/null +++ b/dist/controllers/UsersController.js @@ -0,0 +1,187 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const bcrypt_1 = __importDefault(require("bcrypt")); +const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); +class UsersController { + constructor(userService) { + this.signUp = async (req, res, next) => { + try { + const { email, nickname, password } = req.body; + if (!email || !nickname || !password) { + return res.status(400).json({ message: '모든 정보를 입력해주세요' }); + } + const newUser = await this.userService.signUp(email, nickname, password); + return res.status(201).json({ + message: '회원가입이 완료되었습니다.', + data: newUser, + }); + } + catch (error) { + if (error instanceof Error) { + return res.status(409).json({ message: error.message }); + } + next(error); + } + }; + this.signIn = async (req, res, next) => { + try { + const { email, password } = req.body; + if (!email || !password) { + return res.status(400).json({ message: '이메일과 비밀번호를 모두 입력해주세요.' }); + } + const { accessToken, refreshToken } = await this.userService.signIn(email, password); + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: false, + maxAge: 1000 * 60 * 60 * 24 * 7, + }); + return res.status(200).json({ + message: '로그인에 성공했습니다.', + data: { accessToken }, + }); + } + catch (error) { + if (error instanceof Error) { + return res.status(401).json({ message: error.message }); + } + next(error); + } + }; + this.getMe = async (req, res, next) => { + try { + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + res.status(200).json({ + message: '내 정보 조회 성공', + data: { + id: user.id, + email: user.email, + nickname: user.nickname, + image: user.image, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }, + }); + } + catch (error) { + next(error); + } + }; + this.updateMe = async (req, res, next) => { + try { + const { nickname, image } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!nickname && !image) { + return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); + } + const updatedData = { + ...(nickname && { nickname }), + ...(image && { image }), + }; + const updatedUser = await this.userService.updateUser(user.id, updatedData); + res.status(200).json({ + message: '내 정보 수정에 성공했습니다.', + data: { + id: updatedUser.id, + email: updatedUser.email, + nickname: updatedUser.nickname, + image: updatedUser.image, + createdAt: updatedUser.createdAt, + updatedAt: updatedUser.updatedAt, + }, + }); + } + catch (error) { + next(error); + } + }; + this.changePassword = async (req, res, next) => { + try { + const { currentPassword, newPassword, confirmNewPassword } = req.body; + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + if (!currentPassword || !newPassword || !confirmNewPassword) { + return res.status(400).json({ message: '모든 정보를 입력해주세요.' }); + } + if (newPassword !== confirmNewPassword) { + return res.status(400).json({ message: '새 비밀번호와 확인 비밀번호가 일치하지 않습니다.' }); + } + const existingUser = await this.userService.getUserById(user.id); + if (!existingUser) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const isPasswordMatched = await bcrypt_1.default.compare(currentPassword, existingUser.password); + if (!isPasswordMatched) { + return res.status(401).json({ message: '현재 비밀번호가 일치하지 않습니다.' }); + } + const hashedNewPassword = await bcrypt_1.default.hash(newPassword, 10); + await this.userService.updatePassword(user.id, hashedNewPassword); + res.status(200).json({ message: '비밀번호 변경이 완료되었습니다.' }); + } + catch (error) { + next(error); + } + }; + this.getMyProducts = async (req, res, next) => { + try { + const { user } = req; + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + const products = await this.userService.getProductsByUserId(user.id); + res.status(200).json({ + message: '내가 작성한 상품 목록 조회에 성공했습니다.', + data: products, + }); + } + catch (error) { + next(error); + } + }; + this.refreshToken = async (req, res, next) => { + try { + const { refreshToken } = req.cookies; + if (!refreshToken) { + return res.status(401).json({ message: 'Refresh Token이 없습니다.' }); + } + const decodedToken = jsonwebtoken_1.default.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET_KEY); + const userId = decodedToken.userId; + const user = await this.userService.getUserById(userId); + if (!user) { + return res.status(401).json({ message: '사용자를 찾을 수 없습니다.' }); + } + const isRefreshTokenMatched = await bcrypt_1.default.compare(refreshToken, user.refreshToken); + if (!isRefreshTokenMatched) { + return res.status(401).json({ message: 'Refresh Token이 유효하지 않습니다.' }); + } + const newAccessToken = jsonwebtoken_1.default.sign({ userId: user.id }, process.env.JWT_SECRET_KEY, { + expiresIn: '12h', + }); + return res.status(200).json({ + message: 'Access Token이 재발급되었습니다.', + data: { accessToken: newAccessToken }, + }); + } + catch (error) { + if (error instanceof Error) { + if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') { + return res.status(401).json({ message: 'Refresh Token이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요 ' }); + } + } + next(error); + } + }; + this.userService = userService; + } +} +exports.default = UsersController; diff --git a/dist/dtos/ArticleDto.js b/dist/dtos/ArticleDto.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/dist/dtos/ArticleDto.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/dist/dtos/ProductDto.js b/dist/dtos/ProductDto.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/dist/dtos/ProductDto.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/dist/dtos/UserDto.js b/dist/dtos/UserDto.js new file mode 100644 index 000000000..c8ad2e549 --- /dev/null +++ b/dist/dtos/UserDto.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/dist/repositories/ArticleRepository.js b/dist/repositories/ArticleRepository.js new file mode 100644 index 000000000..0addad0e0 --- /dev/null +++ b/dist/repositories/ArticleRepository.js @@ -0,0 +1,24 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const index_1 = __importDefault(require("../index")); +class ArticleRepository { + async findArticleById(id) { + return index_1.default.article.findUnique({ where: { id } }); + } + async findArticles(options) { + return index_1.default.article.findMany(options); + } + async createArticle(data) { + return index_1.default.article.create({ data }); + } + async updateArticle(id, data) { + return index_1.default.article.update({ where: { id }, data }); + } + async deleteArticle(id) { + return index_1.default.article.delete({ where: { id } }); + } +} +exports.default = ArticleRepository; diff --git a/dist/repositories/CommentRepository.js b/dist/repositories/CommentRepository.js new file mode 100644 index 000000000..912326e01 --- /dev/null +++ b/dist/repositories/CommentRepository.js @@ -0,0 +1,24 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const index_1 = __importDefault(require("../index")); +class CommentRepository { + async findCommentById(id) { + return index_1.default.comment.findUnique({ where: { id } }); + } + async findComments(options) { + return index_1.default.comment.findMany(options); + } + async createComment(data) { + return index_1.default.comment.create({ data }); + } + async updateComment(id, data) { + return index_1.default.comment.update({ where: { id }, data }); + } + async deleteComment(id) { + return index_1.default.comment.delete({ where: { id } }); + } +} +exports.default = CommentRepository; diff --git a/dist/repositories/LikeRepository.js b/dist/repositories/LikeRepository.js new file mode 100644 index 000000000..013636883 --- /dev/null +++ b/dist/repositories/LikeRepository.js @@ -0,0 +1,31 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const index_1 = __importDefault(require("../index")); +class LikeRepository { + async findLikeById(id) { + return index_1.default.like.findUnique({ where: { id } }); + } + async findLikes(options) { + return index_1.default.like.findMany(options); + } + async createLike(data) { + return index_1.default.like.create({ data }); + } + async deleteLike(id) { + return index_1.default.like.delete({ where: { id } }); + } + async findLikeByUserIdAndProductId(userId, productId) { + return index_1.default.like.findFirst({ + where: { userId, productId }, + }); + } + async findLikeByUserIdAndArticleId(userId, articleId) { + return index_1.default.like.findFirst({ + where: { userId, articleId }, + }); + } +} +exports.default = LikeRepository; diff --git a/dist/repositories/ProductRepository.js b/dist/repositories/ProductRepository.js new file mode 100644 index 000000000..06d9bb10b --- /dev/null +++ b/dist/repositories/ProductRepository.js @@ -0,0 +1,24 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const index_1 = __importDefault(require("../index")); +class ProductRepository { + async findProductById(id) { + return index_1.default.product.findUnique({ where: { id } }); + } + async findProducts(options) { + return index_1.default.product.findMany(options); + } + async createProduct(data) { + return index_1.default.product.create({ data }); + } + async updateProduct(id, data) { + return index_1.default.product.update({ where: { id }, data }); + } + async deleteProduct(id) { + return index_1.default.product.delete({ where: { id } }); + } +} +exports.default = ProductRepository; diff --git a/dist/repositories/UserRepository.js b/dist/repositories/UserRepository.js new file mode 100644 index 000000000..aa2a2ecd2 --- /dev/null +++ b/dist/repositories/UserRepository.js @@ -0,0 +1,31 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const index_1 = __importDefault(require("../index")); +class UserRepository { + async findUserById(id) { + return index_1.default.user.findUnique({ where: { id } }); + } + async findUserByEmail(email) { + return index_1.default.user.findUnique({ where: { email } }); + } + async createUser(data) { + return index_1.default.user.create({ data }); + } + async updateUser(id, data) { + return index_1.default.user.update({ where: { id }, data }); + } + async deleteUser(id) { + return index_1.default.user.delete({ where: { id } }); + } + async findProductsByUserId(userId) { + const userWithProducts = await index_1.default.user.findUnique({ + where: { id: userId }, + include: { products: true }, + }); + return userWithProducts ? userWithProducts.products : null; + } +} +exports.default = UserRepository; diff --git a/dist/routes/articles.router.js b/dist/routes/articles.router.js index bf19ccf20..23aae2d50 100644 --- a/dist/routes/articles.router.js +++ b/dist/routes/articles.router.js @@ -4,285 +4,37 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = require("express"); -const index_1 = __importDefault(require("../index")); // Import prisma from index.ts -const validation_middleware_1 = require("../middlewares/validation.middleware"); +const ArticlesController_1 = __importDefault(require("../controllers/ArticlesController")); +const ArticleService_1 = __importDefault(require("../ArticleService")); +const ArticleRepository_1 = __importDefault(require("../repositories/ArticleRepository")); const auth_middleware_1 = __importDefault(require("../middlewares/auth.middleware")); const optionalAuth_middleware_1 = __importDefault(require("../middlewares/optionalAuth.middleware")); +const validation_middleware_1 = require("../middlewares/validation.middleware"); const router = (0, express_1.Router)(); +// Initialize repositories and services +const articleRepository = new ArticleRepository_1.default(); +const articleService = new ArticleService_1.default(articleRepository); +const articlesController = new ArticlesController_1.default(articleService); //article registration router .route('/articles') - .post(auth_middleware_1.default, validation_middleware_1.validateArticle, async (req, res, next) => { - try { - const { title, content } = req.body; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - const article = await index_1.default.article.create({ data: { - title, - content, - userId: user.id, - } - }); - res.status(201).json(article); - } - catch (error) { - next(error); - } -}) + .post(auth_middleware_1.default, validation_middleware_1.validateArticle, articlesController.createArticle) // 게시글 목록 조회 - .get(optionalAuth_middleware_1.default, async (req, res, next) => { - try { - const { sort, search } = req.query; - let page = parseInt(req.query.page) || 1; - let limit = parseInt(req.query.limit) || 10; - let offset = (page - 1) * limit; - const where = search - ? { - OR: [ - { title: { contains: search, mode: 'insensitive' } }, - { content: { contains: search, mode: 'insensitive' } }, - ], - } - : {}; - const user = req.user; - const articles = await index_1.default.article.findMany({ - where, - select: { id: true, title: true, content: true, createdAt: true, userId: true, updatedAt: true }, - orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, - skip: offset, - take: limit, - }); - let responseArticles = articles; - if (user) { - const articleIds = articles.map(article => article.id); - const likes = await index_1.default.like.findMany({ - where: { - userId: user.id, - articleId: { in: articleIds }, - }, - }); - const likedArticleIds = new Set(likes.map(like => like.articleId)); - responseArticles = articles.map(article => ({ - ...article, - isLiked: likedArticleIds.has(article.id), - })); - } - else { - responseArticles = articles.map(article => ({ - ...article, - isLiked: false, - })); - } - res.status(200).json(responseArticles); - } - catch (error) { - next(error); - } -}); + .get(optionalAuth_middleware_1.default, articlesController.getArticles); // article detail, modify, delete router .route('/articles/:articleId') - .get(optionalAuth_middleware_1.default, async (req, res, next) => { - try { - const { articleId } = req.params; - const user = req.user; - const article = await index_1.default.article.findUnique({ - where: { id: parseInt(articleId) }, - select: { id: true, title: true, content: true, createdAt: true, userId: true, updatedAt: true }, - }); - if (!article) { - return res.status(404).json({ message: '게시글을 찾을 수 없습니다.' }); - } - let isLiked = false; - if (user) { - const like = await index_1.default.like.findFirst({ - where: { - articleId: article.id, - userId: user.id, - }, - }); - if (like) { - isLiked = true; - } - } - const responseArticle = { ...article, isLiked }; - res.status(200).json(responseArticle); - } - catch (error) { - next(error); - } -}) - .patch(auth_middleware_1.default, validation_middleware_1.validateArticle, async (req, res, next) => { - try { - const { articleId } = req.params; - const { title, content } = req.body; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - // 게시글 소유 확인 - const article = await index_1.default.article.findUnique({ where: { id: parseInt(articleId) } }); - if (!article || article.userId !== user.id) { - return res.status(403).json({ message: '게시글 수정 권한이 없습니다.' }); - } - const updatedArticle = await index_1.default.article.update({ - where: { id: parseInt(articleId) }, - data: { title, content }, - }); - res.status(200).json(updatedArticle); - } - catch (error) { - next(error); - } -}) - .delete(auth_middleware_1.default, async (req, res, next) => { - try { - const { articleId } = req.params; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - const article = await index_1.default.article.findUnique({ where: { id: parseInt(articleId) } }); - if (!article || article.userId !== user.id) { - return res.status(403).json({ message: '게시글 삭제 권한이 없습니다.' }); - } - await index_1.default.article.delete({ where: { id: parseInt(articleId) } }); - res.status(204).send(); - } - catch (error) { - next(error); - } -}); + .get(optionalAuth_middleware_1.default, articlesController.getArticleById) + .patch(auth_middleware_1.default, validation_middleware_1.validateArticle, articlesController.updateArticle) + .delete(auth_middleware_1.default, articlesController.deleteArticle); // article comment creation -router.post('/articles/:articleId/comments', auth_middleware_1.default, async (req, res, next) => { - try { - const { articleId } = req.params; - const { content } = req.body; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - if (!content) - return res.status(400).json({ message: '댓글을 입력해주세요.' }); - const newComment = await index_1.default.comment.create({ - data: { - content, - articleId: parseInt(articleId), - userId: user.id, - }, - }); - res.status(201).json(newComment); - } - catch (error) { - next(error); - } -}); +router.post('/articles/:articleId/comments', auth_middleware_1.default, articlesController.createComment); // article comments check -router.get('/articles/:articleId/comments', async (req, res, next) => { - try { - const { articleId } = req.params; - let cursor = req.query.cursor ? parseInt(req.query.cursor) : undefined; - let limit = parseInt(req.query.limit) || 10; - const comments = await index_1.default.comment.findMany({ - where: { articleId: parseInt(articleId) }, - select: { id: true, content: true, createdAt: true, userId: true }, - orderBy: { createdAt: 'desc' }, - cursor: cursor ? { id: cursor } : undefined, - take: limit, - skip: cursor ? 1 : 0, - }); - res.status(200).json(comments); - } - catch (error) { - next(error); - } -}); +router.get('/articles/:articleId/comments', articlesController.getComments); //article comment modify -router.patch('/articles/comments/:commentId', auth_middleware_1.default, async (req, res, next) => { - try { - const { commentId } = req.params; - const { content } = req.body; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - if (!content) - return res.status(400).json({ message: '수정할 내용을 입력하세요.' }); - const comment = await index_1.default.comment.findUnique({ where: { id: parseInt(commentId) } }); - if (!comment || comment.userId !== user.id) { - return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); - } - const updatedComment = await index_1.default.comment.update({ - where: { id: parseInt(commentId) }, - data: { content }, - }); - res.status(200).json(updatedComment); - } - catch (error) { - next(error); - } -}); +router.patch('/articles/comments/:commentId', auth_middleware_1.default, articlesController.updateComment); //article comment delete -router.delete('/articles/comments/:commentId', auth_middleware_1.default, async (req, res, next) => { - try { - const { commentId } = req.params; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - const comment = await index_1.default.comment.findUnique({ where: { id: parseInt(commentId) } }); - if (!comment || comment.userId !== user.id) { - return res.status(403).json({ message: '댓글 삭제 권환이 없습니다.' }); - } - await index_1.default.comment.delete({ where: { id: parseInt(commentId) } }); - res.status(204).send(); - } - catch (error) { - next(error); - } -}); +router.delete('/articles/comments/:commentId', auth_middleware_1.default, articlesController.deleteComment); // 게시글 좋아요 API -router.post('/:articleId/like', auth_middleware_1.default, async (req, res, next) => { - try { - const { articleId } = req.params; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - // 게시글 존재 확인 - const article = await index_1.default.article.findUnique({ where: { id: parseInt(articleId) } }); - if (!article) { - return res.status(404).json({ message: '게시글을 찾을 수 없습니다.' }); - } - // 기존 좋아요 확인 - const existingLike = await index_1.default.like.findFirst({ - where: { - userId: user.id, - articleId: parseInt(articleId), - }, - }); - if (existingLike) { - // 좋아요가 이미 존재하면 취소 - await index_1.default.like.delete({ - where: { id: existingLike.id }, - }); - res.status(200).json({ message: '게시글 좋아요를 취소했습니다.' }); - } - else { - // 좋아요가 없으면 좋아요 생성 - await index_1.default.like.create({ - data: { - userId: user.id, - articleId: parseInt(articleId), - }, - }); - res.status(201).json({ message: '게시글에 좋아요를 눌렀습니다.' }); - } - } - catch (error) { - next(error); - } -}); +router.post('/:articleId/like', auth_middleware_1.default, articlesController.toggleLike); exports.default = router; diff --git a/dist/routes/products.router.js b/dist/routes/products.router.js index bdd3ff388..44b03a72f 100644 --- a/dist/routes/products.router.js +++ b/dist/routes/products.router.js @@ -4,289 +4,35 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = require("express"); -const index_1 = __importDefault(require("../index")); // Import prisma from index.ts -const validation_middleware_1 = require("../middlewares/validation.middleware"); +const ProductsController_1 = __importDefault(require("../controllers/ProductsController")); +const ProductService_1 = __importDefault(require("../ProductService")); +const ProductRepository_1 = __importDefault(require("../repositories/ProductRepository")); const auth_middleware_1 = __importDefault(require("../middlewares/auth.middleware")); const optionalAuth_middleware_1 = __importDefault(require("../middlewares/optionalAuth.middleware")); +const validation_middleware_1 = require("../middlewares/validation.middleware"); const router = (0, express_1.Router)(); +// Initialize repositories and services +const productRepository = new ProductRepository_1.default(); +const productService = new ProductService_1.default(productRepository); +const productsController = new ProductsController_1.default(productService); // registration router -router.post('/products', auth_middleware_1.default, validation_middleware_1.validateProduct, async (req, res, next) => { - try { - const { name, description, price } = req.body; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - const product = await index_1.default.product.create({ - data: { - name, - content: description, // Map description to content - price, - userId: user.id, - }, - }); - res.status(201).json(product); - } - catch (error) { - next(error); - } -}); +router.post('/products', auth_middleware_1.default, validation_middleware_1.validateProduct, productsController.createProduct); // cherck router -router.get('/products', optionalAuth_middleware_1.default, async (req, res, next) => { - try { - const { sort, search } = req.query; - let page = parseInt(req.query.page) || 1; - let limit = parseInt(req.query.limit) || 10; - let offset = (page - 1) * limit; - const where = search - ? { - OR: [ - { name: { contains: search, mode: 'insensitive' } }, - { content: { contains: search, mode: 'insensitive' - } }, - ], - } - : {}; - const user = req.user; - const products = await index_1.default.product.findMany({ - where, - select: { id: true, name: true, content: true, createdAt: true, userId: true, updatedAt: true, status: true }, - orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, - skip: offset, - take: limit, - }); - let responseProducts = products; - if (user) { - // 로그인한 사용자의 좋아요 누른 상품 목록 조회 - const productIds = products.map(product => product.id); - const likes = await index_1.default.like.findMany({ - where: { - userId: user.id, - productId: { in: productIds }, - }, - }); - const likedProductIds = new Set(likes.map(like => like.productId)); - // 각 상품에 isLiked 필드 추가 - responseProducts = products.map(product => ({ - ...product, - isLiked: likedProductIds.has(product.id), - })); - } - else { - // 로그인 하지 않은 사용자의 경우 모든 isLiked = false - responseProducts = products.map(product => ({ - ...product, - isLiked: false, - })); - } - res.status(200).json(responseProducts); - } - catch (error) { - next(error); - } -}); +router.get('/products', optionalAuth_middleware_1.default, productsController.getProducts); // datail, modify, delete router .route('/products/:productId') - .get(optionalAuth_middleware_1.default, async (req, res, next) => { - try { - const { productId } = req.params; - const user = req.user; - const product = await index_1.default.product.findUnique({ - where: { id: parseInt(productId) }, - select: { id: true, name: true, content: true, createdAt: true, userId: true, updatedAt: true, status: true }, - }); - if (!product) - return res.status(404).json({ message: '상품을 찾을수 없습니다.' }); - let isLiked = false; - if (user) { - // 로그인한 경우 좋아요 여부 확인 - const like = await index_1.default.like.findFirst({ - where: { - productId: product.id, - userId: user.id, - }, - }); - if (like) { - isLiked = true; - } - } - const responseProduct = { ...product, isLiked }; - res.status(200).json(responseProduct); - } - catch (error) { - next(error); - } -}) - .patch(validation_middleware_1.validateProduct, auth_middleware_1.default, async (req, res, next) => { - try { - const { productId } = req.params; - const { name, description, price } = req.body; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - // 상품 소유자 확인 - const product = await index_1.default.product.findUnique({ where: { id: parseInt(productId) } }); - if (!product || product.userId !== user.id) { - return res.status(403).json({ message: '상품 수정 권한이 없습니다.' }); - } - const updatedProduct = await index_1.default.product.update({ - where: { id: parseInt(productId) }, - data: { name, content: description, price }, - }); - res.status(200).json(updatedProduct); - } - catch (error) { - next(error); - } -}) - .delete(auth_middleware_1.default, async (req, res, next) => { - try { - const { productId } = req.params; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - const product = await index_1.default.product.findUnique({ where: { id: parseInt(productId) } }); - if (!product || product.userId !== user.id) { - return res.status(403).json({ message: '상품 삭제 권한이 없습니다.' }); - } - await index_1.default.product.delete({ where: { id: parseInt(productId) } }); - res.status(204).send(); - } - catch (error) { - next(error); - } -}); + .get(optionalAuth_middleware_1.default, productsController.getProductById) + .patch(validation_middleware_1.validateProduct, auth_middleware_1.default, productsController.updateProduct) + .delete(auth_middleware_1.default, productsController.deleteProduct); // comment -router.post('/products/:productId/comments', auth_middleware_1.default, async (req, res, next) => { - try { - const { productId } = req.params; - const { content } = req.body; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - if (!content) - return res.status(400).json({ message: '댓글을 입력해주세요.' }); - const newComment = await index_1.default.comment.create({ - data: { - content, - productId: parseInt(productId), - userId: user.id, - }, - }); - res.status(201).json(newComment); - } - catch (error) { - next(error); - } -}); +router.post('/products/:productId/comments', auth_middleware_1.default, productsController.createComment); //comment check -router.get('/products/:productId/comments', async (req, res, next) => { - try { - const { productId } = req.params; - let cursor = req.query.cursor ? parseInt(req.query.cursor) : undefined; - let limit = parseInt(req.query.limit) || 10; - const comments = await index_1.default.comment.findMany({ - where: { productId: parseInt(productId) }, - select: { id: true, content: true, createdAt: true, userId: true }, - orderBy: { createdAt: 'desc' }, - cursor: cursor ? { id: cursor } : undefined, - take: limit, - skip: cursor ? 1 : 0, - }); - res.status(200).json(comments); - } - catch (error) { - next(error); - } -}); +router.get('/products/:productId/comments', productsController.getComments); // comment modify -router.patch('/products/comments/:commentId', auth_middleware_1.default, async (req, res, next) => { - try { - const { commentId } = req.params; - const { content } = req.body; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - if (!content) - return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); - const existingComment = await index_1.default.comment.findUnique({ where: { id: parseInt(commentId) } }); - if (!existingComment || existingComment.userId !== user.id) { - return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); - } - const updatedComment = await index_1.default.comment.update({ - where: { id: parseInt(commentId) }, - data: { content }, - }); - res.status(200).json(updatedComment); - } - catch (error) { - next(error); - } -}); +router.patch('/products/comments/:commentId', auth_middleware_1.default, productsController.updateComment); // comment delete -router.delete('/products/comments/:commentId', auth_middleware_1.default, async (req, res, next) => { - try { - const { commentId } = req.params; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - const existingComment = await index_1.default.comment.findUnique({ where: { id: parseInt(commentId) } }); - if (!existingComment || existingComment.userId !== user.id) { - return res.status(403).json({ message: '댓글 삭제 권한이 없습니다.' }); - } - await index_1.default.comment.delete({ where: { id: parseInt(commentId) } }); - res.status(204).send(); - } - catch (error) { - next(error); - } -}); +router.delete('/products/comments/:commentId', auth_middleware_1.default, productsController.deleteComment); // 상품 좋아요 API -router.post('/:productId/like', auth_middleware_1.default, async (req, res, next) => { - try { - const { productId } = req.params; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - // 상품 존재 확인 - const product = await index_1.default.product.findUnique({ where: { id: parseInt(productId) } }); - if (!product) { - return res.status(404).json({ message: '상품을 찾을 수 없습니다.' }); - } - // 기존 좋아요 확인 - const existingLike = await index_1.default.like.findFirst({ - where: { - userId: user.id, - productId: parseInt(productId), - }, - }); - if (existingLike) { - // 좋아요가 이미 있으면 좋아요 취소 - await index_1.default.like.delete({ - where: { id: existingLike.id }, - }); - res.status(200).json({ message: '상품 좋아요를 취소했습니다.' }); - } - else { - // 좋아요가 없으면 좋아요 생성 - await index_1.default.like.create({ - data: { - userId: user.id, - productId: parseInt(productId), - }, - }); - res.status(201).json({ message: '상품에 좋아요를 눌렀습니다.' }); - } - } - catch (error) { - next(error); - } -}); +router.post('/:productId/like', auth_middleware_1.default, productsController.toggleLike); exports.default = router; diff --git a/dist/routes/upload.router.js b/dist/routes/upload.router.js index 230d95cc7..5f9c3ef2c 100644 --- a/dist/routes/upload.router.js +++ b/dist/routes/upload.router.js @@ -4,10 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = require("express"); +const UploadController_1 = __importDefault(require("../controllers/UploadController")); const multer_1 = __importDefault(require("multer")); -const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); -const sharp_1 = __importDefault(require("sharp")); const router = (0, express_1.Router)(); //uploads 디렉토리가 없을 때 생성 const uploadDir = 'uploads/'; @@ -15,25 +14,8 @@ if (!fs_1.default.existsSync(uploadDir)) { fs_1.default.mkdirSync(uploadDir); } const storage = multer_1.default.memoryStorage(); -//multer middleware const upload = (0, multer_1.default)({ storage: storage }); +const uploadController = new UploadController_1.default(); //image api -router.post('/upload', upload.single('image'), async (req, res, next) => { - if (!req.file) { - return res.status(400).json({ message: '이미지 파일이 필요합니다.' }); - } - try { - const ext = path_1.default.extname(req.file.originalname); - const filename = Date.now() + ext; - const imagePath = path_1.default.join(uploadDir, filename); - await (0, sharp_1.default)(req.file.buffer) - .resize({ width: 500 }) - .toFile(imagePath); - const imageUrl = `/uploads/${filename}`; - res.status(201).json({ imageUrl: imageUrl }); - } - catch (error) { - next(error); - } -}); +router.post('/upload', upload.single('image'), uploadController.uploadImage); exports.default = router; diff --git a/dist/routes/users.router.js b/dist/routes/users.router.js index 667156b7f..c3d2d9196 100644 --- a/dist/routes/users.router.js +++ b/dist/routes/users.router.js @@ -4,204 +4,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = require("express"); +const UsersController_1 = __importDefault(require("../controllers/UsersController")); const UserService_1 = __importDefault(require("../UserService")); +const UserRepository_1 = __importDefault(require("../repositories/UserRepository")); const auth_middleware_1 = __importDefault(require("../middlewares/auth.middleware")); -const bcrypt_1 = __importDefault(require("bcrypt")); -const index_1 = __importDefault(require("../index")); // Import prisma from index.ts -const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); const router = (0, express_1.Router)(); -const userService = new UserService_1.default(); +// Initialize repositories and services +const userRepository = new UserRepository_1.default(); +const userService = new UserService_1.default(userRepository); +const usersController = new UsersController_1.default(userService); // 회원가입 API -router.post('/sign-up', async (req, res, next) => { - try { - const { email, nickname, password } = req.body; - // 유효성 검사 - if (!email || !nickname || !password) { - return res.status(400).json({ message: '모든 정보를 입력해주세요' }); - } - const newUser = await userService.signUp(email, nickname, password); - return res.status(201).json({ - message: '회원가입이 완료되었습니다.', - data: newUser, - }); - } - catch (error) { - if (error instanceof Error) { - return res.status(409).json({ message: error.message }); - } - next(error); - } -}); +router.post('/sign-up', usersController.signUp); // 로그인 API -router.post('/sign-in', async (req, res, next) => { - try { - const { email, password } = req.body; - if (!email || !password) { - return res.status(400).json({ message: '이메일과 비밀번호를 모두 입력해주세요.' }); - } - const { accessToken, refreshToken } = await userService.signIn(email, password); - //refresh token을 쿠키에 설정 - res.cookie('refreshToken', refreshToken, { - httpOnly: true, - secure: false, - maxAge: 1000 * 60 * 60 * 24 * 7, - }); - return res.status(200).json({ - message: '로그인에 성공했습니다.', - data: { accessToken }, - }); - } - catch (error) { - if (error instanceof Error) { - return res.status(401).json({ message: error.message }); - } - next(error); - } -}); +router.post('/sign-in', usersController.signIn); // 내 정보 조회 API -router.get('/me', auth_middleware_1.default, async (req, res, next) => { - try { - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - res.status(200).json({ - message: '내 정보 조회 성공', - data: { - id: user.id, - email: user.email, - nickname: user.nickname, - image: user.image, - createdAt: user.createdAt, - updatedAt: user.updatedAt, - }, - }); - } - catch (error) { - next(error); - } -}); +router.get('/me', auth_middleware_1.default, usersController.getMe); // 내 정보 수정 API -router.patch('/me', auth_middleware_1.default, async (req, res, next) => { - try { - const { nickname, image } = req.body; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - if (!nickname && !image) { - return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); - } - // 수정할 사용자 정보 - const updatedData = { - ...(nickname && { nickname }), - ...(image && { image }), - }; - const updatedUser = await index_1.default.user.update({ - where: { id: user.id }, - data: updatedData, - }); - res.status(200).json({ - message: '내 정보 수정에 성공했습니다.', - data: { - id: updatedUser.id, - email: updatedUser.email, - nickname: updatedUser.nickname, - image: updatedUser.image, - createdAt: updatedUser.createdAt, - updatedAt: updatedUser.updatedAt, - }, - }); - } - catch (error) { - next(error); - } -}); +router.patch('/me', auth_middleware_1.default, usersController.updateMe); // 비밀번호 변경 API -router.patch('/me/password', auth_middleware_1.default, async (req, res, next) => { - try { - const { currentPassword, newPassword, confirmNewPassword } = req.body; - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - if (!currentPassword || !newPassword || !confirmNewPassword) { - return res.status(400).json({ message: '모든 정보를 입력해주세요.' }); - } - if (newPassword !== confirmNewPassword) { - return res.status(400).json({ message: '새 비밀번호와 확인 비밀번호가 일치하지 않습니다.' }); - } - // 현재 비밀번호 확인 - const isPasswordMatched = await bcrypt_1.default.compare(currentPassword, user.password); - if (!isPasswordMatched) { - return res.status(401).json({ message: '현재 비밀번호가 일치하지 않습니다.' }); - } - // 새 비밀번호 해싱 - const hashedNewPassword = await bcrypt_1.default.hash(newPassword, 10); - await index_1.default.user.update({ - where: { id: user.id }, - data: { password: hashedNewPassword }, - }); - res.status(200).json({ message: '비밀번호 변경이 완료되었습니다.' }); - } - catch (error) { - next(error); - } -}); +router.patch('/me/password', auth_middleware_1.default, usersController.changePassword); // 내가 작성한 상품 목록 조회 API -router.get('/me/products', auth_middleware_1.default, async (req, res, next) => { - try { - const { user } = req; - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - const products = await index_1.default.product.findMany({ - where: { userId: user.id }, - orderBy: { createdAt: 'desc' }, - }); - res.status(200).json({ - message: '내가 작성한 상품 목록 조회에 성공했습니다.', - data: products, - }); - } - catch (error) { - next(error); - } -}); +router.get('/me/products', auth_middleware_1.default, usersController.getMyProducts); // Token 재발급 API -router.post('/token/refresh', async (req, res, next) => { - try { - const { refreshToken } = req.cookies; - if (!refreshToken) { - return res.status(401).json({ message: 'Refresh Token이 없습니다.' }); - } - const decodedToken = jsonwebtoken_1.default.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET_KEY); - const userId = decodedToken.userId; - const user = await index_1.default.user.findUnique({ where: { id: userId } }); - if (!user) { - return res.status(401).json({ message: '사용자를 찾을 수 없습니다.' }); - } - //DB에 저장된 hashed Refresh Token과 비교 - const isRefreshTokenMatched = await bcrypt_1.default.compare(refreshToken, user.refreshToken); - if (!isRefreshTokenMatched) { - return res.status(401).json({ message: 'Refresh Token이 유효하지 않습니다.' }); - } - // 새로운 Access Token 생성 - const newAccessToken = jsonwebtoken_1.default.sign({ userId: user.id }, process.env.JWT_SECRET_KEY, { - expiresIn: '12h', - }); - return res.status(200).json({ - message: 'Access Token이 재발급되었습니다.', - data: { accessToken: newAccessToken }, - }); - } - catch (error) { - if (error instanceof Error) { - if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') { - return res.status(401).json({ message: 'Refresh Token이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요 ' }); - } - } - next(error); - } -}); +router.post('/token/refresh', usersController.refreshToken); exports.default = router; diff --git a/src/ArticleService.ts b/src/ArticleService.ts index 8cef62625..6ae669771 100644 --- a/src/ArticleService.ts +++ b/src/ArticleService.ts @@ -2,7 +2,9 @@ import { Article as PrismaArticle, Prisma } from '@prisma/client'; import ArticleRepository from './repositories/ArticleRepository'; import { ArticleCreateDto, ArticleUpdateDto } from './dtos/ArticleDto'; -interface ArticleCreateServiceInput extends ArticleCreateDto { +interface ArticleCreateServiceInput { + title: string; + content: string; userId: number; } @@ -14,7 +16,11 @@ class ArticleService { } async createArticle(data: ArticleCreateServiceInput): Promise { - return this.articleRepository.createArticle(data); + const { userId, ...rest } = data; + return this.articleRepository.createArticle({ + ...rest, + user: { connect: { id: userId } }, + }); } async getArticleById(id: number): Promise { @@ -34,4 +40,4 @@ class ArticleService { } } -export default ArticleService; +export default ArticleService; \ No newline at end of file diff --git a/src/CommentService.ts b/src/CommentService.ts index e7d3db1d6..87f8d0193 100644 --- a/src/CommentService.ts +++ b/src/CommentService.ts @@ -20,7 +20,13 @@ class CommentService { } async createComment(data: CommentCreateInput): Promise { - return this.commentRepository.createComment(data); + const { userId, productId, articleId, ...rest } = data; + return this.commentRepository.createComment({ + ...rest, + user: { connect: { id: userId } }, + ...(productId && { product: { connect: { id: productId } } }), + ...(articleId && { article: { connect: { id: articleId } } }), + }); } async getCommentById(id: number): Promise { @@ -40,4 +46,4 @@ class CommentService { } } -export default CommentService; +export default CommentService; \ No newline at end of file diff --git a/src/LikeService.ts b/src/LikeService.ts index 65590bbe4..d4a87db91 100644 --- a/src/LikeService.ts +++ b/src/LikeService.ts @@ -15,7 +15,13 @@ class LikeService { } async createLike(data: LikeCreateInput): Promise { - return this.likeRepository.createLike(data); + const { userId, productId, articleId, ...rest } = data; + return this.likeRepository.createLike({ + ...rest, + user: { connect: { id: userId } }, + ...(productId && { product: { connect: { id: productId } } }), + ...(articleId && { article: { connect: { id: articleId } } }), + }); } async deleteLike(id: number): Promise { @@ -31,4 +37,4 @@ class LikeService { } } -export default LikeService; +export default LikeService; \ No newline at end of file diff --git a/src/ProductService.ts b/src/ProductService.ts index 0ac4fcc9a..cbea5c2fe 100644 --- a/src/ProductService.ts +++ b/src/ProductService.ts @@ -2,7 +2,10 @@ import { Product as PrismaProduct, Prisma } from '@prisma/client'; import ProductRepository from './repositories/ProductRepository'; import { ProductCreateDto, ProductUpdateDto } from './dtos/ProductDto'; -interface ProductCreateServiceInput extends ProductCreateDto { +interface ProductCreateServiceInput { + name: string; + content: string; + price: number; userId: number; status?: string; } @@ -15,7 +18,11 @@ class ProductService { } async createProduct(data: ProductCreateServiceInput): Promise { - return this.productRepository.createProduct(data); + const { userId, ...rest } = data; + return this.productRepository.createProduct({ + ...rest, + user: { connect: { id: userId } }, + }); } async getProductById(id: number): Promise { @@ -35,4 +42,4 @@ class ProductService { } } -export default ProductService; +export default ProductService; \ No newline at end of file From 823e702454b9a6e4ef267110ea7f708f2d53ee77 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:10:57 +0900 Subject: [PATCH 51/63] =?UTF-8?q?import=EB=AC=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/ArticleService.js | 6 +++++- dist/CommentService.js | 8 +++++++- dist/LikeService.js | 8 +++++++- dist/ProductService.js | 6 +++++- src/UserService.ts | 2 +- src/repositories/UserRepository.ts | 2 +- 6 files changed, 26 insertions(+), 6 deletions(-) diff --git a/dist/ArticleService.js b/dist/ArticleService.js index ce82e33e7..86d90a8cc 100644 --- a/dist/ArticleService.js +++ b/dist/ArticleService.js @@ -5,7 +5,11 @@ class ArticleService { this.articleRepository = articleRepository; } async createArticle(data) { - return this.articleRepository.createArticle(data); + const { userId, ...rest } = data; + return this.articleRepository.createArticle({ + ...rest, + user: { connect: { id: userId } }, + }); } async getArticleById(id) { return this.articleRepository.findArticleById(id); diff --git a/dist/CommentService.js b/dist/CommentService.js index 68f616a11..4aa7edb7e 100644 --- a/dist/CommentService.js +++ b/dist/CommentService.js @@ -5,7 +5,13 @@ class CommentService { this.commentRepository = commentRepository; } async createComment(data) { - return this.commentRepository.createComment(data); + const { userId, productId, articleId, ...rest } = data; + return this.commentRepository.createComment({ + ...rest, + user: { connect: { id: userId } }, + ...(productId && { product: { connect: { id: productId } } }), + ...(articleId && { article: { connect: { id: articleId } } }), + }); } async getCommentById(id) { return this.commentRepository.findCommentById(id); diff --git a/dist/LikeService.js b/dist/LikeService.js index 00a8690c4..692a7f6a8 100644 --- a/dist/LikeService.js +++ b/dist/LikeService.js @@ -5,7 +5,13 @@ class LikeService { this.likeRepository = likeRepository; } async createLike(data) { - return this.likeRepository.createLike(data); + const { userId, productId, articleId, ...rest } = data; + return this.likeRepository.createLike({ + ...rest, + user: { connect: { id: userId } }, + ...(productId && { product: { connect: { id: productId } } }), + ...(articleId && { article: { connect: { id: articleId } } }), + }); } async deleteLike(id) { return this.likeRepository.deleteLike(id); diff --git a/dist/ProductService.js b/dist/ProductService.js index fa953cfff..9a9d5961f 100644 --- a/dist/ProductService.js +++ b/dist/ProductService.js @@ -5,7 +5,11 @@ class ProductService { this.productRepository = productRepository; } async createProduct(data) { - return this.productRepository.createProduct(data); + const { userId, ...rest } = data; + return this.productRepository.createProduct({ + ...rest, + user: { connect: { id: userId } }, + }); } async getProductById(id) { return this.productRepository.findProductById(id); diff --git a/src/UserService.ts b/src/UserService.ts index da748b9be..0a0e8881b 100644 --- a/src/UserService.ts +++ b/src/UserService.ts @@ -1,6 +1,6 @@ import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; -import { User as PrismaUser, Prisma } from '@prisma/client'; +import { User as PrismaUser, Product } from '@prisma/client'; import UserRepository from './repositories/UserRepository'; import { UserCreateDto, UserUpdateDto } from './dtos/UserDto'; diff --git a/src/repositories/UserRepository.ts b/src/repositories/UserRepository.ts index a6ce7a1be..5dc53b242 100644 --- a/src/repositories/UserRepository.ts +++ b/src/repositories/UserRepository.ts @@ -1,5 +1,5 @@ import prisma from '../index'; -import { User as PrismaUser, Prisma } from '@prisma/client'; +import { User as PrismaUser, Prisma, Product } from '@prisma/client'; class UserRepository { async findUserById(id: number): Promise { From 9b6f392d16cca3706ba7a59fc585cd3aa4ab74cb Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:12:22 +0900 Subject: [PATCH 52/63] =?UTF-8?q?controller=20-=20select=EB=AC=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ArticlesController.ts | 1 - src/controllers/ProductsController.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/controllers/ArticlesController.ts b/src/controllers/ArticlesController.ts index a9be0d2aa..1064b5042 100644 --- a/src/controllers/ArticlesController.ts +++ b/src/controllers/ArticlesController.ts @@ -60,7 +60,6 @@ class ArticlesController { const user = req.user; const articles = await this.articleService.getArticles({ where, - select: { id: true, title: true, content: true, createdAt: true, userId: true, updatedAt: true }, orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, skip: offset, take: limit, diff --git a/src/controllers/ProductsController.ts b/src/controllers/ProductsController.ts index 7d54c669b..0de7625c1 100644 --- a/src/controllers/ProductsController.ts +++ b/src/controllers/ProductsController.ts @@ -62,7 +62,6 @@ class ProductsController { const user = req.user; const products = await this.productService.getProducts({ where, - select: { id: true, name: true, content: true, price: true, createdAt: true, userId: true, updatedAt: true, status: true }, orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, skip: offset, take: limit, From 9261f171063574bff06781110079dda146c9e69d Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:13:07 +0900 Subject: [PATCH 53/63] =?UTF-8?q?controller=20-=20select=EB=AC=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ArticlesController.ts | 1 - src/controllers/ProductsController.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/controllers/ArticlesController.ts b/src/controllers/ArticlesController.ts index 1064b5042..339482f48 100644 --- a/src/controllers/ArticlesController.ts +++ b/src/controllers/ArticlesController.ts @@ -199,7 +199,6 @@ class ArticlesController { const comments = await this.commentService.getComments({ where: { articleId: parseInt(articleId) }, - select: { id: true, content: true, createdAt: true, userId: true }, orderBy: { createdAt: 'desc' }, cursor: cursor ? { id: cursor } : undefined, take: limit, diff --git a/src/controllers/ProductsController.ts b/src/controllers/ProductsController.ts index 0de7625c1..1246d539d 100644 --- a/src/controllers/ProductsController.ts +++ b/src/controllers/ProductsController.ts @@ -201,7 +201,6 @@ class ProductsController { const comments = await this.commentService.getComments({ where: { productId: parseInt(productId) }, - select: { id: true, content: true, createdAt: true, userId: true }, orderBy: { createdAt: 'desc' }, cursor: cursor ? { id: cursor } : undefined, take: limit, From 72f1bf1de50bb03bb68f31f5ac1731fde041c9ab Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:14:05 +0900 Subject: [PATCH 54/63] =?UTF-8?q?findlikes=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/LikeService.ts | 6 +++++- src/controllers/ArticlesController.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/LikeService.ts b/src/LikeService.ts index d4a87db91..e089a0920 100644 --- a/src/LikeService.ts +++ b/src/LikeService.ts @@ -35,6 +35,10 @@ class LikeService { async findLikeByUserIdAndArticleId(userId: number, articleId: number): Promise { return this.likeRepository.findLikeByUserIdAndArticleId(userId, articleId); } + + async findLikes(options?: { skip?: number; take?: number; where?: Prisma.LikeWhereInput; orderBy?: Prisma.LikeOrderByWithRelationInput }): Promise { + return this.likeRepository.findLikes(options); + } } -export default LikeService; \ No newline at end of file +export default LikeService; diff --git a/src/controllers/ArticlesController.ts b/src/controllers/ArticlesController.ts index 339482f48..335acbe4c 100644 --- a/src/controllers/ArticlesController.ts +++ b/src/controllers/ArticlesController.ts @@ -76,7 +76,7 @@ class ArticlesController { }, }); - const likedArticleIds = new Set(likes.map(like => like.articleId)); + const likedArticleIds = new Set(likes.map((like: Prisma.Like) => like.articleId)); responseArticles = articles.map(article => ({ ...article, From e4603531812dad14a894f57cfb61bdc26440ad3a Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:15:38 +0900 Subject: [PATCH 55/63] =?UTF-8?q?signUp=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ProductsController.ts | 2 +- src/controllers/UsersController.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/ProductsController.ts b/src/controllers/ProductsController.ts index 1246d539d..142ccd980 100644 --- a/src/controllers/ProductsController.ts +++ b/src/controllers/ProductsController.ts @@ -78,7 +78,7 @@ class ProductsController { }, }); - const likedProductIds = new Set(likes.map(like => like.productId)); + const likedProductIds = new Set(likes.map((like: Prisma.Like) => like.productId)); responseProducts = products.map(product => ({ ...product, diff --git a/src/controllers/UsersController.ts b/src/controllers/UsersController.ts index ee17d068b..19dfaf35c 100644 --- a/src/controllers/UsersController.ts +++ b/src/controllers/UsersController.ts @@ -20,7 +20,7 @@ class UsersController { return res.status(400).json({ message: '모든 정보를 입력해주세요'}); } - const newUser = await this.userService.signUp(email, nickname, password); + const newUser = await this.userService.signUp({ email, nickname, password }); return res.status(201).json({ message: '회원가입이 완료되었습니다.', From b60f8e1fff3e7ef5fe972048d6eb9b9027bdd484 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:18:41 +0900 Subject: [PATCH 56/63] =?UTF-8?q?UserService=20import=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/LikeService.js | 3 +++ dist/controllers/ArticlesController.js | 4 +--- dist/controllers/ProductsController.js | 4 +--- dist/controllers/UsersController.js | 2 +- src/UserService.ts | 2 +- src/controllers/ArticlesController.ts | 2 +- 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/dist/LikeService.js b/dist/LikeService.js index 692a7f6a8..8453acd5f 100644 --- a/dist/LikeService.js +++ b/dist/LikeService.js @@ -22,5 +22,8 @@ class LikeService { async findLikeByUserIdAndArticleId(userId, articleId) { return this.likeRepository.findLikeByUserIdAndArticleId(userId, articleId); } + async findLikes(options) { + return this.likeRepository.findLikes(options); + } } exports.default = LikeService; diff --git a/dist/controllers/ArticlesController.js b/dist/controllers/ArticlesController.js index d2bcf3191..ff8c38dbb 100644 --- a/dist/controllers/ArticlesController.js +++ b/dist/controllers/ArticlesController.js @@ -44,7 +44,6 @@ class ArticlesController { const user = req.user; const articles = await this.articleService.getArticles({ where, - select: { id: true, title: true, content: true, createdAt: true, userId: true, updatedAt: true }, orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, skip: offset, take: limit, @@ -58,7 +57,7 @@ class ArticlesController { articleId: { in: articleIds }, }, }); - const likedArticleIds = new Set(likes.map(like => like.articleId)); + const likedArticleIds = new Set(likes.map((like) => like.articleId)); responseArticles = articles.map(article => ({ ...article, isLiked: likedArticleIds.has(article.id), @@ -166,7 +165,6 @@ class ArticlesController { let limit = parseInt(req.query.limit) || 10; const comments = await this.commentService.getComments({ where: { articleId: parseInt(articleId) }, - select: { id: true, content: true, createdAt: true, userId: true }, orderBy: { createdAt: 'desc' }, cursor: cursor ? { id: cursor } : undefined, take: limit, diff --git a/dist/controllers/ProductsController.js b/dist/controllers/ProductsController.js index 3f38af87e..d85ebb9c1 100644 --- a/dist/controllers/ProductsController.js +++ b/dist/controllers/ProductsController.js @@ -46,7 +46,6 @@ class ProductsController { const user = req.user; const products = await this.productService.getProducts({ where, - select: { id: true, name: true, content: true, price: true, createdAt: true, userId: true, updatedAt: true, status: true }, orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, skip: offset, take: limit, @@ -60,7 +59,7 @@ class ProductsController { productId: { in: productIds }, }, }); - const likedProductIds = new Set(likes.map(like => like.productId)); + const likedProductIds = new Set(likes.map((like) => like.productId)); responseProducts = products.map(product => ({ ...product, isLiked: likedProductIds.has(product.id), @@ -168,7 +167,6 @@ class ProductsController { let limit = parseInt(req.query.limit) || 10; const comments = await this.commentService.getComments({ where: { productId: parseInt(productId) }, - select: { id: true, content: true, createdAt: true, userId: true }, orderBy: { createdAt: 'desc' }, cursor: cursor ? { id: cursor } : undefined, take: limit, diff --git a/dist/controllers/UsersController.js b/dist/controllers/UsersController.js index ed1d04e98..e8a161d36 100644 --- a/dist/controllers/UsersController.js +++ b/dist/controllers/UsersController.js @@ -13,7 +13,7 @@ class UsersController { if (!email || !nickname || !password) { return res.status(400).json({ message: '모든 정보를 입력해주세요' }); } - const newUser = await this.userService.signUp(email, nickname, password); + const newUser = await this.userService.signUp({ email, nickname, password }); return res.status(201).json({ message: '회원가입이 완료되었습니다.', data: newUser, diff --git a/src/UserService.ts b/src/UserService.ts index 0a0e8881b..68b157712 100644 --- a/src/UserService.ts +++ b/src/UserService.ts @@ -1,6 +1,6 @@ import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; -import { User as PrismaUser, Product } from '@prisma/client'; +import { User as PrismaUser, Product, Prisma } from '@prisma/client'; import UserRepository from './repositories/UserRepository'; import { UserCreateDto, UserUpdateDto } from './dtos/UserDto'; diff --git a/src/controllers/ArticlesController.ts b/src/controllers/ArticlesController.ts index 335acbe4c..aa7007ebf 100644 --- a/src/controllers/ArticlesController.ts +++ b/src/controllers/ArticlesController.ts @@ -1,6 +1,6 @@ import { Router, Request, Response, NextFunction } from 'express'; import ArticleService from '../ArticleService'; -import { Article as PrismaArticle, Prisma } from '@prisma/client'; +import { Article as PrismaArticle, Prisma, Like } from '@prisma/client'; import prisma from '../index'; import { ArticleCreateDto, ArticleUpdateDto } from '../dtos/ArticleDto'; import CommentService from '../CommentService'; From ba427c856d2fbcde6eb4f5d5ffe014b49aee523d Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:19:34 +0900 Subject: [PATCH 57/63] =?UTF-8?q?=20import=EB=AC=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ProductsController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/ProductsController.ts b/src/controllers/ProductsController.ts index 142ccd980..f53aaaf4c 100644 --- a/src/controllers/ProductsController.ts +++ b/src/controllers/ProductsController.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from 'express'; import ProductService from '../ProductService'; -import { Product as PrismaProduct, Prisma } from '@prisma/client'; +import { Product as PrismaProduct, Prisma, Like } from '@prisma/client'; import prisma from '../index'; import { ProductCreateDto, ProductUpdateDto } from '../dtos/ProductDto'; import CommentService from '../CommentService'; From 64f1a15939d5355091097c021190f11e5bdcbdfc Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:20:55 +0900 Subject: [PATCH 58/63] =?UTF-8?q?cursor,=20data=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ArticlesController.ts | 1 - src/controllers/ProductsController.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/controllers/ArticlesController.ts b/src/controllers/ArticlesController.ts index aa7007ebf..58d2c6492 100644 --- a/src/controllers/ArticlesController.ts +++ b/src/controllers/ArticlesController.ts @@ -200,7 +200,6 @@ class ArticlesController { const comments = await this.commentService.getComments({ where: { articleId: parseInt(articleId) }, orderBy: { createdAt: 'desc' }, - cursor: cursor ? { id: cursor } : undefined, take: limit, skip: cursor ? 1 : 0, }); diff --git a/src/controllers/ProductsController.ts b/src/controllers/ProductsController.ts index f53aaaf4c..db8bf1b9f 100644 --- a/src/controllers/ProductsController.ts +++ b/src/controllers/ProductsController.ts @@ -140,7 +140,7 @@ class ProductsController { const updatedProduct = await this.productService.updateProduct(parseInt(productId), { name, - content: description, + description, price, }); res.status(200).json(updatedProduct); @@ -202,7 +202,6 @@ class ProductsController { const comments = await this.commentService.getComments({ where: { productId: parseInt(productId) }, orderBy: { createdAt: 'desc' }, - cursor: cursor ? { id: cursor } : undefined, take: limit, skip: cursor ? 1 : 0, }); From b73fd07be35ddb737a713f26984eb84d119e4727 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:22:25 +0900 Subject: [PATCH 59/63] =?UTF-8?q?Prisma.Product=20->=20Product=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/controllers/ArticlesController.js | 1 - dist/controllers/ProductsController.js | 3 +-- src/UserService.ts | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/dist/controllers/ArticlesController.js b/dist/controllers/ArticlesController.js index ff8c38dbb..a5026ccd6 100644 --- a/dist/controllers/ArticlesController.js +++ b/dist/controllers/ArticlesController.js @@ -166,7 +166,6 @@ class ArticlesController { const comments = await this.commentService.getComments({ where: { articleId: parseInt(articleId) }, orderBy: { createdAt: 'desc' }, - cursor: cursor ? { id: cursor } : undefined, take: limit, skip: cursor ? 1 : 0, }); diff --git a/dist/controllers/ProductsController.js b/dist/controllers/ProductsController.js index d85ebb9c1..bf4ef522f 100644 --- a/dist/controllers/ProductsController.js +++ b/dist/controllers/ProductsController.js @@ -112,7 +112,7 @@ class ProductsController { } const updatedProduct = await this.productService.updateProduct(parseInt(productId), { name, - content: description, + description, price, }); res.status(200).json(updatedProduct); @@ -168,7 +168,6 @@ class ProductsController { const comments = await this.commentService.getComments({ where: { productId: parseInt(productId) }, orderBy: { createdAt: 'desc' }, - cursor: cursor ? { id: cursor } : undefined, take: limit, skip: cursor ? 1 : 0, }); diff --git a/src/UserService.ts b/src/UserService.ts index 68b157712..380875b59 100644 --- a/src/UserService.ts +++ b/src/UserService.ts @@ -85,7 +85,7 @@ class UserService { return this.userRepository.updateUser(id, { password: newPasswordHash }); }; - public getProductsByUserId = async (userId: number): Promise => { + public getProductsByUserId = async (userId: number): Promise => { return this.userRepository.findProductsByUserId(userId); }; } From a1b0d28e12bcb3bea95dd9d416f3ef674e8a2fce Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:23:11 +0900 Subject: [PATCH 60/63] Prisma.Like -> Like --- src/controllers/ArticlesController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/ArticlesController.ts b/src/controllers/ArticlesController.ts index 58d2c6492..0c864c442 100644 --- a/src/controllers/ArticlesController.ts +++ b/src/controllers/ArticlesController.ts @@ -76,7 +76,7 @@ class ArticlesController { }, }); - const likedArticleIds = new Set(likes.map((like: Prisma.Like) => like.articleId)); + const likedArticleIds = new Set(likes.map((like: Like) => like.articleId)); responseArticles = articles.map(article => ({ ...article, From 6c306f9c15c73031ab685064a190ff6cb0b8c560 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:24:09 +0900 Subject: [PATCH 61/63] =?UTF-8?q?=20Prisma.xxx=20->=20xxx=20=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ProductsController.ts | 2 +- src/repositories/UserRepository.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/ProductsController.ts b/src/controllers/ProductsController.ts index db8bf1b9f..aaac816ff 100644 --- a/src/controllers/ProductsController.ts +++ b/src/controllers/ProductsController.ts @@ -78,7 +78,7 @@ class ProductsController { }, }); - const likedProductIds = new Set(likes.map((like: Prisma.Like) => like.productId)); + const likedProductIds = new Set(likes.map((like: Like) => like.productId)); responseProducts = products.map(product => ({ ...product, diff --git a/src/repositories/UserRepository.ts b/src/repositories/UserRepository.ts index 5dc53b242..bfc733a6a 100644 --- a/src/repositories/UserRepository.ts +++ b/src/repositories/UserRepository.ts @@ -22,7 +22,7 @@ class UserRepository { return prisma.user.delete({ where: { id } }); } - async findProductsByUserId(userId: number): Promise { + async findProductsByUserId(userId: number): Promise { const userWithProducts = await prisma.user.findUnique({ where: { id: userId }, include: { products: true }, From 3d9c8ecb5eaef0b884b843fe9816ee1c239cea4b Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:25:22 +0900 Subject: [PATCH 62/63] README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8fe5a912..8c2262710 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -스프린트 미션2 +스프린트 미션5 From d24de7909ea4a4b541f7bfe44786b1913c0bb939 Mon Sep 17 00:00:00 2001 From: Jerang2 Date: Sun, 21 Sep 2025 16:34:52 +0900 Subject: [PATCH 63/63] =?UTF-8?q?prettier=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=EC=8B=A4=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .prettierrc.json | 7 + package-lock.json | 17 + package.json | 4 +- src/ArticleService.ts | 9 +- src/CommentService.ts | 9 +- src/LikeService.ts | 17 +- src/ProductService.ts | 9 +- src/UserService.ts | 159 +++--- src/controllers/ArticlesController.ts | 553 +++++++++++---------- src/controllers/ProductsController.ts | 549 ++++++++++---------- src/controllers/UploadController.ts | 38 +- src/controllers/UsersController.ts | 418 ++++++++-------- src/index.ts | 3 - src/main.ts | 4 +- src/middlewares/auth.middleware.ts | 96 ++-- src/middlewares/optionalAuth.middleware.ts | 44 +- src/middlewares/validation.middleware.ts | 30 +- src/repositories/ArticleRepository.ts | 7 +- src/repositories/CommentRepository.ts | 7 +- src/repositories/LikeRepository.ts | 17 +- src/repositories/ProductRepository.ts | 7 +- src/repositories/UserRepository.ts | 6 +- src/routes/articles.router.ts | 22 +- src/routes/products.router.ts | 8 +- src/routes/upload.router.ts | 6 +- src/types/express.d.ts | 2 +- 26 files changed, 1072 insertions(+), 976 deletions(-) create mode 100644 .prettierrc.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..3ee282f81 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2 +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6480249e1..fba4ef16b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@types/node": "^24.5.2", "@types/sharp": "^0.31.1", "nodemon": "^3.1.10", + "prettier": "^3.6.2", "prisma": "^6.16.1", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", @@ -2478,6 +2479,22 @@ "node": ">=0.10.0" } }, + "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": "6.16.1", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.1.tgz", diff --git a/package.json b/package.json index c4e2cf21a..0fdfb1991 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "build": "tsc", "dev": "ts-node-dev --respawn --transpile-only src/index.ts", - "start": "node dist/src/index.js" + "start": "node dist/src/index.js", + "format": "prettier --write \"src/**/*.ts\"" }, "repository": { "type": "git", @@ -41,6 +42,7 @@ "@types/node": "^24.5.2", "@types/sharp": "^0.31.1", "nodemon": "^3.1.10", + "prettier": "^3.6.2", "prisma": "^6.16.1", "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", diff --git a/src/ArticleService.ts b/src/ArticleService.ts index 6ae669771..303e3c101 100644 --- a/src/ArticleService.ts +++ b/src/ArticleService.ts @@ -27,7 +27,12 @@ class ArticleService { return this.articleRepository.findArticleById(id); } - async getArticles(options?: { skip?: number; take?: number; where?: Prisma.ArticleWhereInput; orderBy?: Prisma.ArticleOrderByWithRelationInput }): Promise { + async getArticles(options?: { + skip?: number; + take?: number; + where?: Prisma.ArticleWhereInput; + orderBy?: Prisma.ArticleOrderByWithRelationInput; + }): Promise { return this.articleRepository.findArticles(options); } @@ -40,4 +45,4 @@ class ArticleService { } } -export default ArticleService; \ No newline at end of file +export default ArticleService; diff --git a/src/CommentService.ts b/src/CommentService.ts index 87f8d0193..7873da3d8 100644 --- a/src/CommentService.ts +++ b/src/CommentService.ts @@ -33,7 +33,12 @@ class CommentService { return this.commentRepository.findCommentById(id); } - async getComments(options?: { skip?: number; take?: number; where?: Prisma.CommentWhereInput; orderBy?: Prisma.CommentOrderByWithRelationInput }): Promise { + async getComments(options?: { + skip?: number; + take?: number; + where?: Prisma.CommentWhereInput; + orderBy?: Prisma.CommentOrderByWithRelationInput; + }): Promise { return this.commentRepository.findComments(options); } @@ -46,4 +51,4 @@ class CommentService { } } -export default CommentService; \ No newline at end of file +export default CommentService; diff --git a/src/LikeService.ts b/src/LikeService.ts index e089a0920..b44845c8e 100644 --- a/src/LikeService.ts +++ b/src/LikeService.ts @@ -28,15 +28,26 @@ class LikeService { return this.likeRepository.deleteLike(id); } - async findLikeByUserIdAndProductId(userId: number, productId: number): Promise { + async findLikeByUserIdAndProductId( + userId: number, + productId: number, + ): Promise { return this.likeRepository.findLikeByUserIdAndProductId(userId, productId); } - async findLikeByUserIdAndArticleId(userId: number, articleId: number): Promise { + async findLikeByUserIdAndArticleId( + userId: number, + articleId: number, + ): Promise { return this.likeRepository.findLikeByUserIdAndArticleId(userId, articleId); } - async findLikes(options?: { skip?: number; take?: number; where?: Prisma.LikeWhereInput; orderBy?: Prisma.LikeOrderByWithRelationInput }): Promise { + async findLikes(options?: { + skip?: number; + take?: number; + where?: Prisma.LikeWhereInput; + orderBy?: Prisma.LikeOrderByWithRelationInput; + }): Promise { return this.likeRepository.findLikes(options); } } diff --git a/src/ProductService.ts b/src/ProductService.ts index cbea5c2fe..25665f407 100644 --- a/src/ProductService.ts +++ b/src/ProductService.ts @@ -29,7 +29,12 @@ class ProductService { return this.productRepository.findProductById(id); } - async getProducts(options?: { skip?: number; take?: number; where?: Prisma.ProductWhereInput; orderBy?: Prisma.ProductOrderByWithRelationInput }): Promise { + async getProducts(options?: { + skip?: number; + take?: number; + where?: Prisma.ProductWhereInput; + orderBy?: Prisma.ProductOrderByWithRelationInput; + }): Promise { return this.productRepository.findProducts(options); } @@ -42,4 +47,4 @@ class ProductService { } } -export default ProductService; \ No newline at end of file +export default ProductService; diff --git a/src/UserService.ts b/src/UserService.ts index 380875b59..522265ad8 100644 --- a/src/UserService.ts +++ b/src/UserService.ts @@ -7,87 +7,88 @@ import { UserCreateDto, UserUpdateDto } from './dtos/UserDto'; interface UserWithoutPassword extends Omit {} class UserService { - private userRepository: UserRepository; + private userRepository: UserRepository; - constructor(userRepository: UserRepository) { - this.userRepository = userRepository; + constructor(userRepository: UserRepository) { + this.userRepository = userRepository; + } + + // 회원가입 로직 + public signUp = async (userData: UserCreateDto): Promise => { + const { email, nickname, password } = userData; + + // 이메일 중복 확인 + const existingUser = await this.userRepository.findUserByEmail(email); + if (existingUser) { + throw new Error('이미 사용중인 이메일입니다.'); + } + + // 비밀번호 해싱 + const hashedPassword = await bcrypt.hash(password, 10); + + // 유저 생성 + const user = await this.userRepository.createUser({ + email, + nickname, + password: hashedPassword, + }); + + // 사용자 정보 반환 + const { password: _, refreshToken: __, ...userWithoutPassword } = user; + return userWithoutPassword; + }; + + public signIn = async ( + email: string, + password: string, + ): Promise<{ accessToken: string; refreshToken: string }> => { + // 이메일로 사용자 조회 + const user = await this.userRepository.findUserByEmail(email); + if (!user) { + throw new Error('존재하지 않는 이메일입니다.'); + } + + // 비밀번호 확인 + const isPasswordMatched = await bcrypt.compare(password, user.password); + if (!isPasswordMatched) { + throw new Error('비밀번호가 일치하지 않습니다.'); } - // 회원가입 로직 - public signUp = async (userData: UserCreateDto): Promise => { - const { email, nickname, password } = userData; - - // 이메일 중복 확인 - const existingUser = await this.userRepository.findUserByEmail(email); - if (existingUser) { - throw new Error('이미 사용중인 이메일입니다.'); - } - - // 비밀번호 해싱 - const hashedPassword = await bcrypt.hash(password, 10); - - // 유저 생성 - const user = await this.userRepository.createUser({ - email, - nickname, - password: hashedPassword, - }); - - // 사용자 정보 반환 - const { password: _, refreshToken: __, ...userWithoutPassword } = user; - return userWithoutPassword; - }; - - public signIn = async (email: string, password: string): Promise<{ accessToken: string; refreshToken: string }> => { - // 이메일로 사용자 조회 - const user = await this.userRepository.findUserByEmail(email); - if (!user) { - throw new Error('존재하지 않는 이메일입니다.'); - } - - // 비밀번호 확인 - const isPasswordMatched = await bcrypt.compare(password, user.password); - if (!isPasswordMatched) { - throw new Error('비밀번호가 일치하지 않습니다.'); - } - - // Access Token 생성 (12시간) - const accessToken = jwt.sign( - { userId: user.id }, - process.env.JWT_SECRET_KEY as string, - { expiresIn: '12h' } - ); - - // Refresh Token 생성 (7일) - const refreshToken = jwt.sign( - { userId: user.id }, - process.env.REFRESH_TOKEN_SECRET_KEY as string, - { expiresIn: '7d' } - ); - - // Refresh Token을 해싱해서 DB에 저장 - await this.userRepository.updateUser(user.id, { - refreshToken: await bcrypt.hash(refreshToken, 10), - }); - - return { accessToken, refreshToken }; - }; - - public getUserById = async (id: number): Promise => { - return this.userRepository.findUserById(id); - }; - - public updateUser = async (id: number, data: UserUpdateDto): Promise => { - return this.userRepository.updateUser(id, data); - }; - - public updatePassword = async (id: number, newPasswordHash: string): Promise => { - return this.userRepository.updateUser(id, { password: newPasswordHash }); - }; - - public getProductsByUserId = async (userId: number): Promise => { - return this.userRepository.findProductsByUserId(userId); - }; + // Access Token 생성 (12시간) + const accessToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET_KEY as string, { + expiresIn: '12h', + }); + + // Refresh Token 생성 (7일) + const refreshToken = jwt.sign( + { userId: user.id }, + process.env.REFRESH_TOKEN_SECRET_KEY as string, + { expiresIn: '7d' }, + ); + + // Refresh Token을 해싱해서 DB에 저장 + await this.userRepository.updateUser(user.id, { + refreshToken: await bcrypt.hash(refreshToken, 10), + }); + + return { accessToken, refreshToken }; + }; + + public getUserById = async (id: number): Promise => { + return this.userRepository.findUserById(id); + }; + + public updateUser = async (id: number, data: UserUpdateDto): Promise => { + return this.userRepository.updateUser(id, data); + }; + + public updatePassword = async (id: number, newPasswordHash: string): Promise => { + return this.userRepository.updateUser(id, { password: newPasswordHash }); + }; + + public getProductsByUserId = async (userId: number): Promise => { + return this.userRepository.findProductsByUserId(userId); + }; } -export default UserService; \ No newline at end of file +export default UserService; diff --git a/src/controllers/ArticlesController.ts b/src/controllers/ArticlesController.ts index 0c864c442..14d13e421 100644 --- a/src/controllers/ArticlesController.ts +++ b/src/controllers/ArticlesController.ts @@ -9,283 +9,288 @@ import CommentRepository from '../repositories/CommentRepository'; import LikeRepository from '../repositories/LikeRepository'; class ArticlesController { - private articleService: ArticleService; - private commentService: CommentService; - private likeService: LikeService; - - constructor(articleService: ArticleService) { - this.articleService = articleService; - const commentRepository = new CommentRepository(); - this.commentService = new CommentService(commentRepository); - const likeRepository = new LikeRepository(); - this.likeService = new LikeService(likeRepository); + private articleService: ArticleService; + private commentService: CommentService; + private likeService: LikeService; + + constructor(articleService: ArticleService) { + this.articleService = articleService; + const commentRepository = new CommentRepository(); + this.commentService = new CommentService(commentRepository); + const likeRepository = new LikeRepository(); + this.likeService = new LikeService(likeRepository); + } + + createArticle = async (req: Request, res: Response, next: NextFunction) => { + try { + const { title, content }: ArticleCreateDto = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const article = await this.articleService.createArticle({ + title, + content, + userId: user.id, + }); + res.status(201).json(article); + } catch (error) { + next(error); } - - createArticle = async (req: Request, res: Response, next: NextFunction) => { - try { - const { title, content }: ArticleCreateDto = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const article = await this.articleService.createArticle({ - title, - content, - userId: user.id, - }); - res.status(201).json(article); - } catch (error) { - next(error); - } - }; - - getArticles = async (req: Request, res: Response, next: NextFunction) => { - try { - const { sort, search } = req.query as { sort?: string; search?: string }; - let page = parseInt(req.query.page as string) || 1; - let limit = parseInt(req.query.limit as string) || 10; - let offset = (page - 1) * limit; - - const where: Prisma.ArticleWhereInput = search - ? { - OR: [ - { title: { contains: search, mode: 'insensitive' }}, - { content: { contains: search, mode: 'insensitive' }}, - ], - } - : {}; - - const user = req.user; - const articles = await this.articleService.getArticles({ - where, - orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, - skip: offset, - take: limit, - }); - - let responseArticles: (PrismaArticle & { isLiked?: boolean })[] = articles; - - if (user) { - const articleIds = articles.map(article => article.id); - const likes = await this.likeService.findLikes({ - where: { - userId: user.id, - articleId: { in: articleIds }, - }, - }); - - const likedArticleIds = new Set(likes.map((like: Like) => like.articleId)); - - responseArticles = articles.map(article => ({ - ...article, - isLiked: likedArticleIds.has(article.id), - })); - } else { - responseArticles = articles.map(article => ({ - ...article, - isLiked: false, - })); - } - - res.status(200).json(responseArticles); - } catch (error) { - next(error); - } - }; - - getArticleById = async (req: Request, res: Response, next: NextFunction) => { - try { - const { articleId } = req.params; - const user = req.user; - - const article = await this.articleService.getArticleById(parseInt(articleId)); - - if (!article) { - return res.status(404).json({ message: '게시글을 찾을 수 없습니다.'}); - } - - let isLiked = false; - if (user) { - const like = await this.likeService.findLikeByUserIdAndArticleId(user.id, parseInt(articleId)); - if (like) { - isLiked = true; - } - } - - const responseArticle = { ...article, isLiked }; - res.status(200).json(responseArticle); - - } catch (error) { - next(error); - } - }; - - updateArticle = async (req: Request, res: Response, next: NextFunction) => { - try { - const { articleId } = req.params; - const { title, content }: ArticleUpdateDto = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const article = await this.articleService.getArticleById(parseInt(articleId)); - if (!article || article.userId !== user.id) { - return res.status(403).json({ message: '게시글 수정 권한이 없습니다.' }); - } - - const updatedArticle = await this.articleService.updateArticle(parseInt(articleId), { - title, - content, - }); - res.status(200).json(updatedArticle); - } catch (error) { - next(error); - } - }; - - deleteArticle = async (req: Request, res: Response, next: NextFunction) => { - try { - const { articleId } = req.params; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const article = await this.articleService.getArticleById(parseInt(articleId)); - if (!article || article.userId !== user.id) { - return res.status(403).json({ message: '게시글 삭제 권한이 없습니다.' }); - } - - await this.articleService.deleteArticle(parseInt(articleId)); - res.status(204).send(); - } catch (error) { - next(error); - } - }; - - createComment = async (req: Request, res: Response, next: NextFunction) => { - try { - const { articleId } = req.params; - const { content } = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.'}); - - const newComment = await this.commentService.createComment({ - content, - articleId: parseInt(articleId), - userId: user.id, - }); - res.status(201).json(newComment); - } catch (error) { - next(error); - } - }; - - getComments = async (req: Request, res: Response, next: NextFunction) => { - try { - const { articleId } = req.params; - let cursor = req.query.cursor ? parseInt(req.query.cursor as string): undefined; - let limit = parseInt(req.query.limit as string) || 10; - - const comments = await this.commentService.getComments({ - where: { articleId: parseInt(articleId) }, - orderBy: { createdAt: 'desc' }, - take: limit, - skip: cursor ? 1 : 0, - }); - res.status(200).json(comments); - } catch (error) { - next(error); - } - }; - - updateComment = async (req: Request, res: Response, next: NextFunction) => { - try { - const { commentId } = req.params; - const { content } = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - if (!content) return res.status(400).json({ message: '수정할 내용을 입력하세요.' }); - - const comment = await this.commentService.getCommentById(parseInt(commentId)); - if (!comment || comment.userId !== user.id) { - return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); - } - - const updatedComment = await this.commentService.updateComment(parseInt(commentId), { - content, - }); - res.status(200).json(updatedComment); - } catch (error) { - next(error) - } - }; - - deleteComment = async (req: Request, res: Response, next: NextFunction) => { - try { - const { commentId } = req.params; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const comment = await this.commentService.getCommentById(parseInt(commentId) ); - if (!comment || comment.userId !== user.id) { - return res.status(403).json({ message: '댓글 삭제 권환이 없습니다.' }); - } - - await this.commentService.deleteComment(parseInt(commentId)); - res.status(204).send(); - } catch (error) { - next(error); - } - }; - - toggleLike = async (req: Request, res: Response, next: NextFunction) => { - try { - const { articleId } = req.params; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const article = await this.articleService.getArticleById(parseInt(articleId)); - if (!article) { - return res.status(404).json({ message: '게시글을 찾을 수 없습니다.'}); + }; + + getArticles = async (req: Request, res: Response, next: NextFunction) => { + try { + const { sort, search } = req.query as { sort?: string; search?: string }; + let page = parseInt(req.query.page as string) || 1; + let limit = parseInt(req.query.limit as string) || 10; + let offset = (page - 1) * limit; + + const where: Prisma.ArticleWhereInput = search + ? { + OR: [ + { title: { contains: search, mode: 'insensitive' } }, + { content: { contains: search, mode: 'insensitive' } }, + ], + } + : {}; + + const user = req.user; + const articles = await this.articleService.getArticles({ + where, + orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, + skip: offset, + take: limit, + }); + + let responseArticles: (PrismaArticle & { isLiked?: boolean })[] = articles; + + if (user) { + const articleIds = articles.map((article) => article.id); + const likes = await this.likeService.findLikes({ + where: { + userId: user.id, + articleId: { in: articleIds }, + }, + }); + + const likedArticleIds = new Set(likes.map((like: Like) => like.articleId)); + + responseArticles = articles.map((article) => ({ + ...article, + isLiked: likedArticleIds.has(article.id), + })); + } else { + responseArticles = articles.map((article) => ({ + ...article, + isLiked: false, + })); + } + + res.status(200).json(responseArticles); + } catch (error) { + next(error); + } + }; + + getArticleById = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + const user = req.user; + + const article = await this.articleService.getArticleById(parseInt(articleId)); + + if (!article) { + return res.status(404).json({ message: '게시글을 찾을 수 없습니다.' }); + } + + let isLiked = false; + if (user) { + const like = await this.likeService.findLikeByUserIdAndArticleId( + user.id, + parseInt(articleId), + ); + if (like) { + isLiked = true; } + } - const existingLike = await this.likeService.findLikeByUserIdAndArticleId(user.id, parseInt(articleId)); - - if (existingLike) { - await this.likeService.deleteLike(existingLike.id); - res.status(200).json({ message: '게시글 좋아요를 취소했습니다.' }); - } else { - await this.likeService.createLike({ - userId: user.id, - articleId: parseInt(articleId), - }); - res.status(201).json({ message: '게시글에 좋아요를 눌렀습니다.' }); - } - } catch (error) { - next (error); - } - }; + const responseArticle = { ...article, isLiked }; + res.status(200).json(responseArticle); + } catch (error) { + next(error); + } + }; + + updateArticle = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + const { title, content }: ArticleUpdateDto = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article || article.userId !== user.id) { + return res.status(403).json({ message: '게시글 수정 권한이 없습니다.' }); + } + + const updatedArticle = await this.articleService.updateArticle(parseInt(articleId), { + title, + content, + }); + res.status(200).json(updatedArticle); + } catch (error) { + next(error); + } + }; + + deleteArticle = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article || article.userId !== user.id) { + return res.status(403).json({ message: '게시글 삭제 권한이 없습니다.' }); + } + + await this.articleService.deleteArticle(parseInt(articleId)); + res.status(204).send(); + } catch (error) { + next(error); + } + }; + + createComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + const { content } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.' }); + + const newComment = await this.commentService.createComment({ + content, + articleId: parseInt(articleId), + userId: user.id, + }); + res.status(201).json(newComment); + } catch (error) { + next(error); + } + }; + + getComments = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + let cursor = req.query.cursor ? parseInt(req.query.cursor as string) : undefined; + let limit = parseInt(req.query.limit as string) || 10; + + const comments = await this.commentService.getComments({ + where: { articleId: parseInt(articleId) }, + orderBy: { createdAt: 'desc' }, + take: limit, + skip: cursor ? 1 : 0, + }); + res.status(200).json(comments); + } catch (error) { + next(error); + } + }; + + updateComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { commentId } = req.params; + const { content } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!content) return res.status(400).json({ message: '수정할 내용을 입력하세요.' }); + + const comment = await this.commentService.getCommentById(parseInt(commentId)); + if (!comment || comment.userId !== user.id) { + return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); + } + + const updatedComment = await this.commentService.updateComment(parseInt(commentId), { + content, + }); + res.status(200).json(updatedComment); + } catch (error) { + next(error); + } + }; + + deleteComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { commentId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const comment = await this.commentService.getCommentById(parseInt(commentId)); + if (!comment || comment.userId !== user.id) { + return res.status(403).json({ message: '댓글 삭제 권환이 없습니다.' }); + } + + await this.commentService.deleteComment(parseInt(commentId)); + res.status(204).send(); + } catch (error) { + next(error); + } + }; + + toggleLike = async (req: Request, res: Response, next: NextFunction) => { + try { + const { articleId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const article = await this.articleService.getArticleById(parseInt(articleId)); + if (!article) { + return res.status(404).json({ message: '게시글을 찾을 수 없습니다.' }); + } + + const existingLike = await this.likeService.findLikeByUserIdAndArticleId( + user.id, + parseInt(articleId), + ); + + if (existingLike) { + await this.likeService.deleteLike(existingLike.id); + res.status(200).json({ message: '게시글 좋아요를 취소했습니다.' }); + } else { + await this.likeService.createLike({ + userId: user.id, + articleId: parseInt(articleId), + }); + res.status(201).json({ message: '게시글에 좋아요를 눌렀습니다.' }); + } + } catch (error) { + next(error); + } + }; } export default ArticlesController; diff --git a/src/controllers/ProductsController.ts b/src/controllers/ProductsController.ts index aaac816ff..8ce374d18 100644 --- a/src/controllers/ProductsController.ts +++ b/src/controllers/ProductsController.ts @@ -9,285 +9,288 @@ import CommentRepository from '../repositories/CommentRepository'; import LikeRepository from '../repositories/LikeRepository'; class ProductsController { - private productService: ProductService; - private commentService: CommentService; - private likeService: LikeService; - - constructor(productService: ProductService) { - this.productService = productService; - const commentRepository = new CommentRepository(); - this.commentService = new CommentService(commentRepository); - const likeRepository = new LikeRepository(); - this.likeService = new LikeService(likeRepository); + private productService: ProductService; + private commentService: CommentService; + private likeService: LikeService; + + constructor(productService: ProductService) { + this.productService = productService; + const commentRepository = new CommentRepository(); + this.commentService = new CommentService(commentRepository); + const likeRepository = new LikeRepository(); + this.likeService = new LikeService(likeRepository); + } + + createProduct = async (req: Request, res: Response, next: NextFunction) => { + try { + const { name, description, price }: ProductCreateDto = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const product = await this.productService.createProduct({ + name, + content: description, // Map description to content + price, + userId: user.id, + }); + res.status(201).json(product); + } catch (error) { + next(error); } + }; + + getProducts = async (req: Request, res: Response, next: NextFunction) => { + try { + const { sort, search } = req.query as { sort?: string; search?: string }; + let page = parseInt(req.query.page as string) || 1; + let limit = parseInt(req.query.limit as string) || 10; + let offset = (page - 1) * limit; + + const where: Prisma.ProductWhereInput = search + ? { + OR: [ + { name: { contains: search, mode: 'insensitive' } }, + { content: { contains: search, mode: 'insensitive' } }, + ], + } + : {}; + + const user = req.user; + const products = await this.productService.getProducts({ + where, + orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, + skip: offset, + take: limit, + }); + + let responseProducts: (PrismaProduct & { isLiked?: boolean })[] = products; + + if (user) { + const productIds = products.map((product) => product.id); + const likes = await this.likeService.findLikes({ + where: { + userId: user.id, + productId: { in: productIds }, + }, + }); + + const likedProductIds = new Set(likes.map((like: Like) => like.productId)); + + responseProducts = products.map((product) => ({ + ...product, + isLiked: likedProductIds.has(product.id), + })); + } else { + responseProducts = products.map((product) => ({ + ...product, + isLiked: false, + })); + } + + res.status(200).json(responseProducts); + } catch (error) { + next(error); + } + }; - createProduct = async (req: Request, res: Response, next: NextFunction) => { - try { - const { name, description, price }: ProductCreateDto = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const product = await this.productService.createProduct({ - name, - content: description, // Map description to content - price, - userId: user.id, - }); - res.status(201).json(product); - } catch(error) { - next(error); - } - }; - - getProducts = async (req: Request, res: Response, next: NextFunction) => { - try { - const { sort, search } = req.query as { sort?: string; search?: string }; - let page = parseInt(req.query.page as string) || 1; - let limit = parseInt(req.query.limit as string) || 10; - let offset = (page - 1) * limit; - - const where: Prisma.ProductWhereInput = search - ? { - OR: [ - { name: { contains: search, mode: 'insensitive' }}, - { content: { contains: search, mode: 'insensitive' - } }, - ], - } - : {}; - - const user = req.user; - const products = await this.productService.getProducts({ - where, - orderBy: sort === 'recent' ? { createdAt: 'desc' } : undefined, - skip: offset, - take: limit, - }); - - let responseProducts: (PrismaProduct & { isLiked?: boolean })[] = products; - - if (user) { - const productIds = products.map(product => product.id); - const likes = await this.likeService.findLikes({ - where: { - userId: user.id, - productId: { in: productIds }, - }, - }); - - const likedProductIds = new Set(likes.map((like: Like) => like.productId)); - - responseProducts = products.map(product => ({ - ...product, - isLiked: likedProductIds.has(product.id), - })); - } else { - - responseProducts = products.map(product => ({ - ...product, - isLiked: false, - })); - } - - res.status(200).json(responseProducts); - - } catch (error) { - next(error); - } - }; - - getProductById = async (req: Request, res: Response, next: NextFunction) => { - try { - const { productId } = req.params; - const user = req.user; - - const product = await this.productService.getProductById(parseInt(productId)); - - if (!product) return res.status(404).json({ message: '상품을 찾을수 없습니다.'}); - - let isLiked = false; - if (user) { - const like = await this.likeService.findLikeByUserIdAndProductId(user.id, parseInt(productId)); - if (like) { - isLiked = true; - } - } - - const responseProduct = { ...product, isLiked }; - res.status(200).json(responseProduct); - } catch (error) { - next(error); - } - }; - - updateProduct = async (req: Request, res: Response, next: NextFunction) => { - try { - const { productId } = req.params; - const { name, description, price }: ProductUpdateDto = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const product = await this.productService.getProductById(parseInt(productId)); - if (!product || product.userId !== user.id) { - return res.status(403).json({ message: '상품 수정 권한이 없습니다.' }); - } - - const updatedProduct = await this.productService.updateProduct(parseInt(productId), { - name, - description, - price, - }); - res.status(200).json(updatedProduct); - } catch (error) { - next(error); - } - }; - - deleteProduct = async (req: Request, res: Response, next: NextFunction) => { - try { - const { productId } = req.params; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const product = await this.productService.getProductById(parseInt(productId)); - if (!product || product.userId !== user.id) { - return res.status(403).json({ message: '상품 삭제 권한이 없습니다.' }); - } - - await this.productService.deleteProduct(parseInt(productId)); - res.status(204).send(); - } catch (error) { - next(error); - } - }; - - createComment = async (req: Request, res: Response, next: NextFunction) => { - try { - const { productId } = req.params; - const { content } = req.body - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.' }); - - const newComment = await this.commentService.createComment({ - content, - productId: parseInt(productId), - userId: user.id, - }); - res.status(201).json(newComment); - } catch (error) { - next(error); - } - }; - - getComments = async (req: Request, res: Response, next: NextFunction) => { - try { - const { productId } = req.params; - let cursor = req.query.cursor ? parseInt(req.query.cursor as string): undefined; - let limit = parseInt(req.query.limit as string) || 10; - - const comments = await this.commentService.getComments({ - where: { productId: parseInt(productId) }, - orderBy: { createdAt: 'desc' }, - take: limit, - skip: cursor ? 1 : 0, - }); - res.status(200).json(comments); - } catch (error) { - next(error); - } - }; - - updateComment = async (req: Request, res: Response, next: NextFunction) => { - try { - const { commentId } = req.params; - const { content } = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - if (!content) return res.status(400).json({ message: '수정할 내용을 입력해주세요.'}); - - const existingComment = await this.commentService.getCommentById(parseInt(commentId)); - if (!existingComment || existingComment.userId !== user.id) { - return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); - } - - const updatedComment = await this.commentService.updateComment(parseInt(commentId), { - content, - }); - res.status(200).json(updatedComment); - } catch (error) { - next(error); - } - }; - - deleteComment = async (req: Request, res: Response, next: NextFunction) => { - try { - const { commentId } = req.params; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const existingComment = await this.commentService.getCommentById(parseInt(commentId)); - if (!existingComment || existingComment.userId !== user.id) { - return res.status(403).json({ message: '댓글 삭제 권한이 없습니다.' }); - } - - await this.commentService.deleteComment(parseInt(commentId)); - res.status(204).send(); - } catch(error) { - next(error); - } - }; + getProductById = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + const user = req.user; - toggleLike = async (req: Request, res: Response, next: NextFunction) => { - try { - const { productId } = req.params; - const { user } = req; + const product = await this.productService.getProductById(parseInt(productId)); - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } + if (!product) return res.status(404).json({ message: '상품을 찾을수 없습니다.' }); - const product = await this.productService.getProductById(parseInt(productId)); - if (!product) { - return res.status(404).json({ message: '상품을 찾을 수 없습니다.'}); + let isLiked = false; + if (user) { + const like = await this.likeService.findLikeByUserIdAndProductId( + user.id, + parseInt(productId), + ); + if (like) { + isLiked = true; } + } - const existingLike = await this.likeService.findLikeByUserIdAndProductId(user.id, parseInt(productId)); - - if (existingLike) { - await this.likeService.deleteLike(existingLike.id); - res.status(200).json({ message: '상품 좋아요를 취소했습니다.' }); - } else { - await this.likeService.createLike({ - userId: user.id, - productId: parseInt(productId), - }); - res.status(201).json({ message: '상품에 좋아요를 눌렀습니다.' }); - } - } catch (error) { - next(error); - } - }; + const responseProduct = { ...product, isLiked }; + res.status(200).json(responseProduct); + } catch (error) { + next(error); + } + }; + + updateProduct = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + const { name, description, price }: ProductUpdateDto = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const product = await this.productService.getProductById(parseInt(productId)); + if (!product || product.userId !== user.id) { + return res.status(403).json({ message: '상품 수정 권한이 없습니다.' }); + } + + const updatedProduct = await this.productService.updateProduct(parseInt(productId), { + name, + description, + price, + }); + res.status(200).json(updatedProduct); + } catch (error) { + next(error); + } + }; + + deleteProduct = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const product = await this.productService.getProductById(parseInt(productId)); + if (!product || product.userId !== user.id) { + return res.status(403).json({ message: '상품 삭제 권한이 없습니다.' }); + } + + await this.productService.deleteProduct(parseInt(productId)); + res.status(204).send(); + } catch (error) { + next(error); + } + }; + + createComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + const { content } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!content) return res.status(400).json({ message: '댓글을 입력해주세요.' }); + + const newComment = await this.commentService.createComment({ + content, + productId: parseInt(productId), + userId: user.id, + }); + res.status(201).json(newComment); + } catch (error) { + next(error); + } + }; + + getComments = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + let cursor = req.query.cursor ? parseInt(req.query.cursor as string) : undefined; + let limit = parseInt(req.query.limit as string) || 10; + + const comments = await this.commentService.getComments({ + where: { productId: parseInt(productId) }, + orderBy: { createdAt: 'desc' }, + take: limit, + skip: cursor ? 1 : 0, + }); + res.status(200).json(comments); + } catch (error) { + next(error); + } + }; + + updateComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { commentId } = req.params; + const { content } = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!content) return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); + + const existingComment = await this.commentService.getCommentById(parseInt(commentId)); + if (!existingComment || existingComment.userId !== user.id) { + return res.status(403).json({ message: '댓글 수정 권한이 없습니다.' }); + } + + const updatedComment = await this.commentService.updateComment(parseInt(commentId), { + content, + }); + res.status(200).json(updatedComment); + } catch (error) { + next(error); + } + }; + + deleteComment = async (req: Request, res: Response, next: NextFunction) => { + try { + const { commentId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const existingComment = await this.commentService.getCommentById(parseInt(commentId)); + if (!existingComment || existingComment.userId !== user.id) { + return res.status(403).json({ message: '댓글 삭제 권한이 없습니다.' }); + } + + await this.commentService.deleteComment(parseInt(commentId)); + res.status(204).send(); + } catch (error) { + next(error); + } + }; + + toggleLike = async (req: Request, res: Response, next: NextFunction) => { + try { + const { productId } = req.params; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const product = await this.productService.getProductById(parseInt(productId)); + if (!product) { + return res.status(404).json({ message: '상품을 찾을 수 없습니다.' }); + } + + const existingLike = await this.likeService.findLikeByUserIdAndProductId( + user.id, + parseInt(productId), + ); + + if (existingLike) { + await this.likeService.deleteLike(existingLike.id); + res.status(200).json({ message: '상품 좋아요를 취소했습니다.' }); + } else { + await this.likeService.createLike({ + userId: user.id, + productId: parseInt(productId), + }); + res.status(201).json({ message: '상품에 좋아요를 눌렀습니다.' }); + } + } catch (error) { + next(error); + } + }; } -export default ProductsController; \ No newline at end of file +export default ProductsController; diff --git a/src/controllers/UploadController.ts b/src/controllers/UploadController.ts index 1db01657b..739f82872 100644 --- a/src/controllers/UploadController.ts +++ b/src/controllers/UploadController.ts @@ -5,34 +5,32 @@ import fs from 'fs'; import sharp from 'sharp'; const uploadDir = 'uploads/'; -if (!fs.existsSync(uploadDir)){ - fs.mkdirSync(uploadDir); +if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir); } const storage = multer.memoryStorage(); const upload = multer({ storage: storage }); class UploadController { - uploadImage = async (req: Request, res: Response, next: NextFunction) => { - if (!req.file) { - return res.status(400).json({ message: '이미지 파일이 필요합니다.'}); - } + uploadImage = async (req: Request, res: Response, next: NextFunction) => { + if (!req.file) { + return res.status(400).json({ message: '이미지 파일이 필요합니다.' }); + } - try { - const ext = path.extname(req.file.originalname); - const filename = Date.now() + ext; - const imagePath = path.join(uploadDir, filename); + try { + const ext = path.extname(req.file.originalname); + const filename = Date.now() + ext; + const imagePath = path.join(uploadDir, filename); - await sharp(req.file.buffer) - .resize({ width: 500 }) - .toFile(imagePath); - - const imageUrl = `/uploads/${filename}`; - res.status(201).json({ imageUrl: imageUrl }); - } catch (error) { - next(error); - } - }; + await sharp(req.file.buffer).resize({ width: 500 }).toFile(imagePath); + + const imageUrl = `/uploads/${filename}`; + res.status(201).json({ imageUrl: imageUrl }); + } catch (error) { + next(error); + } + }; } export default UploadController; diff --git a/src/controllers/UsersController.ts b/src/controllers/UsersController.ts index 19dfaf35c..79aa6c772 100644 --- a/src/controllers/UsersController.ts +++ b/src/controllers/UsersController.ts @@ -3,216 +3,230 @@ import UserService from '../UserService'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import prisma from '../index'; -import { UserCreateDto, UserSignInDto, UserUpdateDto, UserChangePasswordDto } from '../dtos/UserDto'; +import { + UserCreateDto, + UserSignInDto, + UserUpdateDto, + UserChangePasswordDto, +} from '../dtos/UserDto'; class UsersController { - private userService: UserService; - - constructor(userService: UserService) { - this.userService = userService; + private userService: UserService; + + constructor(userService: UserService) { + this.userService = userService; + } + + signUp = async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, nickname, password }: UserCreateDto = req.body; + + if (!email || !nickname || !password) { + return res.status(400).json({ message: '모든 정보를 입력해주세요' }); + } + + const newUser = await this.userService.signUp({ email, nickname, password }); + + return res.status(201).json({ + message: '회원가입이 완료되었습니다.', + data: newUser, + }); + } catch (error) { + if (error instanceof Error) { + return res.status(409).json({ message: error.message }); + } + next(error); } + }; + + signIn = async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, password }: UserSignInDto = req.body; + + if (!email || !password) { + return res.status(400).json({ message: '이메일과 비밀번호를 모두 입력해주세요.' }); + } + const { accessToken, refreshToken } = await this.userService.signIn(email, password); + + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: false, + maxAge: 1000 * 60 * 60 * 24 * 7, + }); + + return res.status(200).json({ + message: '로그인에 성공했습니다.', + data: { accessToken }, + }); + } catch (error) { + if (error instanceof Error) { + return res.status(401).json({ message: error.message }); + } + next(error); + } + }; + + getMe = async (req: Request, res: Response, next: NextFunction) => { + try { + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + res.status(200).json({ + message: '내 정보 조회 성공', + data: { + id: user.id, + email: user.email, + nickname: user.nickname, + image: user.image, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + }, + }); + } catch (error) { + next(error); + } + }; + + updateMe = async (req: Request, res: Response, next: NextFunction) => { + try { + const { nickname, image }: UserUpdateDto = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!nickname && !image) { + return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); + } + + const updatedData: { nickname?: string; image?: string } = { + ...(nickname && { nickname }), + ...(image && { image }), + }; + + const updatedUser = await this.userService.updateUser(user.id, updatedData); + + res.status(200).json({ + message: '내 정보 수정에 성공했습니다.', + data: { + id: updatedUser.id, + email: updatedUser.email, + nickname: updatedUser.nickname, + image: updatedUser.image, + createdAt: updatedUser.createdAt, + updatedAt: updatedUser.updatedAt, + }, + }); + } catch (error) { + next(error); + } + }; + + changePassword = async (req: Request, res: Response, next: NextFunction) => { + try { + const { currentPassword, newPassword, confirmNewPassword }: UserChangePasswordDto = req.body; + const { user } = req; + + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + if (!currentPassword || !newPassword || !confirmNewPassword) { + return res.status(400).json({ message: '모든 정보를 입력해주세요.' }); + } + if (newPassword !== confirmNewPassword) { + return res + .status(400) + .json({ message: '새 비밀번호와 확인 비밀번호가 일치하지 않습니다.' }); + } + + const existingUser = await this.userService.getUserById(user.id); + if (!existingUser) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } + + const isPasswordMatched = await bcrypt.compare(currentPassword, existingUser.password); + if (!isPasswordMatched) { + return res.status(401).json({ message: '현재 비밀번호가 일치하지 않습니다.' }); + } + + const hashedNewPassword = await bcrypt.hash(newPassword, 10); + + await this.userService.updatePassword(user.id, hashedNewPassword); + + res.status(200).json({ message: '비밀번호 변경이 완료되었습니다.' }); + } catch (error) { + next(error); + } + }; - signUp = async (req: Request, res: Response, next: NextFunction) => { - try { - const { email, nickname, password }: UserCreateDto = req.body; - - if (!email || !nickname || !password) { - return res.status(400).json({ message: '모든 정보를 입력해주세요'}); - } - - const newUser = await this.userService.signUp({ email, nickname, password }); - - return res.status(201).json({ - message: '회원가입이 완료되었습니다.', - data: newUser, - }); - } catch (error) { - if (error instanceof Error) { - return res.status(409).json({ message: error.message }); - } - next(error); - } - }; - - signIn = async (req: Request, res: Response, next: NextFunction) => { - try { - const { email, password }: UserSignInDto = req.body; - - if (!email || !password) { - return res.status(400).json({ message: '이메일과 비밀번호를 모두 입력해주세요.'}); - } - const { accessToken, refreshToken } = await this.userService.signIn(email, password); - - res.cookie('refreshToken', refreshToken, { - httpOnly: true, - secure: false, - maxAge: 1000 * 60 * 60 * 24 * 7, - }); - - return res.status(200).json({ - message: '로그인에 성공했습니다.', - data: { accessToken }, - }); - } catch (error) { - if (error instanceof Error) { - return res.status(401).json({ message: error.message }); - } - next(error); - } - }; - - getMe = async (req: Request, res: Response, next: NextFunction) => { - try { - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - res.status(200).json({ - message: '내 정보 조회 성공', - data: { - id: user.id, - email: user.email, - nickname: user.nickname, - image: user.image, - createdAt: user.createdAt, - updatedAt: user.updatedAt, - }, - }); - } catch (error) { - next(error); - } - }; - - updateMe = async (req: Request, res: Response, next: NextFunction) => { - try { - const { nickname, image }: UserUpdateDto = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - if (!nickname && !image) { - return res.status(400).json({ message: '수정할 내용을 입력해주세요.' }); - } - - const updatedData: { nickname?: string; image?: string } = { - ...(nickname && { nickname }), - ...(image && { image }), - }; - - const updatedUser = await this.userService.updateUser(user.id, updatedData); - - res.status(200).json({ - message: '내 정보 수정에 성공했습니다.', - data: { - id: updatedUser.id, - email: updatedUser.email, - nickname: updatedUser.nickname, - image: updatedUser.image, - createdAt: updatedUser.createdAt, - updatedAt: updatedUser.updatedAt, - }, - }); - } catch (error) { - next(error); - } - }; - - changePassword = async (req: Request, res: Response, next: NextFunction) => { - try { - const { currentPassword, newPassword, confirmNewPassword }: UserChangePasswordDto = req.body; - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - if (!currentPassword || !newPassword || !confirmNewPassword) { - return res.status(400).json({ message: '모든 정보를 입력해주세요.' }); - } - if (newPassword !== confirmNewPassword) { - return res.status(400).json({ message: '새 비밀번호와 확인 비밀번호가 일치하지 않습니다.' }); - } - - const existingUser = await this.userService.getUserById(user.id); - if (!existingUser) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } - - const isPasswordMatched = await bcrypt.compare(currentPassword, existingUser.password); - if (!isPasswordMatched) { - return res.status(401).json({ message: '현재 비밀번호가 일치하지 않습니다.' }); - } - - const hashedNewPassword = await bcrypt.hash(newPassword, 10); - - await this.userService.updatePassword(user.id, hashedNewPassword); - - res.status(200).json({ message: '비밀번호 변경이 완료되었습니다.' }); - } catch (error) { - next(error); - } - }; - - getMyProducts = async (req: Request, res: Response, next: NextFunction) => { - try { - const { user } = req; - - if (!user) { - return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); - } + getMyProducts = async (req: Request, res: Response, next: NextFunction) => { + try { + const { user } = req; - const products = await this.userService.getProductsByUserId(user.id); + if (!user) { + return res.status(401).json({ message: '사용자 정보를 찾을 수 없습니다.' }); + } - res.status(200).json({ - message: '내가 작성한 상품 목록 조회에 성공했습니다.', - data: products, - }); - } catch (error) { - next(error); - } - }; - - refreshToken = async (req: Request, res: Response, next: NextFunction) => { - try { - const { refreshToken } = req.cookies; - if (!refreshToken) { - return res.status(401).json({ message: 'Refresh Token이 없습니다.' }); - } - - const decodedToken = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET_KEY as string) as { userId: number }; - const userId = decodedToken.userId; - - const user = await this.userService.getUserById(userId); - if (!user) { - return res.status(401).json({ message: '사용자를 찾을 수 없습니다.' }); - } - - const isRefreshTokenMatched = await bcrypt.compare(refreshToken, user.refreshToken as string); - if (!isRefreshTokenMatched) { - return res.status(401).json({ message: 'Refresh Token이 유효하지 않습니다.' }); - } - - const newAccessToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET_KEY as string, { - expiresIn: '12h', - }); + const products = await this.userService.getProductsByUserId(user.id); - return res.status(200).json({ - message: 'Access Token이 재발급되었습니다.', - data: { accessToken: newAccessToken }, + res.status(200).json({ + message: '내가 작성한 상품 목록 조회에 성공했습니다.', + data: products, + }); + } catch (error) { + next(error); + } + }; + + refreshToken = async (req: Request, res: Response, next: NextFunction) => { + try { + const { refreshToken } = req.cookies; + if (!refreshToken) { + return res.status(401).json({ message: 'Refresh Token이 없습니다.' }); + } + + const decodedToken = jwt.verify( + refreshToken, + process.env.REFRESH_TOKEN_SECRET_KEY as string, + ) as { userId: number }; + const userId = decodedToken.userId; + + const user = await this.userService.getUserById(userId); + if (!user) { + return res.status(401).json({ message: '사용자를 찾을 수 없습니다.' }); + } + + const isRefreshTokenMatched = await bcrypt.compare(refreshToken, user.refreshToken as string); + if (!isRefreshTokenMatched) { + return res.status(401).json({ message: 'Refresh Token이 유효하지 않습니다.' }); + } + + const newAccessToken = jwt.sign({ userId: user.id }, process.env.JWT_SECRET_KEY as string, { + expiresIn: '12h', + }); + + return res.status(200).json({ + message: 'Access Token이 재발급되었습니다.', + data: { accessToken: newAccessToken }, + }); + } catch (error) { + if (error instanceof Error) { + if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') { + return res + .status(401) + .json({ + message: 'Refresh Token이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요 ', }); - } catch (error) { - if (error instanceof Error) { - if (error.name === 'TokenExpiredError' || error.name === 'JsonWebTokenError') { - return res.status(401).json({ message: 'Refresh Token이 만료되었거나 유효하지 않습니다. 다시 로그인해주세요 ' }); - } - } - next(error); } - }; + } + next(error); + } + }; } -export default UsersController; \ No newline at end of file +export default UsersController; diff --git a/src/index.ts b/src/index.ts index 14f2bc222..e93558336 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,6 @@ app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); - // route settitng app.use('/api', [productRouter, articleRouter, uploadRouter, usersRouter]); @@ -33,10 +32,8 @@ app.use((err: Error, req: Request, res: Response, next: NextFunction) => { res.status(statusCode).json({ message }); }); - app.listen(PORT, () => { console.log(`서버가 ${PORT}번에서 실행중입니다.`); }); export default prisma; - diff --git a/src/main.ts b/src/main.ts index 1ac03b19d..f657aadf8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,2 +1,2 @@ -import ProductService from "./ProductService"; -import ArticleService from "./ArticleService"; +import ProductService from './ProductService'; +import ArticleService from './ArticleService'; diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 5d4bfe3b9..6fa66b7d6 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -4,57 +4,57 @@ import prisma from '../index'; // Import prisma from index.ts import { User as PrismaUser } from '@prisma/client'; interface DecodedToken { - userId: number; + userId: number; } const authMiddleware = async (req: Request, res: Response, next: NextFunction) => { - try { - // 헤더에서 authorization 값 가져오기 - const { authorization } = req.headers; - if (!authorization) { - return res.status(401).json({ message: '인증 정보가 없습니다.' }); - } - - // authorization 값에서 토큰 추출하기 - const [tokenType, token] = authorization.split(' '); - if (tokenType !== 'Bearer') { - return res.status(401).json({ message: '지원하지 않는 인증 방식입니다.'}) - } - - // 토큰이 없을 경우 - if (!token) { - return res.status(401).json({ message: '인증 정보가 없습니다.' }); - } - - // 토큰 검증 (확인) - const decodedToken = jwt.verify(token, process.env.JWT_SECRET_KEY as string) as DecodedToken; - const userId = decodedToken.userId; - - // 토큰 있는 userId로 조회 - const user = await prisma.user.findUnique({ - where: { id: userId}, - }); - if (!user) { - return res.status(401).json({ message: '인증 정보가 유효하지 않습니다.' }); - } - - // 사용자 정보 저장 - req.user = user; - next(); - } catch (error) { - console.error('인증 미들웨어 에러:', error); - if (error instanceof Error) { - switch (error.name) { - case 'TokenExpiredError': - return res.status(401).json({ message: '인증 토큰이 만료되었습니다.' }); - case 'JsonWebTokenError': - return res.status(401).json({ message: '유효하지 않은 인증 토큰입니다.' }); - default: - return res.status(401).json({ message: '인증 정보가 유효하지 않습니다.' }); - } - } - next(error); + try { + // 헤더에서 authorization 값 가져오기 + const { authorization } = req.headers; + if (!authorization) { + return res.status(401).json({ message: '인증 정보가 없습니다.' }); } + + // authorization 값에서 토큰 추출하기 + const [tokenType, token] = authorization.split(' '); + if (tokenType !== 'Bearer') { + return res.status(401).json({ message: '지원하지 않는 인증 방식입니다.' }); + } + + // 토큰이 없을 경우 + if (!token) { + return res.status(401).json({ message: '인증 정보가 없습니다.' }); + } + + // 토큰 검증 (확인) + const decodedToken = jwt.verify(token, process.env.JWT_SECRET_KEY as string) as DecodedToken; + const userId = decodedToken.userId; + + // 토큰 있는 userId로 조회 + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + if (!user) { + return res.status(401).json({ message: '인증 정보가 유효하지 않습니다.' }); + } + + // 사용자 정보 저장 + req.user = user; + next(); + } catch (error) { + console.error('인증 미들웨어 에러:', error); + if (error instanceof Error) { + switch (error.name) { + case 'TokenExpiredError': + return res.status(401).json({ message: '인증 토큰이 만료되었습니다.' }); + case 'JsonWebTokenError': + return res.status(401).json({ message: '유효하지 않은 인증 토큰입니다.' }); + default: + return res.status(401).json({ message: '인증 정보가 유효하지 않습니다.' }); + } + } + next(error); + } }; -export default authMiddleware; \ No newline at end of file +export default authMiddleware; diff --git a/src/middlewares/optionalAuth.middleware.ts b/src/middlewares/optionalAuth.middleware.ts index 6ce49f49e..33cd7856a 100644 --- a/src/middlewares/optionalAuth.middleware.ts +++ b/src/middlewares/optionalAuth.middleware.ts @@ -3,37 +3,37 @@ import jwt from 'jsonwebtoken'; import prisma from '../index'; // Import prisma from index.ts interface DecodedToken { - userId: number; + userId: number; } // 선택적 인증 미들웨어 // 인증에 성공하면 사용자 정보를 추가하고 실패해도 에러없이 다음 미들웨어 진행 const optionalAuthMiddleware = async (req: Request, res: Response, next: NextFunction) => { - try { - const { authorization } = req.headers; - if (!authorization) { - return next(); - } + try { + const { authorization } = req.headers; + if (!authorization) { + return next(); + } - const [tokenType, token] = authorization.split(' '); - if (tokenType !== 'Bearer' || !token) { - return next(); - } + const [tokenType, token] = authorization.split(' '); + if (tokenType !== 'Bearer' || !token) { + return next(); + } - const decodedToken = jwt.verify(token, process.env.JWT_SECRET_KEY as string) as DecodedToken; - const user = await prisma.user.findUnique({ - where: { id: decodedToken.userId }, - }); + const decodedToken = jwt.verify(token, process.env.JWT_SECRET_KEY as string) as DecodedToken; + const user = await prisma.user.findUnique({ + where: { id: decodedToken.userId }, + }); - if (user) { - req.user = user; - } - } catch (error) { - // 에러 발생 시에도 다음 미들웨어로 진행 (선택적 인증이므로) - console.error('선택적 인증 미들웨어 에러:', error); + if (user) { + req.user = user; } + } catch (error) { + // 에러 발생 시에도 다음 미들웨어로 진행 (선택적 인증이므로) + console.error('선택적 인증 미들웨어 에러:', error); + } - return next(); + return next(); }; -export default optionalAuthMiddleware; \ No newline at end of file +export default optionalAuthMiddleware; diff --git a/src/middlewares/validation.middleware.ts b/src/middlewares/validation.middleware.ts index 7c365b1d8..5712e97a5 100644 --- a/src/middlewares/validation.middleware.ts +++ b/src/middlewares/validation.middleware.ts @@ -2,21 +2,21 @@ import { Request, Response, NextFunction } from 'express'; //validation export const validateProduct = (req: Request, res: Response, next: NextFunction) => { - const { name, description, price } = req.body; - if (!name || !description || price == null) { - return res.status(400).json({ message: '이름, 설명, 가격을 입력해야 합니다.'}); - } - if (typeof price !== 'number' || price <= 0) { - return res.status(400).json({ message: '가격은 0보다 커야합니다.'}); - } - next(); -} + const { name, description, price } = req.body; + if (!name || !description || price == null) { + return res.status(400).json({ message: '이름, 설명, 가격을 입력해야 합니다.' }); + } + if (typeof price !== 'number' || price <= 0) { + return res.status(400).json({ message: '가격은 0보다 커야합니다.' }); + } + next(); +}; //article validation export const validateArticle = (req: Request, res: Response, next: NextFunction) => { - const { title, content } = req.body; - if (!title || !content) { - return res.status(400).json({ message: '제목, 내용을 입력해야 합니다.'}); - } - next(); -} + const { title, content } = req.body; + if (!title || !content) { + return res.status(400).json({ message: '제목, 내용을 입력해야 합니다.' }); + } + next(); +}; diff --git a/src/repositories/ArticleRepository.ts b/src/repositories/ArticleRepository.ts index c535f3f66..7055513ab 100644 --- a/src/repositories/ArticleRepository.ts +++ b/src/repositories/ArticleRepository.ts @@ -6,7 +6,12 @@ class ArticleRepository { return prisma.article.findUnique({ where: { id } }); } - async findArticles(options?: { skip?: number; take?: number; where?: Prisma.ArticleWhereInput; orderBy?: Prisma.ArticleOrderByWithRelationInput }): Promise { + async findArticles(options?: { + skip?: number; + take?: number; + where?: Prisma.ArticleWhereInput; + orderBy?: Prisma.ArticleOrderByWithRelationInput; + }): Promise { return prisma.article.findMany(options); } diff --git a/src/repositories/CommentRepository.ts b/src/repositories/CommentRepository.ts index 5ac9cf757..4940b5949 100644 --- a/src/repositories/CommentRepository.ts +++ b/src/repositories/CommentRepository.ts @@ -6,7 +6,12 @@ class CommentRepository { return prisma.comment.findUnique({ where: { id } }); } - async findComments(options?: { skip?: number; take?: number; where?: Prisma.CommentWhereInput; orderBy?: Prisma.CommentOrderByWithRelationInput }): Promise { + async findComments(options?: { + skip?: number; + take?: number; + where?: Prisma.CommentWhereInput; + orderBy?: Prisma.CommentOrderByWithRelationInput; + }): Promise { return prisma.comment.findMany(options); } diff --git a/src/repositories/LikeRepository.ts b/src/repositories/LikeRepository.ts index 6698cee11..2eca01fef 100644 --- a/src/repositories/LikeRepository.ts +++ b/src/repositories/LikeRepository.ts @@ -6,7 +6,12 @@ class LikeRepository { return prisma.like.findUnique({ where: { id } }); } - async findLikes(options?: { skip?: number; take?: number; where?: Prisma.LikeWhereInput; orderBy?: Prisma.LikeOrderByWithRelationInput }): Promise { + async findLikes(options?: { + skip?: number; + take?: number; + where?: Prisma.LikeWhereInput; + orderBy?: Prisma.LikeOrderByWithRelationInput; + }): Promise { return prisma.like.findMany(options); } @@ -18,13 +23,19 @@ class LikeRepository { return prisma.like.delete({ where: { id } }); } - async findLikeByUserIdAndProductId(userId: number, productId: number): Promise { + async findLikeByUserIdAndProductId( + userId: number, + productId: number, + ): Promise { return prisma.like.findFirst({ where: { userId, productId }, }); } - async findLikeByUserIdAndArticleId(userId: number, articleId: number): Promise { + async findLikeByUserIdAndArticleId( + userId: number, + articleId: number, + ): Promise { return prisma.like.findFirst({ where: { userId, articleId }, }); diff --git a/src/repositories/ProductRepository.ts b/src/repositories/ProductRepository.ts index e4b7bee7c..f65911eb8 100644 --- a/src/repositories/ProductRepository.ts +++ b/src/repositories/ProductRepository.ts @@ -6,7 +6,12 @@ class ProductRepository { return prisma.product.findUnique({ where: { id } }); } - async findProducts(options?: { skip?: number; take?: number; where?: Prisma.ProductWhereInput; orderBy?: Prisma.ProductOrderByWithRelationInput }): Promise { + async findProducts(options?: { + skip?: number; + take?: number; + where?: Prisma.ProductWhereInput; + orderBy?: Prisma.ProductOrderByWithRelationInput; + }): Promise { return prisma.product.findMany(options); } diff --git a/src/repositories/UserRepository.ts b/src/repositories/UserRepository.ts index bfc733a6a..173c2a58b 100644 --- a/src/repositories/UserRepository.ts +++ b/src/repositories/UserRepository.ts @@ -24,11 +24,11 @@ class UserRepository { async findProductsByUserId(userId: number): Promise { const userWithProducts = await prisma.user.findUnique({ - where: { id: userId }, - include: { products: true }, + where: { id: userId }, + include: { products: true }, }); return userWithProducts ? userWithProducts.products : null; } } -export default UserRepository; \ No newline at end of file +export default UserRepository; diff --git a/src/routes/articles.router.ts b/src/routes/articles.router.ts index 598015af3..003bf62ef 100644 --- a/src/routes/articles.router.ts +++ b/src/routes/articles.router.ts @@ -13,23 +13,23 @@ const articleRepository = new ArticleRepository(); const articleService = new ArticleService(articleRepository); const articlesController = new ArticlesController(articleService); -//article registration +//article registration router - .route('/articles') - .post(authMiddleware, validateArticle, articlesController.createArticle) + .route('/articles') + .post(authMiddleware, validateArticle, articlesController.createArticle) // 게시글 목록 조회 - .get(optionalAuthMiddleware, articlesController.getArticles); + .get(optionalAuthMiddleware, articlesController.getArticles); // article detail, modify, delete router - .route('/articles/:articleId') - .get(optionalAuthMiddleware, articlesController.getArticleById) - .patch(authMiddleware, validateArticle, articlesController.updateArticle) - .delete(authMiddleware, articlesController.deleteArticle); + .route('/articles/:articleId') + .get(optionalAuthMiddleware, articlesController.getArticleById) + .patch(authMiddleware, validateArticle, articlesController.updateArticle) + .delete(authMiddleware, articlesController.deleteArticle); -// article comment creation +// article comment creation router.post('/articles/:articleId/comments', authMiddleware, articlesController.createComment); - + // article comments check router.get('/articles/:articleId/comments', articlesController.getComments); @@ -41,5 +41,5 @@ router.delete('/articles/comments/:commentId', authMiddleware, articlesControlle // 게시글 좋아요 API router.post('/:articleId/like', authMiddleware, articlesController.toggleLike); - + export default router; diff --git a/src/routes/products.router.ts b/src/routes/products.router.ts index 6f1f69df3..6401e47d8 100644 --- a/src/routes/products.router.ts +++ b/src/routes/products.router.ts @@ -21,10 +21,10 @@ router.get('/products', optionalAuthMiddleware, productsController.getProducts); // datail, modify, delete router - .route('/products/:productId') - .get(optionalAuthMiddleware, productsController.getProductById) - .patch(validateProduct, authMiddleware, productsController.updateProduct) - .delete(authMiddleware, productsController.deleteProduct); + .route('/products/:productId') + .get(optionalAuthMiddleware, productsController.getProductById) + .patch(validateProduct, authMiddleware, productsController.updateProduct) + .delete(authMiddleware, productsController.deleteProduct); // comment router.post('/products/:productId/comments', authMiddleware, productsController.createComment); diff --git a/src/routes/upload.router.ts b/src/routes/upload.router.ts index a1b01367a..f095013c8 100644 --- a/src/routes/upload.router.ts +++ b/src/routes/upload.router.ts @@ -9,8 +9,8 @@ const router = Router(); //uploads 디렉토리가 없을 때 생성 const uploadDir = 'uploads/'; -if (!fs.existsSync(uploadDir)){ - fs.mkdirSync(uploadDir); +if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir); } const storage = multer.memoryStorage(); @@ -20,5 +20,5 @@ const uploadController = new UploadController(); //image api router.post('/upload', upload.single('image'), uploadController.uploadImage); - + export default router; diff --git a/src/types/express.d.ts b/src/types/express.d.ts index 54e9e7830..90f7ec13b 100644 --- a/src/types/express.d.ts +++ b/src/types/express.d.ts @@ -6,4 +6,4 @@ declare global { user?: PrismaUser; // Or a more specific subset of PrismaUser if only certain fields are always present } } -} \ No newline at end of file +}