From e4c8592ef6735c5e1dc6e95d6427e4f73e5cfc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Tue, 2 Sep 2025 15:25:19 +0900 Subject: [PATCH 01/54] =?UTF-8?q?feat:=20User=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 불필요한 주석 제거 및 User 모델 추가 --- prisma/schema.prisma | 10 ++++++++++ src/api/middlewares/upload.js | 1 - 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1dcf6bdc4..8fe4ea851 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,6 +13,16 @@ datasource db { url = env("DATABASE_URL") } +model User { + id Int @id @default(autoincrement()) + email String + nickname String + image String + password String + createAt DateTime @default(now()) + updateAt DateTime @updatedAt +} + model Product { id Int @id @default(autoincrement()) name String diff --git a/src/api/middlewares/upload.js b/src/api/middlewares/upload.js index 6521caa0f..dfad77e9d 100644 --- a/src/api/middlewares/upload.js +++ b/src/api/middlewares/upload.js @@ -1,4 +1,3 @@ -// multer.config.js (별도의 파일로 관리) import multer from "multer"; const storage = multer.diskStorage({ From 40b82fbb053243a4a1a9298126d252e610a70d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Tue, 2 Sep 2025 16:06:43 +0900 Subject: [PATCH 02/54] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=ED=95=B4=EC=8B=B1=20=EB=AF=B8=EB=93=A4=EC=9B=A8?= =?UTF-8?q?=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 --- src/api/middlewares/hashing.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/api/middlewares/hashing.js diff --git a/src/api/middlewares/hashing.js b/src/api/middlewares/hashing.js new file mode 100644 index 000000000..29cd731d4 --- /dev/null +++ b/src/api/middlewares/hashing.js @@ -0,0 +1,21 @@ +import bcrypt from "bcrypt"; + +export default function hassingPassword() { + return async (req, res, next) => { + const { password } = req.body; + + if (!password) { + return next(); + } + + try { + const salt = await bcrypt.genSalt(10); + const hashedPassword = await bcrypt.hash(password, salt); + + req.body.password = hashedPassword; + next(); + } catch (err) { + next(err); + } + }; +} From cfd26c66bf065572601c9922cd12618b8564e623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Tue, 2 Sep 2025 17:27:44 +0900 Subject: [PATCH 03/54] =?UTF-8?q?feat:=20User=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=ED=95=84=EB=93=9C=20null=20=EA=B0=92=20=ED=97=88?= =?UTF-8?q?=EC=9A=A9=20=EB=B0=8F=20email=20unique?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 15 +++++++++++++++ prisma/schema.prisma | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20250902082534_add_unique_email_to_user/migration.sql diff --git a/prisma/migrations/20250902082534_add_unique_email_to_user/migration.sql b/prisma/migrations/20250902082534_add_unique_email_to_user/migration.sql new file mode 100644 index 000000000..c8b5e20bd --- /dev/null +++ b/prisma/migrations/20250902082534_add_unique_email_to_user/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "public"."User" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "nickname" TEXT NOT NULL, + "image" TEXT, + "password" TEXT NOT NULL, + "createAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updateAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8fe4ea851..8d962650f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -15,9 +15,9 @@ datasource db { model User { id Int @id @default(autoincrement()) - email String + email String @unique nickname String - image String + image String? password String createAt DateTime @default(now()) updateAt DateTime @updatedAt From 63233b2ace06389cb9494bcee22d42f3a6008391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Tue, 2 Sep 2025 17:33:20 +0900 Subject: [PATCH 04/54] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85,=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 회원가입 API 구현 토큰 기반 로그인 기능 구현 --- package-lock.json | 788 ++++++++---------- package.json | 2 + src/api/controllers/AuthController.js | 28 + src/api/routes/AuthRouter.js | 10 + .../routes/{ImageRoute.js => ImageRouter.js} | 0 src/api/services/AuthService.js | 58 ++ src/main.js | 5 +- 7 files changed, 442 insertions(+), 449 deletions(-) create mode 100644 src/api/controllers/AuthController.js create mode 100644 src/api/routes/AuthRouter.js rename src/api/routes/{ImageRoute.js => ImageRouter.js} (100%) create mode 100644 src/api/services/AuthService.js diff --git a/package-lock.json b/package-lock.json index 2c3db5cc1..3e5e974a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,9 @@ "dependencies": { "@prisma/client": "^6.13.0", "axios": "^1.10.0", + "bcrypt": "^6.0.0", "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", "prisma": "^6.13.0" }, @@ -24,29 +26,6 @@ "typescript-eslint": "^8.37.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -105,9 +84,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -115,9 +94,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -165,9 +144,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", "dev": true, "license": "MIT", "engines": { @@ -188,13 +167,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -306,9 +285,9 @@ } }, "node_modules/@prisma/client": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.13.0.tgz", - "integrity": "sha512-8m2+I3dQovkV8CkDMluiwEV1TxV9EXdT6xaCz39O6jYw7mkf5gwfmi+cL4LJsEPwz5tG7sreBwkRpEMJedGYUQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.15.0.tgz", + "integrity": "sha512-wR2LXUbOH4cL/WToatI/Y2c7uzni76oNFND7+23ypLllBmIS8e3ZHhO+nud9iXSXKFt1SoM3fTZvHawg63emZw==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { @@ -328,60 +307,60 @@ } }, "node_modules/@prisma/config": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.13.0.tgz", - "integrity": "sha512-OYMM+pcrvj/NqNWCGESSxVG3O7kX6oWuGyvufTUNnDw740KIQvNyA4v0eILgkpuwsKIDU36beZCkUtIt0naTog==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.15.0.tgz", + "integrity": "sha512-KMEoec9b2u6zX0EbSEx/dRpx1oNLjqJEBZYyK0S3TTIbZ7GEGoVyGyFRk4C72+A38cuPLbfQGQvgOD+gBErKlA==", "license": "Apache-2.0", "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.16.12", - "read-package-up": "11.0.0" + "empathic": "2.0.0" } }, "node_modules/@prisma/debug": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.13.0.tgz", - "integrity": "sha512-um+9pfKJW0ihmM83id9FXGi5qEbVJ0Vxi1Gm0xpYsjwUBnw6s2LdPBbrsG9QXRX46K4CLWCTNvskXBup4i9hlw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.15.0.tgz", + "integrity": "sha512-y7cSeLuQmyt+A3hstAs6tsuAiVXSnw9T55ra77z0nbNkA8Lcq9rNcQg6PI00by/+WnE/aMRJ/W7sZWn2cgIy1g==", "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.13.0.tgz", - "integrity": "sha512-D+1B79LFvtWA0KTt8ALekQ6A/glB9w10ETknH5Y9g1k2NYYQOQy93ffiuqLn3Pl6IPJG3EsK/YMROKEaq8KBrA==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.15.0.tgz", + "integrity": "sha512-opITiR5ddFJ1N2iqa7mkRlohCZqVSsHhRcc29QXeldMljOf4FSellLT0J5goVb64EzRTKcIDeIsJBgmilNcKxA==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.13.0", - "@prisma/engines-version": "6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd", - "@prisma/fetch-engine": "6.13.0", - "@prisma/get-platform": "6.13.0" + "@prisma/debug": "6.15.0", + "@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb", + "@prisma/fetch-engine": "6.15.0", + "@prisma/get-platform": "6.15.0" } }, "node_modules/@prisma/engines-version": { - "version": "6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd.tgz", - "integrity": "sha512-MpPyKSzBX7P/ZY9odp9TSegnS/yH3CSbchQE9f0yBg3l2QyN59I6vGXcoYcqKC9VTniS1s18AMmhyr1OWavjHg==", + "version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb.tgz", + "integrity": "sha512-a/46aK5j6L3ePwilZYEgYDPrhBQ/n4gYjLxT5YncUTJJNRnTCVjPF86QdzUOLRdYjCLfhtZp9aum90W0J+trrg==", "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.13.0.tgz", - "integrity": "sha512-grmmq+4FeFKmaaytA8Ozc2+Tf3BC8xn/DVJos6LL022mfRlMZYjT3hZM0/xG7+5fO95zFG9CkDUs0m1S2rXs5Q==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.15.0.tgz", + "integrity": "sha512-xcT5f6b+OWBq6vTUnRCc7qL+Im570CtwvgSj+0MTSGA1o9UDSKZ/WANvwtiRXdbYWECpyC3CukoG3A04VTAPHw==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.13.0", - "@prisma/engines-version": "6.13.0-35.361e86d0ea4987e9f53a565309b3eed797a6bcbd", - "@prisma/get-platform": "6.13.0" + "@prisma/debug": "6.15.0", + "@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb", + "@prisma/get-platform": "6.15.0" } }, "node_modules/@prisma/get-platform": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.13.0.tgz", - "integrity": "sha512-Nii2pX50fY4QKKxQwm7/vvqT6Ku8yYJLZAFX4e2vzHwRdMqjugcOG5hOSLjxqoXb0cvOspV70TOhMzrw8kqAnw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.15.0.tgz", + "integrity": "sha512-Jbb+Xbxyp05NSR1x2epabetHiXvpO8tdN2YNoWoA/ZsbYyxxu/CO/ROBauIFuMXs3Ti+W7N7SJtWsHGaWte9Rg==", "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.13.0" + "@prisma/debug": "6.15.0" } }, "node_modules/@standard-schema/spec": { @@ -404,24 +383,18 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", - "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz", + "integrity": "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/type-utils": "8.38.0", - "@typescript-eslint/utils": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/type-utils": "8.41.0", + "@typescript-eslint/utils": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -435,9 +408,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.38.0", + "@typescript-eslint/parser": "^8.41.0", "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -451,16 +424,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", - "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.41.0.tgz", + "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4" }, "engines": { @@ -472,18 +445,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", - "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.41.0.tgz", + "integrity": "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.38.0", - "@typescript-eslint/types": "^8.38.0", + "@typescript-eslint/tsconfig-utils": "^8.41.0", + "@typescript-eslint/types": "^8.41.0", "debug": "^4.3.4" }, "engines": { @@ -494,18 +467,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", - "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.41.0.tgz", + "integrity": "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0" + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -516,9 +489,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", - "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz", + "integrity": "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==", "dev": true, "license": "MIT", "engines": { @@ -529,19 +502,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", - "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.41.0.tgz", + "integrity": "sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/utils": "8.41.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -554,13 +527,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", - "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.41.0.tgz", + "integrity": "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==", "dev": true, "license": "MIT", "engines": { @@ -572,16 +545,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", - "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.41.0.tgz", + "integrity": "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.38.0", - "@typescript-eslint/tsconfig-utils": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/visitor-keys": "8.38.0", + "@typescript-eslint/project-service": "8.41.0", + "@typescript-eslint/tsconfig-utils": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -597,7 +570,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -640,16 +613,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", - "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.41.0.tgz", + "integrity": "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.38.0", - "@typescript-eslint/types": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0" + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -660,17 +633,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", - "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.41.0.tgz", + "integrity": "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/types": "8.41.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -694,27 +667,6 @@ "node": ">= 0.6" } }, - "node_modules/accepts/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/accepts/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/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -955,13 +907,13 @@ } }, "node_modules/axios": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", - "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -972,6 +924,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/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -1016,6 +982,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", @@ -1484,6 +1456,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", @@ -1500,6 +1481,15 @@ "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==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1702,20 +1692,20 @@ } }, "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.34.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -1940,27 +1930,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/express/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/express/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/exsolve": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", @@ -2110,18 +2079,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-up-simple": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", - "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -2144,9 +2101,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -2195,6 +2152,27 @@ "node": ">= 6" } }, + "node_modules/form-data/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/form-data/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/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2478,18 +2456,6 @@ "node": ">= 0.4" } }, - "node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -2564,18 +2530,6 @@ "node": ">=0.8.19" } }, - "node_modules/index-to-position": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz", - "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3048,6 +3002,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -3084,6 +3039,40 @@ "dev": true, "license": "MIT" }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -3100,6 +3089,27 @@ "node": ">=4.0" } }, + "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/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3140,6 +3150,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3147,6 +3193,12 @@ "dev": true, "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/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3160,12 +3212,6 @@ "loose-envify": "cli.js" } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3221,21 +3267,21 @@ } }, "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==", + "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": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "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.52.0" + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -3308,6 +3354,27 @@ "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", @@ -3337,36 +3404,30 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-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==", "license": "MIT" }, - "node_modules/normalize-package-data": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^7.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data/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==", - "license": "ISC", + "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": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" } }, "node_modules/nypm": { @@ -3602,23 +3663,6 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "index-to-position": "^1.1.0", - "type-fest": "^4.39.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3676,12 +3720,6 @@ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", "license": "MIT" }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -3696,9 +3734,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==", "license": "MIT", "dependencies": { "confbox": "^0.2.2", @@ -3743,14 +3781,14 @@ } }, "node_modules/prisma": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.13.0.tgz", - "integrity": "sha512-dfzORf0AbcEyyzxuv2lEwG8g+WRGF/qDQTpHf/6JoHsyF5MyzCEZwClVaEmw3WXcobgadosOboKUgQU0kFs9kw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.15.0.tgz", + "integrity": "sha512-E6RCgOt+kUVtjtZgLQDBJ6md2tDItLJNExwI0XJeBc1FKL+Vwb+ovxXxuok9r8oBgsOXBA33fGDuE/0qDdCWqQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "6.13.0", - "@prisma/engines": "6.13.0" + "@prisma/config": "6.15.0", + "@prisma/engines": "6.15.0" }, "bin": { "prisma": "build/index.js" @@ -3901,42 +3939,6 @@ "dev": true, "license": "MIT" }, - "node_modules/read-package-up": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", - "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", - "license": "MIT", - "dependencies": { - "find-up-simple": "^1.0.0", - "read-pkg": "^9.0.0", - "type-fest": "^4.6.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", - "license": "MIT", - "dependencies": { - "@types/normalize-package-data": "^2.4.3", - "normalize-package-data": "^6.0.0", - "parse-json": "^8.0.0", - "type-fest": "^4.6.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -4200,27 +4202,6 @@ "node": ">= 18" } }, - "node_modules/send/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/send/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/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -4386,38 +4367,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", - "license": "CC0-1.0" - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -4649,18 +4598,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -4675,27 +4612,6 @@ "node": ">= 0.6" } }, - "node_modules/type-is/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/type-is/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/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -4781,9 +4697,9 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "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", "peer": true, @@ -4796,16 +4712,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.38.0.tgz", - "integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.41.0.tgz", + "integrity": "sha512-n66rzs5OBXW3SFSnZHr2T685q1i4ODm2nulFJhMZBotaTavsS8TrI3d7bDlRSs9yWo7HmyWrN9qDu14Qv7Y0Dw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.38.0", - "@typescript-eslint/parser": "8.38.0", - "@typescript-eslint/typescript-estree": "8.38.0", - "@typescript-eslint/utils": "8.38.0" + "@typescript-eslint/eslint-plugin": "8.41.0", + "@typescript-eslint/parser": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/utils": "8.41.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4816,7 +4732,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/unbox-primitive": { @@ -4838,18 +4754,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -4875,16 +4779,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index b3dd7dd58..294972ef4 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,9 @@ "dependencies": { "@prisma/client": "^6.13.0", "axios": "^1.10.0", + "bcrypt": "^6.0.0", "express": "^5.1.0", + "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", "prisma": "^6.13.0" }, diff --git a/src/api/controllers/AuthController.js b/src/api/controllers/AuthController.js new file mode 100644 index 000000000..a438c49b4 --- /dev/null +++ b/src/api/controllers/AuthController.js @@ -0,0 +1,28 @@ +import AuthService from "../services/AuthService.js"; + +const AuthController = { + async signup(req, res, next) { + try { + const { email, nickname, password } = req.body; + + const signupData = { email, nickname, password }; + const newUser = await AuthService.signup(signupData); + + res.status(201).json(newUser); + } catch (err) { + next(err); + } + }, + + async login(req, res, next) { + try { + const { user, token } = await AuthService.login(req.body); + res.status(200).json({ user, accessToken: token }); + next(); + } catch (err) { + next(err); + } + }, +}; + +export default AuthController; diff --git a/src/api/routes/AuthRouter.js b/src/api/routes/AuthRouter.js new file mode 100644 index 000000000..8b78028f8 --- /dev/null +++ b/src/api/routes/AuthRouter.js @@ -0,0 +1,10 @@ +import express from "express"; +import AuthController from "../controllers/AuthController.js"; +import hassingPassword from "../middlewares/hashing.js"; + +const router = express.Router(); + +router.post("/register", hassingPassword(), AuthController.signup); +router.post("/login", AuthController.login); // 나중에 user 유효성 검사 추가 + +export default router; diff --git a/src/api/routes/ImageRoute.js b/src/api/routes/ImageRouter.js similarity index 100% rename from src/api/routes/ImageRoute.js rename to src/api/routes/ImageRouter.js diff --git a/src/api/services/AuthService.js b/src/api/services/AuthService.js new file mode 100644 index 000000000..2335c0153 --- /dev/null +++ b/src/api/services/AuthService.js @@ -0,0 +1,58 @@ +import prisma from "../prismaClient.js"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; + +const JWT_SECRET = process.env.JWT_SECRET; + +const AuthService = { + async signup(signupData) { + const existingUser = await prisma.user.findUnique({ + where: { email: signupData.email }, + }); + + if (existingUser) { + const error = new Error("이미 가입된 이메일입니다."); + error.statusCode = 409; + throw error; + } + + // 사용자 생성 + const newUser = await prisma.user.create({ + data: signupData, + }); + + const { password: _, ...userWithoutPassword } = newUser; + return userWithoutPassword; + }, + + async login(loginData) { + const user = await prisma.user.findUnique({ + where: { email: loginData.email }, + }); + if (!user) { + const error = new Error("가입되지 않은 사용자입니다"); + error.statusCode = 401; + throw error; + } + + const isPasswordVaild = await bcrypt.compare( + loginData.password, + user.password + ); + + if (!isPasswordVaild) { + const error = new Error("비밀번호가 일치하지 않습니다."); + error.statusCode = 401; + throw error; + } + + // 토큰 생성 + const token = jwt.sign({ userId: user.id }, JWT_SECRET, { + expiresIn: "1h", + }); + + return { user, token }; + }, +}; + +export default AuthService; diff --git a/src/main.js b/src/main.js index 071ec1ea4..ddfa61a09 100644 --- a/src/main.js +++ b/src/main.js @@ -3,7 +3,8 @@ import ProductRouter from "./api/routes/ProductRouter.js"; import ArticleRouter from "./api/routes/ArticleRouter.js"; import CommentRouter from "./api/routes/CommentRouter.js"; import errorHandler from "./api/middlewares/errorHandler.js"; -import imageRouter from "./api/routes/ImageRoute.js"; +import imageRouter from "./api/routes/ImageRouter.js"; +import AuthRouter from "./api/routes/AuthRouter.js"; // import { testAllArticleService } from "./external/tests/testArticleService.js"; // import { testAllProductService } from "./external/tests/testProductService.js"; @@ -21,7 +22,7 @@ app.use("/comments", CommentRouter); app.use("/uploads", express.static("uploads")); app.use("/images", imageRouter); - +app.use("/auth", AuthRouter); app.use(errorHandler); app.listen(port, () => { From a654760d991e490356f0ae40a39eb089db00d9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Tue, 2 Sep 2025 18:24:53 +0900 Subject: [PATCH 05/54] =?UTF-8?q?feat:=20Refresh=20Token=20=EB=A1=9C?= =?UTF-8?q?=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 --- package-lock.json | 20 ++++++ package.json | 1 + .../migration.sql | 2 + prisma/schema.prisma | 15 ++-- src/api/controllers/AuthController.js | 30 +++++++- src/api/routes/AuthRouter.js | 2 +- src/api/services/AuthService.js | 68 ++++++++++++++++++- src/main.js | 2 + 8 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 prisma/migrations/20250902084804_add_refresh_token_to_user/migration.sql diff --git a/package-lock.json b/package-lock.json index 3e5e974a9..20fc37255 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@prisma/client": "^6.13.0", "axios": "^1.10.0", "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", @@ -1240,6 +1241,25 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", diff --git a/package.json b/package.json index 294972ef4..23e25c469 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@prisma/client": "^6.13.0", "axios": "^1.10.0", "bcrypt": "^6.0.0", + "cookie-parser": "^1.4.7", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", diff --git a/prisma/migrations/20250902084804_add_refresh_token_to_user/migration.sql b/prisma/migrations/20250902084804_add_refresh_token_to_user/migration.sql new file mode 100644 index 000000000..5d337e200 --- /dev/null +++ b/prisma/migrations/20250902084804_add_refresh_token_to_user/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "public"."User" ADD COLUMN "refreshToken" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8d962650f..64597d03a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,13 +14,14 @@ datasource db { } model User { - id Int @id @default(autoincrement()) - email String @unique - nickname String - image String? - password String - createAt DateTime @default(now()) - updateAt DateTime @updatedAt + id Int @id @default(autoincrement()) + email String @unique + nickname String + image String? + password String + refreshToken String? + createAt DateTime @default(now()) + updateAt DateTime @updatedAt } model Product { diff --git a/src/api/controllers/AuthController.js b/src/api/controllers/AuthController.js index a438c49b4..3fe44188b 100644 --- a/src/api/controllers/AuthController.js +++ b/src/api/controllers/AuthController.js @@ -16,9 +16,33 @@ const AuthController = { async login(req, res, next) { try { - const { user, token } = await AuthService.login(req.body); - res.status(200).json({ user, accessToken: token }); - next(); + const { user, accessToken, refreshToken } = await AuthService.login( + req.body + ); + + res.cookie("refreshToken", refreshToken, { + httpOnly: true, + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + + res.status(200).json({ user, accessToken, refreshToken }); + } catch (err) { + next(err); + } + }, + + async refreshToken(req, res, next) { + try { + const oldRefreshToken = req.cookies.refreshToken; + const { accessToken, refreshToken: newRefreshToken } = + await AuthService.refreshAccessToken(oldRefreshToken); + + res.cookie("refreshToken", newRefreshToken, { + httpOnly: true, + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + + res.status(200).json({ accessToken }); } catch (err) { next(err); } diff --git a/src/api/routes/AuthRouter.js b/src/api/routes/AuthRouter.js index 8b78028f8..f978c4fbf 100644 --- a/src/api/routes/AuthRouter.js +++ b/src/api/routes/AuthRouter.js @@ -6,5 +6,5 @@ const router = express.Router(); router.post("/register", hassingPassword(), AuthController.signup); router.post("/login", AuthController.login); // 나중에 user 유효성 검사 추가 - +router.post("/refresh-token", AuthController.refreshToken); export default router; diff --git a/src/api/services/AuthService.js b/src/api/services/AuthService.js index 2335c0153..a700259ab 100644 --- a/src/api/services/AuthService.js +++ b/src/api/services/AuthService.js @@ -3,6 +3,7 @@ import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; const JWT_SECRET = process.env.JWT_SECRET; +const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET; const AuthService = { async signup(signupData) { @@ -47,11 +48,74 @@ const AuthService = { } // 토큰 생성 - const token = jwt.sign({ userId: user.id }, JWT_SECRET, { + const accessToken = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: "1h", }); - return { user, token }; + // 로그인 시 RefreshToken 생성 + const refreshToken = jwt.sign({ userId: user.id }, REFRESH_TOKEN_SECRET, { + expiresIn: "7d", + }); + + await prisma.user.update({ + where: { id: user.id }, + data: { refreshToken }, + }); + + const { password: _, ...userWithoutPassword } = user; + // 테스트를 위해 refreshToken 출력 + return { userWithoutPassword, accessToken, refreshToken }; + }, + + async refreshAccessToken(oldRefreshToken) { + if (!oldRefreshToken) { + const error = new Error("Refresh Token이 제공되지 않았습니다."); + error.statusCode = 401; + throw error; + } + + try { + const decoded = jwt.verify(oldRefreshToken, REFRESH_TOKEN_SECRET); + const userId = decoded.userId; + + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user || user.refreshToken !== oldRefreshToken) { + const error = new Error("유효하지 않은 Refresh Token입니다."); + error.statusCode = 403; + throw error; + } + + // 새로운 Access Token 생성 + const newAccessToken = jwt.sign({ userId: user.id }, JWT_SECRET, { + expiresIn: "1h", + }); + + // prettier-ignore + const newRefreshToken = jwt.sign({ userId: user.id }, REFRESH_TOKEN_SECRET, { + expiresIn: "7d", + }) + + await prisma.user.update({ + where: { id: user.id }, + data: { refreshToken: newRefreshToken }, + }); + + return { accessToken: newAccessToken, refreshToken: newRefreshToken }; + } catch (err) { + if (err.name === "TokenExpiredError") { + const error = new Error( + "Refresh Token이 만료되었습니다. 다시 로그인해주세요." + ); + error.statusCode = 401; + throw error; + } + const error = new Error("Refresh Token 검증에 실패했습니다."); + error.statusCode = 403; + throw error; + } }, }; diff --git a/src/main.js b/src/main.js index ddfa61a09..bc55f55fc 100644 --- a/src/main.js +++ b/src/main.js @@ -5,6 +5,7 @@ import CommentRouter from "./api/routes/CommentRouter.js"; import errorHandler from "./api/middlewares/errorHandler.js"; import imageRouter from "./api/routes/ImageRouter.js"; import AuthRouter from "./api/routes/AuthRouter.js"; +import cookieParser from "cookie-parser"; // import { testAllArticleService } from "./external/tests/testArticleService.js"; // import { testAllProductService } from "./external/tests/testProductService.js"; @@ -15,6 +16,7 @@ const app = express(); const port = 3000; app.use(express.json()); +app.use(cookieParser()); app.use("/products", ProductRouter); app.use("/articles", ArticleRouter); From 598e48a5a0b860b82a7644b5b4a55218a815cee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 3 Sep 2025 09:37:32 +0900 Subject: [PATCH 06/54] =?UTF-8?q?feat:=20Refresh=20Token=20=ED=95=B4?= =?UTF-8?q?=EC=8B=B1=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 --- src/api/services/AuthService.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/api/services/AuthService.js b/src/api/services/AuthService.js index a700259ab..da43acfeb 100644 --- a/src/api/services/AuthService.js +++ b/src/api/services/AuthService.js @@ -1,6 +1,7 @@ import prisma from "../prismaClient.js"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; +import crypto from "crypto"; const JWT_SECRET = process.env.JWT_SECRET; const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET; @@ -26,6 +27,11 @@ const AuthService = { return userWithoutPassword; }, + // 토큰 해싱 메서드 정의 + hashToken(token) { + return crypto.createHash("sha256").update(token).digest("hex"); + }, + async login(loginData) { const user = await prisma.user.findUnique({ where: { email: loginData.email }, @@ -57,9 +63,11 @@ const AuthService = { expiresIn: "7d", }); + const hashedRefreshToken = this.hashToken(refreshToken); + await prisma.user.update({ where: { id: user.id }, - data: { refreshToken }, + data: { refreshToken: hashedRefreshToken }, }); const { password: _, ...userWithoutPassword } = user; @@ -67,6 +75,7 @@ const AuthService = { return { userWithoutPassword, accessToken, refreshToken }; }, + // AcessToken & RefreshToken을 재발급 받는 코드 async refreshAccessToken(oldRefreshToken) { if (!oldRefreshToken) { const error = new Error("Refresh Token이 제공되지 않았습니다."); @@ -74,6 +83,8 @@ const AuthService = { throw error; } + const hashedOldRefreshToken = this.hashToken(oldRefreshToken); + try { const decoded = jwt.verify(oldRefreshToken, REFRESH_TOKEN_SECRET); const userId = decoded.userId; @@ -82,7 +93,7 @@ const AuthService = { where: { id: userId }, }); - if (!user || user.refreshToken !== oldRefreshToken) { + if (!user || user.refreshToken !== hashedOldRefreshToken) { const error = new Error("유효하지 않은 Refresh Token입니다."); error.statusCode = 403; throw error; @@ -98,9 +109,11 @@ const AuthService = { expiresIn: "7d", }) + const hashedNewRefreshToken = this.hashToken(newRefreshToken); + await prisma.user.update({ where: { id: user.id }, - data: { refreshToken: newRefreshToken }, + data: { refreshToken: hashedNewRefreshToken }, }); return { accessToken: newAccessToken, refreshToken: newRefreshToken }; From e662697b39453cd411af407c525d23c8de302cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 3 Sep 2025 10:27:43 +0900 Subject: [PATCH 07/54] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=9D=B8=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User 모델과 상품 기능 1:N 관계 설정 쿠키 accessToken 누락 수정 쿠키에서 accessToken 추출 미들웨어 추가 상품 기능 인가 추가 --- .../migration.sql | 11 +++++ prisma/schema.prisma | 12 ++++-- src/api/controllers/AuthController.js | 9 ++++ src/api/controllers/ProductController.js | 16 +++++-- src/api/middlewares/authenticate.js | 42 +++++++++++++++++++ src/api/routes/AuthRouter.js | 2 +- src/api/routes/ProductRouter.js | 21 ++++++++-- src/api/services/ProductService.js | 27 ++++++++---- 8 files changed, 122 insertions(+), 18 deletions(-) create mode 100644 prisma/migrations/20250902093000_add_user_relation_to_product/migration.sql create mode 100644 src/api/middlewares/authenticate.js diff --git a/prisma/migrations/20250902093000_add_user_relation_to_product/migration.sql b/prisma/migrations/20250902093000_add_user_relation_to_product/migration.sql new file mode 100644 index 000000000..2bfe792e9 --- /dev/null +++ b/prisma/migrations/20250902093000_add_user_relation_to_product/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - Added the required column `userId` to the `products` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "public"."products" ADD COLUMN "userId" INTEGER NOT NULL; + +-- AddForeignKey +ALTER TABLE "public"."products" ADD CONSTRAINT "products_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 64597d03a..94ef5c6d6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,14 +14,15 @@ datasource db { } model User { - id Int @id @default(autoincrement()) - email String @unique + id Int @id @default(autoincrement()) + email String @unique nickname String image String? password String refreshToken String? - createAt DateTime @default(now()) - updateAt DateTime @updatedAt + createAt DateTime @default(now()) + updateAt DateTime @updatedAt + products Product[] } model Product { @@ -34,6 +35,9 @@ model Product { updatedAt DateTime @updatedAt Comment Comment[] + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + @@map("products") } diff --git a/src/api/controllers/AuthController.js b/src/api/controllers/AuthController.js index 3fe44188b..b84933c1f 100644 --- a/src/api/controllers/AuthController.js +++ b/src/api/controllers/AuthController.js @@ -25,6 +25,10 @@ const AuthController = { maxAge: 7 * 24 * 60 * 60 * 1000, }); + res.cookie("accessToken", accessToken, { + maxAge: 1 * 60 * 60 * 1000, + }); + res.status(200).json({ user, accessToken, refreshToken }); } catch (err) { next(err); @@ -37,11 +41,16 @@ const AuthController = { const { accessToken, refreshToken: newRefreshToken } = await AuthService.refreshAccessToken(oldRefreshToken); + // 쿠키에 토큰 저장 res.cookie("refreshToken", newRefreshToken, { httpOnly: true, maxAge: 7 * 24 * 60 * 60 * 1000, }); + res.cookie("accessToken", accessToken, { + maxAge: 1 * 60 * 60 * 1000, + }); + res.status(200).json({ accessToken }); } catch (err) { next(err); diff --git a/src/api/controllers/ProductController.js b/src/api/controllers/ProductController.js index afb9b74f6..6c99741d8 100644 --- a/src/api/controllers/ProductController.js +++ b/src/api/controllers/ProductController.js @@ -3,9 +3,13 @@ import ProductService from "../services/ProductService.js"; const ProductController = { async createProduct(req, res, next) { try { + const { id: userId } = req.user; const { name, description, price, tags } = req.body; const productData = { name, description, price, tags }; - const newProduct = await ProductService.createProduct(productData); + const newProduct = await ProductService.createProduct( + productData, + userId + ); res.status(201).json(newProduct); } catch (err) { @@ -32,9 +36,14 @@ const ProductController = { async patchProduct(req, res, next) { try { const { id } = req.params; + const { id: userId } = req.user; const updateData = req.body; - const product = await ProductService.patchProduct(Number(id), updateData); + const product = await ProductService.patchProduct( + Number(id), + updateData, + userId + ); if (!product) { return res.status(404).json({ error: "수정할 상품이 없음" }); } @@ -47,7 +56,8 @@ const ProductController = { async deleteProduct(req, res, next) { try { const { id } = req.params; - await ProductService.deleteProduct(Number(id)); + const { id: userId } = req.user; + await ProductService.deleteProduct(Number(id), userId); res.status(201).json({ success: "상품 삭제 성공" }); } catch (err) { diff --git a/src/api/middlewares/authenticate.js b/src/api/middlewares/authenticate.js new file mode 100644 index 000000000..29b8a078f --- /dev/null +++ b/src/api/middlewares/authenticate.js @@ -0,0 +1,42 @@ +import jwt from "jsonwebtoken"; +import prisma from "../prismaClient.js"; + +const JWT_SECRET = process.env.JWT_SECRET; + +export default async function authenticate(req, res, next) { + const { accessToken } = req.cookies; + + if (!accessToken) { + const error = new Error("인증 토큰이 필요합니다."); + error.statusCode = 401; + return next(error); + } + + try { + const decoded = jwt.verify(accessToken, JWT_SECRET); + const userId = decoded.userId; + + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + const error = new Error("토큰에 해당하는 사용자를 찾을 수 없습니다."); + error.statusCode = 404; + return next(error); + } + + req.user = user; + next(); + } catch (err) { + let message; + if (err.name === "TokenExpiredError") { + message = "토큰이 만료되었습니다."; + } else { + message = "유효하지 않은 토큰입니다."; + } + const error = new Error(message); + error.statusCode = 401; + next(error); + } +} diff --git a/src/api/routes/AuthRouter.js b/src/api/routes/AuthRouter.js index f978c4fbf..e26a08ad3 100644 --- a/src/api/routes/AuthRouter.js +++ b/src/api/routes/AuthRouter.js @@ -4,7 +4,7 @@ import hassingPassword from "../middlewares/hashing.js"; const router = express.Router(); -router.post("/register", hassingPassword(), AuthController.signup); +router.post("/signup", hassingPassword(), AuthController.signup); router.post("/login", AuthController.login); // 나중에 user 유효성 검사 추가 router.post("/refresh-token", AuthController.refreshToken); export default router; diff --git a/src/api/routes/ProductRouter.js b/src/api/routes/ProductRouter.js index 1ee2043e3..158de4fe8 100644 --- a/src/api/routes/ProductRouter.js +++ b/src/api/routes/ProductRouter.js @@ -1,13 +1,28 @@ import express from "express"; import ProductController from "../controllers/ProductController.js"; import validateProduct from "../middlewares/validateProduct.js"; +import authenticate from "../middlewares/authenticate.js"; const router = express.Router(); -router.post("/", validateProduct, ProductController.createProduct); +router.post( + "/", + authenticate, + validateProduct, + ProductController.createProduct +); + router.get("/:id", ProductController.findUniqueProduct); -router.patch("/:id", validateProduct, ProductController.patchProduct); -router.delete("/:id", ProductController.deleteProduct); + +router.patch( + "/:id", + authenticate, + validateProduct, + ProductController.patchProduct +); + +router.delete("/:id", authenticate, ProductController.deleteProduct); + router.get("/", ProductController.findManyProduct); export default router; diff --git a/src/api/services/ProductService.js b/src/api/services/ProductService.js index 8add5694e..38377919d 100644 --- a/src/api/services/ProductService.js +++ b/src/api/services/ProductService.js @@ -1,9 +1,9 @@ import prisma from "../prismaClient.js"; const ProductService = { - async createProduct(productData) { + async createProduct(productData, userId) { const newProduct = await prisma.product.create({ - data: productData, + data: { ...productData, userId }, }); return newProduct; }, @@ -15,16 +15,29 @@ const ProductService = { return product; }, - async patchProduct(id, updateData) { - const product = await prisma.product.update({ + async patchProduct(id, updateData, userId) { + const product = await prisma.product.findUnique({ where: { id } }); + + if (product.userId != userId) { + const error = new Error("상품을 수정할 권한이 없습니다."); + error.statusCode = 403; + throw error; + } + return await prisma.product.update({ where: { id }, data: updateData, }); - return product; }, - async deleteProduct(id) { - await prisma.product.delete({ + async deleteProduct(id, userId) { + const product = await prisma.product.findUnique({ where: { id } }); + + if (product.userId !== userId) { + const error = new Error("상품을 삭제할 권한이 없습니다."); + error.statusCode = 403; + throw error; + } + return await prisma.product.delete({ where: { id }, }); }, From e56baf7fe4df9bb4486276f97d63beecb580823b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 3 Sep 2025 10:49:52 +0900 Subject: [PATCH 08/54] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=9D=B8=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 11 ++++++++ prisma/schema.prisma | 4 +++ src/api/controllers/ArticleController.js | 13 ++++++--- src/api/routes/ArticleRouter.js | 17 +++++++++--- src/api/services/ArticleService.js | 27 ++++++++++++++----- 5 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 prisma/migrations/20250903014719_add_user_relation_to_article/migration.sql diff --git a/prisma/migrations/20250903014719_add_user_relation_to_article/migration.sql b/prisma/migrations/20250903014719_add_user_relation_to_article/migration.sql new file mode 100644 index 000000000..c4d7a7df1 --- /dev/null +++ b/prisma/migrations/20250903014719_add_user_relation_to_article/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - Added the required column `userId` to the `articles` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "public"."articles" ADD COLUMN "userId" INTEGER NOT NULL; + +-- AddForeignKey +ALTER TABLE "public"."articles" ADD CONSTRAINT "articles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 94ef5c6d6..13daa1e58 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -23,6 +23,7 @@ model User { createAt DateTime @default(now()) updateAt DateTime @updatedAt products Product[] + articles Article[] } model Product { @@ -49,6 +50,9 @@ model Article { updatedAt DateTime @updatedAt Comment Comment[] + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + @@map("articles") } diff --git a/src/api/controllers/ArticleController.js b/src/api/controllers/ArticleController.js index 82ff2b316..05030981f 100644 --- a/src/api/controllers/ArticleController.js +++ b/src/api/controllers/ArticleController.js @@ -3,12 +3,16 @@ import ArticleService from "../services/ArticleService.js"; const ArticleController = { async createArticle(req, res, next) { try { + const { id: userId } = req.user; const { title, content } = req.body; if (!title || !content) { return res.status(400).send("제목과 게시글을 입력해주세요."); } const articleData = { title, content }; - const newArticle = await ArticleService.createArticle(articleData); + const newArticle = await ArticleService.createArticle( + articleData, + userId + ); res.status(201).json(newArticle); } catch (err) { @@ -28,11 +32,13 @@ const ArticleController = { async updateArticle(req, res, next) { try { + const { id: userId } = req.user; const { id } = req.params; const updateData = req.body; const article = await ArticleService.updateArticle( Number(id), - updateData + updateData, + userId ); res.status(201).json(article); } catch (err) { @@ -42,8 +48,9 @@ const ArticleController = { async deleteArticle(req, res, next) { try { + const { id: userId } = req.user; const { id } = req.params; - await ArticleService.deleteArticle(Number(id)); + await ArticleService.deleteArticle(Number(id), userId); res.status(201).json({ success: "상품 삭제 성공" }); } catch (err) { next(err); diff --git a/src/api/routes/ArticleRouter.js b/src/api/routes/ArticleRouter.js index 9b97f7499..062299040 100644 --- a/src/api/routes/ArticleRouter.js +++ b/src/api/routes/ArticleRouter.js @@ -1,13 +1,24 @@ import express from "express"; import ArticleController from "../controllers/ArticleController.js"; import validateArticle from "../middlewares/validateArticle.js"; +import authenticate from "../middlewares/authenticate.js"; const router = express.Router(); -router.post("/", validateArticle, ArticleController.createArticle); +router.post( + "/", + authenticate, + validateArticle, + ArticleController.createArticle +); router.get("/:id", ArticleController.findUniqueArticle); -router.patch("/:id", validateArticle, ArticleController.updateArticle); -router.delete("/:id", ArticleController.deleteArticle); +router.patch( + "/:id", + authenticate, + validateArticle, + ArticleController.updateArticle +); +router.delete("/:id", authenticate, ArticleController.deleteArticle); router.get("/", ArticleController.findManyArticle); export default router; diff --git a/src/api/services/ArticleService.js b/src/api/services/ArticleService.js index 43b9188a1..c88f5deee 100644 --- a/src/api/services/ArticleService.js +++ b/src/api/services/ArticleService.js @@ -1,9 +1,9 @@ import prisma from "../prismaClient.js"; const ArticleService = { - async createArticle(articleData) { + async createArticle(articleData, userId) { const newArticle = await prisma.article.create({ - data: articleData, + data: { ...articleData, userId }, }); return newArticle; }, @@ -15,15 +15,30 @@ const ArticleService = { return article; }, - async updateArticle(id, updateData) { - const article = await prisma.article.update({ + async updateArticle(id, updateData, userId) { + const article = await prisma.article.findUnique({ where: { id } }); + + if (article.userId != userId) { + const error = new Error("게시글을 수정할 권한이 없습니다."); + error.statusCode = 403; + throw error; + } + + return await prisma.article.update({ where: { id }, data: updateData, }); - return article; }, - async deleteArticle(id) { + async deleteArticle(id, userId) { + const article = await prisma.article.findUnique({ where: { id } }); + + if (article.userId != userId) { + const error = new Error("게시글을 삭제할 권한이 없습니다."); + error.statusCode = 403; + throw error; + } + await prisma.article.delete({ where: { id }, }); From 762b045a32b66d8a088967f81fa31d2cd9ce2eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 3 Sep 2025 11:17:25 +0900 Subject: [PATCH 09/54] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=9D=B8=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 11 ++++++++ prisma/schema.prisma | 4 +++ src/api/controllers/CommentController.js | 9 ++++-- src/api/routes/CommentRouter.js | 7 +++-- src/api/services/CommentService.js | 28 +++++++++++++++---- 5 files changed, 48 insertions(+), 11 deletions(-) create mode 100644 prisma/migrations/20250903020948_add_user_relation_to_comment/migration.sql diff --git a/prisma/migrations/20250903020948_add_user_relation_to_comment/migration.sql b/prisma/migrations/20250903020948_add_user_relation_to_comment/migration.sql new file mode 100644 index 000000000..d73e278e0 --- /dev/null +++ b/prisma/migrations/20250903020948_add_user_relation_to_comment/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - Added the required column `userId` to the `comments` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "public"."comments" ADD COLUMN "userId" INTEGER NOT NULL; + +-- AddForeignKey +ALTER TABLE "public"."comments" ADD CONSTRAINT "comments_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 13daa1e58..24d45b5c0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -24,6 +24,7 @@ model User { updateAt DateTime @updatedAt products Product[] articles Article[] + Comment Comment[] } model Product { @@ -67,5 +68,8 @@ model Comment { product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) productId Int? + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + @@map("comments") } diff --git a/src/api/controllers/CommentController.js b/src/api/controllers/CommentController.js index 85ee18a94..37e942538 100644 --- a/src/api/controllers/CommentController.js +++ b/src/api/controllers/CommentController.js @@ -3,6 +3,7 @@ import CommentService from "../services/CommentService.js"; const CommentController = { async createComment(req, res, next) { try { + const { id: userId } = req.user; const { content, productId, articleId } = req.body; if (!content || (!productId && !articleId)) { @@ -19,6 +20,7 @@ const CommentController = { content, productId, articleId, + userId, }); res.status(201).json(newComment); } catch (err) { @@ -32,12 +34,14 @@ const CommentController = { async updateComment(req, res, next) { try { + const { id: userId } = req.user; const { id } = req.params; const updateData = req.body; const comment = await CommentService.updateComment( Number(id), - updateData + updateData, + userId ); res.status(201).json(comment, next); } catch (err) { @@ -50,8 +54,9 @@ const CommentController = { async deleteComment(req, res, next) { try { + const { id: userId } = req.user; const { id } = req.params; - await CommentService.deleteComment(Number(id)); + await CommentService.deleteComment(Number(id), userId); res.status(201).json({ success: "댓글 삭제" }); } catch (err) { if (err.code === "P2025") { diff --git a/src/api/routes/CommentRouter.js b/src/api/routes/CommentRouter.js index 1768f987a..44ebb4572 100644 --- a/src/api/routes/CommentRouter.js +++ b/src/api/routes/CommentRouter.js @@ -1,11 +1,12 @@ import express from "express"; import CommentController from "../controllers/CommentController.js"; +import authenticate from "../middlewares/authenticate.js"; const router = express.Router(); -router.post("/", CommentController.createComment); -router.patch("/:id", CommentController.updateComment); -router.delete("/:id", CommentController.deleteComment); +router.post("/", authenticate, CommentController.createComment); +router.patch("/:id", authenticate, CommentController.updateComment); +router.delete("/:id", authenticate, CommentController.deleteComment); router.get("/", CommentController.findManyComment); export default router; diff --git a/src/api/services/CommentService.js b/src/api/services/CommentService.js index 6c1cc2ea8..579dd79f2 100644 --- a/src/api/services/CommentService.js +++ b/src/api/services/CommentService.js @@ -1,27 +1,43 @@ import prisma from "../prismaClient.js"; const CommentService = { - async createComment({ content, productId, articleId }) { + async createComment({ content, productId, articleId, userId }) { const newComment = await prisma.comment.create({ data: { content, productId: productId || null, articleId: articleId || null, + userId, }, }); return newComment; }, - async updateComment(id, updateData) { - const comment = await prisma.comment.update({ + async updateComment(id, updateData, userId) { + const comment = await prisma.comment.findUnique({ where: { id } }); + + if (comment.userId != userId) { + const error = new Error("댓글을 수정할 권한이 없습니다."); + error.status = 403; + throw error; + } + + return await prisma.comment.update({ where: { id }, data: updateData, }); - return comment; }, - async deleteComment(id) { - await prisma.comment.delete({ + async deleteComment(id, userId) { + const comment = await prisma.comment.findUnique({ where: { id } }); + + if (comment.userId != userId) { + const error = new Error("댓글을 삭제할 권한이 없습니다."); + error.status = 403; + throw error; + } + + return await prisma.comment.delete({ where: { id }, }); }, From 9544433ee5aabdd1598e242e122de165479ab50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 3 Sep 2025 14:04:33 +0900 Subject: [PATCH 10/54] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=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 해싱 관련 미들웨어를 만들었는데, 비밀번호 수정에서 해당 미들웨어 사용가능한지 확인해보기 지금은 service단에만 구현함 --- src/api/controllers/MypageController.js | 50 +++++++++++++++ src/api/routes/MypageRouter.js | 11 ++++ src/api/services/MypageService.js | 83 +++++++++++++++++++++++++ src/main.js | 2 + 4 files changed, 146 insertions(+) create mode 100644 src/api/controllers/MypageController.js create mode 100644 src/api/routes/MypageRouter.js create mode 100644 src/api/services/MypageService.js diff --git a/src/api/controllers/MypageController.js b/src/api/controllers/MypageController.js new file mode 100644 index 000000000..87c4b17cc --- /dev/null +++ b/src/api/controllers/MypageController.js @@ -0,0 +1,50 @@ +import MypageService from "../services/MypageService.js"; + +const MypageController = { + async getUser(req, res, next) { + try { + const { id: userId } = req.user; + + const user = await MypageService.getUser(userId); + res.status(200).json(user); + } catch (err) { + next(err); + } + }, + + async updateUser(req, res, next) { + try { + const { id: userId } = req.user; + const updateData = req.body; + const updatedUser = await MypageService.updateUser(userId, updateData); + res.status(201).json(updatedUser); + } catch (err) { + next(err); + } + }, + + async updatePassword(req, res, next) { + try { + const { id: userId } = req.user; + const { oldPassword, newPassword } = req.body; + + await MypageService.updatePassword(userId, oldPassword, newPassword); + res.status(201).json("비밀번호 변경이 완료되었습니다."); + } catch (err) { + next(err); + } + }, + + async getProducts(req, res, next) { + try { + const { id: userId } = req.user; + + const products = await MypageService.getProducts(userId); + res.status(200).json(products); + } catch (err) { + next(err); + } + }, +}; + +export default MypageController; diff --git a/src/api/routes/MypageRouter.js b/src/api/routes/MypageRouter.js new file mode 100644 index 000000000..88016a03b --- /dev/null +++ b/src/api/routes/MypageRouter.js @@ -0,0 +1,11 @@ +import express from "express"; +import authenticate from "../middlewares/authenticate.js"; +import MypageController from "../controllers/MypageController.js"; +const router = express.Router(); + +router.get("/:id", authenticate, MypageController.getUser); +router.patch("/:id", authenticate, MypageController.updateUser); +router.patch("/:id/password", authenticate, MypageController.updatePassword); +router.get("/:id/products", authenticate, MypageController.getProducts); + +export default router; diff --git a/src/api/services/MypageService.js b/src/api/services/MypageService.js new file mode 100644 index 000000000..090142cf7 --- /dev/null +++ b/src/api/services/MypageService.js @@ -0,0 +1,83 @@ +import jwt from "jsonwebtoken"; +import prisma from "../prismaClient.js"; +import bcrypt from "bcrypt"; + +const JWT_SECRET = process.env.JWT_SECRET; + +const MypageService = { + async getUser(userId) { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { + email: true, + nickname: true, + image: true, + }, + }); + + if (!user) { + const error = new Error("사용자를 찾을 수 없습니다."); + error.statusCode = 404; + } + + return user; + }, + + async updateUser(userId, updateData) { + const { email, nickname, image } = updateData; + + const updatedUser = await prisma.user.update({ + where: { id: userId }, + data: { + email, + nickname, + image, + }, + }); + + const { password, refreshToken, ...UserData } = updatedUser; + return UserData; + }, + + async updatePassword(userId, oldPassword, newPassword) { + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + const error = new Error("사용자를 찾을 수 없습니다."); + error.status = 404; + throw error; + } + + // 기존 비밀번호 일치 여부 확인 (입력값으로 기존 비밀번호 받음) + const isPasswordValid = await bcrypt.compare(oldPassword, user.password); + if (!isPasswordValid) { + const error = new Error("기존 비밀번호가 일치하지 않습니다."); + error.statusCode = 403; + throw error; + } + + // 새로운 비밀번호 해시 처리 + const salt = await bcrypt.genSalt(10); + const hashedNewPassword = await bcrypt.hash(newPassword, salt); + + return await prisma.user.update({ + where: { id: userId }, + data: { password: hashedNewPassword }, + }); + }, + + async getProducts(userId) { + const products = await prisma.product.findMany({ + where: { userId }, + orderBy: { + createdAt: "desc", + }, + }); + + return products; + }, +}; + +export default MypageService; diff --git a/src/main.js b/src/main.js index bc55f55fc..7e45c2857 100644 --- a/src/main.js +++ b/src/main.js @@ -5,6 +5,7 @@ import CommentRouter from "./api/routes/CommentRouter.js"; import errorHandler from "./api/middlewares/errorHandler.js"; import imageRouter from "./api/routes/ImageRouter.js"; import AuthRouter from "./api/routes/AuthRouter.js"; +import MypageRouter from "./api/routes/MypageRouter.js"; import cookieParser from "cookie-parser"; // import { testAllArticleService } from "./external/tests/testArticleService.js"; // import { testAllProductService } from "./external/tests/testProductService.js"; @@ -21,6 +22,7 @@ app.use(cookieParser()); app.use("/products", ProductRouter); app.use("/articles", ArticleRouter); app.use("/comments", CommentRouter); +app.use("/mypage", MypageRouter); app.use("/uploads", express.static("uploads")); app.use("/images", imageRouter); From bfacd19f316cf62d299d5c85857d3434a6342896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 3 Sep 2025 14:23:33 +0900 Subject: [PATCH 11/54] =?UTF-8?q?feat:=20user=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EB=AF=B8=EB=93=A4=EC=9B=A8=EC=96=B4=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 zod로 구현 이후에 기존 유효성 검증 미들웨어도 zod로 바꾸기 --- package-lock.json | 12 +++++++++++- package.json | 3 ++- src/api/middlewares/validate.js | 20 ++++++++++++++++++++ src/api/middlewares/validateUser.js | 19 +++++++++++++++++++ src/api/routes/AuthRouter.js | 11 +++++++++-- src/api/services/MypageService.js | 3 --- 6 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 src/api/middlewares/validate.js create mode 100644 src/api/middlewares/validateUser.js diff --git a/package-lock.json b/package-lock.json index 20fc37255..6637fbf9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,8 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", - "prisma": "^6.13.0" + "prisma": "^6.13.0", + "zod": "^4.1.5" }, "devDependencies": { "@eslint/js": "^9.31.0", @@ -4950,6 +4951,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.5.tgz", + "integrity": "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 23e25c469..2aad12a5b 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", - "prisma": "^6.13.0" + "prisma": "^6.13.0", + "zod": "^4.1.5" }, "devDependencies": { "@eslint/js": "^9.31.0", diff --git a/src/api/middlewares/validate.js b/src/api/middlewares/validate.js new file mode 100644 index 000000000..93bd730cb --- /dev/null +++ b/src/api/middlewares/validate.js @@ -0,0 +1,20 @@ +import { ZodError } from "zod"; + +const validate = (schema) => (req, res, next) => { + try { + schema.parse(req.body); + next(); + } catch (err) { + if (err instanceof ZodError) { + return res.status(400).json({ + errors: err.errors.map((e) => ({ + field: e.path.join("."), + message: e.message, + })), + }); + } + next(err); + } +}; + +export default validate; diff --git a/src/api/middlewares/validateUser.js b/src/api/middlewares/validateUser.js new file mode 100644 index 000000000..988ce4f5c --- /dev/null +++ b/src/api/middlewares/validateUser.js @@ -0,0 +1,19 @@ +import { z } from "zod"; + +// User 유효성 검사 미들웨어는 zod 활용해서 구현해봄 +// 이후에 시간 남으면 다른 유효성 검사도 zod로 바꾸기 +export const signupSchema = z.object({ + email: z.string().email({ message: "유효한 이메일 형식이 아닙니다." }), + password: z + .string() + .min(8, { message: "비밀번호는 최소 8자 이상이어야합니다." }), + nickname: z + .string() + .min(2, { message: "닉네임은 최소 2자 이상이어야 합니다." }), + image: z.string().url({ message: "유효한 URL 형식이 아닙니다." }).nullable(), +}); + +export const loginSchema = z.object({ + email: z.string().email({ message: "유효한 이메일 형식이 아닙니다." }), + password: z.string().min(1, { message: "비밀번호를 입력해주세요." }), +}); diff --git a/src/api/routes/AuthRouter.js b/src/api/routes/AuthRouter.js index e26a08ad3..cae1f94e3 100644 --- a/src/api/routes/AuthRouter.js +++ b/src/api/routes/AuthRouter.js @@ -1,10 +1,17 @@ import express from "express"; import AuthController from "../controllers/AuthController.js"; import hassingPassword from "../middlewares/hashing.js"; +import validate from "../middlewares/validate.js"; +import { signupSchema, loginSchema } from "../middlewares/validateUser.js"; const router = express.Router(); -router.post("/signup", hassingPassword(), AuthController.signup); -router.post("/login", AuthController.login); // 나중에 user 유효성 검사 추가 +router.post( + "/signup", + validate(signupSchema), + hassingPassword(), + AuthController.signup +); +router.post("/login", validate(loginSchema), AuthController.login); // 나중에 user 유효성 검사 추가 router.post("/refresh-token", AuthController.refreshToken); export default router; diff --git a/src/api/services/MypageService.js b/src/api/services/MypageService.js index 090142cf7..ae9492d09 100644 --- a/src/api/services/MypageService.js +++ b/src/api/services/MypageService.js @@ -1,9 +1,6 @@ -import jwt from "jsonwebtoken"; import prisma from "../prismaClient.js"; import bcrypt from "bcrypt"; -const JWT_SECRET = process.env.JWT_SECRET; - const MypageService = { async getUser(userId) { const user = await prisma.user.findUnique({ From 5254cccaa2ce49c34c9cc27f89815412826c1eaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 3 Sep 2025 15:16:49 +0900 Subject: [PATCH 12/54] =?UTF-8?q?feat:=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=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 --- .../migration.sql | 25 ++++++++++++++++ prisma/schema.prisma | 25 ++++++++++++++-- src/api/controllers/LikeController.js | 18 +++++++++++ src/api/routes/LikeRouter.js | 9 ++++++ src/api/services/LikeService.js | 30 +++++++++++++++++++ src/main.js | 3 ++ 6 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20250903054051_add_like_model/migration.sql create mode 100644 src/api/controllers/LikeController.js create mode 100644 src/api/routes/LikeRouter.js create mode 100644 src/api/services/LikeService.js diff --git a/prisma/migrations/20250903054051_add_like_model/migration.sql b/prisma/migrations/20250903054051_add_like_model/migration.sql new file mode 100644 index 000000000..6e87214c4 --- /dev/null +++ b/prisma/migrations/20250903054051_add_like_model/migration.sql @@ -0,0 +1,25 @@ +-- CreateTable +CREATE TABLE "public"."likes" ( + "id" SERIAL NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "articleId" INTEGER, + "productId" INTEGER, + "userId" INTEGER NOT NULL, + + CONSTRAINT "likes_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "likes_userId_articleId_key" ON "public"."likes"("userId", "articleId"); + +-- CreateIndex +CREATE UNIQUE INDEX "likes_userId_productId_key" ON "public"."likes"("userId", "productId"); + +-- AddForeignKey +ALTER TABLE "public"."likes" ADD CONSTRAINT "likes_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "public"."articles"("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_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 24d45b5c0..122deafdb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -25,6 +25,7 @@ model User { products Product[] articles Article[] Comment Comment[] + Like Like[] } model Product { @@ -38,7 +39,8 @@ model Product { Comment Comment[] userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + Like Like[] @@map("products") } @@ -52,7 +54,8 @@ model Article { Comment Comment[] userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + Like Like[] @@map("articles") } @@ -73,3 +76,21 @@ model Comment { @@map("comments") } + +model Like { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + + articleId Int? + article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) + + productId Int? + product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) + + userId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, articleId]) + @@unique([userId, productId]) + @@map("likes") +} diff --git a/src/api/controllers/LikeController.js b/src/api/controllers/LikeController.js new file mode 100644 index 000000000..eaf66a9ad --- /dev/null +++ b/src/api/controllers/LikeController.js @@ -0,0 +1,18 @@ +import LikeService from "../services/LikeService.js"; + +const LikeController = { + async toggleLike(req, res, next) { + try { + const { id: userId } = req.user; + const { type, id } = req.params; + const contentId = Number(id); + + const result = await LikeService.toggleLike(userId, type, contentId); + res.status(200).json(result); + } catch (err) { + next(err); + } + }, +}; + +export default LikeController; diff --git a/src/api/routes/LikeRouter.js b/src/api/routes/LikeRouter.js new file mode 100644 index 000000000..e2f6fb7c7 --- /dev/null +++ b/src/api/routes/LikeRouter.js @@ -0,0 +1,9 @@ +import express from "express"; +import authenticate from "../middlewares/authenticate.js"; +import LikeController from "../controllers/LikeController.js"; + +const router = express.Router(); + +router.post("/:type/:id", authenticate, LikeController.toggleLike); + +export default router; diff --git a/src/api/services/LikeService.js b/src/api/services/LikeService.js new file mode 100644 index 000000000..f37ce9f23 --- /dev/null +++ b/src/api/services/LikeService.js @@ -0,0 +1,30 @@ +import prisma from "../prismaClient.js"; + +const LikeService = { + async toggleLike(userId, type, contentId) { + const user = { userId }; + if (type === "article") { + user.articleId = contentId; + } else { + user.productId = contentId; + } + + const existingLike = await prisma.like.findFirst({ + where: user, + }); + + if (existingLike) { + await prisma.like.delete({ + where: { id: existingLike.id }, + }); + return { message: "좋아요가 취소되었습니다.", liked: false }; + } else { + await prisma.like.create({ + data: user, + }); + return { message: "좋아요를 눌렀습니다.", liked: true }; + } + }, +}; + +export default LikeService; diff --git a/src/main.js b/src/main.js index 7e45c2857..d62461a3b 100644 --- a/src/main.js +++ b/src/main.js @@ -6,7 +6,9 @@ import errorHandler from "./api/middlewares/errorHandler.js"; import imageRouter from "./api/routes/ImageRouter.js"; import AuthRouter from "./api/routes/AuthRouter.js"; import MypageRouter from "./api/routes/MypageRouter.js"; +import LikeRouter from "./api/routes/LikeRouter.js"; import cookieParser from "cookie-parser"; + // import { testAllArticleService } from "./external/tests/testArticleService.js"; // import { testAllProductService } from "./external/tests/testProductService.js"; @@ -23,6 +25,7 @@ app.use("/products", ProductRouter); app.use("/articles", ArticleRouter); app.use("/comments", CommentRouter); app.use("/mypage", MypageRouter); +app.use("/likes", LikeRouter); app.use("/uploads", express.static("uploads")); app.use("/images", imageRouter); From bb5c0f38462b791d87faba70591e2451c4644352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 3 Sep 2025 15:40:35 +0900 Subject: [PATCH 13/54] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20=EC=97=AC=EB=B6=80=20=EA=B0=99=EC=9D=B4=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/controllers/ArticleController.js | 18 +++++++++++++++++- src/api/services/ArticleService.js | 15 ++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/api/controllers/ArticleController.js b/src/api/controllers/ArticleController.js index 05030981f..90383844c 100644 --- a/src/api/controllers/ArticleController.js +++ b/src/api/controllers/ArticleController.js @@ -1,4 +1,7 @@ import ArticleService from "../services/ArticleService.js"; +import jwt from "jsonwebtoken"; + +const JWT_SECRET = process.env.JWT_SECRET; const ArticleController = { async createArticle(req, res, next) { @@ -23,7 +26,20 @@ const ArticleController = { async findUniqueArticle(req, res, next) { try { const { id } = req.params; - const article = await ArticleService.findUniqueArticle(Number(id)); + const articleId = Number(id); + + let userId = null; + const token = req.cookies.accessToken; + + if (token) { + try { + const decoded = jwt.verify(token, JWT_SECRET); + userId = decoded.userId; + } catch (err) { + console.log("로그인되지 않은 사용자입니다(유효하지 않은 토큰)"); + } + } + const article = await ArticleService.findUniqueArticle(articleId, userId); res.status(200).json(article); } catch (err) { next(err); diff --git a/src/api/services/ArticleService.js b/src/api/services/ArticleService.js index c88f5deee..20a06c36e 100644 --- a/src/api/services/ArticleService.js +++ b/src/api/services/ArticleService.js @@ -1,3 +1,4 @@ +import { isValid } from "zod/v3"; import prisma from "../prismaClient.js"; const ArticleService = { @@ -8,11 +9,19 @@ const ArticleService = { return newArticle; }, - async findUniqueArticle(id) { + async findUniqueArticle(articleId, userId) { const article = await prisma.article.findUnique({ - where: { id }, + where: { id: articleId }, + }); + + if (!userId) { + return { ...article, isLiked: false }; + } + + const like = await prisma.like.findFirst({ + where: { userId, articleId }, }); - return article; + return { ...article, isLiked: !!like }; }, async updateArticle(id, updateData, userId) { From 06535d330aa29eee463576f5a369d76e392a980b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 3 Sep 2025 16:01:22 +0900 Subject: [PATCH 14/54] =?UTF-8?q?feat:=20=EC=83=81=ED=92=88=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94=20=EC=97=AC=EB=B6=80=20=EA=B0=99=EC=9D=B4=20=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 게시글 상품 상세 조회 시 에러 메시지 수정도 진행 --- src/api/controllers/ArticleController.js | 2 +- src/api/controllers/ProductController.js | 18 +++++++++++++++--- src/api/services/ProductService.js | 14 +++++++++++--- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/api/controllers/ArticleController.js b/src/api/controllers/ArticleController.js index 90383844c..670ce0ef6 100644 --- a/src/api/controllers/ArticleController.js +++ b/src/api/controllers/ArticleController.js @@ -36,7 +36,7 @@ const ArticleController = { const decoded = jwt.verify(token, JWT_SECRET); userId = decoded.userId; } catch (err) { - console.log("로그인되지 않은 사용자입니다(유효하지 않은 토큰)"); + console.error("토큰 검증 오류:", err); } } const article = await ArticleService.findUniqueArticle(articleId, userId); diff --git a/src/api/controllers/ProductController.js b/src/api/controllers/ProductController.js index 6c99741d8..3f2651b85 100644 --- a/src/api/controllers/ProductController.js +++ b/src/api/controllers/ProductController.js @@ -1,4 +1,7 @@ import ProductService from "../services/ProductService.js"; +import jwt from "jsonwebtoken"; + +const JWT_SECRET = process.env.JWT_SECRET; const ProductController = { async createProduct(req, res, next) { @@ -21,12 +24,21 @@ const ProductController = { try { //throw new Error("🔥에러 핸들러 테스트"); const { id } = req.params; - const product = await ProductService.findUniqueProduct(Number(id)); + const productId = Number(id); - if (!product) { - return res.status(404).json({ error: "해당 상품을 찾을 수 없음" }); + let userId = null; + const token = req.cookies.accessToken; + + if (token) { + try { + const decoded = jwt.verify(token, JWT_SECRET); + userId = decoded.userId; + } catch (err) { + console.error("토큰 검증 오류:", err); + } } + const product = await ProductService.findUniqueProduct(productId, userId); res.status(200).json(product); } catch (err) { next(err); diff --git a/src/api/services/ProductService.js b/src/api/services/ProductService.js index 38377919d..ae93df6e5 100644 --- a/src/api/services/ProductService.js +++ b/src/api/services/ProductService.js @@ -8,11 +8,19 @@ const ProductService = { return newProduct; }, - async findUniqueProduct(id) { + async findUniqueProduct(productId, userId) { const product = await prisma.product.findUnique({ - where: { id }, + where: { id: productId }, + }); + + if (!userId) { + return { ...product, isLiked: false }; + } + + const like = await prisma.like.findFirst({ + where: { userId, productId }, }); - return product; + return { ...product, isLiked: !!like }; }, async patchProduct(id, updateData, userId) { From 94629e4c5c8b71bd265cf63c6c265ca59af66955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 3 Sep 2025 16:09:51 +0900 Subject: [PATCH 15/54] =?UTF-8?q?feat:=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EB=88=84=EB=A5=B8=20=EC=83=81=ED=92=88=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/controllers/MypageController.js | 11 +++++++++++ src/api/routes/MypageRouter.js | 9 +++++---- src/api/services/MypageService.js | 14 ++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/api/controllers/MypageController.js b/src/api/controllers/MypageController.js index 87c4b17cc..e14e3be10 100644 --- a/src/api/controllers/MypageController.js +++ b/src/api/controllers/MypageController.js @@ -45,6 +45,17 @@ const MypageController = { next(err); } }, + + async getLikeProducts(req, res, next) { + try { + const { id: userId } = req.user; + + const products = await MypageService.getLikeProducts(userId); + res.status(200).json(products); + } catch (err) { + next(err); + } + }, }; export default MypageController; diff --git a/src/api/routes/MypageRouter.js b/src/api/routes/MypageRouter.js index 88016a03b..fcb71592d 100644 --- a/src/api/routes/MypageRouter.js +++ b/src/api/routes/MypageRouter.js @@ -3,9 +3,10 @@ import authenticate from "../middlewares/authenticate.js"; import MypageController from "../controllers/MypageController.js"; const router = express.Router(); -router.get("/:id", authenticate, MypageController.getUser); -router.patch("/:id", authenticate, MypageController.updateUser); -router.patch("/:id/password", authenticate, MypageController.updatePassword); -router.get("/:id/products", authenticate, MypageController.getProducts); +router.get("/", authenticate, MypageController.getUser); +router.patch("/", authenticate, MypageController.updateUser); +router.patch("/password", authenticate, MypageController.updatePassword); +router.get("/products", authenticate, MypageController.getProducts); +router.get("/like-products", authenticate, MypageController.getLikeProducts); export default router; diff --git a/src/api/services/MypageService.js b/src/api/services/MypageService.js index ae9492d09..75a375251 100644 --- a/src/api/services/MypageService.js +++ b/src/api/services/MypageService.js @@ -75,6 +75,20 @@ const MypageService = { return products; }, + + async getLikeProducts(userId) { + const likedProducts = await prisma.like.findMany({ + where: { userId, productId: { not: null } }, + include: { + product: true, + }, + orderBy: { + createdAt: "desc", + }, + }); + + return likedProducts.map((like) => like.product); + }, }; export default MypageService; From 9bb398f0d14c2cd9c3004f3e001df6827ecbb179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 3 Sep 2025 18:40:11 +0900 Subject: [PATCH 16/54] =?UTF-8?q?refactor:=20=EB=AF=B8=EB=93=A4=EC=9B=A8?= =?UTF-8?q?=EC=96=B4=20=ED=8F=B4=EB=8D=94=20=EB=82=B4=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=A1=9C=EC=A7=81=EB=A7=8C=20?= =?UTF-8?q?=EB=94=B0=EB=A1=9C=20=EB=AA=A8=EC=95=84=EB=91=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/middlewares/{ => validators}/validate.js | 0 src/api/middlewares/{ => validators}/validateArticle.js | 0 src/api/middlewares/{ => validators}/validateProduct.js | 0 src/api/middlewares/{ => validators}/validateUser.js | 0 src/api/routes/ArticleRouter.js | 2 +- src/api/routes/AuthRouter.js | 7 +++++-- src/api/routes/ProductRouter.js | 2 +- 7 files changed, 7 insertions(+), 4 deletions(-) rename src/api/middlewares/{ => validators}/validate.js (100%) rename src/api/middlewares/{ => validators}/validateArticle.js (100%) rename src/api/middlewares/{ => validators}/validateProduct.js (100%) rename src/api/middlewares/{ => validators}/validateUser.js (100%) diff --git a/src/api/middlewares/validate.js b/src/api/middlewares/validators/validate.js similarity index 100% rename from src/api/middlewares/validate.js rename to src/api/middlewares/validators/validate.js diff --git a/src/api/middlewares/validateArticle.js b/src/api/middlewares/validators/validateArticle.js similarity index 100% rename from src/api/middlewares/validateArticle.js rename to src/api/middlewares/validators/validateArticle.js diff --git a/src/api/middlewares/validateProduct.js b/src/api/middlewares/validators/validateProduct.js similarity index 100% rename from src/api/middlewares/validateProduct.js rename to src/api/middlewares/validators/validateProduct.js diff --git a/src/api/middlewares/validateUser.js b/src/api/middlewares/validators/validateUser.js similarity index 100% rename from src/api/middlewares/validateUser.js rename to src/api/middlewares/validators/validateUser.js diff --git a/src/api/routes/ArticleRouter.js b/src/api/routes/ArticleRouter.js index 062299040..c594af6f7 100644 --- a/src/api/routes/ArticleRouter.js +++ b/src/api/routes/ArticleRouter.js @@ -1,6 +1,6 @@ import express from "express"; import ArticleController from "../controllers/ArticleController.js"; -import validateArticle from "../middlewares/validateArticle.js"; +import validateArticle from "../middlewares/validators/validateArticle.js"; import authenticate from "../middlewares/authenticate.js"; const router = express.Router(); diff --git a/src/api/routes/AuthRouter.js b/src/api/routes/AuthRouter.js index cae1f94e3..6430e5169 100644 --- a/src/api/routes/AuthRouter.js +++ b/src/api/routes/AuthRouter.js @@ -1,8 +1,11 @@ import express from "express"; import AuthController from "../controllers/AuthController.js"; import hassingPassword from "../middlewares/hashing.js"; -import validate from "../middlewares/validate.js"; -import { signupSchema, loginSchema } from "../middlewares/validateUser.js"; +import validate from "../middlewares/validators/validate.js"; +import { + signupSchema, + loginSchema, +} from "../middlewares/validators/validateUser.js"; const router = express.Router(); diff --git a/src/api/routes/ProductRouter.js b/src/api/routes/ProductRouter.js index 158de4fe8..701313617 100644 --- a/src/api/routes/ProductRouter.js +++ b/src/api/routes/ProductRouter.js @@ -1,6 +1,6 @@ import express from "express"; import ProductController from "../controllers/ProductController.js"; -import validateProduct from "../middlewares/validateProduct.js"; +import validateProduct from "../middlewares/validators/validateProduct.js"; import authenticate from "../middlewares/authenticate.js"; const router = express.Router(); From f3520c9271e8d5a5974a27165828eea53ce7bab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 3 Sep 2025 18:47:09 +0900 Subject: [PATCH 17/54] =?UTF-8?q?refactor:=20libs=20=ED=8F=B4=EB=8D=94=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20prisma=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/{ => libs}/prismaClient.js | 0 src/api/middlewares/authenticate.js | 2 +- src/api/services/ArticleService.js | 2 +- src/api/services/AuthService.js | 2 +- src/api/services/CommentService.js | 2 +- src/api/services/LikeService.js | 2 +- src/api/services/MypageService.js | 2 +- src/api/services/ProductService.js | 2 +- 8 files changed, 7 insertions(+), 7 deletions(-) rename src/api/{ => libs}/prismaClient.js (100%) diff --git a/src/api/prismaClient.js b/src/api/libs/prismaClient.js similarity index 100% rename from src/api/prismaClient.js rename to src/api/libs/prismaClient.js diff --git a/src/api/middlewares/authenticate.js b/src/api/middlewares/authenticate.js index 29b8a078f..107856a02 100644 --- a/src/api/middlewares/authenticate.js +++ b/src/api/middlewares/authenticate.js @@ -1,5 +1,5 @@ import jwt from "jsonwebtoken"; -import prisma from "../prismaClient.js"; +import prisma from "../libs/prismaClient.js"; const JWT_SECRET = process.env.JWT_SECRET; diff --git a/src/api/services/ArticleService.js b/src/api/services/ArticleService.js index 20a06c36e..c3036ba8e 100644 --- a/src/api/services/ArticleService.js +++ b/src/api/services/ArticleService.js @@ -1,5 +1,5 @@ import { isValid } from "zod/v3"; -import prisma from "../prismaClient.js"; +import prisma from "../libs/prismaClient.js"; const ArticleService = { async createArticle(articleData, userId) { diff --git a/src/api/services/AuthService.js b/src/api/services/AuthService.js index da43acfeb..d4624bc3a 100644 --- a/src/api/services/AuthService.js +++ b/src/api/services/AuthService.js @@ -1,4 +1,4 @@ -import prisma from "../prismaClient.js"; +import prisma from "../libs/prismaClient.js"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import crypto from "crypto"; diff --git a/src/api/services/CommentService.js b/src/api/services/CommentService.js index 579dd79f2..68c7b355d 100644 --- a/src/api/services/CommentService.js +++ b/src/api/services/CommentService.js @@ -1,4 +1,4 @@ -import prisma from "../prismaClient.js"; +import prisma from "../libs/prismaClient.js"; const CommentService = { async createComment({ content, productId, articleId, userId }) { diff --git a/src/api/services/LikeService.js b/src/api/services/LikeService.js index f37ce9f23..947e610c6 100644 --- a/src/api/services/LikeService.js +++ b/src/api/services/LikeService.js @@ -1,4 +1,4 @@ -import prisma from "../prismaClient.js"; +import prisma from "../libs/prismaClient.js"; const LikeService = { async toggleLike(userId, type, contentId) { diff --git a/src/api/services/MypageService.js b/src/api/services/MypageService.js index 75a375251..b65bca810 100644 --- a/src/api/services/MypageService.js +++ b/src/api/services/MypageService.js @@ -1,4 +1,4 @@ -import prisma from "../prismaClient.js"; +import prisma from "../libs/prismaClient.js"; import bcrypt from "bcrypt"; const MypageService = { diff --git a/src/api/services/ProductService.js b/src/api/services/ProductService.js index ae93df6e5..b0220fa62 100644 --- a/src/api/services/ProductService.js +++ b/src/api/services/ProductService.js @@ -1,4 +1,4 @@ -import prisma from "../prismaClient.js"; +import prisma from "../libs/prismaClient.js"; const ProductService = { async createProduct(productData, userId) { From 93a3e4dc8c453394da2b1061dcd13c322d6c5f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Thu, 4 Sep 2025 09:16:33 +0900 Subject: [PATCH 18/54] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=95=A8=EC=88=98=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?&=20=EC=9D=BC=EB=B6=80=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validate.js 에러 메시지 오류 수정 불필요한 import 제거 토큰 생성 함수 분리 --- src/api/libs/constants.js | 8 +++++++ src/api/libs/token.js | 15 ++++++++++++ src/api/middlewares/validators/validate.js | 2 +- .../middlewares/validators/validateUser.js | 1 - src/api/services/ArticleService.js | 1 - src/api/services/AuthService.js | 24 ++++--------------- 6 files changed, 29 insertions(+), 22 deletions(-) create mode 100644 src/api/libs/constants.js create mode 100644 src/api/libs/token.js diff --git a/src/api/libs/constants.js b/src/api/libs/constants.js new file mode 100644 index 000000000..614e3dfed --- /dev/null +++ b/src/api/libs/constants.js @@ -0,0 +1,8 @@ +import dotenv from "dotenv"; + +dotenv.config(); + +const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET; +const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET; + +export { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET }; diff --git a/src/api/libs/token.js b/src/api/libs/token.js new file mode 100644 index 000000000..2e50ac6c7 --- /dev/null +++ b/src/api/libs/token.js @@ -0,0 +1,15 @@ +import jwt from "jsonwebtoken"; +import { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET } from "./constants.js"; + +function generateTokens(userId) { + const accessToken = jwt.sign({ id: userId }, ACCESS_TOKEN_SECRET, { + expiresIn: "1h", + }); + const refreshToken = jwt.sign({ id: userId }, REFRESH_TOKEN_SECRET, { + expiresIn: "7d", + }); + + return { accessToken, refreshToken }; +} + +export { generateTokens }; diff --git a/src/api/middlewares/validators/validate.js b/src/api/middlewares/validators/validate.js index 93bd730cb..2bb5dfe4b 100644 --- a/src/api/middlewares/validators/validate.js +++ b/src/api/middlewares/validators/validate.js @@ -7,7 +7,7 @@ const validate = (schema) => (req, res, next) => { } catch (err) { if (err instanceof ZodError) { return res.status(400).json({ - errors: err.errors.map((e) => ({ + errors: err.issues.map((e) => ({ field: e.path.join("."), message: e.message, })), diff --git a/src/api/middlewares/validators/validateUser.js b/src/api/middlewares/validators/validateUser.js index 988ce4f5c..201e77c46 100644 --- a/src/api/middlewares/validators/validateUser.js +++ b/src/api/middlewares/validators/validateUser.js @@ -10,7 +10,6 @@ export const signupSchema = z.object({ nickname: z .string() .min(2, { message: "닉네임은 최소 2자 이상이어야 합니다." }), - image: z.string().url({ message: "유효한 URL 형식이 아닙니다." }).nullable(), }); export const loginSchema = z.object({ diff --git a/src/api/services/ArticleService.js b/src/api/services/ArticleService.js index c3036ba8e..09db54ed3 100644 --- a/src/api/services/ArticleService.js +++ b/src/api/services/ArticleService.js @@ -1,4 +1,3 @@ -import { isValid } from "zod/v3"; import prisma from "../libs/prismaClient.js"; const ArticleService = { diff --git a/src/api/services/AuthService.js b/src/api/services/AuthService.js index d4624bc3a..595ad6890 100644 --- a/src/api/services/AuthService.js +++ b/src/api/services/AuthService.js @@ -2,8 +2,8 @@ import prisma from "../libs/prismaClient.js"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import crypto from "crypto"; +import { generateTokens } from "../libs/token.js"; -const JWT_SECRET = process.env.JWT_SECRET; const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET; const AuthService = { @@ -53,15 +53,8 @@ const AuthService = { throw error; } - // 토큰 생성 - const accessToken = jwt.sign({ userId: user.id }, JWT_SECRET, { - expiresIn: "1h", - }); - - // 로그인 시 RefreshToken 생성 - const refreshToken = jwt.sign({ userId: user.id }, REFRESH_TOKEN_SECRET, { - expiresIn: "7d", - }); + // 액세스 토큰 및 리프레시 토큰 생성 + const { accessToken, refreshToken } = generateTokens(user.id); const hashedRefreshToken = this.hashToken(refreshToken); @@ -99,15 +92,8 @@ const AuthService = { throw error; } - // 새로운 Access Token 생성 - const newAccessToken = jwt.sign({ userId: user.id }, JWT_SECRET, { - expiresIn: "1h", - }); - - // prettier-ignore - const newRefreshToken = jwt.sign({ userId: user.id }, REFRESH_TOKEN_SECRET, { - expiresIn: "7d", - }) + // 새로운 Access Token, Refresh Token 생성 + const { newAccessToken, newRefreshToken } = generateTokens(user.id); const hashedNewRefreshToken = this.hashToken(newRefreshToken); From 0cb20ac9db7d4b2472c8f6c49cdac04a08848e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Thu, 4 Sep 2025 09:40:10 +0900 Subject: [PATCH 19/54] =?UTF-8?q?refactor:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=95=B4=EC=8B=B1=20=ED=95=A8=EC=88=98=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 불필요해진 주석 제거 및 회원가입 로직 리팩토링 기존 미들웨어로 관리되던 해싱 관련 파일을 libs로 이동 --- src/api/controllers/AuthController.js | 6 +----- src/api/libs/hashing.js | 13 +++++++++++++ src/api/middlewares/hashing.js | 21 --------------------- src/api/routes/AuthRouter.js | 10 ++-------- src/api/services/AuthService.js | 20 ++++++++++---------- 5 files changed, 26 insertions(+), 44 deletions(-) create mode 100644 src/api/libs/hashing.js delete mode 100644 src/api/middlewares/hashing.js diff --git a/src/api/controllers/AuthController.js b/src/api/controllers/AuthController.js index b84933c1f..dea9d8f1d 100644 --- a/src/api/controllers/AuthController.js +++ b/src/api/controllers/AuthController.js @@ -3,11 +3,7 @@ import AuthService from "../services/AuthService.js"; const AuthController = { async signup(req, res, next) { try { - const { email, nickname, password } = req.body; - - const signupData = { email, nickname, password }; - const newUser = await AuthService.signup(signupData); - + const newUser = await AuthService.signup(req.body); res.status(201).json(newUser); } catch (err) { next(err); diff --git a/src/api/libs/hashing.js b/src/api/libs/hashing.js new file mode 100644 index 000000000..6b9162097 --- /dev/null +++ b/src/api/libs/hashing.js @@ -0,0 +1,13 @@ +import bcrypt from "bcrypt"; + +async function hashing(hashword) { + try { + const salt = await bcrypt.genSalt(10); + const hashedWord = await bcrypt.hash(hashword, salt); + return hashedWord; + } catch (err) { + throw err; + } +} + +export { hashing }; diff --git a/src/api/middlewares/hashing.js b/src/api/middlewares/hashing.js deleted file mode 100644 index 29cd731d4..000000000 --- a/src/api/middlewares/hashing.js +++ /dev/null @@ -1,21 +0,0 @@ -import bcrypt from "bcrypt"; - -export default function hassingPassword() { - return async (req, res, next) => { - const { password } = req.body; - - if (!password) { - return next(); - } - - try { - const salt = await bcrypt.genSalt(10); - const hashedPassword = await bcrypt.hash(password, salt); - - req.body.password = hashedPassword; - next(); - } catch (err) { - next(err); - } - }; -} diff --git a/src/api/routes/AuthRouter.js b/src/api/routes/AuthRouter.js index 6430e5169..96a971906 100644 --- a/src/api/routes/AuthRouter.js +++ b/src/api/routes/AuthRouter.js @@ -1,6 +1,5 @@ import express from "express"; import AuthController from "../controllers/AuthController.js"; -import hassingPassword from "../middlewares/hashing.js"; import validate from "../middlewares/validators/validate.js"; import { signupSchema, @@ -9,12 +8,7 @@ import { const router = express.Router(); -router.post( - "/signup", - validate(signupSchema), - hassingPassword(), - AuthController.signup -); -router.post("/login", validate(loginSchema), AuthController.login); // 나중에 user 유효성 검사 추가 +router.post("/signup", validate(signupSchema), AuthController.signup); +router.post("/login", validate(loginSchema), AuthController.login); router.post("/refresh-token", AuthController.refreshToken); export default router; diff --git a/src/api/services/AuthService.js b/src/api/services/AuthService.js index 595ad6890..4a3cc5b76 100644 --- a/src/api/services/AuthService.js +++ b/src/api/services/AuthService.js @@ -1,13 +1,14 @@ import prisma from "../libs/prismaClient.js"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; -import crypto from "crypto"; +import { hashing } from "../libs/hashing.js"; import { generateTokens } from "../libs/token.js"; const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET; const AuthService = { async signup(signupData) { + // 이메일로 이미 존재하는 사용자인지 확인 const existingUser = await prisma.user.findUnique({ where: { email: signupData.email }, }); @@ -18,20 +19,19 @@ const AuthService = { throw error; } + // 사용자 생성 전 비밀번호 해싱 + const { email, nickname, password } = signupData; + const hashedPassword = await hashing(password); + // 사용자 생성 const newUser = await prisma.user.create({ - data: signupData, + data: { email, nickname, password: hashedPassword }, }); const { password: _, ...userWithoutPassword } = newUser; return userWithoutPassword; }, - // 토큰 해싱 메서드 정의 - hashToken(token) { - return crypto.createHash("sha256").update(token).digest("hex"); - }, - async login(loginData) { const user = await prisma.user.findUnique({ where: { email: loginData.email }, @@ -56,7 +56,7 @@ const AuthService = { // 액세스 토큰 및 리프레시 토큰 생성 const { accessToken, refreshToken } = generateTokens(user.id); - const hashedRefreshToken = this.hashToken(refreshToken); + const hashedRefreshToken = await hashing(refreshToken); await prisma.user.update({ where: { id: user.id }, @@ -76,7 +76,7 @@ const AuthService = { throw error; } - const hashedOldRefreshToken = this.hashToken(oldRefreshToken); + const hashedOldRefreshToken = await hashing(oldRefreshToken); try { const decoded = jwt.verify(oldRefreshToken, REFRESH_TOKEN_SECRET); @@ -95,7 +95,7 @@ const AuthService = { // 새로운 Access Token, Refresh Token 생성 const { newAccessToken, newRefreshToken } = generateTokens(user.id); - const hashedNewRefreshToken = this.hashToken(newRefreshToken); + const hashedNewRefreshToken = await hashing(newRefreshToken); await prisma.user.update({ where: { id: user.id }, From 91d5bf270868e20e8a47e1e38876e0a5d013902a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Thu, 4 Sep 2025 10:35:45 +0900 Subject: [PATCH 20/54] =?UTF-8?q?refactor:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/controllers/AuthController.js | 10 +++---- src/api/libs/hashing.js | 6 +++- src/api/services/AuthService.js | 43 +++++++++++++++------------ 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/api/controllers/AuthController.js b/src/api/controllers/AuthController.js index dea9d8f1d..1d22364ed 100644 --- a/src/api/controllers/AuthController.js +++ b/src/api/controllers/AuthController.js @@ -12,9 +12,7 @@ const AuthController = { async login(req, res, next) { try { - const { user, accessToken, refreshToken } = await AuthService.login( - req.body - ); + const { user, accessToken, refreshToken } = await AuthService.login(req.body); res.cookie("refreshToken", refreshToken, { httpOnly: true, @@ -33,9 +31,9 @@ const AuthController = { async refreshToken(req, res, next) { try { - const oldRefreshToken = req.cookies.refreshToken; - const { accessToken, refreshToken: newRefreshToken } = - await AuthService.refreshAccessToken(oldRefreshToken); + const { accessToken, refreshToken: newRefreshToken } = await AuthService.refreshAccessToken( + req.cookies.refreshToken + ); // 쿠키에 토큰 저장 res.cookie("refreshToken", newRefreshToken, { diff --git a/src/api/libs/hashing.js b/src/api/libs/hashing.js index 6b9162097..077ada88d 100644 --- a/src/api/libs/hashing.js +++ b/src/api/libs/hashing.js @@ -10,4 +10,8 @@ async function hashing(hashword) { } } -export { hashing }; +async function compareWords(word, hashedWord) { + return await bcrypt.compare(word, hashedWord); +} + +export { hashing, compareWords }; diff --git a/src/api/services/AuthService.js b/src/api/services/AuthService.js index 4a3cc5b76..787f649c7 100644 --- a/src/api/services/AuthService.js +++ b/src/api/services/AuthService.js @@ -1,7 +1,6 @@ import prisma from "../libs/prismaClient.js"; -import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; -import { hashing } from "../libs/hashing.js"; +import { hashing, compareWords } from "../libs/hashing.js"; import { generateTokens } from "../libs/token.js"; const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET; @@ -33,19 +32,19 @@ const AuthService = { }, async login(loginData) { + // 이메일로 존재하는 사용자인지 확인 const user = await prisma.user.findUnique({ where: { email: loginData.email }, }); + if (!user) { const error = new Error("가입되지 않은 사용자입니다"); error.statusCode = 401; throw error; } - const isPasswordVaild = await bcrypt.compare( - loginData.password, - user.password - ); + // 비밀번호 일치 여부 확인 + const isPasswordVaild = await compareWords(loginData.password, user.password); if (!isPasswordVaild) { const error = new Error("비밀번호가 일치하지 않습니다."); @@ -56,8 +55,8 @@ const AuthService = { // 액세스 토큰 및 리프레시 토큰 생성 const { accessToken, refreshToken } = generateTokens(user.id); + // DB에 리프레시 토큰 저장 const hashedRefreshToken = await hashing(refreshToken); - await prisma.user.update({ where: { id: user.id }, data: { refreshToken: hashedRefreshToken }, @@ -68,7 +67,7 @@ const AuthService = { return { userWithoutPassword, accessToken, refreshToken }; }, - // AcessToken & RefreshToken을 재발급 받는 코드 + // AcessToken & RefreshToken을 재발급 받는 메서드 async refreshAccessToken(oldRefreshToken) { if (!oldRefreshToken) { const error = new Error("Refresh Token이 제공되지 않았습니다."); @@ -76,38 +75,44 @@ const AuthService = { throw error; } - const hashedOldRefreshToken = await hashing(oldRefreshToken); - try { + // 토큰 디코딩해서 토큰의 User 확인 및 변조 여부 확인 const decoded = jwt.verify(oldRefreshToken, REFRESH_TOKEN_SECRET); - const userId = decoded.userId; + const userId = decoded.id; const user = await prisma.user.findUnique({ where: { id: userId }, }); - if (!user || user.refreshToken !== hashedOldRefreshToken) { + if (!user) { + const error = new Error("해당하는 user가 없습니다. (Refresh Token 에러)"); + error.statusCode = 403; + throw error; + } + + // DB에 저장된 refreshToken 일치 여부 확인 + const isTokenValid = await compareWords(oldRefreshToken, user.refreshToken); + + if (!isTokenValid) { const error = new Error("유효하지 않은 Refresh Token입니다."); error.statusCode = 403; throw error; } // 새로운 Access Token, Refresh Token 생성 - const { newAccessToken, newRefreshToken } = generateTokens(user.id); - - const hashedNewRefreshToken = await hashing(newRefreshToken); + const { accessToken, refreshToken } = generateTokens(user.id); + const hashedNewRefreshToken = await hashing(refreshToken); await prisma.user.update({ where: { id: user.id }, data: { refreshToken: hashedNewRefreshToken }, }); - return { accessToken: newAccessToken, refreshToken: newRefreshToken }; + return { accessToken, refreshToken }; } catch (err) { + console.error("Refresh Token 실제 오류:", err); if (err.name === "TokenExpiredError") { - const error = new Error( - "Refresh Token이 만료되었습니다. 다시 로그인해주세요." - ); + const error = new Error("Refresh Token이 만료되었습니다. 다시 로그인해주세요."); error.statusCode = 401; throw error; } From 54e74a25608e498e8e1e76c477e416c8a01559a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Thu, 4 Sep 2025 11:01:25 +0900 Subject: [PATCH 21/54] =?UTF-8?q?refactor:=20cloudinary=EB=A5=BC=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9=ED=95=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 초급 프로젝트에서 진행한 방식으로 이미지 클라우드 사용 --- package-lock.json | 31 ++++++++++++++++++++++++++ package.json | 1 + src/api/controllers/ImageController.js | 2 +- src/api/libs/constants.js | 4 ++++ src/api/middlewares/upload.js | 22 +++++++++++------- 5 files changed, 51 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6637fbf9c..d9a157652 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@prisma/client": "^6.13.0", "axios": "^1.10.0", "bcrypt": "^6.0.0", + "cloudinary": "^2.7.0", "cookie-parser": "^1.4.7", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", @@ -1143,6 +1144,19 @@ "consola": "^3.2.3" } }, + "node_modules/cloudinary": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.7.0.tgz", + "integrity": "sha512-qrqDn31+qkMCzKu1GfRpzPNAO86jchcNwEHCUiqvPHNSFqu7FTNF9FuAkBUyvM1CFFgFPu64NT0DyeREwLwK0w==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "q": "^1.5.1" + }, + "engines": { + "node": ">=9" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3171,6 +3185,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -3883,6 +3903,17 @@ ], "license": "MIT" }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "license": "MIT", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", diff --git a/package.json b/package.json index 2aad12a5b..c09098ca5 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@prisma/client": "^6.13.0", "axios": "^1.10.0", "bcrypt": "^6.0.0", + "cloudinary": "^2.7.0", "cookie-parser": "^1.4.7", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", diff --git a/src/api/controllers/ImageController.js b/src/api/controllers/ImageController.js index 390149d83..cf6d6b96e 100644 --- a/src/api/controllers/ImageController.js +++ b/src/api/controllers/ImageController.js @@ -4,7 +4,7 @@ const ImageController = { return res.status(400).json({ error: "이미지 파일 없음" }); } - const imageUrl = `/uploads/${req.file.filename}`; + const imageUrl = req.file.path; res.status(200).json({ imageUrl }); }, }; diff --git a/src/api/libs/constants.js b/src/api/libs/constants.js index 614e3dfed..d12bbf7df 100644 --- a/src/api/libs/constants.js +++ b/src/api/libs/constants.js @@ -5,4 +5,8 @@ dotenv.config(); const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET; const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET; +CLOUDINARY_CLOUD_NAME = process.env.CLOUDINARY_CLOUD_NAME; +CLOUDINARY_API_KEY = process.env.CLOUDINARY_API_KEY; +CLOUDINARY_API_SECRET = process.env.CLOUDINARY_API_SECRET; + export { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET }; diff --git a/src/api/middlewares/upload.js b/src/api/middlewares/upload.js index dfad77e9d..bc2c31d6a 100644 --- a/src/api/middlewares/upload.js +++ b/src/api/middlewares/upload.js @@ -1,15 +1,21 @@ import multer from "multer"; +import { v2 as cloudinary } from "cloudinary"; +import { CloudinaryStorage } from "multer-storage-cloudinary"; +import { CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET } from "../libs/constants.js"; -const storage = multer.diskStorage({ - destination: (req, file, cb) => { - cb(null, "uploads/"); - }, - filename: (req, file, cb) => { - const unique = Date.now() + "-" + Math.round(Math.random() * 1e9); - cb(null, `${unique}-${file.originalname}`); +cloudinary.config({ + cloud_name: CLOUDINARY_CLOUD_NAME, + api_key: CLOUDINARY_API_KEY, + api_secret: CLOUDINARY_API_SECRET, +}); + +const storage = new CloudinaryStorage({ + cloudinary: cloudinary, + params: { + folder: "sprint-mission-uploads", + allowed_formats: ["jpg", "png", "jpeg"], }, }); const upload = multer({ storage }); - export default upload; From d96ba8410c3c94d6ae519baf83e0e130afbfba78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Thu, 4 Sep 2025 11:40:56 +0900 Subject: [PATCH 22/54] =?UTF-8?q?refactor:=20=EC=9C=A0=ED=9A=A8=EC=84=B1?= =?UTF-8?q?=20=EA=B2=80=EC=A6=9D=20=EB=AF=B8=EB=93=A4=EC=9B=A8=EC=96=B4=20?= =?UTF-8?q?zod=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 --- package-lock.json | 42 ++++++++++++++++--- package.json | 3 +- src/api/libs/constants.js | 8 ++-- src/api/middlewares/authenticate.js | 7 ++-- src/api/middlewares/upload.js | 6 +-- .../middlewares/validators/validateArticle.js | 17 +++----- .../middlewares/validators/validateProduct.js | 21 +++------- .../middlewares/validators/validateUser.js | 4 +- src/api/routes/ArticleRouter.js | 17 ++------ src/api/routes/ProductRouter.js | 22 ++-------- 10 files changed, 69 insertions(+), 78 deletions(-) diff --git a/package-lock.json b/package-lock.json index d9a157652..aa2646fdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,12 @@ "@prisma/client": "^6.13.0", "axios": "^1.10.0", "bcrypt": "^6.0.0", - "cloudinary": "^2.7.0", + "cloudinary": "^1.41.3", "cookie-parser": "^1.4.7", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", + "multer-storage-cloudinary": "^4.0.0", "prisma": "^6.13.0", "zod": "^4.1.5" }, @@ -1145,16 +1146,27 @@ } }, "node_modules/cloudinary": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.7.0.tgz", - "integrity": "sha512-qrqDn31+qkMCzKu1GfRpzPNAO86jchcNwEHCUiqvPHNSFqu7FTNF9FuAkBUyvM1CFFgFPu64NT0DyeREwLwK0w==", + "version": "1.41.3", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-1.41.3.tgz", + "integrity": "sha512-4o84y+E7dbif3lMns+p3UW6w6hLHEifbX/7zBJvaih1E9QNMZITENQ14GPYJC4JmhygYXsuuBb9bRA3xWEoOfg==", "license": "MIT", "dependencies": { + "cloudinary-core": "^2.13.0", + "core-js": "^3.30.1", "lodash": "^4.17.21", "q": "^1.5.1" }, "engines": { - "node": ">=9" + "node": ">=0.6" + } + }, + "node_modules/cloudinary-core": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/cloudinary-core/-/cloudinary-core-2.14.0.tgz", + "integrity": "sha512-L+kjoYgU+5wyiPkSnmeCbmtT6DwSyYUN/WoI/fEb6Xsx2gtB3iuf/50W0SvcQkeKzllfH5Knh8I4ST924DkkRw==", + "license": "MIT", + "peerDependencies": { + "lodash": ">=4.0" } }, "node_modules/color-convert": { @@ -1284,6 +1296,17 @@ "node": ">=6.6.0" } }, + "node_modules/core-js": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", + "integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3386,6 +3409,15 @@ "node": ">= 10.16.0" } }, + "node_modules/multer-storage-cloudinary": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/multer-storage-cloudinary/-/multer-storage-cloudinary-4.0.0.tgz", + "integrity": "sha512-25lm9R6o5dWrHLqLvygNX+kBOxprzpmZdnVKH4+r68WcfCt8XV6xfQaMuAg+kUE5Xmr8mJNA4gE0AcBj9FJyWA==", + "license": "MIT", + "peerDependencies": { + "cloudinary": "^1.21.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", diff --git a/package.json b/package.json index c09098ca5..7dd212d72 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,12 @@ "@prisma/client": "^6.13.0", "axios": "^1.10.0", "bcrypt": "^6.0.0", - "cloudinary": "^2.7.0", + "cloudinary": "^1.41.3", "cookie-parser": "^1.4.7", "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", + "multer-storage-cloudinary": "^4.0.0", "prisma": "^6.13.0", "zod": "^4.1.5" }, diff --git a/src/api/libs/constants.js b/src/api/libs/constants.js index d12bbf7df..763e80442 100644 --- a/src/api/libs/constants.js +++ b/src/api/libs/constants.js @@ -5,8 +5,8 @@ dotenv.config(); const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET; const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET; -CLOUDINARY_CLOUD_NAME = process.env.CLOUDINARY_CLOUD_NAME; -CLOUDINARY_API_KEY = process.env.CLOUDINARY_API_KEY; -CLOUDINARY_API_SECRET = process.env.CLOUDINARY_API_SECRET; +const CLOUDINARY_CLOUD_NAME = process.env.CLOUDINARY_CLOUD_NAME; +const CLOUDINARY_API_KEY = process.env.CLOUDINARY_API_KEY; +const CLOUDINARY_API_SECRET = process.env.CLOUDINARY_API_SECRET; -export { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET }; +export { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET, CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET }; diff --git a/src/api/middlewares/authenticate.js b/src/api/middlewares/authenticate.js index 107856a02..582756245 100644 --- a/src/api/middlewares/authenticate.js +++ b/src/api/middlewares/authenticate.js @@ -1,7 +1,6 @@ import jwt from "jsonwebtoken"; import prisma from "../libs/prismaClient.js"; - -const JWT_SECRET = process.env.JWT_SECRET; +import { ACCESS_TOKEN_SECRET } from "../libs/constants.js"; export default async function authenticate(req, res, next) { const { accessToken } = req.cookies; @@ -13,8 +12,8 @@ export default async function authenticate(req, res, next) { } try { - const decoded = jwt.verify(accessToken, JWT_SECRET); - const userId = decoded.userId; + const decoded = jwt.verify(accessToken, ACCESS_TOKEN_SECRET); + const userId = decoded.id; const user = await prisma.user.findUnique({ where: { id: userId }, diff --git a/src/api/middlewares/upload.js b/src/api/middlewares/upload.js index bc2c31d6a..1f0be811f 100644 --- a/src/api/middlewares/upload.js +++ b/src/api/middlewares/upload.js @@ -1,16 +1,16 @@ import multer from "multer"; -import { v2 as cloudinary } from "cloudinary"; +import cloudinary from "cloudinary"; import { CloudinaryStorage } from "multer-storage-cloudinary"; import { CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET } from "../libs/constants.js"; -cloudinary.config({ +cloudinary.v2.config({ cloud_name: CLOUDINARY_CLOUD_NAME, api_key: CLOUDINARY_API_KEY, api_secret: CLOUDINARY_API_SECRET, }); const storage = new CloudinaryStorage({ - cloudinary: cloudinary, + cloudinary: cloudinary.v2, params: { folder: "sprint-mission-uploads", allowed_formats: ["jpg", "png", "jpeg"], diff --git a/src/api/middlewares/validators/validateArticle.js b/src/api/middlewares/validators/validateArticle.js index 8232aeaa3..ba15bd759 100644 --- a/src/api/middlewares/validators/validateArticle.js +++ b/src/api/middlewares/validators/validateArticle.js @@ -1,13 +1,6 @@ -export default function validateArticle(req, res, next) { - const { title, content } = req.body; +import { z } from "zod"; - if (!title || typeof title !== "string") { - return res.status(400).json({ error: "제목을 입력하세요 (문자열)" }); - } - - if (!content || typeof content !== "string") { - return res.status(400).json({ error: "내용을 입력하세요 (문자열)" }); - } - - next(); -} +export const ArticleSchema = z.object({ + title: z.string().min(1, { message: "제목을 입력하세요" }), + content: z.string().min(1, { message: "내용을 입력하세요." }), +}); diff --git a/src/api/middlewares/validators/validateProduct.js b/src/api/middlewares/validators/validateProduct.js index a84b6ee27..fb2b25c7f 100644 --- a/src/api/middlewares/validators/validateProduct.js +++ b/src/api/middlewares/validators/validateProduct.js @@ -1,17 +1,6 @@ -export default function validateProduct(req, res, next) { - const { name, price } = req.body; +import { z } from "zod"; - if (!name || typeof name !== "string") { - return res - .status(400) - .json({ error: "상품 이름은 필수값입니다. (문자열)" }); - } - - if (!price || typeof price !== "number" || price <= 0) { - return res - .status(400) - .json({ error: "상품 가격은 0보다 큰 숫자 (필수값)" }); - } - - next(); -} +export const ProductSchema = z.object({ + name: z.string().min(1, { message: "상품 이름을 입력하세요" }), + price: z.number().gte(0), +}); diff --git a/src/api/middlewares/validators/validateUser.js b/src/api/middlewares/validators/validateUser.js index 201e77c46..9681ee35e 100644 --- a/src/api/middlewares/validators/validateUser.js +++ b/src/api/middlewares/validators/validateUser.js @@ -3,7 +3,7 @@ import { z } from "zod"; // User 유효성 검사 미들웨어는 zod 활용해서 구현해봄 // 이후에 시간 남으면 다른 유효성 검사도 zod로 바꾸기 export const signupSchema = z.object({ - email: z.string().email({ message: "유효한 이메일 형식이 아닙니다." }), + email: z.email({ message: "유효한 이메일 형식이 아닙니다." }), password: z .string() .min(8, { message: "비밀번호는 최소 8자 이상이어야합니다." }), @@ -13,6 +13,6 @@ export const signupSchema = z.object({ }); export const loginSchema = z.object({ - email: z.string().email({ message: "유효한 이메일 형식이 아닙니다." }), + email: z.email({ message: "유효한 이메일 형식이 아닙니다." }), password: z.string().min(1, { message: "비밀번호를 입력해주세요." }), }); diff --git a/src/api/routes/ArticleRouter.js b/src/api/routes/ArticleRouter.js index c594af6f7..c93f627d2 100644 --- a/src/api/routes/ArticleRouter.js +++ b/src/api/routes/ArticleRouter.js @@ -1,23 +1,14 @@ import express from "express"; import ArticleController from "../controllers/ArticleController.js"; -import validateArticle from "../middlewares/validators/validateArticle.js"; import authenticate from "../middlewares/authenticate.js"; +import validate from "../middlewares/validators/validate.js"; +import { ArticleSchema } from "../middlewares/validators/validateArticle.js"; const router = express.Router(); -router.post( - "/", - authenticate, - validateArticle, - ArticleController.createArticle -); +router.post("/", authenticate, validate(ArticleSchema), ArticleController.createArticle); router.get("/:id", ArticleController.findUniqueArticle); -router.patch( - "/:id", - authenticate, - validateArticle, - ArticleController.updateArticle -); +router.patch("/:id", authenticate, validate(ArticleSchema), ArticleController.updateArticle); router.delete("/:id", authenticate, ArticleController.deleteArticle); router.get("/", ArticleController.findManyArticle); diff --git a/src/api/routes/ProductRouter.js b/src/api/routes/ProductRouter.js index 701313617..5f794b514 100644 --- a/src/api/routes/ProductRouter.js +++ b/src/api/routes/ProductRouter.js @@ -1,28 +1,14 @@ import express from "express"; import ProductController from "../controllers/ProductController.js"; -import validateProduct from "../middlewares/validators/validateProduct.js"; import authenticate from "../middlewares/authenticate.js"; - +import validate from "../middlewares/validators/validate.js"; +import { ProductSchema } from "../middlewares/validators/validateProduct.js"; const router = express.Router(); -router.post( - "/", - authenticate, - validateProduct, - ProductController.createProduct -); - +router.post("/", authenticate, validate(ProductSchema), ProductController.createProduct); router.get("/:id", ProductController.findUniqueProduct); - -router.patch( - "/:id", - authenticate, - validateProduct, - ProductController.patchProduct -); - +router.patch("/:id", authenticate, validate(ProductSchema), ProductController.patchProduct); router.delete("/:id", authenticate, ProductController.deleteProduct); - router.get("/", ProductController.findManyProduct); export default router; From 70004c4c4bfa439003cdc1b7eff25e29f18b3851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 10 Sep 2025 10:06:56 +0900 Subject: [PATCH 23/54] =?UTF-8?q?chore:=20TS=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tsconfig.json 파일을 생성하고, 필요한 옵션을 설정 필요한 npm script를 설정 ts-node nodemon을 사용해 .ts 코드가 변경될 때마다 서버가 다시 실행되는 npm script --- package-lock.json | 551 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 10 +- tsconfig.json | 46 ++++ 3 files changed, 605 insertions(+), 2 deletions(-) create mode 100644 tsconfig.json diff --git a/package-lock.json b/package-lock.json index aa2646fdf..b25521e14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,13 +23,32 @@ }, "devDependencies": { "@eslint/js": "^9.31.0", + "@types/express": "^5.0.3", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^24.3.1", "eslint": "^9.31.0", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", + "nodemon": "^3.1.10", "prettier": "^3.6.2", + "ts-node": "^10.9.2", + "typescript": "^5.9.2", "typescript-eslint": "^8.37.0" } }, + "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/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -250,6 +269,34 @@ "url": "https://github.com/sponsors/nzakas" } }, + "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/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -373,6 +420,55 @@ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "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/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/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -380,6 +476,38 @@ "dev": true, "license": "MIT" }, + "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/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -387,6 +515,78 @@ "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/node": { + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.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/@typescript-eslint/eslint-plugin": { "version": "8.41.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz", @@ -694,6 +894,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.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/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -727,12 +940,33 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/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/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/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -942,6 +1176,19 @@ "node": ">= 18" } }, + "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", @@ -1307,6 +1554,13 @@ "url": "https://opencollective.com/core-js" } }, + "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/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1475,6 +1729,16 @@ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", "license": "MIT" }, + "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/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -2249,6 +2513,21 @@ "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", @@ -2561,6 +2840,13 @@ "node": ">= 4" } }, + "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/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -2672,6 +2958,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -3276,6 +3575,13 @@ "loose-envify": "cli.js" } }, + "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", @@ -3503,6 +3809,132 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/nodemon/node_modules/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/nodemon/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/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/nodemon/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/nodemon/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/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", @@ -3909,6 +4341,13 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "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/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4451,6 +4890,32 @@ "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/simple-update-notifier/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/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -4656,6 +5121,16 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -4669,6 +5144,50 @@ "typescript": ">=4.8.4" } }, + "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/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4786,7 +5305,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4838,6 +5356,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -4863,6 +5395,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", @@ -5002,6 +5541,16 @@ "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" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 7dd212d72..b5e8dd255 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "main": "src/main.js", "type": "module", "scripts": { - "start": "node src/main.js", + "build": "tsc", + "start": "node dist/main.js", + "dev": "nodemon --watch 'src/**/*.ts' --exec 'node --loader ts-node/esm' src/main.ts", "test": "echo \"Error: no test specified\" && exit 1", "lint": "eslint .", "format": "prettier --write ." @@ -28,10 +30,16 @@ }, "devDependencies": { "@eslint/js": "^9.31.0", + "@types/express": "^5.0.3", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^24.3.1", "eslint": "^9.31.0", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", + "nodemon": "^3.1.10", "prettier": "^3.6.2", + "ts-node": "^10.9.2", + "typescript": "^5.9.2", "typescript-eslint": "^8.37.0" }, "prisma": { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..b5eb867fb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,46 @@ +{ + "compilerOptions": { + // Basics + "module": "nodenext", + "target": "es2024", + "lib": ["esnext"], + "types": ["node"], // install -D @types/node + + // File Layout + "baseUrl": "./", + "rootDir": "./src", + "outDir": "./dist", + + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + "removeComments": true, + + // Decorators + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + + // Style Options + "noImplicitAny": true, + "noImplicitOverride": true, + "noUncheckedSideEffectImports": true, + "forceConsistentCasingInFileNames": false, + "strictNullChecks": true, + "allowSyntheticDefaultImports": true, + + // Recommended Options + "incremental": true, + "skipLibCheck": true, + "strict": true, + "isolatedModules": true, + "moduleDetection": "force", + "verbatimModuleSyntax": true, + "moduleResolution": "nodenext" + }, + "include": ["src/**/*", "test"] +} From 23407d66d1c8762457b1e323c900b46dc9ef15c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 10 Sep 2025 11:39:40 +0900 Subject: [PATCH 24/54] =?UTF-8?q?refactor:=20libs=20=ED=8F=B4=EB=8D=94=20?= =?UTF-8?q?=EB=82=B4=20=ED=8C=8C=EC=9D=BC=EB=93=A4=20ts=EB=A1=9C=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + package-lock.json | 11 ++++++ package.json | 1 + src/api/controllers/ProductController.js | 11 +----- src/api/libs/{constants.js => constants.ts} | 0 src/api/libs/{hashing.js => hashing.ts} | 4 +- .../libs/{prismaClient.js => prismaClient.ts} | 0 src/api/libs/{token.js => token.ts} | 5 ++- src/main.js | 37 ------------------- src/main.ts | 37 +++++++++++++++++++ tsconfig.json | 3 +- 11 files changed, 61 insertions(+), 50 deletions(-) rename src/api/libs/{constants.js => constants.ts} (100%) rename src/api/libs/{hashing.js => hashing.ts} (72%) rename src/api/libs/{prismaClient.js => prismaClient.ts} (100%) rename src/api/libs/{token.js => token.ts} (68%) delete mode 100644 src/main.js create mode 100644 src/main.ts diff --git a/.gitignore b/.gitignore index 907865ae9..8e4cd655f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules/ /generated/prisma .env +dist/ +tsconfig.tsbuildinfo \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b25521e14..30500bc3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ }, "devDependencies": { "@eslint/js": "^9.31.0", + "@types/bcrypt": "^6.0.0", "@types/express": "^5.0.3", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.3.1", @@ -448,6 +449,16 @@ "dev": 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", diff --git a/package.json b/package.json index b5e8dd255..caac082dc 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ }, "devDependencies": { "@eslint/js": "^9.31.0", + "@types/bcrypt": "^6.0.0", "@types/express": "^5.0.3", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.3.1", diff --git a/src/api/controllers/ProductController.js b/src/api/controllers/ProductController.js index 3f2651b85..a64c4baaa 100644 --- a/src/api/controllers/ProductController.js +++ b/src/api/controllers/ProductController.js @@ -9,10 +9,7 @@ const ProductController = { const { id: userId } = req.user; const { name, description, price, tags } = req.body; const productData = { name, description, price, tags }; - const newProduct = await ProductService.createProduct( - productData, - userId - ); + const newProduct = await ProductService.createProduct(productData, userId); res.status(201).json(newProduct); } catch (err) { @@ -51,11 +48,7 @@ const ProductController = { const { id: userId } = req.user; const updateData = req.body; - const product = await ProductService.patchProduct( - Number(id), - updateData, - userId - ); + const product = await ProductService.patchProduct(Number(id), updateData, userId); if (!product) { return res.status(404).json({ error: "수정할 상품이 없음" }); } diff --git a/src/api/libs/constants.js b/src/api/libs/constants.ts similarity index 100% rename from src/api/libs/constants.js rename to src/api/libs/constants.ts diff --git a/src/api/libs/hashing.js b/src/api/libs/hashing.ts similarity index 72% rename from src/api/libs/hashing.js rename to src/api/libs/hashing.ts index 077ada88d..5fe575248 100644 --- a/src/api/libs/hashing.js +++ b/src/api/libs/hashing.ts @@ -1,6 +1,6 @@ import bcrypt from "bcrypt"; -async function hashing(hashword) { +async function hashing(hashword: string) { try { const salt = await bcrypt.genSalt(10); const hashedWord = await bcrypt.hash(hashword, salt); @@ -10,7 +10,7 @@ async function hashing(hashword) { } } -async function compareWords(word, hashedWord) { +async function compareWords(word: string, hashedWord: string) { return await bcrypt.compare(word, hashedWord); } diff --git a/src/api/libs/prismaClient.js b/src/api/libs/prismaClient.ts similarity index 100% rename from src/api/libs/prismaClient.js rename to src/api/libs/prismaClient.ts diff --git a/src/api/libs/token.js b/src/api/libs/token.ts similarity index 68% rename from src/api/libs/token.js rename to src/api/libs/token.ts index 2e50ac6c7..594c7a96c 100644 --- a/src/api/libs/token.js +++ b/src/api/libs/token.ts @@ -1,7 +1,10 @@ import jwt from "jsonwebtoken"; import { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET } from "./constants.js"; -function generateTokens(userId) { +function generateTokens(userId: number) { + if (!ACCESS_TOKEN_SECRET || !REFRESH_TOKEN_SECRET) { + throw new Error("토큰 시크릿이 설정되지 않았습니다."); + } const accessToken = jwt.sign({ id: userId }, ACCESS_TOKEN_SECRET, { expiresIn: "1h", }); diff --git a/src/main.js b/src/main.js deleted file mode 100644 index d62461a3b..000000000 --- a/src/main.js +++ /dev/null @@ -1,37 +0,0 @@ -import express from "express"; -import ProductRouter from "./api/routes/ProductRouter.js"; -import ArticleRouter from "./api/routes/ArticleRouter.js"; -import CommentRouter from "./api/routes/CommentRouter.js"; -import errorHandler from "./api/middlewares/errorHandler.js"; -import imageRouter from "./api/routes/ImageRouter.js"; -import AuthRouter from "./api/routes/AuthRouter.js"; -import MypageRouter from "./api/routes/MypageRouter.js"; -import LikeRouter from "./api/routes/LikeRouter.js"; -import cookieParser from "cookie-parser"; - -// import { testAllArticleService } from "./external/tests/testArticleService.js"; -// import { testAllProductService } from "./external/tests/testProductService.js"; - -// //testAllArticleService(); -// testAllProductService(); - -const app = express(); -const port = 3000; - -app.use(express.json()); -app.use(cookieParser()); - -app.use("/products", ProductRouter); -app.use("/articles", ArticleRouter); -app.use("/comments", CommentRouter); -app.use("/mypage", MypageRouter); -app.use("/likes", LikeRouter); - -app.use("/uploads", express.static("uploads")); -app.use("/images", imageRouter); -app.use("/auth", AuthRouter); -app.use(errorHandler); - -app.listen(port, () => { - console.log(`Server is running at http://localhost:${port}`); -}); diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 000000000..d1c611d38 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,37 @@ +import express from "express"; +// import ProductRouter from "./api/routes/ProductRouter.js"; +// import ArticleRouter from "./api/routes/ArticleRouter.js"; +// import CommentRouter from "./api/routes/CommentRouter.js"; +// import errorHandler from "./api/middlewares/errorHandler.js"; +// import imageRouter from "./api/routes/ImageRouter.js"; +// import AuthRouter from "./api/routes/AuthRouter.js"; +// import MypageRouter from "./api/routes/MypageRouter.js"; +// import LikeRouter from "./api/routes/LikeRouter.js"; +// import cookieParser from "cookie-parser"; + +// import { testAllArticleService } from "./external/tests/testArticleService.js"; +// import { testAllProductService } from "./external/tests/testProductService.js"; + +// //testAllArticleService(); +// testAllProductService(); + +const app = express(); +const port = 3000; + +app.use(express.json()); +// app.use(cookieParser()); + +// app.use("/products", ProductRouter); +// app.use("/articles", ArticleRouter); +// app.use("/comments", CommentRouter); +// app.use("/mypage", MypageRouter); +// app.use("/likes", LikeRouter); + +// app.use("/uploads", express.static("uploads")); +// app.use("/images", imageRouter); +// app.use("/auth", AuthRouter); +// app.use(errorHandler); + +app.listen(port, () => { + console.log(`Server is running at http://localhost:${port}`); +}); diff --git a/tsconfig.json b/tsconfig.json index b5eb867fb..7aed0da82 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,11 +32,12 @@ "forceConsistentCasingInFileNames": false, "strictNullChecks": true, "allowSyntheticDefaultImports": true, + "esModuleInterop": true, // Recommended Options "incremental": true, "skipLibCheck": true, - "strict": true, + //"strict": true, "isolatedModules": true, "moduleDetection": "force", "verbatimModuleSyntax": true, From 64b4e4907427d01683dca944f4ede6596c6b172f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 10 Sep 2025 13:18:29 +0900 Subject: [PATCH 25/54] =?UTF-8?q?refactor:=20external/classes=20ts?= =?UTF-8?q?=EB=A1=9C=20refactoring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../classes/{Article.js => Article.ts} | 8 +++++- src/external/classes/ElectronicProduct.js | 8 ------ src/external/classes/ElectronicProduct.ts | 26 +++++++++++++++++ src/external/classes/Product.js | 14 ---------- src/external/classes/Product.ts | 28 +++++++++++++++++++ 5 files changed, 61 insertions(+), 23 deletions(-) rename src/external/classes/{Article.js => Article.ts} (53%) delete mode 100644 src/external/classes/ElectronicProduct.js create mode 100644 src/external/classes/ElectronicProduct.ts delete mode 100644 src/external/classes/Product.js create mode 100644 src/external/classes/Product.ts diff --git a/src/external/classes/Article.js b/src/external/classes/Article.ts similarity index 53% rename from src/external/classes/Article.js rename to src/external/classes/Article.ts index 359cb794f..59489ff3b 100644 --- a/src/external/classes/Article.js +++ b/src/external/classes/Article.ts @@ -1,5 +1,11 @@ class Article { - constructor(title, content, writer, likeCount) { + title: string; + content: string; + writer: string; + likeCount: number; + createdAt: number; + + constructor(title: string, content: string, writer: string, likeCount: number) { this.title = title; this.content = content; this.writer = writer; diff --git a/src/external/classes/ElectronicProduct.js b/src/external/classes/ElectronicProduct.js deleted file mode 100644 index 5a1fbf630..000000000 --- a/src/external/classes/ElectronicProduct.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Product } from "./Product.js"; - -export class ElectronicProduct extends Product { - constructor({ manufacturer, ...rest }) { - super(rest); - this.manufacturer = manufacturer; - } -} diff --git a/src/external/classes/ElectronicProduct.ts b/src/external/classes/ElectronicProduct.ts new file mode 100644 index 000000000..36620c01c --- /dev/null +++ b/src/external/classes/ElectronicProduct.ts @@ -0,0 +1,26 @@ +import { Product } from "./Product.js"; + +export class ElectronicProduct extends Product { + manufacturer: string; + + constructor({ + manufacturer, + name, + description, + price, + tags, + images, + favoriteCount, + }: { + manufacturer: string; + name: string; + description: string; + price: number; + tags: string[]; + images: string[]; + favoriteCount: number; + }) { + super(name, description, price, tags, images, favoriteCount); + this.manufacturer = manufacturer; + } +} diff --git a/src/external/classes/Product.js b/src/external/classes/Product.js deleted file mode 100644 index 2e1c31a1a..000000000 --- a/src/external/classes/Product.js +++ /dev/null @@ -1,14 +0,0 @@ -export class Product { - constructor({ name, description, price, tags, images, favoriteCount }) { - this.name = name; - this.description = description; - this.price = price; - this.tags = tags; - this.images = images; - this.favoriteCount = favoriteCount; - } - - favorite() { - this.favoriteCount += 1; - } -} diff --git a/src/external/classes/Product.ts b/src/external/classes/Product.ts new file mode 100644 index 000000000..0b3284d72 --- /dev/null +++ b/src/external/classes/Product.ts @@ -0,0 +1,28 @@ +export class Product { + name: string; + description: string; + price: number; + tags: string[]; + images: string[]; + favoriteCount: number; + + constructor( + name: string, + description: string, + price: number, + tags: string[], + images: string[], + favoriteCount: number + ) { + this.name = name; + this.description = description; + this.price = price; + this.tags = tags; + this.images = images; + this.favoriteCount = favoriteCount; + } + + favorite() { + this.favoriteCount += 1; + } +} From f568904bd22cc2f13a84955661c8ea2f102686fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 10 Sep 2025 15:24:41 +0900 Subject: [PATCH 26/54] =?UTF-8?q?refactor:=20service=20=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=20ts=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/config/env.ts | 14 +++ .../{ArticleService.js => ArticleService.ts} | 35 +++++-- .../{AuthService.js => AuthService.ts} | 41 +++++--- src/api/services/CommentService.js | 75 --------------- src/api/services/CommentService.ts | 93 +++++++++++++++++++ .../{LikeService.js => LikeService.ts} | 5 +- .../{MypageService.js => MypageService.ts} | 28 +++--- .../{ProductService.js => ProductService.ts} | 31 +++++-- src/api/types/article.ts | 23 +++++ src/api/types/comment.ts | 17 ++++ src/api/types/error.ts | 4 + src/api/types/like.ts | 5 + src/api/types/login.ts | 4 + src/api/types/product.ts | 22 +++++ src/api/types/signup.ts | 5 + src/api/types/user.ts | 5 + 16 files changed, 282 insertions(+), 125 deletions(-) create mode 100644 src/api/config/env.ts rename src/api/services/{ArticleService.js => ArticleService.ts} (58%) rename src/api/services/{AuthService.js => AuthService.ts} (66%) delete mode 100644 src/api/services/CommentService.js create mode 100644 src/api/services/CommentService.ts rename src/api/services/{LikeService.js => LikeService.ts} (80%) rename src/api/services/{MypageService.js => MypageService.ts} (71%) rename src/api/services/{ProductService.js => ProductService.ts} (60%) create mode 100644 src/api/types/article.ts create mode 100644 src/api/types/comment.ts create mode 100644 src/api/types/error.ts create mode 100644 src/api/types/like.ts create mode 100644 src/api/types/login.ts create mode 100644 src/api/types/product.ts create mode 100644 src/api/types/signup.ts create mode 100644 src/api/types/user.ts diff --git a/src/api/config/env.ts b/src/api/config/env.ts new file mode 100644 index 000000000..e13bc6337 --- /dev/null +++ b/src/api/config/env.ts @@ -0,0 +1,14 @@ +interface EnvConfig { + REFRESH_TOKEN_SECRET: string; +} + +const getEnvConfig = (): EnvConfig => { + const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET; + if (!REFRESH_TOKEN_SECRET) { + throw new Error("환경 변수 REFRESH_TOKEN_SECRET이 설정되지 않았습니다. 설정해주세요."); + } + + return { REFRESH_TOKEN_SECRET }; +}; + +export default getEnvConfig(); diff --git a/src/api/services/ArticleService.js b/src/api/services/ArticleService.ts similarity index 58% rename from src/api/services/ArticleService.js rename to src/api/services/ArticleService.ts index 09db54ed3..c69790657 100644 --- a/src/api/services/ArticleService.js +++ b/src/api/services/ArticleService.ts @@ -1,14 +1,17 @@ import prisma from "../libs/prismaClient.js"; +import type { CreateArticleData, UpdateArticleData, FindManyArticleParams, ArticleOrder } from "../types/article.js"; +import type { CustomError } from "../types/error.js"; +import { Prisma } from "@prisma/client"; const ArticleService = { - async createArticle(articleData, userId) { + async createArticle(articleData: CreateArticleData, userId: number) { const newArticle = await prisma.article.create({ data: { ...articleData, userId }, }); return newArticle; }, - async findUniqueArticle(articleId, userId) { + async findUniqueArticle(articleId: number, userId?: number) { const article = await prisma.article.findUnique({ where: { id: articleId }, }); @@ -23,11 +26,17 @@ const ArticleService = { return { ...article, isLiked: !!like }; }, - async updateArticle(id, updateData, userId) { + async updateArticle(id: number, updateData: UpdateArticleData, userId: number) { const article = await prisma.article.findUnique({ where: { id } }); + if (!article) { + const error: CustomError = new Error("존재하지 않는 게시글입니다."); + error.statusCode = 404; + throw error; + } + if (article.userId != userId) { - const error = new Error("게시글을 수정할 권한이 없습니다."); + const error: CustomError = new Error("게시글을 수정할 권한이 없습니다."); error.statusCode = 403; throw error; } @@ -38,11 +47,17 @@ const ArticleService = { }); }, - async deleteArticle(id, userId) { + async deleteArticle(id: number, userId: number) { const article = await prisma.article.findUnique({ where: { id } }); + if (!article) { + const error: CustomError = new Error("존재하지 않는 게시글입니다."); + error.statusCode = 404; + throw error; + } + if (article.userId != userId) { - const error = new Error("게시글을 삭제할 권한이 없습니다."); + const error: CustomError = new Error("게시글을 삭제할 권한이 없습니다."); error.statusCode = 403; throw error; } @@ -52,8 +67,8 @@ const ArticleService = { }); }, - async findManyArticle({ offset, limit, order, keyword }) { - let orderBy; + async findManyArticle({ offset, limit, order, keyword }: FindManyArticleParams) { + let orderBy: Prisma.ArticleOrderByWithRelationInput; switch (order) { case "oldest": orderBy = { createdAt: "asc" }; @@ -76,8 +91,8 @@ const ArticleService = { const articles = await prisma.article.findMany({ select: { id: true, title: true, content: true, createdAt: true }, - skip: parseInt(offset), - take: parseInt(limit), + skip: offset, + take: limit, orderBy, where, }); diff --git a/src/api/services/AuthService.js b/src/api/services/AuthService.ts similarity index 66% rename from src/api/services/AuthService.js rename to src/api/services/AuthService.ts index 787f649c7..1ed76cec6 100644 --- a/src/api/services/AuthService.js +++ b/src/api/services/AuthService.ts @@ -1,19 +1,21 @@ import prisma from "../libs/prismaClient.js"; import jwt from "jsonwebtoken"; +import env from "../config/env.js"; import { hashing, compareWords } from "../libs/hashing.js"; import { generateTokens } from "../libs/token.js"; - -const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET; +import type { CustomError } from "src/api/types/error.js"; +import type { SignupData } from "src/api/types/signup.js"; +import type { loginData } from "src/api/types/login.js"; const AuthService = { - async signup(signupData) { + async signup(signupData: SignupData) { // 이메일로 이미 존재하는 사용자인지 확인 const existingUser = await prisma.user.findUnique({ where: { email: signupData.email }, }); if (existingUser) { - const error = new Error("이미 가입된 이메일입니다."); + const error: CustomError = new Error("이미 가입된 이메일입니다."); error.statusCode = 409; throw error; } @@ -31,14 +33,14 @@ const AuthService = { return userWithoutPassword; }, - async login(loginData) { + async login(loginData: loginData) { // 이메일로 존재하는 사용자인지 확인 const user = await prisma.user.findUnique({ where: { email: loginData.email }, }); if (!user) { - const error = new Error("가입되지 않은 사용자입니다"); + const error: CustomError = new Error("가입되지 않은 사용자입니다"); error.statusCode = 401; throw error; } @@ -47,7 +49,7 @@ const AuthService = { const isPasswordVaild = await compareWords(loginData.password, user.password); if (!isPasswordVaild) { - const error = new Error("비밀번호가 일치하지 않습니다."); + const error: CustomError = new Error("비밀번호가 일치하지 않습니다."); error.statusCode = 401; throw error; } @@ -68,16 +70,22 @@ const AuthService = { }, // AcessToken & RefreshToken을 재발급 받는 메서드 - async refreshAccessToken(oldRefreshToken) { + async refreshAccessToken(oldRefreshToken: string) { if (!oldRefreshToken) { - const error = new Error("Refresh Token이 제공되지 않았습니다."); + const error: CustomError = new Error("Refresh Token이 제공되지 않았습니다."); error.statusCode = 401; throw error; } try { // 토큰 디코딩해서 토큰의 User 확인 및 변조 여부 확인 - const decoded = jwt.verify(oldRefreshToken, REFRESH_TOKEN_SECRET); + const decoded = jwt.verify(oldRefreshToken, env.REFRESH_TOKEN_SECRET); + if (typeof decoded === "string" || !decoded.id) { + const error: CustomError = new Error("유효하지 않은 Refresh Token입니다."); + error.statusCode = 403; + throw error; + } + const userId = decoded.id; const user = await prisma.user.findUnique({ @@ -85,16 +93,21 @@ const AuthService = { }); if (!user) { - const error = new Error("해당하는 user가 없습니다. (Refresh Token 에러)"); + const error: CustomError = new Error("해당하는 user가 없습니다. (Refresh Token 에러)"); error.statusCode = 403; throw error; } // DB에 저장된 refreshToken 일치 여부 확인 + if (!user.refreshToken) { + console.warn(`사용자 ${user.id}에게 저장된 리프레시 토큰이 없습니다. 제공된 토큰을 무효화합니다.`); + return false; + } + const isTokenValid = await compareWords(oldRefreshToken, user.refreshToken); if (!isTokenValid) { - const error = new Error("유효하지 않은 Refresh Token입니다."); + const error: CustomError = new Error("유효하지 않은 Refresh Token입니다."); error.statusCode = 403; throw error; } @@ -112,11 +125,11 @@ const AuthService = { } catch (err) { console.error("Refresh Token 실제 오류:", err); if (err.name === "TokenExpiredError") { - const error = new Error("Refresh Token이 만료되었습니다. 다시 로그인해주세요."); + const error: CustomError = new Error("Refresh Token이 만료되었습니다. 다시 로그인해주세요."); error.statusCode = 401; throw error; } - const error = new Error("Refresh Token 검증에 실패했습니다."); + const error: CustomError = new Error("Refresh Token 검증에 실패했습니다."); error.statusCode = 403; throw error; } diff --git a/src/api/services/CommentService.js b/src/api/services/CommentService.js deleted file mode 100644 index 68c7b355d..000000000 --- a/src/api/services/CommentService.js +++ /dev/null @@ -1,75 +0,0 @@ -import prisma from "../libs/prismaClient.js"; - -const CommentService = { - async createComment({ content, productId, articleId, userId }) { - const newComment = await prisma.comment.create({ - data: { - content, - productId: productId || null, - articleId: articleId || null, - userId, - }, - }); - return newComment; - }, - - async updateComment(id, updateData, userId) { - const comment = await prisma.comment.findUnique({ where: { id } }); - - if (comment.userId != userId) { - const error = new Error("댓글을 수정할 권한이 없습니다."); - error.status = 403; - throw error; - } - - return await prisma.comment.update({ - where: { id }, - data: updateData, - }); - }, - - async deleteComment(id, userId) { - const comment = await prisma.comment.findUnique({ where: { id } }); - - if (comment.userId != userId) { - const error = new Error("댓글을 삭제할 권한이 없습니다."); - error.status = 403; - throw error; - } - - return await prisma.comment.delete({ - where: { id }, - }); - }, - - async findManyComment({ productId, articleId, cursor, limit }) { - let where = {}; - if (productId) { - where.productId = productId; - } else { - where.articleId = articleId; - } - - let skip; - if (cursor) { - skip = 1; - cursor = { id: Number(cursor) }; - } - - const comments = await prisma.comment.findMany({ - where, - orderBy: { id: "asc" }, - take: parseInt(limit), - select: { - id: true, - content: true, - createdAt: true, - }, - skip, - cursor, - }); - return comments; - }, -}; - -export default CommentService; diff --git a/src/api/services/CommentService.ts b/src/api/services/CommentService.ts new file mode 100644 index 000000000..cbc593c42 --- /dev/null +++ b/src/api/services/CommentService.ts @@ -0,0 +1,93 @@ +import prisma from "../libs/prismaClient.js"; +import type { CustomError } from "../types/error.js"; +import type { CreateCommentData, UpdateCommentData, FindManyCommentParams } from "../types/comment.js"; +import { Prisma } from "@prisma/client"; + +const CommentService = { + async createComment({ content, productId, articleId, userId }: CreateCommentData) { + const newComment = await prisma.comment.create({ + data: { + content, + productId: productId || null, + articleId: articleId || null, + userId, + }, + }); + return newComment; + }, + + async updateComment(id: number, updateData: UpdateCommentData, userId: number) { + const comment = await prisma.comment.findUnique({ where: { id } }); + + if (!comment) { + const error: CustomError = new Error("존재하지 않는 댓글입니다."); + error.statusCode = 404; + throw error; + } + + if (comment.userId != userId) { + const error: CustomError = new Error("댓글을 수정할 권한이 없습니다."); + error.status = 403; + throw error; + } + + return await prisma.comment.update({ + where: { id }, + data: updateData, + }); + }, + + async deleteComment(id: number, userId: number) { + const comment = await prisma.comment.findUnique({ where: { id } }); + + if (!comment) { + const error: CustomError = new Error("존재하지 않는 댓글입니다."); + error.statusCode = 404; + throw error; + } + + if (comment.userId != userId) { + const error: CustomError = new Error("댓글을 삭제할 권한이 없습니다."); + error.statusCode = 403; + throw error; + } + + return await prisma.comment.delete({ + where: { id }, + }); + }, + + async findManyComment({ productId, articleId, cursor: inputCursor, limit }: FindManyCommentParams) { + let where: Prisma.CommentWhereInput = {}; + + if (productId) { + where.productId = productId; + } else if (articleId) { + where.articleId = articleId; + } + + let skip; + let prismaCursor: Prisma.CommentWhereUniqueInput | undefined; + + if (inputCursor) { + skip = 1; + prismaCursor = { id: Number(inputCursor) }; + } + + const comments = await prisma.comment.findMany({ + where, + orderBy: { id: "asc" }, + take: parseInt(limit), + select: { + id: true, + content: true, + createdAt: true, + }, + ...(skip !== undefined && { skip }), + ...(prismaCursor && { cursor: prismaCursor }), + }); + return comments; + }, +}; + +export default CommentService; diff --git a/src/api/services/LikeService.js b/src/api/services/LikeService.ts similarity index 80% rename from src/api/services/LikeService.js rename to src/api/services/LikeService.ts index 947e610c6..3d1942eb8 100644 --- a/src/api/services/LikeService.js +++ b/src/api/services/LikeService.ts @@ -1,8 +1,9 @@ import prisma from "../libs/prismaClient.js"; +import type { LikeData } from "../types/like.js"; const LikeService = { - async toggleLike(userId, type, contentId) { - const user = { userId }; + async toggleLike(userId: number, type: string, contentId: number) { + const user: LikeData = { userId }; if (type === "article") { user.articleId = contentId; } else { diff --git a/src/api/services/MypageService.js b/src/api/services/MypageService.ts similarity index 71% rename from src/api/services/MypageService.js rename to src/api/services/MypageService.ts index b65bca810..b99cce177 100644 --- a/src/api/services/MypageService.js +++ b/src/api/services/MypageService.ts @@ -1,8 +1,10 @@ import prisma from "../libs/prismaClient.js"; import bcrypt from "bcrypt"; +import type { UserData } from "../types/user.js"; +import type { CustomError } from "../types/error.js"; const MypageService = { - async getUser(userId) { + async getUser(userId: number) { const user = await prisma.user.findUnique({ where: { id: userId }, select: { @@ -13,44 +15,38 @@ const MypageService = { }); if (!user) { - const error = new Error("사용자를 찾을 수 없습니다."); + const error: CustomError = new Error("사용자를 찾을 수 없습니다."); error.statusCode = 404; } return user; }, - async updateUser(userId, updateData) { - const { email, nickname, image } = updateData; - + async updateUser(userId: number, updateData: UserData) { const updatedUser = await prisma.user.update({ where: { id: userId }, - data: { - email, - nickname, - image, - }, + data: updateData, }); const { password, refreshToken, ...UserData } = updatedUser; return UserData; }, - async updatePassword(userId, oldPassword, newPassword) { + async updatePassword(userId: number, oldPassword: string, newPassword: string) { const user = await prisma.user.findUnique({ where: { id: userId }, }); if (!user) { - const error = new Error("사용자를 찾을 수 없습니다."); - error.status = 404; + const error: CustomError = new Error("사용자를 찾을 수 없습니다."); + error.statusCode = 404; throw error; } // 기존 비밀번호 일치 여부 확인 (입력값으로 기존 비밀번호 받음) const isPasswordValid = await bcrypt.compare(oldPassword, user.password); if (!isPasswordValid) { - const error = new Error("기존 비밀번호가 일치하지 않습니다."); + const error: CustomError = new Error("기존 비밀번호가 일치하지 않습니다."); error.statusCode = 403; throw error; } @@ -65,7 +61,7 @@ const MypageService = { }); }, - async getProducts(userId) { + async getProducts(userId: number) { const products = await prisma.product.findMany({ where: { userId }, orderBy: { @@ -76,7 +72,7 @@ const MypageService = { return products; }, - async getLikeProducts(userId) { + async getLikeProducts(userId: number) { const likedProducts = await prisma.like.findMany({ where: { userId, productId: { not: null } }, include: { diff --git a/src/api/services/ProductService.js b/src/api/services/ProductService.ts similarity index 60% rename from src/api/services/ProductService.js rename to src/api/services/ProductService.ts index b0220fa62..6a155c988 100644 --- a/src/api/services/ProductService.js +++ b/src/api/services/ProductService.ts @@ -1,14 +1,17 @@ import prisma from "../libs/prismaClient.js"; +import type { CustomError } from "../types/error.js"; +import type { CreateProductData, UpdateProductData, FindManyProductParams } from "../types/product.js"; +import { Prisma } from "@prisma/client"; const ProductService = { - async createProduct(productData, userId) { + async createProduct(productData: CreateProductData, userId: number) { const newProduct = await prisma.product.create({ data: { ...productData, userId }, }); return newProduct; }, - async findUniqueProduct(productId, userId) { + async findUniqueProduct(productId: number, userId: number) { const product = await prisma.product.findUnique({ where: { id: productId }, }); @@ -23,11 +26,17 @@ const ProductService = { return { ...product, isLiked: !!like }; }, - async patchProduct(id, updateData, userId) { + async patchProduct(id: number, updateData: UpdateProductData, userId: number) { const product = await prisma.product.findUnique({ where: { id } }); + if (!product) { + const error: CustomError = new Error("존재하지 않는 상품입니다."); + error.statusCode = 404; + throw error; + } + if (product.userId != userId) { - const error = new Error("상품을 수정할 권한이 없습니다."); + const error: CustomError = new Error("상품을 수정할 권한이 없습니다."); error.statusCode = 403; throw error; } @@ -37,11 +46,17 @@ const ProductService = { }); }, - async deleteProduct(id, userId) { + async deleteProduct(id: number, userId: number) { const product = await prisma.product.findUnique({ where: { id } }); + if (!product) { + const error: CustomError = new Error("존재하지 않는 상품입니다."); + error.statusCode = 404; + throw error; + } + if (product.userId !== userId) { - const error = new Error("상품을 삭제할 권한이 없습니다."); + const error: CustomError = new Error("상품을 삭제할 권한이 없습니다."); error.statusCode = 403; throw error; } @@ -50,8 +65,8 @@ const ProductService = { }); }, - async findManyProduct({ offset, limit, order, keyword }) { - let orderBy; + async findManyProduct({ offset, limit, order, keyword }: FindManyProductParams) { + let orderBy: Prisma.ProductOrderByWithRelationInput; switch (order) { case "oldest": orderBy = { createdAt: "asc" }; diff --git a/src/api/types/article.ts b/src/api/types/article.ts new file mode 100644 index 000000000..be8dae321 --- /dev/null +++ b/src/api/types/article.ts @@ -0,0 +1,23 @@ +export interface CreateArticleData { + title: string; + content: string; +} + +export interface UpdateArticleData { + title?: string; + content?: string; +} + +export type ArticleOrder = "oldest" | "recent"; + +export interface FindManyArticleParams { + offset: number; + limit: number; + order: ArticleOrder; + keyword?: string; +} + +export interface FindManyArticleParams { + offset: number; + limit: number; +} diff --git a/src/api/types/comment.ts b/src/api/types/comment.ts new file mode 100644 index 000000000..437d0eda3 --- /dev/null +++ b/src/api/types/comment.ts @@ -0,0 +1,17 @@ +export interface CreateCommentData { + content: string; + productId?: number; + articleId?: number; + userId: number; +} + +export interface UpdateCommentData { + content?: string; +} + +export interface FindManyCommentParams { + productId?: number; + articleId?: number; + cursor?: string; + limit: string; +} diff --git a/src/api/types/error.ts b/src/api/types/error.ts new file mode 100644 index 000000000..bdaa3ca61 --- /dev/null +++ b/src/api/types/error.ts @@ -0,0 +1,4 @@ +export interface CustomError extends Error { + statusCode?: number; + status?: number; +} diff --git a/src/api/types/like.ts b/src/api/types/like.ts new file mode 100644 index 000000000..858071e94 --- /dev/null +++ b/src/api/types/like.ts @@ -0,0 +1,5 @@ +export interface LikeData { + userId: number; + articleId?: number; + productId?: number; +} diff --git a/src/api/types/login.ts b/src/api/types/login.ts new file mode 100644 index 000000000..c49460f4a --- /dev/null +++ b/src/api/types/login.ts @@ -0,0 +1,4 @@ +export interface loginData { + email: string; + password: string; +} diff --git a/src/api/types/product.ts b/src/api/types/product.ts new file mode 100644 index 000000000..907ae8463 --- /dev/null +++ b/src/api/types/product.ts @@ -0,0 +1,22 @@ +export interface CreateProductData { + name: string; + description?: string; + price: number; + tags: string[]; +} + +export interface UpdateProductData { + name?: string; + description?: string; + price?: number; + tags?: string[]; +} + +export type ProductOrder = "oldest" | "recent"; + +export interface FindManyProductParams { + offset: string; + limit: string; + order?: ProductOrder; + keyword?: string; +} diff --git a/src/api/types/signup.ts b/src/api/types/signup.ts new file mode 100644 index 000000000..d33ab8ec7 --- /dev/null +++ b/src/api/types/signup.ts @@ -0,0 +1,5 @@ +export interface SignupData { + email: string; + nickname: string; + password: string; +} diff --git a/src/api/types/user.ts b/src/api/types/user.ts new file mode 100644 index 000000000..4deaf23db --- /dev/null +++ b/src/api/types/user.ts @@ -0,0 +1,5 @@ +export interface UserData { + email: string; + nickname?: string; + image?: string; +} From fd0ea8ea0aac365acc81491b2e6d86b42475ccf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 10 Sep 2025 18:39:58 +0900 Subject: [PATCH 27/54] =?UTF-8?q?refactor:=20=EB=AF=B8=EB=93=A4=EC=9B=A8?= =?UTF-8?q?=EC=96=B4=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20ts=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 --- src/api/middlewares/validators/{validate.js => validate.ts} | 3 ++- .../validators/{validateArticle.js => validateArticle.ts} | 0 .../validators/{validateProduct.js => validateProduct.ts} | 0 .../validators/{validateUser.js => validateUser.ts} | 0 4 files changed, 2 insertions(+), 1 deletion(-) rename src/api/middlewares/validators/{validate.js => validate.ts} (69%) rename src/api/middlewares/validators/{validateArticle.js => validateArticle.ts} (100%) rename src/api/middlewares/validators/{validateProduct.js => validateProduct.ts} (100%) rename src/api/middlewares/validators/{validateUser.js => validateUser.ts} (100%) diff --git a/src/api/middlewares/validators/validate.js b/src/api/middlewares/validators/validate.ts similarity index 69% rename from src/api/middlewares/validators/validate.js rename to src/api/middlewares/validators/validate.ts index 2bb5dfe4b..23bc012b5 100644 --- a/src/api/middlewares/validators/validate.js +++ b/src/api/middlewares/validators/validate.ts @@ -1,6 +1,7 @@ import { ZodError } from "zod"; +import type { Request, Response, NextFunction } from "express"; -const validate = (schema) => (req, res, next) => { +const validate = (schema: any) => (req: Request, res: Response, next: NextFunction) => { try { schema.parse(req.body); next(); diff --git a/src/api/middlewares/validators/validateArticle.js b/src/api/middlewares/validators/validateArticle.ts similarity index 100% rename from src/api/middlewares/validators/validateArticle.js rename to src/api/middlewares/validators/validateArticle.ts diff --git a/src/api/middlewares/validators/validateProduct.js b/src/api/middlewares/validators/validateProduct.ts similarity index 100% rename from src/api/middlewares/validators/validateProduct.js rename to src/api/middlewares/validators/validateProduct.ts diff --git a/src/api/middlewares/validators/validateUser.js b/src/api/middlewares/validators/validateUser.ts similarity index 100% rename from src/api/middlewares/validators/validateUser.js rename to src/api/middlewares/validators/validateUser.ts From dd0b3c2f93c30ce39f073090b1f40723109be93a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 10 Sep 2025 18:49:22 +0900 Subject: [PATCH 28/54] =?UTF-8?q?refactor:=20=EB=AF=B8=EB=93=A4=EC=9B=A8?= =?UTF-8?q?=EC=96=B4=20ts=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 --- src/api/middlewares/authenticate.js | 41 ---------------- src/api/middlewares/authenticate.ts | 48 +++++++++++++++++++ .../{errorHandler.js => errorHandler.ts} | 4 +- src/api/middlewares/{upload.js => upload.ts} | 9 +++- 4 files changed, 58 insertions(+), 44 deletions(-) delete mode 100644 src/api/middlewares/authenticate.js create mode 100644 src/api/middlewares/authenticate.ts rename src/api/middlewares/{errorHandler.js => errorHandler.ts} (57%) rename src/api/middlewares/{upload.js => upload.ts} (68%) diff --git a/src/api/middlewares/authenticate.js b/src/api/middlewares/authenticate.js deleted file mode 100644 index 582756245..000000000 --- a/src/api/middlewares/authenticate.js +++ /dev/null @@ -1,41 +0,0 @@ -import jwt from "jsonwebtoken"; -import prisma from "../libs/prismaClient.js"; -import { ACCESS_TOKEN_SECRET } from "../libs/constants.js"; - -export default async function authenticate(req, res, next) { - const { accessToken } = req.cookies; - - if (!accessToken) { - const error = new Error("인증 토큰이 필요합니다."); - error.statusCode = 401; - return next(error); - } - - try { - const decoded = jwt.verify(accessToken, ACCESS_TOKEN_SECRET); - const userId = decoded.id; - - const user = await prisma.user.findUnique({ - where: { id: userId }, - }); - - if (!user) { - const error = new Error("토큰에 해당하는 사용자를 찾을 수 없습니다."); - error.statusCode = 404; - return next(error); - } - - req.user = user; - next(); - } catch (err) { - let message; - if (err.name === "TokenExpiredError") { - message = "토큰이 만료되었습니다."; - } else { - message = "유효하지 않은 토큰입니다."; - } - const error = new Error(message); - error.statusCode = 401; - next(error); - } -} diff --git a/src/api/middlewares/authenticate.ts b/src/api/middlewares/authenticate.ts new file mode 100644 index 000000000..b7bcb1335 --- /dev/null +++ b/src/api/middlewares/authenticate.ts @@ -0,0 +1,48 @@ +import jwt from "jsonwebtoken"; +import prisma from "../libs/prismaClient.js"; +import type { Request, Response, NextFunction } from "express"; +import type { CustomError } from "src/api/types/error.js"; +import env from "../config/env.js"; + +export default async function authenticate(req: Request, res: Response, next: NextFunction) { + const { accessToken } = req.cookies; + + if (!accessToken) { + const error: CustomError = new Error("인증 토큰이 필요합니다."); + error.statusCode = 401; + return next(error); + } + + try { + const decoded = jwt.verify(accessToken, env.ACCESS_TOKEN_SECRET); + if (typeof decoded === "string" || !decoded.id) { + const error: CustomError = new Error("유효하지 않은 Access Token입니다."); + error.statusCode = 403; + throw error; + } + const userId = decoded.id; + + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + const error: CustomError = new Error("토큰에 해당하는 사용자를 찾을 수 없습니다."); + error.statusCode = 404; + return next(error); + } + + req.user = user; + next(); + } catch (err) { + let message; + if (err.name === "TokenExpiredError") { + message = "토큰이 만료되었습니다."; + } else { + message = "유효하지 않은 토큰입니다."; + } + const error: CustomError = new Error(message); + error.statusCode = 401; + next(error); + } +} diff --git a/src/api/middlewares/errorHandler.js b/src/api/middlewares/errorHandler.ts similarity index 57% rename from src/api/middlewares/errorHandler.js rename to src/api/middlewares/errorHandler.ts index a8dee7769..8bcf8f1c8 100644 --- a/src/api/middlewares/errorHandler.js +++ b/src/api/middlewares/errorHandler.ts @@ -1,4 +1,6 @@ -export default function errorHandler(err, req, res, next) { +import type { Request, Response, NextFunction } from "express"; + +export default function errorHandler(err: any, req: Request, res: Response, next: NextFunction) { console.error(err.stack); if (res.headersSent) return next(err); diff --git a/src/api/middlewares/upload.js b/src/api/middlewares/upload.ts similarity index 68% rename from src/api/middlewares/upload.js rename to src/api/middlewares/upload.ts index 1f0be811f..f6f29f650 100644 --- a/src/api/middlewares/upload.js +++ b/src/api/middlewares/upload.ts @@ -3,6 +3,11 @@ import cloudinary from "cloudinary"; import { CloudinaryStorage } from "multer-storage-cloudinary"; import { CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET } from "../libs/constants.js"; +// cloudinary.v2.config 호출 직전으로 교체 +if (!CLOUDINARY_CLOUD_NAME || !CLOUDINARY_API_KEY || !CLOUDINARY_API_SECRET) { + throw new Error("Cloudinary 환경 변수가 설정되지 않았습니다. 설정해주세요."); +} + cloudinary.v2.config({ cloud_name: CLOUDINARY_CLOUD_NAME, api_key: CLOUDINARY_API_KEY, @@ -11,10 +16,10 @@ cloudinary.v2.config({ const storage = new CloudinaryStorage({ cloudinary: cloudinary.v2, - params: { + params: async () => ({ folder: "sprint-mission-uploads", allowed_formats: ["jpg", "png", "jpeg"], - }, + }), }); const upload = multer({ storage }); From 5cd26cf642b821c3a2fb53f1468b26cfb0a0d1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 10 Sep 2025 18:51:44 +0900 Subject: [PATCH 29/54] =?UTF-8?q?refactor:=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=20=EB=B0=8F=20install=20@type/multer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 22 ++++++++++------------ package.json | 1 + src/api/config/env.ts | 14 +++++++++++++- src/api/types/article.ts | 9 ++------- src/api/types/comment.ts | 6 +++--- src/api/types/express.d.ts | 11 +++++++++++ src/api/types/product.ts | 6 +++--- 7 files changed, 43 insertions(+), 26 deletions(-) create mode 100644 src/api/types/express.d.ts diff --git a/package-lock.json b/package-lock.json index 30500bc3e..3259b8c08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@prisma/client": "^6.13.0", + "@types/multer": "^2.0.0", "axios": "^1.10.0", "bcrypt": "^6.0.0", "cloudinary": "^1.41.3", @@ -463,7 +464,6 @@ "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": "*", @@ -474,7 +474,6 @@ "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": "*" @@ -491,7 +490,6 @@ "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": "*", @@ -503,7 +501,6 @@ "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": "*", @@ -516,7 +513,6 @@ "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/json-schema": { @@ -541,7 +537,6 @@ "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": { @@ -551,11 +546,19 @@ "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==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "24.3.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.10.0" @@ -565,21 +568,18 @@ "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", @@ -590,7 +590,6 @@ "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": "*", @@ -5378,7 +5377,6 @@ "version": "7.10.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { diff --git a/package.json b/package.json index caac082dc..c7c002e39 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "description": "", "dependencies": { "@prisma/client": "^6.13.0", + "@types/multer": "^2.0.0", "axios": "^1.10.0", "bcrypt": "^6.0.0", "cloudinary": "^1.41.3", diff --git a/src/api/config/env.ts b/src/api/config/env.ts index e13bc6337..b1abaff99 100644 --- a/src/api/config/env.ts +++ b/src/api/config/env.ts @@ -1,14 +1,26 @@ interface EnvConfig { REFRESH_TOKEN_SECRET: string; + JWT_SECRET: string; + ACCESS_TOKEN_SECRET: string; } const getEnvConfig = (): EnvConfig => { const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET; + const JWT_SECRET = process.env.JWT_SECRET; + const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET; + + if (!ACCESS_TOKEN_SECRET) { + throw new Error("환경 변수 ACCESS_TOKEN_SECRET이 설정되지 않았습니다. 설정해주세요."); + } + if (!REFRESH_TOKEN_SECRET) { throw new Error("환경 변수 REFRESH_TOKEN_SECRET이 설정되지 않았습니다. 설정해주세요."); } - return { REFRESH_TOKEN_SECRET }; + if (!JWT_SECRET) { + throw new Error("환경 변수 JWT_SECRET이 설정되지 않았습니다. 설정해주세요."); + } + return { REFRESH_TOKEN_SECRET, JWT_SECRET, ACCESS_TOKEN_SECRET }; }; export default getEnvConfig(); diff --git a/src/api/types/article.ts b/src/api/types/article.ts index be8dae321..32e35dfa1 100644 --- a/src/api/types/article.ts +++ b/src/api/types/article.ts @@ -10,14 +10,9 @@ export interface UpdateArticleData { export type ArticleOrder = "oldest" | "recent"; -export interface FindManyArticleParams { +export type FindManyArticleParams = { offset: number; limit: number; order: ArticleOrder; keyword?: string; -} - -export interface FindManyArticleParams { - offset: number; - limit: number; -} +}; diff --git a/src/api/types/comment.ts b/src/api/types/comment.ts index 437d0eda3..92220b155 100644 --- a/src/api/types/comment.ts +++ b/src/api/types/comment.ts @@ -9,9 +9,9 @@ export interface UpdateCommentData { content?: string; } -export interface FindManyCommentParams { +export type FindManyCommentParams = { productId?: number; articleId?: number; cursor?: string; - limit: string; -} + limit: number; +}; diff --git a/src/api/types/express.d.ts b/src/api/types/express.d.ts new file mode 100644 index 000000000..92be171b1 --- /dev/null +++ b/src/api/types/express.d.ts @@ -0,0 +1,11 @@ +import { Request } from "express"; +import { AuthenticatedUser } from "./user"; + +declare global { + namespace Express { + interface Request { + user?: AuthenticatedUser; + file?: Multer.File; + } + } +} diff --git a/src/api/types/product.ts b/src/api/types/product.ts index 907ae8463..51225af21 100644 --- a/src/api/types/product.ts +++ b/src/api/types/product.ts @@ -15,8 +15,8 @@ export interface UpdateProductData { export type ProductOrder = "oldest" | "recent"; export interface FindManyProductParams { - offset: string; - limit: string; - order?: ProductOrder; + offset: number; + limit: number; + order?: string; keyword?: string; } From 6befb67aa8a02933bcbf72597fcb8c25bc6645ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 10 Sep 2025 20:26:27 +0900 Subject: [PATCH 30/54] =?UTF-8?q?refactor:=20controller=20ts=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...icleController.js => ArticleController.ts} | 33 ++++++++------- .../{AuthController.js => AuthController.ts} | 17 ++++---- ...mentController.js => CommentController.ts} | 41 +++++++++---------- ...{ImageController.js => ImageController.ts} | 4 +- .../{LikeController.js => LikeController.ts} | 8 +++- ...ypageController.js => MypageController.ts} | 11 ++--- ...ductController.js => ProductController.ts} | 23 +++++++---- src/api/types/article.ts | 12 +++--- src/api/types/product.ts | 12 +++--- 9 files changed, 89 insertions(+), 72 deletions(-) rename src/api/controllers/{ArticleController.js => ArticleController.ts} (64%) rename src/api/controllers/{AuthController.js => AuthController.ts} (59%) rename src/api/controllers/{CommentController.js => CommentController.ts} (56%) rename src/api/controllers/{ImageController.js => ImageController.ts} (71%) rename src/api/controllers/{LikeController.js => LikeController.ts} (57%) rename src/api/controllers/{MypageController.js => MypageController.ts} (75%) rename src/api/controllers/{ProductController.js => ProductController.ts} (71%) diff --git a/src/api/controllers/ArticleController.js b/src/api/controllers/ArticleController.ts similarity index 64% rename from src/api/controllers/ArticleController.js rename to src/api/controllers/ArticleController.ts index 670ce0ef6..4c26e11e4 100644 --- a/src/api/controllers/ArticleController.js +++ b/src/api/controllers/ArticleController.ts @@ -1,10 +1,11 @@ import ArticleService from "../services/ArticleService.js"; import jwt from "jsonwebtoken"; - -const JWT_SECRET = process.env.JWT_SECRET; +import type { Request, Response, NextFunction } from "express"; +import env from "../config/env.js"; +import type { CustomError } from "src/api/types/error.js"; const ArticleController = { - async createArticle(req, res, next) { + async createArticle(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; const { title, content } = req.body; @@ -12,10 +13,7 @@ const ArticleController = { return res.status(400).send("제목과 게시글을 입력해주세요."); } const articleData = { title, content }; - const newArticle = await ArticleService.createArticle( - articleData, - userId - ); + const newArticle = await ArticleService.createArticle(articleData, userId); res.status(201).json(newArticle); } catch (err) { @@ -23,7 +21,7 @@ const ArticleController = { } }, - async findUniqueArticle(req, res, next) { + async findUniqueArticle(req: Request, res: Response, next: NextFunction) { try { const { id } = req.params; const articleId = Number(id); @@ -33,7 +31,12 @@ const ArticleController = { if (token) { try { - const decoded = jwt.verify(token, JWT_SECRET); + const decoded = jwt.verify(token, env.JWT_SECRET); + if (typeof decoded === "string" || !decoded.id) { + const error: CustomError = new Error("유효하지 않은 Token입니다."); + error.statusCode = 403; + throw error; + } userId = decoded.userId; } catch (err) { console.error("토큰 검증 오류:", err); @@ -46,23 +49,19 @@ const ArticleController = { } }, - async updateArticle(req, res, next) { + async updateArticle(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; const { id } = req.params; const updateData = req.body; - const article = await ArticleService.updateArticle( - Number(id), - updateData, - userId - ); + const article = await ArticleService.updateArticle(Number(id), updateData, userId); res.status(201).json(article); } catch (err) { next(err); } }, - async deleteArticle(req, res, next) { + async deleteArticle(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; const { id } = req.params; @@ -73,7 +72,7 @@ const ArticleController = { } }, - async findManyArticle(req, res, next) { + async findManyArticle(req: Request, res: Response, next: NextFunction) { try { const { offset = 0, limit = 10, order = "recent", keyword } = req.query; const articles = await ArticleService.findManyArticle({ diff --git a/src/api/controllers/AuthController.js b/src/api/controllers/AuthController.ts similarity index 59% rename from src/api/controllers/AuthController.js rename to src/api/controllers/AuthController.ts index 1d22364ed..04909d544 100644 --- a/src/api/controllers/AuthController.js +++ b/src/api/controllers/AuthController.ts @@ -1,7 +1,8 @@ import AuthService from "../services/AuthService.js"; +import type { Request, Response, NextFunction } from "express"; const AuthController = { - async signup(req, res, next) { + async signup(req: Request, res: Response, next: NextFunction) { try { const newUser = await AuthService.signup(req.body); res.status(201).json(newUser); @@ -10,9 +11,9 @@ const AuthController = { } }, - async login(req, res, next) { + async login(req: Request, res: Response, next: NextFunction) { try { - const { user, accessToken, refreshToken } = await AuthService.login(req.body); + const { userWithoutPassword: user, accessToken, refreshToken } = await AuthService.login(req.body); res.cookie("refreshToken", refreshToken, { httpOnly: true, @@ -29,11 +30,13 @@ const AuthController = { } }, - async refreshToken(req, res, next) { + async refreshToken(req: Request, res: Response, next: NextFunction) { try { - const { accessToken, refreshToken: newRefreshToken } = await AuthService.refreshAccessToken( - req.cookies.refreshToken - ); + const result = await AuthService.refreshAccessToken(req.cookies.refreshToken); + if (!result) { + return res.status(401).json({ error: "유효하지 않은 리프레시 토큰입니다." }); + } + const { accessToken, refreshToken: newRefreshToken } = result; // 쿠키에 토큰 저장 res.cookie("refreshToken", newRefreshToken, { diff --git a/src/api/controllers/CommentController.js b/src/api/controllers/CommentController.ts similarity index 56% rename from src/api/controllers/CommentController.js rename to src/api/controllers/CommentController.ts index 37e942538..6e89d02a9 100644 --- a/src/api/controllers/CommentController.js +++ b/src/api/controllers/CommentController.ts @@ -1,7 +1,8 @@ import CommentService from "../services/CommentService.js"; +import type { Request, Response, NextFunction } from "express"; const CommentController = { - async createComment(req, res, next) { + async createComment(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; const { content, productId, articleId } = req.body; @@ -11,9 +12,7 @@ const CommentController = { } if (productId && articleId) { - return res - .status(400) - .json({ error: "productId 혹은 articleId 둘 중 하나만 있어야 함" }); + return res.status(400).json({ error: "productId 혹은 articleId 둘 중 하나만 있어야 함" }); } const newComment = await CommentService.createComment({ @@ -32,18 +31,14 @@ const CommentController = { } }, - async updateComment(req, res, next) { + async updateComment(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; const { id } = req.params; const updateData = req.body; - const comment = await CommentService.updateComment( - Number(id), - updateData, - userId - ); - res.status(201).json(comment, next); + const comment = await CommentService.updateComment(Number(id), updateData, userId); + res.status(201).json(comment); } catch (err) { if (err.code === "P2025") { res.status(404).json({ error: "해당 댓글이 존재하지 않음" }); @@ -52,7 +47,7 @@ const CommentController = { } }, - async deleteComment(req, res, next) { + async deleteComment(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; const { id } = req.params; @@ -65,20 +60,24 @@ const CommentController = { next(err); } }, - async findManyComment(req, res, next) { + + async findManyComment(req: Request, res: Response, next: NextFunction) { try { - const { productId, articleId, cursor, limit = 10 } = req.query; + const q = req.query; - if (productId && articleId) { - return res - .status(400) - .json({ error: "productId 혹은 articleId 둘 중 하나만 있어야 함" }); + const productId = typeof q.productId === "string" ? Number(q.productId) : undefined; + const articleId = typeof q.articleId === "string" ? Number(q.articleId) : undefined; + const cursor = typeof q.cursor === "string" ? q.cursor : undefined; + const limit = typeof q.limit === "string" ? parseInt(q.limit, 10) : 10; + + if (productId !== undefined && articleId !== undefined) { + return res.status(400).json({ error: "productId 혹은 articleId 둘 중 하나만 있어야 함" }); } const comments = await CommentService.findManyComment({ - productId: Number(productId), - articleId: Number(articleId), - cursor, + ...(productId !== undefined && { productId }), + ...(articleId !== undefined && { articleId }), + ...(cursor !== undefined && { cursor }), limit, }); diff --git a/src/api/controllers/ImageController.js b/src/api/controllers/ImageController.ts similarity index 71% rename from src/api/controllers/ImageController.js rename to src/api/controllers/ImageController.ts index cf6d6b96e..23d2d6190 100644 --- a/src/api/controllers/ImageController.js +++ b/src/api/controllers/ImageController.ts @@ -1,5 +1,7 @@ +import type { Request, Response } from "express"; + const ImageController = { - uploadImage(req, res) { + uploadImage(req: Request, res: Response) { if (!req.file) { return res.status(400).json({ error: "이미지 파일 없음" }); } diff --git a/src/api/controllers/LikeController.js b/src/api/controllers/LikeController.ts similarity index 57% rename from src/api/controllers/LikeController.js rename to src/api/controllers/LikeController.ts index eaf66a9ad..f90ea39b3 100644 --- a/src/api/controllers/LikeController.js +++ b/src/api/controllers/LikeController.ts @@ -1,10 +1,16 @@ import LikeService from "../services/LikeService.js"; +import type { Request, Response, NextFunction } from "express"; const LikeController = { - async toggleLike(req, res, next) { + async toggleLike(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; const { type, id } = req.params; + + if (type !== "product" && type !== "article") { + return res.status(400).json({ error: "type은 product 혹은 article이어야 합니다." }); + } + const contentId = Number(id); const result = await LikeService.toggleLike(userId, type, contentId); diff --git a/src/api/controllers/MypageController.js b/src/api/controllers/MypageController.ts similarity index 75% rename from src/api/controllers/MypageController.js rename to src/api/controllers/MypageController.ts index e14e3be10..4ae28b100 100644 --- a/src/api/controllers/MypageController.js +++ b/src/api/controllers/MypageController.ts @@ -1,7 +1,8 @@ import MypageService from "../services/MypageService.js"; +import type { Request, Response, NextFunction } from "express"; const MypageController = { - async getUser(req, res, next) { + async getUser(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; @@ -12,7 +13,7 @@ const MypageController = { } }, - async updateUser(req, res, next) { + async updateUser(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; const updateData = req.body; @@ -23,7 +24,7 @@ const MypageController = { } }, - async updatePassword(req, res, next) { + async updatePassword(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; const { oldPassword, newPassword } = req.body; @@ -35,7 +36,7 @@ const MypageController = { } }, - async getProducts(req, res, next) { + async getProducts(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; @@ -46,7 +47,7 @@ const MypageController = { } }, - async getLikeProducts(req, res, next) { + async getLikeProducts(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; diff --git a/src/api/controllers/ProductController.js b/src/api/controllers/ProductController.ts similarity index 71% rename from src/api/controllers/ProductController.js rename to src/api/controllers/ProductController.ts index a64c4baaa..acb8b736b 100644 --- a/src/api/controllers/ProductController.js +++ b/src/api/controllers/ProductController.ts @@ -1,10 +1,11 @@ import ProductService from "../services/ProductService.js"; import jwt from "jsonwebtoken"; - -const JWT_SECRET = process.env.JWT_SECRET; +import type { Request, Response, NextFunction } from "express"; +import env from "../config/env.js"; +import type { CustomError } from "src/api/types/error.js"; const ProductController = { - async createProduct(req, res, next) { + async createProduct(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; const { name, description, price, tags } = req.body; @@ -17,7 +18,7 @@ const ProductController = { } }, - async findUniqueProduct(req, res, next) { + async findUniqueProduct(req: Request, res: Response, next: NextFunction) { try { //throw new Error("🔥에러 핸들러 테스트"); const { id } = req.params; @@ -28,7 +29,12 @@ const ProductController = { if (token) { try { - const decoded = jwt.verify(token, JWT_SECRET); + const decoded = jwt.verify(token, env.JWT_SECRET); + if (typeof decoded === "string" || !decoded.id) { + const error: CustomError = new Error("유효하지 않은 Token입니다."); + error.statusCode = 403; + throw error; + } userId = decoded.userId; } catch (err) { console.error("토큰 검증 오류:", err); @@ -42,7 +48,7 @@ const ProductController = { } }, - async patchProduct(req, res, next) { + async patchProduct(req: Request, res: Response, next: NextFunction) { try { const { id } = req.params; const { id: userId } = req.user; @@ -58,7 +64,7 @@ const ProductController = { } }, - async deleteProduct(req, res, next) { + async deleteProduct(req: Request, res: Response, next: NextFunction) { try { const { id } = req.params; const { id: userId } = req.user; @@ -70,9 +76,10 @@ const ProductController = { } }, - async findManyProduct(req, res, next) { + async findManyProduct(req: Request, res: Response, next: NextFunction) { try { const { offset = 0, limit = 10, order = "recent", keyword } = req.query; + const products = await ProductService.findManyProduct({ offset, limit, diff --git a/src/api/types/article.ts b/src/api/types/article.ts index 32e35dfa1..a8b84c4b6 100644 --- a/src/api/types/article.ts +++ b/src/api/types/article.ts @@ -10,9 +10,9 @@ export interface UpdateArticleData { export type ArticleOrder = "oldest" | "recent"; -export type FindManyArticleParams = { - offset: number; - limit: number; - order: ArticleOrder; - keyword?: string; -}; +// export type FindManyArticleParams = { +// offset: number; +// limit: number; +// order: ArticleOrder; +// keyword?: string; +// }; diff --git a/src/api/types/product.ts b/src/api/types/product.ts index 51225af21..35234c937 100644 --- a/src/api/types/product.ts +++ b/src/api/types/product.ts @@ -14,9 +14,9 @@ export interface UpdateProductData { export type ProductOrder = "oldest" | "recent"; -export interface FindManyProductParams { - offset: number; - limit: number; - order?: string; - keyword?: string; -} +// export interface FindManyProductParams { +// offset: number; +// limit: number; +// order?: string; +// keyword?: string; +// } From 295f482c9b257d514af46bc100b5fe6fb4f50cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 10 Sep 2025 20:34:14 +0900 Subject: [PATCH 31/54] =?UTF-8?q?refactor:=20routes=20=EB=B0=8F=20main.ts?= =?UTF-8?q?=20ts=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20&=20@types/cookie-parser?= =?UTF-8?q?=20install?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 11 ++++++ package.json | 1 + .../{ArticleRouter.js => ArticleRouter.ts} | 0 .../routes/{AuthRouter.js => AuthRouter.ts} | 0 .../{CommentRouter.js => CommentRouter.ts} | 0 .../routes/{ImageRouter.js => ImageRouter.ts} | 0 .../routes/{LikeRouter.js => LikeRouter.ts} | 0 .../{MypageRouter.js => MypageRouter.ts} | 0 .../{ProductRouter.js => ProductRouter.ts} | 0 src/main.ts | 38 +++++++++---------- 10 files changed, 31 insertions(+), 19 deletions(-) rename src/api/routes/{ArticleRouter.js => ArticleRouter.ts} (100%) rename src/api/routes/{AuthRouter.js => AuthRouter.ts} (100%) rename src/api/routes/{CommentRouter.js => CommentRouter.ts} (100%) rename src/api/routes/{ImageRouter.js => ImageRouter.ts} (100%) rename src/api/routes/{LikeRouter.js => LikeRouter.ts} (100%) rename src/api/routes/{MypageRouter.js => MypageRouter.ts} (100%) rename src/api/routes/{ProductRouter.js => ProductRouter.ts} (100%) diff --git a/package-lock.json b/package-lock.json index 3259b8c08..0e60cf6ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "devDependencies": { "@eslint/js": "^9.31.0", "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.9", "@types/express": "^5.0.3", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.3.1", @@ -479,6 +480,16 @@ "@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/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", diff --git a/package.json b/package.json index c7c002e39..cece911e6 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "devDependencies": { "@eslint/js": "^9.31.0", "@types/bcrypt": "^6.0.0", + "@types/cookie-parser": "^1.4.9", "@types/express": "^5.0.3", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.3.1", diff --git a/src/api/routes/ArticleRouter.js b/src/api/routes/ArticleRouter.ts similarity index 100% rename from src/api/routes/ArticleRouter.js rename to src/api/routes/ArticleRouter.ts diff --git a/src/api/routes/AuthRouter.js b/src/api/routes/AuthRouter.ts similarity index 100% rename from src/api/routes/AuthRouter.js rename to src/api/routes/AuthRouter.ts diff --git a/src/api/routes/CommentRouter.js b/src/api/routes/CommentRouter.ts similarity index 100% rename from src/api/routes/CommentRouter.js rename to src/api/routes/CommentRouter.ts diff --git a/src/api/routes/ImageRouter.js b/src/api/routes/ImageRouter.ts similarity index 100% rename from src/api/routes/ImageRouter.js rename to src/api/routes/ImageRouter.ts diff --git a/src/api/routes/LikeRouter.js b/src/api/routes/LikeRouter.ts similarity index 100% rename from src/api/routes/LikeRouter.js rename to src/api/routes/LikeRouter.ts diff --git a/src/api/routes/MypageRouter.js b/src/api/routes/MypageRouter.ts similarity index 100% rename from src/api/routes/MypageRouter.js rename to src/api/routes/MypageRouter.ts diff --git a/src/api/routes/ProductRouter.js b/src/api/routes/ProductRouter.ts similarity index 100% rename from src/api/routes/ProductRouter.js rename to src/api/routes/ProductRouter.ts diff --git a/src/main.ts b/src/main.ts index d1c611d38..d62461a3b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,13 @@ import express from "express"; -// import ProductRouter from "./api/routes/ProductRouter.js"; -// import ArticleRouter from "./api/routes/ArticleRouter.js"; -// import CommentRouter from "./api/routes/CommentRouter.js"; -// import errorHandler from "./api/middlewares/errorHandler.js"; -// import imageRouter from "./api/routes/ImageRouter.js"; -// import AuthRouter from "./api/routes/AuthRouter.js"; -// import MypageRouter from "./api/routes/MypageRouter.js"; -// import LikeRouter from "./api/routes/LikeRouter.js"; -// import cookieParser from "cookie-parser"; +import ProductRouter from "./api/routes/ProductRouter.js"; +import ArticleRouter from "./api/routes/ArticleRouter.js"; +import CommentRouter from "./api/routes/CommentRouter.js"; +import errorHandler from "./api/middlewares/errorHandler.js"; +import imageRouter from "./api/routes/ImageRouter.js"; +import AuthRouter from "./api/routes/AuthRouter.js"; +import MypageRouter from "./api/routes/MypageRouter.js"; +import LikeRouter from "./api/routes/LikeRouter.js"; +import cookieParser from "cookie-parser"; // import { testAllArticleService } from "./external/tests/testArticleService.js"; // import { testAllProductService } from "./external/tests/testProductService.js"; @@ -19,18 +19,18 @@ const app = express(); const port = 3000; app.use(express.json()); -// app.use(cookieParser()); +app.use(cookieParser()); -// app.use("/products", ProductRouter); -// app.use("/articles", ArticleRouter); -// app.use("/comments", CommentRouter); -// app.use("/mypage", MypageRouter); -// app.use("/likes", LikeRouter); +app.use("/products", ProductRouter); +app.use("/articles", ArticleRouter); +app.use("/comments", CommentRouter); +app.use("/mypage", MypageRouter); +app.use("/likes", LikeRouter); -// app.use("/uploads", express.static("uploads")); -// app.use("/images", imageRouter); -// app.use("/auth", AuthRouter); -// app.use(errorHandler); +app.use("/uploads", express.static("uploads")); +app.use("/images", imageRouter); +app.use("/auth", AuthRouter); +app.use(errorHandler); app.listen(port, () => { console.log(`Server is running at http://localhost:${port}`); From ad99104548bdb9074f705ab37149cffb44ae052b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Thu, 11 Sep 2025 10:28:08 +0900 Subject: [PATCH 32/54] =?UTF-8?q?bug:=20article,=20product=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit undefined 타입 오류 수정 main 주석 처리 제거 및 테스트 진행 errorHandler에 error any type으로 처리하던거 customError로 수정 --- src/api/controllers/ArticleController.ts | 16 +++++++++++----- src/api/controllers/ProductController.ts | 13 +++++++++---- src/api/middlewares/errorHandler.ts | 3 ++- src/api/services/ArticleService.ts | 2 +- src/api/services/CommentService.ts | 4 ++-- src/api/services/ProductService.ts | 4 ++-- src/api/types/article.ts | 14 ++++++-------- src/api/types/product.ts | 12 ++++++------ src/main.ts | 2 +- 9 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/api/controllers/ArticleController.ts b/src/api/controllers/ArticleController.ts index 4c26e11e4..acd4b823d 100644 --- a/src/api/controllers/ArticleController.ts +++ b/src/api/controllers/ArticleController.ts @@ -74,12 +74,18 @@ const ArticleController = { async findManyArticle(req: Request, res: Response, next: NextFunction) { try { - const { offset = 0, limit = 10, order = "recent", keyword } = req.query; + const { offset = 0, limit = 10, order, keyword } = req.query; + + const finalOffset = Number(offset) || 0; + const finalLimit = Number(limit) || 10; + const finalOrder = typeof order === "string" ? order : "recent"; + const finalKeyword = typeof keyword === "string" ? keyword : undefined; + const articles = await ArticleService.findManyArticle({ - offset, - limit, - order, - keyword, + offset: finalOffset, + limit: finalLimit, + order: finalOrder, + ...(finalKeyword && { keyword: finalKeyword }), }); res.status(200).json(articles); } catch (err) { diff --git a/src/api/controllers/ProductController.ts b/src/api/controllers/ProductController.ts index acb8b736b..ee23b1339 100644 --- a/src/api/controllers/ProductController.ts +++ b/src/api/controllers/ProductController.ts @@ -80,11 +80,16 @@ const ProductController = { try { const { offset = 0, limit = 10, order = "recent", keyword } = req.query; + const finalOffset = Number(offset) || 0; + const finalLimit = Number(limit) || 10; + const finalOrder = typeof order === "string" ? order : "recent"; + const finalKeyword = typeof keyword === "string" ? keyword : undefined; + const products = await ProductService.findManyProduct({ - offset, - limit, - order, - keyword, + offset: finalOffset, + limit: finalLimit, + order: finalOrder, + ...(finalKeyword && { keyword: finalKeyword }), }); res.status(200).json(products); } catch (err) { diff --git a/src/api/middlewares/errorHandler.ts b/src/api/middlewares/errorHandler.ts index 8bcf8f1c8..74588fd24 100644 --- a/src/api/middlewares/errorHandler.ts +++ b/src/api/middlewares/errorHandler.ts @@ -1,6 +1,7 @@ import type { Request, Response, NextFunction } from "express"; +import type { CustomError } from "../types/error.js"; -export default function errorHandler(err: any, req: Request, res: Response, next: NextFunction) { +export default function errorHandler(err: CustomError, req: Request, res: Response, next: NextFunction) { console.error(err.stack); if (res.headersSent) return next(err); diff --git a/src/api/services/ArticleService.ts b/src/api/services/ArticleService.ts index c69790657..d29ce81f2 100644 --- a/src/api/services/ArticleService.ts +++ b/src/api/services/ArticleService.ts @@ -1,5 +1,5 @@ import prisma from "../libs/prismaClient.js"; -import type { CreateArticleData, UpdateArticleData, FindManyArticleParams, ArticleOrder } from "../types/article.js"; +import type { CreateArticleData, UpdateArticleData, FindManyArticleParams } from "../types/article.js"; import type { CustomError } from "../types/error.js"; import { Prisma } from "@prisma/client"; diff --git a/src/api/services/CommentService.ts b/src/api/services/CommentService.ts index cbc593c42..cdc481b5d 100644 --- a/src/api/services/CommentService.ts +++ b/src/api/services/CommentService.ts @@ -27,7 +27,7 @@ const CommentService = { if (comment.userId != userId) { const error: CustomError = new Error("댓글을 수정할 권한이 없습니다."); - error.status = 403; + error.statusCode = 403; throw error; } @@ -77,7 +77,7 @@ const CommentService = { const comments = await prisma.comment.findMany({ where, orderBy: { id: "asc" }, - take: parseInt(limit), + take: limit, select: { id: true, content: true, diff --git a/src/api/services/ProductService.ts b/src/api/services/ProductService.ts index 6a155c988..2445c4184 100644 --- a/src/api/services/ProductService.ts +++ b/src/api/services/ProductService.ts @@ -88,8 +88,8 @@ const ProductService = { const products = await prisma.product.findMany({ select: { id: true, name: true, price: true, createdAt: true }, - skip: parseInt(offset), - take: parseInt(limit), + skip: offset, + take: limit, orderBy, where, }); diff --git a/src/api/types/article.ts b/src/api/types/article.ts index a8b84c4b6..509713098 100644 --- a/src/api/types/article.ts +++ b/src/api/types/article.ts @@ -8,11 +8,9 @@ export interface UpdateArticleData { content?: string; } -export type ArticleOrder = "oldest" | "recent"; - -// export type FindManyArticleParams = { -// offset: number; -// limit: number; -// order: ArticleOrder; -// keyword?: string; -// }; +export type FindManyArticleParams = { + offset: number; + limit: number; + order?: string; + keyword?: string; +}; diff --git a/src/api/types/product.ts b/src/api/types/product.ts index 35234c937..51225af21 100644 --- a/src/api/types/product.ts +++ b/src/api/types/product.ts @@ -14,9 +14,9 @@ export interface UpdateProductData { export type ProductOrder = "oldest" | "recent"; -// export interface FindManyProductParams { -// offset: number; -// limit: number; -// order?: string; -// keyword?: string; -// } +export interface FindManyProductParams { + offset: number; + limit: number; + order?: string; + keyword?: string; +} diff --git a/src/main.ts b/src/main.ts index d62461a3b..c86d33fcc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,7 +12,7 @@ import cookieParser from "cookie-parser"; // import { testAllArticleService } from "./external/tests/testArticleService.js"; // import { testAllProductService } from "./external/tests/testProductService.js"; -// //testAllArticleService(); +// testAllArticleService(); // testAllProductService(); const app = express(); From d6fa75db1cbf5dd9c5a8af27cbad72fc5748131e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Thu, 11 Sep 2025 15:42:58 +0900 Subject: [PATCH 33/54] =?UTF-8?q?refactor:=20Controller,=20Service,=20Repo?= =?UTF-8?q?sitory=EB=A1=9C=20=EB=82=98=EB=88=84=EC=96=B4=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20=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/api/repositories/ArticleRepository.ts | 65 ++++++++++++++++++++++ src/api/repositories/AuthRepository.ts | 32 +++++++++++ src/api/repositories/CommentRepository.ts | 56 +++++++++++++++++++ src/api/repositories/LikeRepository.ts | 14 +++++ src/api/repositories/MypageRepository.ts | 54 +++++++++++++++++++ src/api/repositories/ProductRepository.ts | 61 +++++++++++++++++++++ src/api/services/ArticleService.ts | 66 +++++++---------------- src/api/services/AuthService.ts | 32 ++++------- src/api/services/CommentService.ts | 59 +++++--------------- src/api/services/LikeService.ts | 20 +++---- src/api/services/MypageService.ts | 42 +++------------ src/api/services/ProductService.ts | 61 +++++---------------- 12 files changed, 350 insertions(+), 212 deletions(-) create mode 100644 src/api/repositories/ArticleRepository.ts create mode 100644 src/api/repositories/AuthRepository.ts create mode 100644 src/api/repositories/CommentRepository.ts create mode 100644 src/api/repositories/LikeRepository.ts create mode 100644 src/api/repositories/MypageRepository.ts create mode 100644 src/api/repositories/ProductRepository.ts diff --git a/src/api/repositories/ArticleRepository.ts b/src/api/repositories/ArticleRepository.ts new file mode 100644 index 000000000..46100d0e3 --- /dev/null +++ b/src/api/repositories/ArticleRepository.ts @@ -0,0 +1,65 @@ +import prisma from "../libs/prismaClient.js"; +import { Prisma } from "@prisma/client"; +import type { FindManyArticleParams } from "../types/article.js"; + +// 게시글 생성 +export const create = async (data: Prisma.ArticleCreateInput) => { + return await prisma.article.create({ data }); +}; + +// ID로 게시글 조회 +export const findById = async (id: number) => { + return await prisma.article.findUnique({ where: { id } }); +}; + +// 여러 게시글 조회 +export const findMany = async ({ offset, limit, order, keyword }: FindManyArticleParams) => { + let orderBy: Prisma.ArticleOrderByWithRelationInput; + switch (order) { + case "oldest": + orderBy = { createdAt: "asc" }; + break; + case "recent": + default: + orderBy = { createdAt: "desc" }; + } + + const where: Prisma.ArticleWhereInput = keyword + ? { + OR: [ + { title: { contains: keyword, mode: "insensitive" } }, + { content: { contains: keyword, mode: "insensitive" } }, + ], + } + : {}; + + return await prisma.article.findMany({ + select: { id: true, title: true, content: true, createdAt: true }, + skip: offset, + take: limit, + orderBy, + where, + }); +}; + +// 게시글 수정 +export const update = async (id: number, data: Prisma.ArticleUpdateInput) => { + return await prisma.article.update({ + where: { id }, + data, + }); +}; + +// 게시글 삭제 +export const remove = async (id: number) => { + return await prisma.article.delete({ + where: { id }, + }); +}; + +// 특정 사용자가 특정 게시글에 좋아요 눌렀는지 확인 +export const findLikeByUserAndArticle = async (userId: number, articleId: number) => { + return await prisma.like.findFirst({ + where: { userId, articleId }, + }); +}; diff --git a/src/api/repositories/AuthRepository.ts b/src/api/repositories/AuthRepository.ts new file mode 100644 index 000000000..65f9590cf --- /dev/null +++ b/src/api/repositories/AuthRepository.ts @@ -0,0 +1,32 @@ +import prisma from "../libs/prismaClient.js"; +import type { Prisma } from "@prisma/client"; + +// User 생성 +export const create = async (data: Prisma.UserCreateInput) => { + return await prisma.user.create({ data }); +}; + +// 이메일로 사용자 찾기 +export const findByEmail = async (email: string) => { + return await prisma.user.findUnique({ where: { email } }); +}; + +// id로 사용자 찾기 +export const findById = async (id: number) => { + return await prisma.user.findUnique({ where: { id } }); +}; + +// user 정보 수정 +export const update = async (id: number, data: Prisma.UserUpdateInput) => { + return await prisma.user.update({ + where: { id }, + data, + }); +}; + +export const updateUserRefreshToken = async (id: number, refreshToken: string | null) => { + return await prisma.user.update({ + where: { id }, + data: { refreshToken }, + }); +}; diff --git a/src/api/repositories/CommentRepository.ts b/src/api/repositories/CommentRepository.ts new file mode 100644 index 000000000..6c22b09e5 --- /dev/null +++ b/src/api/repositories/CommentRepository.ts @@ -0,0 +1,56 @@ +import prisma from "../libs/prismaClient.js"; +import type { FindManyCommentParams } from "../types/comment.js"; +import { Prisma } from "@prisma/client"; + +export const create = async (data: Prisma.CommentCreateInput) => { + return await prisma.comment.create({ data }); +}; + +export const findById = async (id: number) => { + return await prisma.comment.findUnique({ + where: { id }, + }); +}; + +export const update = async (id: number, data: Prisma.CommentUpdateInput) => { + return await prisma.comment.update({ + where: { id }, + data, + }); +}; + +export const remove = async (id: number) => { + return await prisma.comment.delete({ where: { id } }); +}; + +export const findMany = async ({ productId, articleId, cursor, limit }: FindManyCommentParams) => { + let where: Prisma.CommentWhereInput = {}; + + if (productId) { + where.productId = productId; + } else if (articleId) { + where.articleId = articleId; + } + + let skip; + let prismaCursor: Prisma.CommentWhereUniqueInput | undefined; + + if (cursor) { + skip = 1; + prismaCursor = { id: Number(cursor) }; + } + + const comments = await prisma.comment.findMany({ + where, + orderBy: { id: "asc" }, + take: limit, + select: { + id: true, + content: true, + createdAt: true, + }, + ...(skip !== undefined && { skip }), + ...(prismaCursor && { cursor: prismaCursor }), + }); + return comments; +}; diff --git a/src/api/repositories/LikeRepository.ts b/src/api/repositories/LikeRepository.ts new file mode 100644 index 000000000..94bfa8a0f --- /dev/null +++ b/src/api/repositories/LikeRepository.ts @@ -0,0 +1,14 @@ +import prisma from "../libs/prismaClient.js"; +import { Prisma } from "@prisma/client"; + +export const findFirst = async (where: Prisma.LikeWhereInput) => { + return await prisma.like.findFirst({ where }); +}; + +export const create = async (data: Prisma.LikeCreateInput) => { + return await prisma.like.create({ data }); +}; + +export const remove = async (id: number) => { + return await prisma.like.delete({ where: { id } }); +}; diff --git a/src/api/repositories/MypageRepository.ts b/src/api/repositories/MypageRepository.ts new file mode 100644 index 000000000..6f18d87dd --- /dev/null +++ b/src/api/repositories/MypageRepository.ts @@ -0,0 +1,54 @@ +import prisma from "../libs/prismaClient.js"; +import type { Prisma } from "@prisma/client"; + +export const findUserProfile = async (userId: number) => { + return await prisma.user.findUnique({ + where: { id: userId }, + select: { + email: true, + nickname: true, + image: true, + }, + }); +}; + +export const findUserForAuth = async (userId: number) => { + return await prisma.user.findUnique({ + where: { id: userId }, + }); +}; + +export const update = async (userId: number, data: Prisma.UserUpdateInput) => { + return await prisma.user.update({ + where: { id: userId }, + data, + }); +}; + +export const updatePassword = async (userId: number, hashedNewPassword: string) => { + return await prisma.user.update({ + where: { id: userId }, + data: { password: hashedNewPassword }, + }); +}; + +export const findProductsByUserId = async (userId: number) => { + return await prisma.product.findMany({ + where: { userId }, + orderBy: { + createdAt: "desc", + }, + }); +}; + +export const findLikedProductsByUserId = async (userId: number) => { + return await prisma.like.findMany({ + where: { userId, productId: { not: null } }, + include: { + product: true, + }, + orderBy: { + createdAt: "desc", + }, + }); +}; diff --git a/src/api/repositories/ProductRepository.ts b/src/api/repositories/ProductRepository.ts new file mode 100644 index 000000000..29f53c897 --- /dev/null +++ b/src/api/repositories/ProductRepository.ts @@ -0,0 +1,61 @@ +import prisma from "../libs/prismaClient.js"; +import type { FindManyProductParams } from "../types/product.js"; +import { Prisma } from "@prisma/client"; + +export const create = async (data: Prisma.ProductCreateInput) => { + return await prisma.product.create({ data }); +}; + +export const findById = async (productId: number) => { + return await prisma.product.findUnique({ + where: { id: productId }, + }); +}; + +export const findLikeByUserAndProduct = async (userId: number, productId: number) => { + return await prisma.like.findFirst({ + where: { userId, productId }, + }); +}; + +export const update = async (id: number, data: Prisma.ProductUpdateInput) => { + return await prisma.product.update({ + where: { id }, + data, + }); +}; + +export const remove = async (id: number) => { + return await prisma.product.delete({ + where: { id }, + }); +}; + +export const findMany = async ({ offset, limit, order, keyword }: FindManyProductParams) => { + let orderBy: Prisma.ProductOrderByWithRelationInput; + switch (order) { + case "oldest": + orderBy = { createdAt: "asc" }; + break; + case "recent": + default: + orderBy = { createdAt: "desc" }; + } + + let where = {}; + if (keyword) { + where = { + OR: [ + { name: { contains: keyword, mode: "insensitive" } }, + { description: { contains: keyword, mode: "insensitive" } }, + ], + }; + } + return await prisma.product.findMany({ + select: { id: true, name: true, price: true, createdAt: true }, + skip: offset, + take: limit, + orderBy, + where, + }); +}; diff --git a/src/api/services/ArticleService.ts b/src/api/services/ArticleService.ts index d29ce81f2..7e100c763 100644 --- a/src/api/services/ArticleService.ts +++ b/src/api/services/ArticleService.ts @@ -1,33 +1,35 @@ -import prisma from "../libs/prismaClient.js"; import type { CreateArticleData, UpdateArticleData, FindManyArticleParams } from "../types/article.js"; import type { CustomError } from "../types/error.js"; -import { Prisma } from "@prisma/client"; +import * as ArticleRepository from "../repositories/ArticleRepository.js"; const ArticleService = { async createArticle(articleData: CreateArticleData, userId: number) { - const newArticle = await prisma.article.create({ - data: { ...articleData, userId }, + const newArticle = await ArticleRepository.create({ + ...articleData, + user: { connect: { id: userId } }, }); return newArticle; }, async findUniqueArticle(articleId: number, userId?: number) { - const article = await prisma.article.findUnique({ - where: { id: articleId }, - }); + const article = await ArticleRepository.findById(articleId); + + if (!article) { + throw new Error("존재하지 않는 게시글입니다."); + } if (!userId) { return { ...article, isLiked: false }; } - const like = await prisma.like.findFirst({ - where: { userId, articleId }, - }); + // 좋아요 정보 조회 + const like = await ArticleRepository.findLikeByUserAndArticle(userId, articleId); + return { ...article, isLiked: !!like }; }, async updateArticle(id: number, updateData: UpdateArticleData, userId: number) { - const article = await prisma.article.findUnique({ where: { id } }); + const article = await ArticleRepository.findById(id); if (!article) { const error: CustomError = new Error("존재하지 않는 게시글입니다."); @@ -41,14 +43,11 @@ const ArticleService = { throw error; } - return await prisma.article.update({ - where: { id }, - data: updateData, - }); + return await ArticleRepository.update(id, updateData); }, async deleteArticle(id: number, userId: number) { - const article = await prisma.article.findUnique({ where: { id } }); + const article = await ArticleRepository.findById(id); if (!article) { const error: CustomError = new Error("존재하지 않는 게시글입니다."); @@ -62,40 +61,11 @@ const ArticleService = { throw error; } - await prisma.article.delete({ - where: { id }, - }); + await ArticleRepository.remove(id); }, - async findManyArticle({ offset, limit, order, keyword }: FindManyArticleParams) { - let orderBy: Prisma.ArticleOrderByWithRelationInput; - switch (order) { - case "oldest": - orderBy = { createdAt: "asc" }; - break; - case "recent": - default: - orderBy = { createdAt: "desc" }; - } - - let where = {}; - - if (keyword) { - where = { - OR: [ - { title: { contains: keyword, mode: "insensitive" } }, - { content: { contains: keyword, mode: "insensitive" } }, - ], - }; - } - - const articles = await prisma.article.findMany({ - select: { id: true, title: true, content: true, createdAt: true }, - skip: offset, - take: limit, - orderBy, - where, - }); + async findManyArticle(params: FindManyArticleParams) { + const articles = await ArticleRepository.findMany(params); return articles; }, }; diff --git a/src/api/services/AuthService.ts b/src/api/services/AuthService.ts index 1ed76cec6..9df4a2c48 100644 --- a/src/api/services/AuthService.ts +++ b/src/api/services/AuthService.ts @@ -1,4 +1,3 @@ -import prisma from "../libs/prismaClient.js"; import jwt from "jsonwebtoken"; import env from "../config/env.js"; import { hashing, compareWords } from "../libs/hashing.js"; @@ -6,13 +5,12 @@ import { generateTokens } from "../libs/token.js"; import type { CustomError } from "src/api/types/error.js"; import type { SignupData } from "src/api/types/signup.js"; import type { loginData } from "src/api/types/login.js"; +import * as AuthRepository from "../repositories/AuthRepository.js"; const AuthService = { async signup(signupData: SignupData) { // 이메일로 이미 존재하는 사용자인지 확인 - const existingUser = await prisma.user.findUnique({ - where: { email: signupData.email }, - }); + const existingUser = await AuthRepository.findByEmail(signupData.email); if (existingUser) { const error: CustomError = new Error("이미 가입된 이메일입니다."); @@ -25,9 +23,7 @@ const AuthService = { const hashedPassword = await hashing(password); // 사용자 생성 - const newUser = await prisma.user.create({ - data: { email, nickname, password: hashedPassword }, - }); + const newUser = await AuthRepository.create(signupData); const { password: _, ...userWithoutPassword } = newUser; return userWithoutPassword; @@ -35,9 +31,7 @@ const AuthService = { async login(loginData: loginData) { // 이메일로 존재하는 사용자인지 확인 - const user = await prisma.user.findUnique({ - where: { email: loginData.email }, - }); + const user = await AuthRepository.findByEmail(loginData.email); if (!user) { const error: CustomError = new Error("가입되지 않은 사용자입니다"); @@ -54,15 +48,12 @@ const AuthService = { throw error; } - // 액세스 토큰 및 리프레시 토큰 생성 + // 액세스 토큰 및 리프레시 토큰 생성 및 해싱 const { accessToken, refreshToken } = generateTokens(user.id); + const hashedRefreshToken = await hashing(refreshToken); // DB에 리프레시 토큰 저장 - const hashedRefreshToken = await hashing(refreshToken); - await prisma.user.update({ - where: { id: user.id }, - data: { refreshToken: hashedRefreshToken }, - }); + await AuthRepository.updateUserRefreshToken(user.id, hashedRefreshToken); const { password: _, ...userWithoutPassword } = user; // 테스트를 위해 refreshToken 출력 @@ -88,9 +79,7 @@ const AuthService = { const userId = decoded.id; - const user = await prisma.user.findUnique({ - where: { id: userId }, - }); + const user = await AuthRepository.findById(userId); if (!user) { const error: CustomError = new Error("해당하는 user가 없습니다. (Refresh Token 에러)"); @@ -116,10 +105,7 @@ const AuthService = { const { accessToken, refreshToken } = generateTokens(user.id); const hashedNewRefreshToken = await hashing(refreshToken); - await prisma.user.update({ - where: { id: user.id }, - data: { refreshToken: hashedNewRefreshToken }, - }); + await AuthRepository.updateUserRefreshToken(user.id, hashedNewRefreshToken); return { accessToken, refreshToken }; } catch (err) { diff --git a/src/api/services/CommentService.ts b/src/api/services/CommentService.ts index cdc481b5d..c1df61feb 100644 --- a/src/api/services/CommentService.ts +++ b/src/api/services/CommentService.ts @@ -1,23 +1,20 @@ -import prisma from "../libs/prismaClient.js"; import type { CustomError } from "../types/error.js"; import type { CreateCommentData, UpdateCommentData, FindManyCommentParams } from "../types/comment.js"; -import { Prisma } from "@prisma/client"; +import * as CommentRepository from "../repositories/CommentRepository.js"; const CommentService = { async createComment({ content, productId, articleId, userId }: CreateCommentData) { - const newComment = await prisma.comment.create({ - data: { - content, - productId: productId || null, - articleId: articleId || null, - userId, - }, + const newComment = await CommentRepository.create({ + content, + ...(productId && { product: { connect: { id: productId } } }), + ...(articleId && { article: { connect: { id: articleId } } }), + user: { connect: { id: userId } }, }); return newComment; }, async updateComment(id: number, updateData: UpdateCommentData, userId: number) { - const comment = await prisma.comment.findUnique({ where: { id } }); + const comment = await CommentRepository.findById(id); if (!comment) { const error: CustomError = new Error("존재하지 않는 댓글입니다."); @@ -31,14 +28,11 @@ const CommentService = { throw error; } - return await prisma.comment.update({ - where: { id }, - data: updateData, - }); + return await CommentRepository.update(id, updateData); }, async deleteComment(id: number, userId: number) { - const comment = await prisma.comment.findUnique({ where: { id } }); + const comment = await CommentRepository.findById(id); if (!comment) { const error: CustomError = new Error("존재하지 않는 댓글입니다."); @@ -52,40 +46,11 @@ const CommentService = { throw error; } - return await prisma.comment.delete({ - where: { id }, - }); + return await CommentRepository.remove(id); }, - async findManyComment({ productId, articleId, cursor: inputCursor, limit }: FindManyCommentParams) { - let where: Prisma.CommentWhereInput = {}; - - if (productId) { - where.productId = productId; - } else if (articleId) { - where.articleId = articleId; - } - - let skip; - let prismaCursor: Prisma.CommentWhereUniqueInput | undefined; - - if (inputCursor) { - skip = 1; - prismaCursor = { id: Number(inputCursor) }; - } - - const comments = await prisma.comment.findMany({ - where, - orderBy: { id: "asc" }, - take: limit, - select: { - id: true, - content: true, - createdAt: true, - }, - ...(skip !== undefined && { skip }), - ...(prismaCursor && { cursor: prismaCursor }), - }); + async findManyComment(params: FindManyCommentParams) { + const comments = await CommentRepository.findMany(params); return comments; }, }; diff --git a/src/api/services/LikeService.ts b/src/api/services/LikeService.ts index 3d1942eb8..68e0c6354 100644 --- a/src/api/services/LikeService.ts +++ b/src/api/services/LikeService.ts @@ -1,5 +1,6 @@ -import prisma from "../libs/prismaClient.js"; import type { LikeData } from "../types/like.js"; +import * as LikeRepository from "../repositories/LikeRepository.js"; +import type { Prisma } from "@prisma/client"; const LikeService = { async toggleLike(userId: number, type: string, contentId: number) { @@ -10,19 +11,18 @@ const LikeService = { user.productId = contentId; } - const existingLike = await prisma.like.findFirst({ - where: user, - }); + const existingLike = await LikeRepository.findFirst(user); if (existingLike) { - await prisma.like.delete({ - where: { id: existingLike.id }, - }); + await LikeRepository.remove(existingLike.id); return { message: "좋아요가 취소되었습니다.", liked: false }; } else { - await prisma.like.create({ - data: user, - }); + const createData: Prisma.LikeCreateInput = { + user: { connect: { id: userId } }, + ...(type === "article" && { article: { connect: { id: contentId } } }), + ...(type === "product" && { product: { connect: { id: contentId } } }), + }; + await LikeRepository.create(createData); return { message: "좋아요를 눌렀습니다.", liked: true }; } }, diff --git a/src/api/services/MypageService.ts b/src/api/services/MypageService.ts index b99cce177..228fc9496 100644 --- a/src/api/services/MypageService.ts +++ b/src/api/services/MypageService.ts @@ -1,18 +1,11 @@ -import prisma from "../libs/prismaClient.js"; import bcrypt from "bcrypt"; import type { UserData } from "../types/user.js"; import type { CustomError } from "../types/error.js"; +import * as MypageRepository from "../repositories/MypageRepository.js"; const MypageService = { async getUser(userId: number) { - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { - email: true, - nickname: true, - image: true, - }, - }); + const user = await MypageRepository.findUserProfile(userId); if (!user) { const error: CustomError = new Error("사용자를 찾을 수 없습니다."); @@ -23,19 +16,14 @@ const MypageService = { }, async updateUser(userId: number, updateData: UserData) { - const updatedUser = await prisma.user.update({ - where: { id: userId }, - data: updateData, - }); + const updatedUser = await MypageRepository.update(userId, updateData); const { password, refreshToken, ...UserData } = updatedUser; return UserData; }, async updatePassword(userId: number, oldPassword: string, newPassword: string) { - const user = await prisma.user.findUnique({ - where: { id: userId }, - }); + const user = await MypageRepository.findUserForAuth(userId); if (!user) { const error: CustomError = new Error("사용자를 찾을 수 없습니다."); @@ -55,33 +43,17 @@ const MypageService = { const salt = await bcrypt.genSalt(10); const hashedNewPassword = await bcrypt.hash(newPassword, salt); - return await prisma.user.update({ - where: { id: userId }, - data: { password: hashedNewPassword }, - }); + return await MypageRepository.updatePassword(userId, hashedNewPassword); }, async getProducts(userId: number) { - const products = await prisma.product.findMany({ - where: { userId }, - orderBy: { - createdAt: "desc", - }, - }); + const products = await MypageRepository.findProductsByUserId(userId); return products; }, async getLikeProducts(userId: number) { - const likedProducts = await prisma.like.findMany({ - where: { userId, productId: { not: null } }, - include: { - product: true, - }, - orderBy: { - createdAt: "desc", - }, - }); + const likedProducts = await MypageRepository.findLikedProductsByUserId(userId); return likedProducts.map((like) => like.product); }, diff --git a/src/api/services/ProductService.ts b/src/api/services/ProductService.ts index 2445c4184..18e37ce4e 100644 --- a/src/api/services/ProductService.ts +++ b/src/api/services/ProductService.ts @@ -1,33 +1,29 @@ -import prisma from "../libs/prismaClient.js"; import type { CustomError } from "../types/error.js"; import type { CreateProductData, UpdateProductData, FindManyProductParams } from "../types/product.js"; -import { Prisma } from "@prisma/client"; +import * as ProductRepository from "../repositories/ProductRepository.js"; const ProductService = { async createProduct(productData: CreateProductData, userId: number) { - const newProduct = await prisma.product.create({ - data: { ...productData, userId }, + const newProduct = await ProductRepository.create({ + ...productData, + user: { connect: { id: userId } }, }); return newProduct; }, async findUniqueProduct(productId: number, userId: number) { - const product = await prisma.product.findUnique({ - where: { id: productId }, - }); + const product = await ProductRepository.findById(productId); if (!userId) { return { ...product, isLiked: false }; } - const like = await prisma.like.findFirst({ - where: { userId, productId }, - }); + const like = await ProductRepository.findLikeByUserAndProduct(userId, productId); return { ...product, isLiked: !!like }; }, async patchProduct(id: number, updateData: UpdateProductData, userId: number) { - const product = await prisma.product.findUnique({ where: { id } }); + const product = await ProductRepository.findById(id); if (!product) { const error: CustomError = new Error("존재하지 않는 상품입니다."); @@ -40,14 +36,11 @@ const ProductService = { error.statusCode = 403; throw error; } - return await prisma.product.update({ - where: { id }, - data: updateData, - }); + return await ProductRepository.update(id, updateData); }, async deleteProduct(id: number, userId: number) { - const product = await prisma.product.findUnique({ where: { id } }); + const product = await ProductRepository.findById(id); if (!product) { const error: CustomError = new Error("존재하지 않는 상품입니다."); @@ -60,41 +53,11 @@ const ProductService = { error.statusCode = 403; throw error; } - return await prisma.product.delete({ - where: { id }, - }); + return await ProductRepository.remove(id); }, - async findManyProduct({ offset, limit, order, keyword }: FindManyProductParams) { - let orderBy: Prisma.ProductOrderByWithRelationInput; - switch (order) { - case "oldest": - orderBy = { createdAt: "asc" }; - break; - case "recent": - default: - orderBy = { createdAt: "desc" }; - } - - let where = {}; - if (keyword) { - where = { - OR: [ - { name: { contains: keyword, mode: "insensitive" } }, - { description: { contains: keyword, mode: "insensitive" } }, - ], - }; - } - - const products = await prisma.product.findMany({ - select: { id: true, name: true, price: true, createdAt: true }, - skip: offset, - take: limit, - orderBy, - where, - }); - - return products; + async findManyProduct(params: FindManyProductParams) { + return await ProductRepository.findMany(params); }, }; From 0147e0dbb00e68628e79714ee1ad748128dd579e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Fri, 12 Sep 2025 09:23:43 +0900 Subject: [PATCH 34/54] =?UTF-8?q?refactor:=20article=20dto=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 types 폴더 아래에 dtos 폴더 추가 article dto 추가 validate.ts validators 폴더 밖으로 분리 (validators 폴더 제거 예정) - 관련 import 구문 수정 controller, service, router 수정 --- src/api/controllers/ArticleController.ts | 9 +++------ src/api/middlewares/{validators => }/validate.ts | 0 src/api/routes/ArticleRouter.ts | 4 ++-- src/api/routes/AuthRouter.ts | 7 ++----- src/api/routes/ProductRouter.ts | 2 +- src/api/services/ArticleService.ts | 5 +++-- .../validateArticle.ts => types/dtos/article.dto.ts} | 2 ++ 7 files changed, 13 insertions(+), 16 deletions(-) rename src/api/middlewares/{validators => }/validate.ts (100%) rename src/api/{middlewares/validators/validateArticle.ts => types/dtos/article.dto.ts} (78%) diff --git a/src/api/controllers/ArticleController.ts b/src/api/controllers/ArticleController.ts index acd4b823d..3a0a040a2 100644 --- a/src/api/controllers/ArticleController.ts +++ b/src/api/controllers/ArticleController.ts @@ -3,16 +3,13 @@ import jwt from "jsonwebtoken"; import type { Request, Response, NextFunction } from "express"; import env from "../config/env.js"; import type { CustomError } from "src/api/types/error.js"; +import type { ArticleDto } from "../types/dtos/article.dto.js"; const ArticleController = { async createArticle(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; - const { title, content } = req.body; - if (!title || !content) { - return res.status(400).send("제목과 게시글을 입력해주세요."); - } - const articleData = { title, content }; + const articleData: ArticleDto = req.body; const newArticle = await ArticleService.createArticle(articleData, userId); res.status(201).json(newArticle); @@ -53,7 +50,7 @@ const ArticleController = { try { const { id: userId } = req.user; const { id } = req.params; - const updateData = req.body; + const updateData: ArticleDto = req.body; const article = await ArticleService.updateArticle(Number(id), updateData, userId); res.status(201).json(article); } catch (err) { diff --git a/src/api/middlewares/validators/validate.ts b/src/api/middlewares/validate.ts similarity index 100% rename from src/api/middlewares/validators/validate.ts rename to src/api/middlewares/validate.ts diff --git a/src/api/routes/ArticleRouter.ts b/src/api/routes/ArticleRouter.ts index c93f627d2..6636cfdc4 100644 --- a/src/api/routes/ArticleRouter.ts +++ b/src/api/routes/ArticleRouter.ts @@ -1,8 +1,8 @@ import express from "express"; import ArticleController from "../controllers/ArticleController.js"; import authenticate from "../middlewares/authenticate.js"; -import validate from "../middlewares/validators/validate.js"; -import { ArticleSchema } from "../middlewares/validators/validateArticle.js"; +import validate from "../middlewares/validate.js"; +import { ArticleSchema } from "../types/dtos/article.dto.js"; const router = express.Router(); diff --git a/src/api/routes/AuthRouter.ts b/src/api/routes/AuthRouter.ts index 96a971906..3bbb9b5c0 100644 --- a/src/api/routes/AuthRouter.ts +++ b/src/api/routes/AuthRouter.ts @@ -1,10 +1,7 @@ import express from "express"; import AuthController from "../controllers/AuthController.js"; -import validate from "../middlewares/validators/validate.js"; -import { - signupSchema, - loginSchema, -} from "../middlewares/validators/validateUser.js"; +import validate from "../middlewares/validate.js"; +import { signupSchema, loginSchema } from "../middlewares/validators/validateUser.js"; const router = express.Router(); diff --git a/src/api/routes/ProductRouter.ts b/src/api/routes/ProductRouter.ts index 5f794b514..8fbfa930b 100644 --- a/src/api/routes/ProductRouter.ts +++ b/src/api/routes/ProductRouter.ts @@ -1,7 +1,7 @@ import express from "express"; import ProductController from "../controllers/ProductController.js"; import authenticate from "../middlewares/authenticate.js"; -import validate from "../middlewares/validators/validate.js"; +import validate from "../middlewares/validate.js"; import { ProductSchema } from "../middlewares/validators/validateProduct.js"; const router = express.Router(); diff --git a/src/api/services/ArticleService.ts b/src/api/services/ArticleService.ts index 7e100c763..ee82d6248 100644 --- a/src/api/services/ArticleService.ts +++ b/src/api/services/ArticleService.ts @@ -1,9 +1,10 @@ import type { CreateArticleData, UpdateArticleData, FindManyArticleParams } from "../types/article.js"; import type { CustomError } from "../types/error.js"; import * as ArticleRepository from "../repositories/ArticleRepository.js"; +import type { ArticleDto } from "../types/dtos/article.dto.js"; const ArticleService = { - async createArticle(articleData: CreateArticleData, userId: number) { + async createArticle(articleData: ArticleDto, userId: number) { const newArticle = await ArticleRepository.create({ ...articleData, user: { connect: { id: userId } }, @@ -28,7 +29,7 @@ const ArticleService = { return { ...article, isLiked: !!like }; }, - async updateArticle(id: number, updateData: UpdateArticleData, userId: number) { + async updateArticle(id: number, updateData: ArticleDto, userId: number) { const article = await ArticleRepository.findById(id); if (!article) { diff --git a/src/api/middlewares/validators/validateArticle.ts b/src/api/types/dtos/article.dto.ts similarity index 78% rename from src/api/middlewares/validators/validateArticle.ts rename to src/api/types/dtos/article.dto.ts index ba15bd759..d003fc83a 100644 --- a/src/api/middlewares/validators/validateArticle.ts +++ b/src/api/types/dtos/article.dto.ts @@ -4,3 +4,5 @@ export const ArticleSchema = z.object({ title: z.string().min(1, { message: "제목을 입력하세요" }), content: z.string().min(1, { message: "내용을 입력하세요." }), }); + +export type ArticleDto = z.infer; From b144b00ea4fa4d480e54025e7cd93e3559ac75a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Fri, 12 Sep 2025 09:36:36 +0900 Subject: [PATCH 35/54] =?UTF-8?q?refactor:=20product=20dto=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/api/controllers/ProductController.ts | 6 +++--- src/api/middlewares/validators/validateProduct.ts | 6 ------ src/api/routes/ProductRouter.ts | 3 ++- src/api/services/ProductService.ts | 5 +++-- src/api/types/dtos/product.dto.ts | 9 +++++++++ 5 files changed, 17 insertions(+), 12 deletions(-) delete mode 100644 src/api/middlewares/validators/validateProduct.ts create mode 100644 src/api/types/dtos/product.dto.ts diff --git a/src/api/controllers/ProductController.ts b/src/api/controllers/ProductController.ts index ee23b1339..a39ff4b1a 100644 --- a/src/api/controllers/ProductController.ts +++ b/src/api/controllers/ProductController.ts @@ -3,13 +3,13 @@ import jwt from "jsonwebtoken"; import type { Request, Response, NextFunction } from "express"; import env from "../config/env.js"; import type { CustomError } from "src/api/types/error.js"; +import type { ProductDto } from "../types/dtos/product.dto.js"; const ProductController = { async createProduct(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; - const { name, description, price, tags } = req.body; - const productData = { name, description, price, tags }; + const productData: ProductDto = req.body; const newProduct = await ProductService.createProduct(productData, userId); res.status(201).json(newProduct); @@ -52,7 +52,7 @@ const ProductController = { try { const { id } = req.params; const { id: userId } = req.user; - const updateData = req.body; + const updateData: ProductDto = req.body; const product = await ProductService.patchProduct(Number(id), updateData, userId); if (!product) { diff --git a/src/api/middlewares/validators/validateProduct.ts b/src/api/middlewares/validators/validateProduct.ts deleted file mode 100644 index fb2b25c7f..000000000 --- a/src/api/middlewares/validators/validateProduct.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { z } from "zod"; - -export const ProductSchema = z.object({ - name: z.string().min(1, { message: "상품 이름을 입력하세요" }), - price: z.number().gte(0), -}); diff --git a/src/api/routes/ProductRouter.ts b/src/api/routes/ProductRouter.ts index 8fbfa930b..1fee957d8 100644 --- a/src/api/routes/ProductRouter.ts +++ b/src/api/routes/ProductRouter.ts @@ -2,7 +2,8 @@ import express from "express"; import ProductController from "../controllers/ProductController.js"; import authenticate from "../middlewares/authenticate.js"; import validate from "../middlewares/validate.js"; -import { ProductSchema } from "../middlewares/validators/validateProduct.js"; +import { ProductSchema } from "../types/dtos/product.dto.js"; + const router = express.Router(); router.post("/", authenticate, validate(ProductSchema), ProductController.createProduct); diff --git a/src/api/services/ProductService.ts b/src/api/services/ProductService.ts index 18e37ce4e..e514bcf34 100644 --- a/src/api/services/ProductService.ts +++ b/src/api/services/ProductService.ts @@ -1,9 +1,10 @@ import type { CustomError } from "../types/error.js"; import type { CreateProductData, UpdateProductData, FindManyProductParams } from "../types/product.js"; import * as ProductRepository from "../repositories/ProductRepository.js"; +import type { ProductDto } from "../types/dtos/product.dto.js"; const ProductService = { - async createProduct(productData: CreateProductData, userId: number) { + async createProduct(productData: ProductDto, userId: number) { const newProduct = await ProductRepository.create({ ...productData, user: { connect: { id: userId } }, @@ -22,7 +23,7 @@ const ProductService = { return { ...product, isLiked: !!like }; }, - async patchProduct(id: number, updateData: UpdateProductData, userId: number) { + async patchProduct(id: number, updateData: ProductDto, userId: number) { const product = await ProductRepository.findById(id); if (!product) { diff --git a/src/api/types/dtos/product.dto.ts b/src/api/types/dtos/product.dto.ts new file mode 100644 index 000000000..baf226f17 --- /dev/null +++ b/src/api/types/dtos/product.dto.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const ProductSchema = z.object({ + name: z.string().min(1, "상품 이름을 입력하세요"), + price: z.number().positive("상품 가격은 0보다 커야 합니다."), + tags: z.array(z.string(), "태그는 문자열 배열이어야 합니다"), +}); + +export type ProductDto = z.infer; From 4ff4e672c81ae7b1c0402390a9da49418189ceec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Fri, 12 Sep 2025 10:18:35 +0900 Subject: [PATCH 36/54] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20dto=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/controllers/AuthController.ts | 7 +++++-- src/api/routes/AuthRouter.ts | 3 +-- src/api/services/AuthService.ts | 16 +++++++++------- .../validateUser.ts => types/dtos/user.dto.ts} | 12 ++++++------ 4 files changed, 21 insertions(+), 17 deletions(-) rename src/api/{middlewares/validators/validateUser.ts => types/dtos/user.dto.ts} (61%) diff --git a/src/api/controllers/AuthController.ts b/src/api/controllers/AuthController.ts index 04909d544..6d1933ee6 100644 --- a/src/api/controllers/AuthController.ts +++ b/src/api/controllers/AuthController.ts @@ -1,10 +1,12 @@ import AuthService from "../services/AuthService.js"; import type { Request, Response, NextFunction } from "express"; +import type { SignupDto, LoginDto } from "../types/dtos/user.dto.js"; const AuthController = { async signup(req: Request, res: Response, next: NextFunction) { try { - const newUser = await AuthService.signup(req.body); + const signupData: SignupDto = req.body; + const newUser = await AuthService.signup(signupData); res.status(201).json(newUser); } catch (err) { next(err); @@ -13,7 +15,8 @@ const AuthController = { async login(req: Request, res: Response, next: NextFunction) { try { - const { userWithoutPassword: user, accessToken, refreshToken } = await AuthService.login(req.body); + const loginData: LoginDto = req.body; + const { userWithoutPassword: user, accessToken, refreshToken } = await AuthService.login(loginData); res.cookie("refreshToken", refreshToken, { httpOnly: true, diff --git a/src/api/routes/AuthRouter.ts b/src/api/routes/AuthRouter.ts index 3bbb9b5c0..e14a71b1c 100644 --- a/src/api/routes/AuthRouter.ts +++ b/src/api/routes/AuthRouter.ts @@ -1,8 +1,7 @@ import express from "express"; import AuthController from "../controllers/AuthController.js"; import validate from "../middlewares/validate.js"; -import { signupSchema, loginSchema } from "../middlewares/validators/validateUser.js"; - +import { loginSchema, signupSchema } from "../types/dtos/user.dto.js"; const router = express.Router(); router.post("/signup", validate(signupSchema), AuthController.signup); diff --git a/src/api/services/AuthService.ts b/src/api/services/AuthService.ts index 9df4a2c48..c7ed128ce 100644 --- a/src/api/services/AuthService.ts +++ b/src/api/services/AuthService.ts @@ -3,12 +3,11 @@ import env from "../config/env.js"; import { hashing, compareWords } from "../libs/hashing.js"; import { generateTokens } from "../libs/token.js"; import type { CustomError } from "src/api/types/error.js"; -import type { SignupData } from "src/api/types/signup.js"; -import type { loginData } from "src/api/types/login.js"; import * as AuthRepository from "../repositories/AuthRepository.js"; +import type { SignupDto, LoginDto } from "../types/dtos/user.dto.js"; const AuthService = { - async signup(signupData: SignupData) { + async signup(signupData: SignupDto) { // 이메일로 이미 존재하는 사용자인지 확인 const existingUser = await AuthRepository.findByEmail(signupData.email); @@ -19,17 +18,20 @@ const AuthService = { } // 사용자 생성 전 비밀번호 해싱 - const { email, nickname, password } = signupData; - const hashedPassword = await hashing(password); + const hashedPassword = await hashing(signupData.password); // 사용자 생성 - const newUser = await AuthRepository.create(signupData); + const newUser = await AuthRepository.create({ + email: signupData.email, + password: hashedPassword, + nickname: signupData.nickname, + }); const { password: _, ...userWithoutPassword } = newUser; return userWithoutPassword; }, - async login(loginData: loginData) { + async login(loginData: LoginDto) { // 이메일로 존재하는 사용자인지 확인 const user = await AuthRepository.findByEmail(loginData.email); diff --git a/src/api/middlewares/validators/validateUser.ts b/src/api/types/dtos/user.dto.ts similarity index 61% rename from src/api/middlewares/validators/validateUser.ts rename to src/api/types/dtos/user.dto.ts index 9681ee35e..01c518c2e 100644 --- a/src/api/middlewares/validators/validateUser.ts +++ b/src/api/types/dtos/user.dto.ts @@ -4,15 +4,15 @@ import { z } from "zod"; // 이후에 시간 남으면 다른 유효성 검사도 zod로 바꾸기 export const signupSchema = z.object({ email: z.email({ message: "유효한 이메일 형식이 아닙니다." }), - password: z - .string() - .min(8, { message: "비밀번호는 최소 8자 이상이어야합니다." }), - nickname: z - .string() - .min(2, { message: "닉네임은 최소 2자 이상이어야 합니다." }), + password: z.string().min(8, { message: "비밀번호는 최소 8자 이상이어야합니다." }), + nickname: z.string().min(2, { message: "닉네임은 최소 2자 이상이어야 합니다." }), }); +export type SignupDto = z.infer; + export const loginSchema = z.object({ email: z.email({ message: "유효한 이메일 형식이 아닙니다." }), password: z.string().min(1, { message: "비밀번호를 입력해주세요." }), }); + +export type LoginDto = z.infer; From 04166e422c6181508edf9df4f821aad7fa5baed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Fri, 12 Sep 2025 10:33:40 +0900 Subject: [PATCH 37/54] =?UTF-8?q?refactor:=20comment=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20dto=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/controllers/CommentController.ts | 17 ++++------------ src/api/routes/CommentRouter.ts | 6 ++++-- src/api/services/CommentService.ts | 8 +++++--- src/api/types/dtos/comment.dto.ts | 25 ++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 18 deletions(-) create mode 100644 src/api/types/dtos/comment.dto.ts diff --git a/src/api/controllers/CommentController.ts b/src/api/controllers/CommentController.ts index 6e89d02a9..297ded636 100644 --- a/src/api/controllers/CommentController.ts +++ b/src/api/controllers/CommentController.ts @@ -1,24 +1,15 @@ import CommentService from "../services/CommentService.js"; import type { Request, Response, NextFunction } from "express"; +import type { CommentDto } from "../types/dtos/comment.dto.js"; const CommentController = { async createComment(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; - const { content, productId, articleId } = req.body; - - if (!content || (!productId && !articleId)) { - return res.status(400).json({ error: "댓글과 게시글 ID는 필수값" }); - } - - if (productId && articleId) { - return res.status(400).json({ error: "productId 혹은 articleId 둘 중 하나만 있어야 함" }); - } + const commentData: CommentDto = req.body; const newComment = await CommentService.createComment({ - content, - productId, - articleId, + ...commentData, userId, }); res.status(201).json(newComment); @@ -35,7 +26,7 @@ const CommentController = { try { const { id: userId } = req.user; const { id } = req.params; - const updateData = req.body; + const updateData: CommentDto = req.body; const comment = await CommentService.updateComment(Number(id), updateData, userId); res.status(201).json(comment); diff --git a/src/api/routes/CommentRouter.ts b/src/api/routes/CommentRouter.ts index 44ebb4572..c74e27528 100644 --- a/src/api/routes/CommentRouter.ts +++ b/src/api/routes/CommentRouter.ts @@ -1,11 +1,13 @@ import express from "express"; import CommentController from "../controllers/CommentController.js"; import authenticate from "../middlewares/authenticate.js"; +import validate from "../middlewares/validate.js"; +import { commentSchema } from "../types/dtos/comment.dto.js"; const router = express.Router(); -router.post("/", authenticate, CommentController.createComment); -router.patch("/:id", authenticate, CommentController.updateComment); +router.post("/", authenticate, validate(commentSchema), CommentController.createComment); +router.patch("/:id", authenticate, validate(commentSchema), CommentController.updateComment); router.delete("/:id", authenticate, CommentController.deleteComment); router.get("/", CommentController.findManyComment); diff --git a/src/api/services/CommentService.ts b/src/api/services/CommentService.ts index c1df61feb..31d8d1674 100644 --- a/src/api/services/CommentService.ts +++ b/src/api/services/CommentService.ts @@ -1,9 +1,11 @@ import type { CustomError } from "../types/error.js"; -import type { CreateCommentData, UpdateCommentData, FindManyCommentParams } from "../types/comment.js"; +import type { FindManyCommentParams } from "../types/comment.js"; import * as CommentRepository from "../repositories/CommentRepository.js"; +import type { CommentDto } from "../types/dtos/comment.dto.js"; const CommentService = { - async createComment({ content, productId, articleId, userId }: CreateCommentData) { + async createComment(data: CommentDto & { userId: number }) { + const { content, productId, articleId, userId } = data; const newComment = await CommentRepository.create({ content, ...(productId && { product: { connect: { id: productId } } }), @@ -13,7 +15,7 @@ const CommentService = { return newComment; }, - async updateComment(id: number, updateData: UpdateCommentData, userId: number) { + async updateComment(id: number, updateData: CommentDto, userId: number) { const comment = await CommentRepository.findById(id); if (!comment) { diff --git a/src/api/types/dtos/comment.dto.ts b/src/api/types/dtos/comment.dto.ts new file mode 100644 index 000000000..1eae6b375 --- /dev/null +++ b/src/api/types/dtos/comment.dto.ts @@ -0,0 +1,25 @@ +import { z } from "zod"; + +export const commentSchema = z + .object({ + content: z + .string() + .min(1, { message: "댓글 내용은 최소 1자 이상이어야 합니다." }) + .max(500, { message: "댓글 내용은 최대 500자 이하여야 합니다." }), + productId: z.number().optional(), + articleId: z.number().optional(), + }) + .refine( + (data) => { + return ( + (data.productId !== undefined && data.articleId === undefined) || + (data.productId == undefined && data.articleId != undefined) + ); + }, + { + message: "productId와 articleId 중 하나만 제공되어야 합니다.", + path: ["productId", "articleId"], + } + ); + +export type CommentDto = z.infer; From 142c1e3a443577af0d24fab4c0d2310f3c267824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Fri, 12 Sep 2025 10:43:56 +0900 Subject: [PATCH 38/54] refactor: user.dto -> auth.dto --- src/api/controllers/AuthController.ts | 2 +- src/api/routes/AuthRouter.ts | 2 +- src/api/services/AuthService.ts | 2 +- src/api/types/dtos/{user.dto.ts => auth.dto.ts} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename src/api/types/dtos/{user.dto.ts => auth.dto.ts} (100%) diff --git a/src/api/controllers/AuthController.ts b/src/api/controllers/AuthController.ts index 6d1933ee6..0d34b959d 100644 --- a/src/api/controllers/AuthController.ts +++ b/src/api/controllers/AuthController.ts @@ -1,6 +1,6 @@ import AuthService from "../services/AuthService.js"; import type { Request, Response, NextFunction } from "express"; -import type { SignupDto, LoginDto } from "../types/dtos/user.dto.js"; +import type { SignupDto, LoginDto } from "../types/dtos/auth.dto.js"; const AuthController = { async signup(req: Request, res: Response, next: NextFunction) { diff --git a/src/api/routes/AuthRouter.ts b/src/api/routes/AuthRouter.ts index e14a71b1c..4be1f12e3 100644 --- a/src/api/routes/AuthRouter.ts +++ b/src/api/routes/AuthRouter.ts @@ -1,7 +1,7 @@ import express from "express"; import AuthController from "../controllers/AuthController.js"; import validate from "../middlewares/validate.js"; -import { loginSchema, signupSchema } from "../types/dtos/user.dto.js"; +import { loginSchema, signupSchema } from "../types/dtos/auth.dto.js"; const router = express.Router(); router.post("/signup", validate(signupSchema), AuthController.signup); diff --git a/src/api/services/AuthService.ts b/src/api/services/AuthService.ts index c7ed128ce..913bd7270 100644 --- a/src/api/services/AuthService.ts +++ b/src/api/services/AuthService.ts @@ -4,7 +4,7 @@ import { hashing, compareWords } from "../libs/hashing.js"; import { generateTokens } from "../libs/token.js"; import type { CustomError } from "src/api/types/error.js"; import * as AuthRepository from "../repositories/AuthRepository.js"; -import type { SignupDto, LoginDto } from "../types/dtos/user.dto.js"; +import type { SignupDto, LoginDto } from "../types/dtos/auth.dto.js"; const AuthService = { async signup(signupData: SignupDto) { diff --git a/src/api/types/dtos/user.dto.ts b/src/api/types/dtos/auth.dto.ts similarity index 100% rename from src/api/types/dtos/user.dto.ts rename to src/api/types/dtos/auth.dto.ts From 73684ff82ad6f9ec04870ac3b28ad25ec8eb32ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Fri, 12 Sep 2025 11:35:32 +0900 Subject: [PATCH 39/54] =?UTF-8?q?refactor:=20mypage=20=EC=9C=A0=ED=9A=A8?= =?UTF-8?q?=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=B0=8F=20dto=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/api/controllers/MypageController.ts | 7 ++++--- src/api/routes/MypageRouter.ts | 7 +++++-- src/api/services/MypageService.ts | 20 +++++++++++++++++--- src/api/types/dtos/mypage.dto.ts | 22 ++++++++++++++++++++++ 4 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 src/api/types/dtos/mypage.dto.ts diff --git a/src/api/controllers/MypageController.ts b/src/api/controllers/MypageController.ts index 4ae28b100..3aeab9ea8 100644 --- a/src/api/controllers/MypageController.ts +++ b/src/api/controllers/MypageController.ts @@ -1,5 +1,6 @@ import MypageService from "../services/MypageService.js"; import type { Request, Response, NextFunction } from "express"; +import type { UpdateUserDTO, UpdatePasswordDTO } from "../types/dtos/mypage.dto.js"; const MypageController = { async getUser(req: Request, res: Response, next: NextFunction) { @@ -16,7 +17,7 @@ const MypageController = { async updateUser(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; - const updateData = req.body; + const updateData: UpdateUserDTO = req.body; const updatedUser = await MypageService.updateUser(userId, updateData); res.status(201).json(updatedUser); } catch (err) { @@ -27,9 +28,9 @@ const MypageController = { async updatePassword(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; - const { oldPassword, newPassword } = req.body; + const updatePasswordData: UpdatePasswordDTO = req.body; - await MypageService.updatePassword(userId, oldPassword, newPassword); + await MypageService.updatePassword(userId, updatePasswordData); res.status(201).json("비밀번호 변경이 완료되었습니다."); } catch (err) { next(err); diff --git a/src/api/routes/MypageRouter.ts b/src/api/routes/MypageRouter.ts index fcb71592d..cb5a25364 100644 --- a/src/api/routes/MypageRouter.ts +++ b/src/api/routes/MypageRouter.ts @@ -1,11 +1,14 @@ import express from "express"; import authenticate from "../middlewares/authenticate.js"; import MypageController from "../controllers/MypageController.js"; +import validate from "../middlewares/validate.js"; +import { updateUserSchema, updatePasswordSchema } from "../types/dtos/mypage.dto.js"; + const router = express.Router(); router.get("/", authenticate, MypageController.getUser); -router.patch("/", authenticate, MypageController.updateUser); -router.patch("/password", authenticate, MypageController.updatePassword); +router.patch("/", authenticate, validate(updateUserSchema), MypageController.updateUser); +router.patch("/password", authenticate, validate(updatePasswordSchema), MypageController.updatePassword); router.get("/products", authenticate, MypageController.getProducts); router.get("/like-products", authenticate, MypageController.getLikeProducts); diff --git a/src/api/services/MypageService.ts b/src/api/services/MypageService.ts index 228fc9496..7e126aca6 100644 --- a/src/api/services/MypageService.ts +++ b/src/api/services/MypageService.ts @@ -2,6 +2,8 @@ import bcrypt from "bcrypt"; import type { UserData } from "../types/user.js"; import type { CustomError } from "../types/error.js"; import * as MypageRepository from "../repositories/MypageRepository.js"; +import type { UpdateUserDTO, UpdatePasswordDTO } from "../types/dtos/mypage.dto.js"; +import type { Prisma } from "@prisma/client"; const MypageService = { async getUser(userId: number) { @@ -10,19 +12,31 @@ const MypageService = { if (!user) { const error: CustomError = new Error("사용자를 찾을 수 없습니다."); error.statusCode = 404; + throw error; } return user; }, - async updateUser(userId: number, updateData: UserData) { - const updatedUser = await MypageRepository.update(userId, updateData); + async updateUser(userId: number, updateData: UpdateUserDTO) { + const dataToUpdate: Prisma.UserUpdateInput = {}; + + if (updateData.nickname !== undefined) { + dataToUpdate.nickname = updateData.nickname; + } + + if (updateData.image !== undefined) { + dataToUpdate.nickname = updateData.image; + } + + const updatedUser = await MypageRepository.update(userId, dataToUpdate); const { password, refreshToken, ...UserData } = updatedUser; return UserData; }, - async updatePassword(userId: number, oldPassword: string, newPassword: string) { + async updatePassword(userId: number, updatePasswordDTO: UpdatePasswordDTO) { + const { oldPassword, newPassword } = updatePasswordDTO; const user = await MypageRepository.findUserForAuth(userId); if (!user) { diff --git a/src/api/types/dtos/mypage.dto.ts b/src/api/types/dtos/mypage.dto.ts new file mode 100644 index 000000000..2a50ac7cf --- /dev/null +++ b/src/api/types/dtos/mypage.dto.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +export const updateUserSchema = z + .object({ + nickname: z.string().min(2, { message: "닉네임은 최소 2자 이상이어야 합니다." }).optional(), + image: z.url({ message: "유효한 URL 형식이 아닙니다." }).optional(), + }) + .refine( + (data) => { + return Object.keys(data).length > 0; + }, + { message: "수정할 내용을 하나 이상 입력해주세요." } + ); + +export type UpdateUserDTO = z.infer; + +export const updatePasswordSchema = z.object({ + oldPassword: z.string().min(1, { message: "기존 비밀번호를 입력해주세요." }), + newPassword: z.string().min(8, { message: "새로운 비밀번호는 최소 8자 이상이어야합니다." }), +}); + +export type UpdatePasswordDTO = z.infer; From fc3b8d640f818a6076bf2e1d1404010aa4841668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Tue, 16 Sep 2025 09:27:59 +0900 Subject: [PATCH 40/54] =?UTF-8?q?refactor:=20Token=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=83=81=EC=88=98=20=EC=A4=91=EB=B3=B5=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit config/env와 libs/constant로 중복 관리되던 환경변수 상수들 libs/constant로 통일 --- src/api/config/env.ts | 26 ------------------------ src/api/controllers/ArticleController.ts | 4 ++-- src/api/controllers/ProductController.ts | 5 ++--- src/api/libs/constants.ts | 18 ++++++++++------ src/api/middlewares/authenticate.ts | 4 ++-- src/api/services/AuthService.ts | 4 ++-- 6 files changed, 20 insertions(+), 41 deletions(-) delete mode 100644 src/api/config/env.ts diff --git a/src/api/config/env.ts b/src/api/config/env.ts deleted file mode 100644 index b1abaff99..000000000 --- a/src/api/config/env.ts +++ /dev/null @@ -1,26 +0,0 @@ -interface EnvConfig { - REFRESH_TOKEN_SECRET: string; - JWT_SECRET: string; - ACCESS_TOKEN_SECRET: string; -} - -const getEnvConfig = (): EnvConfig => { - const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET; - const JWT_SECRET = process.env.JWT_SECRET; - const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET; - - if (!ACCESS_TOKEN_SECRET) { - throw new Error("환경 변수 ACCESS_TOKEN_SECRET이 설정되지 않았습니다. 설정해주세요."); - } - - if (!REFRESH_TOKEN_SECRET) { - throw new Error("환경 변수 REFRESH_TOKEN_SECRET이 설정되지 않았습니다. 설정해주세요."); - } - - if (!JWT_SECRET) { - throw new Error("환경 변수 JWT_SECRET이 설정되지 않았습니다. 설정해주세요."); - } - return { REFRESH_TOKEN_SECRET, JWT_SECRET, ACCESS_TOKEN_SECRET }; -}; - -export default getEnvConfig(); diff --git a/src/api/controllers/ArticleController.ts b/src/api/controllers/ArticleController.ts index 3a0a040a2..0b0be160f 100644 --- a/src/api/controllers/ArticleController.ts +++ b/src/api/controllers/ArticleController.ts @@ -1,7 +1,7 @@ import ArticleService from "../services/ArticleService.js"; import jwt from "jsonwebtoken"; import type { Request, Response, NextFunction } from "express"; -import env from "../config/env.js"; +import { ACCESS_TOKEN_SECRET } from "../libs/constants.js"; import type { CustomError } from "src/api/types/error.js"; import type { ArticleDto } from "../types/dtos/article.dto.js"; @@ -28,7 +28,7 @@ const ArticleController = { if (token) { try { - const decoded = jwt.verify(token, env.JWT_SECRET); + const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET); if (typeof decoded === "string" || !decoded.id) { const error: CustomError = new Error("유효하지 않은 Token입니다."); error.statusCode = 403; diff --git a/src/api/controllers/ProductController.ts b/src/api/controllers/ProductController.ts index a39ff4b1a..51154132b 100644 --- a/src/api/controllers/ProductController.ts +++ b/src/api/controllers/ProductController.ts @@ -1,7 +1,7 @@ import ProductService from "../services/ProductService.js"; import jwt from "jsonwebtoken"; +import { ACCESS_TOKEN_SECRET } from "../libs/constants.js"; import type { Request, Response, NextFunction } from "express"; -import env from "../config/env.js"; import type { CustomError } from "src/api/types/error.js"; import type { ProductDto } from "../types/dtos/product.dto.js"; @@ -20,7 +20,6 @@ const ProductController = { async findUniqueProduct(req: Request, res: Response, next: NextFunction) { try { - //throw new Error("🔥에러 핸들러 테스트"); const { id } = req.params; const productId = Number(id); @@ -29,7 +28,7 @@ const ProductController = { if (token) { try { - const decoded = jwt.verify(token, env.JWT_SECRET); + const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET); if (typeof decoded === "string" || !decoded.id) { const error: CustomError = new Error("유효하지 않은 Token입니다."); error.statusCode = 403; diff --git a/src/api/libs/constants.ts b/src/api/libs/constants.ts index 763e80442..9ccd8529b 100644 --- a/src/api/libs/constants.ts +++ b/src/api/libs/constants.ts @@ -2,11 +2,17 @@ import dotenv from "dotenv"; dotenv.config(); -const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET; -const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET; +function getEnvVar(name: string): string { + const value = process.env[name]; + if (value === undefined) { + throw new Error(`환경 변수 ${name}이(가) 설정되지 않았습니다.`); + } + return value; +} -const CLOUDINARY_CLOUD_NAME = process.env.CLOUDINARY_CLOUD_NAME; -const CLOUDINARY_API_KEY = process.env.CLOUDINARY_API_KEY; -const CLOUDINARY_API_SECRET = process.env.CLOUDINARY_API_SECRET; +export const ACCESS_TOKEN_SECRET = getEnvVar("ACCESS_TOKEN_SECRET"); +export const REFRESH_TOKEN_SECRET = getEnvVar("REFRESH_TOKEN_SECRET"); -export { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET, CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET }; +export const CLOUDINARY_CLOUD_NAME = getEnvVar("CLOUDINARY_CLOUD_NAME"); +export const CLOUDINARY_API_KEY = getEnvVar("CLOUDINARY_API_KEY"); +export const CLOUDINARY_API_SECRET = getEnvVar("CLOUDINARY_API_SECRET"); diff --git a/src/api/middlewares/authenticate.ts b/src/api/middlewares/authenticate.ts index b7bcb1335..fd89ef6f7 100644 --- a/src/api/middlewares/authenticate.ts +++ b/src/api/middlewares/authenticate.ts @@ -2,7 +2,7 @@ import jwt from "jsonwebtoken"; import prisma from "../libs/prismaClient.js"; import type { Request, Response, NextFunction } from "express"; import type { CustomError } from "src/api/types/error.js"; -import env from "../config/env.js"; +import { ACCESS_TOKEN_SECRET } from "../libs/constants.js"; export default async function authenticate(req: Request, res: Response, next: NextFunction) { const { accessToken } = req.cookies; @@ -14,7 +14,7 @@ export default async function authenticate(req: Request, res: Response, next: Ne } try { - const decoded = jwt.verify(accessToken, env.ACCESS_TOKEN_SECRET); + const decoded = jwt.verify(accessToken, ACCESS_TOKEN_SECRET); if (typeof decoded === "string" || !decoded.id) { const error: CustomError = new Error("유효하지 않은 Access Token입니다."); error.statusCode = 403; diff --git a/src/api/services/AuthService.ts b/src/api/services/AuthService.ts index 913bd7270..35c4911df 100644 --- a/src/api/services/AuthService.ts +++ b/src/api/services/AuthService.ts @@ -1,7 +1,7 @@ import jwt from "jsonwebtoken"; -import env from "../config/env.js"; import { hashing, compareWords } from "../libs/hashing.js"; import { generateTokens } from "../libs/token.js"; +import { REFRESH_TOKEN_SECRET } from "../libs/constants.js"; import type { CustomError } from "src/api/types/error.js"; import * as AuthRepository from "../repositories/AuthRepository.js"; import type { SignupDto, LoginDto } from "../types/dtos/auth.dto.js"; @@ -72,7 +72,7 @@ const AuthService = { try { // 토큰 디코딩해서 토큰의 User 확인 및 변조 여부 확인 - const decoded = jwt.verify(oldRefreshToken, env.REFRESH_TOKEN_SECRET); + const decoded = jwt.verify(oldRefreshToken, REFRESH_TOKEN_SECRET); if (typeof decoded === "string" || !decoded.id) { const error: CustomError = new Error("유효하지 않은 Refresh Token입니다."); error.statusCode = 403; From e8bc5a00ce17bccd3403557eecde43ea05353b6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Tue, 16 Sep 2025 09:48:38 +0900 Subject: [PATCH 41/54] =?UTF-8?q?fix:=20=EC=83=81=ED=83=9C=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 생성: 201 조회/수정: 200 삭제: 204 --- src/api/controllers/ArticleController.ts | 4 ++-- src/api/controllers/CommentController.ts | 4 ++-- src/api/controllers/MypageController.ts | 4 ++-- src/api/controllers/ProductController.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/api/controllers/ArticleController.ts b/src/api/controllers/ArticleController.ts index 0b0be160f..18f5879de 100644 --- a/src/api/controllers/ArticleController.ts +++ b/src/api/controllers/ArticleController.ts @@ -52,7 +52,7 @@ const ArticleController = { const { id } = req.params; const updateData: ArticleDto = req.body; const article = await ArticleService.updateArticle(Number(id), updateData, userId); - res.status(201).json(article); + res.status(200).json(article); } catch (err) { next(err); } @@ -63,7 +63,7 @@ const ArticleController = { const { id: userId } = req.user; const { id } = req.params; await ArticleService.deleteArticle(Number(id), userId); - res.status(201).json({ success: "상품 삭제 성공" }); + res.status(204).json({ success: "상품 삭제 성공" }); } catch (err) { next(err); } diff --git a/src/api/controllers/CommentController.ts b/src/api/controllers/CommentController.ts index 297ded636..dc5cc26cb 100644 --- a/src/api/controllers/CommentController.ts +++ b/src/api/controllers/CommentController.ts @@ -29,7 +29,7 @@ const CommentController = { const updateData: CommentDto = req.body; const comment = await CommentService.updateComment(Number(id), updateData, userId); - res.status(201).json(comment); + res.status(200).json(comment); } catch (err) { if (err.code === "P2025") { res.status(404).json({ error: "해당 댓글이 존재하지 않음" }); @@ -43,7 +43,7 @@ const CommentController = { const { id: userId } = req.user; const { id } = req.params; await CommentService.deleteComment(Number(id), userId); - res.status(201).json({ success: "댓글 삭제" }); + res.status(204).json({ success: "댓글 삭제" }); } catch (err) { if (err.code === "P2025") { res.status(404).json({ error: "해당 댓글이 존재하지 않음" }); diff --git a/src/api/controllers/MypageController.ts b/src/api/controllers/MypageController.ts index 3aeab9ea8..d3f449f21 100644 --- a/src/api/controllers/MypageController.ts +++ b/src/api/controllers/MypageController.ts @@ -19,7 +19,7 @@ const MypageController = { const { id: userId } = req.user; const updateData: UpdateUserDTO = req.body; const updatedUser = await MypageService.updateUser(userId, updateData); - res.status(201).json(updatedUser); + res.status(200).json(updatedUser); } catch (err) { next(err); } @@ -31,7 +31,7 @@ const MypageController = { const updatePasswordData: UpdatePasswordDTO = req.body; await MypageService.updatePassword(userId, updatePasswordData); - res.status(201).json("비밀번호 변경이 완료되었습니다."); + res.status(200).json("비밀번호 변경이 완료되었습니다."); } catch (err) { next(err); } diff --git a/src/api/controllers/ProductController.ts b/src/api/controllers/ProductController.ts index 51154132b..c50c27f2f 100644 --- a/src/api/controllers/ProductController.ts +++ b/src/api/controllers/ProductController.ts @@ -57,7 +57,7 @@ const ProductController = { if (!product) { return res.status(404).json({ error: "수정할 상품이 없음" }); } - res.status(201).json(product); + res.status(200).json(product); } catch (err) { next(err); } @@ -69,7 +69,7 @@ const ProductController = { const { id: userId } = req.user; await ProductService.deleteProduct(Number(id), userId); - res.status(201).json({ success: "상품 삭제 성공" }); + res.status(204).json({ success: "상품 삭제 성공" }); } catch (err) { next(err); } From 3f3288ef8687a42ada29e4064b6762f90f38af61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Tue, 16 Sep 2025 10:43:27 +0900 Subject: [PATCH 42/54] =?UTF-8?q?feat:=20seed=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EC=97=90=20user=20=EC=A0=95=EB=B3=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20createAt=20->=20createdAt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration.sql | 13 +++++++++++++ prisma/schema.prisma | 4 ++-- prisma/seed.js | 13 +++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20250916014222_user_created_at_updated_at/migration.sql diff --git a/prisma/migrations/20250916014222_user_created_at_updated_at/migration.sql b/prisma/migrations/20250916014222_user_created_at_updated_at/migration.sql new file mode 100644 index 000000000..790c3fae1 --- /dev/null +++ b/prisma/migrations/20250916014222_user_created_at_updated_at/migration.sql @@ -0,0 +1,13 @@ +/* + Warnings: + + - You are about to drop the column `createAt` on the `User` table. All the data in the column will be lost. + - You are about to drop the column `updateAt` on the `User` table. All the data in the column will be lost. + - Added the required column `updatedAt` to the `User` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "public"."User" DROP COLUMN "createAt", +DROP COLUMN "updateAt", +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 122deafdb..973d3a77e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,8 +20,8 @@ model User { image String? password String refreshToken String? - createAt DateTime @default(now()) - updateAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt products Product[] articles Article[] Comment Comment[] diff --git a/prisma/seed.js b/prisma/seed.js index e819605e6..74d613f87 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -2,6 +2,15 @@ import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); async function main() { + // User 생성 + const user = await prisma.user.create({ + data: { + email: "yonjin.oh@gmail.com", + nickname: "오연진", + password: "qwer1234!", + }, + }); + // Product 하나 생성 const product = await prisma.product.create({ data: { @@ -9,6 +18,7 @@ async function main() { description: "상품 설명", price: 19900, tags: ["전자제품", "할인"], + userId: user.id, }, }); @@ -17,6 +27,7 @@ async function main() { data: { title: "자유게시판 글", content: "어쩌구 저쩌구", + userId: user.id, }, }); @@ -26,10 +37,12 @@ async function main() { { content: "상품 댓글 어쩌구", productId: product.id, + userId: user.id, }, { content: "게시글 댓글 어쩌구", articleId: article.id, + userId: user.id, }, ], }); From 632d80c2e20478f85b1447318f92514294b9b745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Tue, 16 Sep 2025 11:41:45 +0900 Subject: [PATCH 43/54] =?UTF-8?q?feat:=20=EB=B9=84=EB=B0=80=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20accessToken,=20ref?= =?UTF-8?q?reshToken=20=EC=9E=AC=EB=B0=9C=EA=B8=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/controllers/MypageController.ts | 14 ++++++++++++-- src/api/services/MypageService.ts | 18 +++++++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/api/controllers/MypageController.ts b/src/api/controllers/MypageController.ts index d3f449f21..a336010c0 100644 --- a/src/api/controllers/MypageController.ts +++ b/src/api/controllers/MypageController.ts @@ -30,8 +30,18 @@ const MypageController = { const { id: userId } = req.user; const updatePasswordData: UpdatePasswordDTO = req.body; - await MypageService.updatePassword(userId, updatePasswordData); - res.status(200).json("비밀번호 변경이 완료되었습니다."); + const { accessToken, refreshToken } = await MypageService.updatePassword(userId, updatePasswordData); + res.cookie("accessToken", accessToken, { + httpOnly: true, + maxAge: 1 * 60 * 60 * 1000, + }); + + res.cookie("refreshToken", refreshToken, { + httpOnly: true, + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + + res.status(200).json("비밀번호 변경 및 토큰 재발급이 완료되었습니다."); } catch (err) { next(err); } diff --git a/src/api/services/MypageService.ts b/src/api/services/MypageService.ts index 7e126aca6..e2612e5e8 100644 --- a/src/api/services/MypageService.ts +++ b/src/api/services/MypageService.ts @@ -1,9 +1,11 @@ import bcrypt from "bcrypt"; -import type { UserData } from "../types/user.js"; import type { CustomError } from "../types/error.js"; import * as MypageRepository from "../repositories/MypageRepository.js"; import type { UpdateUserDTO, UpdatePasswordDTO } from "../types/dtos/mypage.dto.js"; import type { Prisma } from "@prisma/client"; +import { generateTokens } from "../libs/token.js"; +import { hashing } from "../libs/hashing.js"; +import * as AuthRepository from "../repositories/AuthRepository.js"; const MypageService = { async getUser(userId: number) { @@ -54,10 +56,16 @@ const MypageService = { } // 새로운 비밀번호 해시 처리 - const salt = await bcrypt.genSalt(10); - const hashedNewPassword = await bcrypt.hash(newPassword, salt); + const hashedNewPassword = await hashing(newPassword); + await MypageRepository.updatePassword(userId, hashedNewPassword); - return await MypageRepository.updatePassword(userId, hashedNewPassword); + // 새로운 토큰 발급 (Access, Refresh) + const { accessToken, refreshToken } = generateTokens(userId); + + const hashedRefreshToken = await hashing(refreshToken); + await AuthRepository.updateUserRefreshToken(userId, hashedRefreshToken); + + return { accessToken, refreshToken }; }, async getProducts(userId: number) { @@ -69,7 +77,7 @@ const MypageService = { async getLikeProducts(userId: number) { const likedProducts = await MypageRepository.findLikedProductsByUserId(userId); - return likedProducts.map((like) => like.product); + return likedProducts.map((like: { product: any }) => like.product); }, }; From 5ad050db692362429f1d55cd45ab68c089d8aa79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Tue, 16 Sep 2025 13:08:10 +0900 Subject: [PATCH 44/54] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=ED=83=88?= =?UTF-8?q?=ED=87=B4=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 --- src/api/controllers/MypageController.ts | 13 +++++++++++++ src/api/repositories/MypageRepository.ts | 6 ++++++ src/api/routes/MypageRouter.ts | 1 + src/api/services/MypageService.ts | 13 +++++++++++++ 4 files changed, 33 insertions(+) diff --git a/src/api/controllers/MypageController.ts b/src/api/controllers/MypageController.ts index a336010c0..d74e5705e 100644 --- a/src/api/controllers/MypageController.ts +++ b/src/api/controllers/MypageController.ts @@ -47,6 +47,19 @@ const MypageController = { } }, + async deleteUser(req: Request, res: Response, next: NextFunction) { + try { + const { id: userId } = req.user; + + await MypageService.deleteUser(userId); + res.clearCookie("accessToken"); + res.clearCookie("refreshToken"); + res.status(204).json("회원 탈퇴가 완료되었습니다."); + } catch (err) { + next(err); + } + }, + async getProducts(req: Request, res: Response, next: NextFunction) { try { const { id: userId } = req.user; diff --git a/src/api/repositories/MypageRepository.ts b/src/api/repositories/MypageRepository.ts index 6f18d87dd..4f296896d 100644 --- a/src/api/repositories/MypageRepository.ts +++ b/src/api/repositories/MypageRepository.ts @@ -32,6 +32,12 @@ export const updatePassword = async (userId: number, hashedNewPassword: string) }); }; +export const deleteUser = async (userId: number) => { + return await prisma.user.delete({ + where: { id: userId }, + }); +}; + export const findProductsByUserId = async (userId: number) => { return await prisma.product.findMany({ where: { userId }, diff --git a/src/api/routes/MypageRouter.ts b/src/api/routes/MypageRouter.ts index cb5a25364..5017f892b 100644 --- a/src/api/routes/MypageRouter.ts +++ b/src/api/routes/MypageRouter.ts @@ -9,6 +9,7 @@ const router = express.Router(); router.get("/", authenticate, MypageController.getUser); router.patch("/", authenticate, validate(updateUserSchema), MypageController.updateUser); router.patch("/password", authenticate, validate(updatePasswordSchema), MypageController.updatePassword); +router.delete("/", authenticate, MypageController.deleteUser); router.get("/products", authenticate, MypageController.getProducts); router.get("/like-products", authenticate, MypageController.getLikeProducts); diff --git a/src/api/services/MypageService.ts b/src/api/services/MypageService.ts index e2612e5e8..42a8e9367 100644 --- a/src/api/services/MypageService.ts +++ b/src/api/services/MypageService.ts @@ -6,6 +6,7 @@ import type { Prisma } from "@prisma/client"; import { generateTokens } from "../libs/token.js"; import { hashing } from "../libs/hashing.js"; import * as AuthRepository from "../repositories/AuthRepository.js"; +import { log } from "console"; const MypageService = { async getUser(userId: number) { @@ -68,6 +69,18 @@ const MypageService = { return { accessToken, refreshToken }; }, + async deleteUser(userId: number) { + const user = await MypageRepository.findUserProfile(userId); + + if (!user) { + const error: CustomError = new Error("사용자를 찾을 수 없습니다."); + error.statusCode = 404; + throw error; + } + + await MypageRepository.deleteUser(userId); + }, + async getProducts(userId: number) { const products = await MypageRepository.findProductsByUserId(userId); From d01a3dc6460b76c58946ffec0f4a37a4e975470d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Tue, 16 Sep 2025 13:15:03 +0900 Subject: [PATCH 45/54] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/controllers/AuthController.ts | 18 ++++++++++++++++++ src/api/routes/AuthRouter.ts | 2 ++ src/api/services/AuthService.ts | 5 +++++ 3 files changed, 25 insertions(+) diff --git a/src/api/controllers/AuthController.ts b/src/api/controllers/AuthController.ts index 0d34b959d..d15ec1b17 100644 --- a/src/api/controllers/AuthController.ts +++ b/src/api/controllers/AuthController.ts @@ -33,6 +33,24 @@ const AuthController = { } }, + async logout(req: Request, res: Response, next: NextFunction) { + try { + if (!req.user || !req.user.id) { + return res.status(401).json({ error: "로그인된 사용자만 로그아웃할 수 있습니다." }); + } + + await AuthService.logout(req.user.id); + + // 쿠키에서 토큰 제거 + res.clearCookie("refreshToken"); + res.clearCookie("accessToken"); + + res.status(200).json({ message: "로그아웃 되었습니다." }); + } catch (err) { + next(err); + } + }, + async refreshToken(req: Request, res: Response, next: NextFunction) { try { const result = await AuthService.refreshAccessToken(req.cookies.refreshToken); diff --git a/src/api/routes/AuthRouter.ts b/src/api/routes/AuthRouter.ts index 4be1f12e3..42fa161b5 100644 --- a/src/api/routes/AuthRouter.ts +++ b/src/api/routes/AuthRouter.ts @@ -2,9 +2,11 @@ import express from "express"; import AuthController from "../controllers/AuthController.js"; import validate from "../middlewares/validate.js"; import { loginSchema, signupSchema } from "../types/dtos/auth.dto.js"; +import authenticate from "../middlewares/authenticate.js"; const router = express.Router(); router.post("/signup", validate(signupSchema), AuthController.signup); router.post("/login", validate(loginSchema), AuthController.login); +router.post("/logout", authenticate, AuthController.logout); router.post("/refresh-token", AuthController.refreshToken); export default router; diff --git a/src/api/services/AuthService.ts b/src/api/services/AuthService.ts index 35c4911df..97e710efd 100644 --- a/src/api/services/AuthService.ts +++ b/src/api/services/AuthService.ts @@ -62,6 +62,11 @@ const AuthService = { return { userWithoutPassword, accessToken, refreshToken }; }, + // 로그아웃 + async logout(userId: number) { + return await AuthRepository.updateUserRefreshToken(userId, null); + }, + // AcessToken & RefreshToken을 재발급 받는 메서드 async refreshAccessToken(oldRefreshToken: string) { if (!oldRefreshToken) { From aa0a52f3d6b3779b042c1d243d35b67ff38133f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Tue, 16 Sep 2025 14:16:43 +0900 Subject: [PATCH 46/54] =?UTF-8?q?feat:=20access-token=EB=8F=84=20httpOnly?= =?UTF-8?q?=EB=A1=9C=20=EC=84=A4=EC=A0=95=ED=95=98=EA=B3=A0,=20=EB=B3=B4?= =?UTF-8?q?=EC=95=88=20=EC=98=B5=EC=85=98=EC=9D=84=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/controllers/AuthController.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/api/controllers/AuthController.ts b/src/api/controllers/AuthController.ts index d15ec1b17..d96b9343d 100644 --- a/src/api/controllers/AuthController.ts +++ b/src/api/controllers/AuthController.ts @@ -20,10 +20,13 @@ const AuthController = { res.cookie("refreshToken", refreshToken, { httpOnly: true, + sameSite: "lax", maxAge: 7 * 24 * 60 * 60 * 1000, }); res.cookie("accessToken", accessToken, { + httpOnly: true, + sameSite: "lax", maxAge: 1 * 60 * 60 * 1000, }); @@ -62,10 +65,13 @@ const AuthController = { // 쿠키에 토큰 저장 res.cookie("refreshToken", newRefreshToken, { httpOnly: true, + sameSite: "lax", maxAge: 7 * 24 * 60 * 60 * 1000, }); res.cookie("accessToken", accessToken, { + httpOnly: true, + sameSite: "lax", maxAge: 1 * 60 * 60 * 1000, }); From fb0d36b9647edfcf90eb92c855422c3af97ff574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Thu, 18 Sep 2025 18:35:15 +0900 Subject: [PATCH 47/54] =?UTF-8?q?refactor:=20service=20=EB=82=B4=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=EB=B3=84=20=ED=8F=B4=EB=8D=94=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/controllers/ArticleController.ts | 2 +- src/api/controllers/AuthController.ts | 2 +- src/api/controllers/CommentController.ts | 2 +- src/api/controllers/LikeController.ts | 2 +- src/api/controllers/MypageController.ts | 2 +- src/api/controllers/ProductController.ts | 2 +- src/api/services/{ => article}/ArticleService.ts | 8 ++++---- src/api/services/{ => auth}/AuthService.ts | 10 +++++----- src/api/services/{ => comment}/CommentService.ts | 8 ++++---- src/api/services/{ => like}/LikeService.ts | 4 ++-- src/api/services/{ => mypage}/MypageService.ts | 12 ++++++------ src/api/services/{ => product}/ProductService.ts | 8 ++++---- 12 files changed, 31 insertions(+), 31 deletions(-) rename src/api/services/{ => article}/ArticleService.ts (89%) rename src/api/services/{ => auth}/AuthService.ts (93%) rename src/api/services/{ => comment}/CommentService.ts (86%) rename src/api/services/{ => like}/LikeService.ts (88%) rename src/api/services/{ => mypage}/MypageService.ts (87%) rename src/api/services/{ => product}/ProductService.ts (88%) diff --git a/src/api/controllers/ArticleController.ts b/src/api/controllers/ArticleController.ts index 18f5879de..a2ec941a7 100644 --- a/src/api/controllers/ArticleController.ts +++ b/src/api/controllers/ArticleController.ts @@ -1,4 +1,4 @@ -import ArticleService from "../services/ArticleService.js"; +import ArticleService from "../services/article/ArticleService.js"; import jwt from "jsonwebtoken"; import type { Request, Response, NextFunction } from "express"; import { ACCESS_TOKEN_SECRET } from "../libs/constants.js"; diff --git a/src/api/controllers/AuthController.ts b/src/api/controllers/AuthController.ts index d96b9343d..397c77734 100644 --- a/src/api/controllers/AuthController.ts +++ b/src/api/controllers/AuthController.ts @@ -1,4 +1,4 @@ -import AuthService from "../services/AuthService.js"; +import AuthService from "../services/auth/AuthService.js"; import type { Request, Response, NextFunction } from "express"; import type { SignupDto, LoginDto } from "../types/dtos/auth.dto.js"; diff --git a/src/api/controllers/CommentController.ts b/src/api/controllers/CommentController.ts index dc5cc26cb..d6fe04b29 100644 --- a/src/api/controllers/CommentController.ts +++ b/src/api/controllers/CommentController.ts @@ -1,4 +1,4 @@ -import CommentService from "../services/CommentService.js"; +import CommentService from "../services/comment/CommentService.js"; import type { Request, Response, NextFunction } from "express"; import type { CommentDto } from "../types/dtos/comment.dto.js"; diff --git a/src/api/controllers/LikeController.ts b/src/api/controllers/LikeController.ts index f90ea39b3..8341420df 100644 --- a/src/api/controllers/LikeController.ts +++ b/src/api/controllers/LikeController.ts @@ -1,4 +1,4 @@ -import LikeService from "../services/LikeService.js"; +import LikeService from "../services/like/LikeService.js"; import type { Request, Response, NextFunction } from "express"; const LikeController = { diff --git a/src/api/controllers/MypageController.ts b/src/api/controllers/MypageController.ts index d74e5705e..413da9fc7 100644 --- a/src/api/controllers/MypageController.ts +++ b/src/api/controllers/MypageController.ts @@ -1,4 +1,4 @@ -import MypageService from "../services/MypageService.js"; +import MypageService from "../services/mypage/MypageService.js"; import type { Request, Response, NextFunction } from "express"; import type { UpdateUserDTO, UpdatePasswordDTO } from "../types/dtos/mypage.dto.js"; diff --git a/src/api/controllers/ProductController.ts b/src/api/controllers/ProductController.ts index c50c27f2f..5e3596637 100644 --- a/src/api/controllers/ProductController.ts +++ b/src/api/controllers/ProductController.ts @@ -1,4 +1,4 @@ -import ProductService from "../services/ProductService.js"; +import ProductService from "../services/product/ProductService.js"; import jwt from "jsonwebtoken"; import { ACCESS_TOKEN_SECRET } from "../libs/constants.js"; import type { Request, Response, NextFunction } from "express"; diff --git a/src/api/services/ArticleService.ts b/src/api/services/article/ArticleService.ts similarity index 89% rename from src/api/services/ArticleService.ts rename to src/api/services/article/ArticleService.ts index ee82d6248..13f0df2cf 100644 --- a/src/api/services/ArticleService.ts +++ b/src/api/services/article/ArticleService.ts @@ -1,7 +1,7 @@ -import type { CreateArticleData, UpdateArticleData, FindManyArticleParams } from "../types/article.js"; -import type { CustomError } from "../types/error.js"; -import * as ArticleRepository from "../repositories/ArticleRepository.js"; -import type { ArticleDto } from "../types/dtos/article.dto.js"; +import type { CreateArticleData, UpdateArticleData, FindManyArticleParams } from "../../types/article.js"; +import type { CustomError } from "../../types/error.js"; +import * as ArticleRepository from "../../repositories/ArticleRepository.js"; +import type { ArticleDto } from "../../types/dtos/article.dto.js"; const ArticleService = { async createArticle(articleData: ArticleDto, userId: number) { diff --git a/src/api/services/AuthService.ts b/src/api/services/auth/AuthService.ts similarity index 93% rename from src/api/services/AuthService.ts rename to src/api/services/auth/AuthService.ts index 97e710efd..f658426e1 100644 --- a/src/api/services/AuthService.ts +++ b/src/api/services/auth/AuthService.ts @@ -1,10 +1,10 @@ import jwt from "jsonwebtoken"; -import { hashing, compareWords } from "../libs/hashing.js"; -import { generateTokens } from "../libs/token.js"; -import { REFRESH_TOKEN_SECRET } from "../libs/constants.js"; +import { hashing, compareWords } from "../../libs/hashing.js"; +import { generateTokens } from "../../libs/token.js"; +import { REFRESH_TOKEN_SECRET } from "../../libs/constants.js"; import type { CustomError } from "src/api/types/error.js"; -import * as AuthRepository from "../repositories/AuthRepository.js"; -import type { SignupDto, LoginDto } from "../types/dtos/auth.dto.js"; +import * as AuthRepository from "../../repositories/AuthRepository.js"; +import type { SignupDto, LoginDto } from "../../types/dtos/auth.dto.js"; const AuthService = { async signup(signupData: SignupDto) { diff --git a/src/api/services/CommentService.ts b/src/api/services/comment/CommentService.ts similarity index 86% rename from src/api/services/CommentService.ts rename to src/api/services/comment/CommentService.ts index 31d8d1674..a91225f63 100644 --- a/src/api/services/CommentService.ts +++ b/src/api/services/comment/CommentService.ts @@ -1,7 +1,7 @@ -import type { CustomError } from "../types/error.js"; -import type { FindManyCommentParams } from "../types/comment.js"; -import * as CommentRepository from "../repositories/CommentRepository.js"; -import type { CommentDto } from "../types/dtos/comment.dto.js"; +import type { CustomError } from "../../types/error.js"; +import type { FindManyCommentParams } from "../../types/comment.js"; +import * as CommentRepository from "../../repositories/CommentRepository.js"; +import type { CommentDto } from "../../types/dtos/comment.dto.js"; const CommentService = { async createComment(data: CommentDto & { userId: number }) { diff --git a/src/api/services/LikeService.ts b/src/api/services/like/LikeService.ts similarity index 88% rename from src/api/services/LikeService.ts rename to src/api/services/like/LikeService.ts index 68e0c6354..67ec0ae73 100644 --- a/src/api/services/LikeService.ts +++ b/src/api/services/like/LikeService.ts @@ -1,5 +1,5 @@ -import type { LikeData } from "../types/like.js"; -import * as LikeRepository from "../repositories/LikeRepository.js"; +import type { LikeData } from "../../types/like.js"; +import * as LikeRepository from "../../repositories/LikeRepository.js"; import type { Prisma } from "@prisma/client"; const LikeService = { diff --git a/src/api/services/MypageService.ts b/src/api/services/mypage/MypageService.ts similarity index 87% rename from src/api/services/MypageService.ts rename to src/api/services/mypage/MypageService.ts index 42a8e9367..b429b0de3 100644 --- a/src/api/services/MypageService.ts +++ b/src/api/services/mypage/MypageService.ts @@ -1,11 +1,11 @@ import bcrypt from "bcrypt"; -import type { CustomError } from "../types/error.js"; -import * as MypageRepository from "../repositories/MypageRepository.js"; -import type { UpdateUserDTO, UpdatePasswordDTO } from "../types/dtos/mypage.dto.js"; +import type { CustomError } from "../../types/error.js"; +import * as MypageRepository from "../../repositories/MypageRepository.js"; +import type { UpdateUserDTO, UpdatePasswordDTO } from "../../types/dtos/mypage.dto.js"; import type { Prisma } from "@prisma/client"; -import { generateTokens } from "../libs/token.js"; -import { hashing } from "../libs/hashing.js"; -import * as AuthRepository from "../repositories/AuthRepository.js"; +import { generateTokens } from "../../libs/token.js"; +import { hashing } from "../../libs/hashing.js"; +import * as AuthRepository from "../../repositories/AuthRepository.js"; import { log } from "console"; const MypageService = { diff --git a/src/api/services/ProductService.ts b/src/api/services/product/ProductService.ts similarity index 88% rename from src/api/services/ProductService.ts rename to src/api/services/product/ProductService.ts index e514bcf34..8596af28e 100644 --- a/src/api/services/ProductService.ts +++ b/src/api/services/product/ProductService.ts @@ -1,7 +1,7 @@ -import type { CustomError } from "../types/error.js"; -import type { CreateProductData, UpdateProductData, FindManyProductParams } from "../types/product.js"; -import * as ProductRepository from "../repositories/ProductRepository.js"; -import type { ProductDto } from "../types/dtos/product.dto.js"; +import type { CustomError } from "../../types/error.js"; +import type { CreateProductData, UpdateProductData, FindManyProductParams } from "../../types/product.js"; +import * as ProductRepository from "../../repositories/ProductRepository.js"; +import type { ProductDto } from "../../types/dtos/product.dto.js"; const ProductService = { async createProduct(productData: ProductDto, userId: number) { From bfc881c72761647920cc3b7f54117df79f3ea1ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Thu, 18 Sep 2025 18:54:01 +0900 Subject: [PATCH 48/54] =?UTF-8?q?refactor:=20=ED=8C=8C=EC=9D=BC=EB=AA=85?= =?UTF-8?q?=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...{ArticleController.ts => article.controller.ts} | 2 +- .../{AuthController.ts => auth.controller.ts} | 2 +- ...{CommentController.ts => comment.controller.ts} | 2 +- .../{ImageController.ts => image.controller.ts} | 0 .../{LikeController.ts => like.controller.ts} | 2 +- .../{MypageController.ts => mypage.controller.ts} | 2 +- ...{ProductController.ts => product.controller.ts} | 2 +- ...{ArticleRepository.ts => article.repository.ts} | 0 .../{AuthRepository.ts => auth.repository.ts} | 0 ...{CommentRepository.ts => comment.repository.ts} | 0 .../{LikeRepository.ts => like.repository.ts} | 0 .../{MypageRepository.ts => mypage.repository.ts} | 0 ...{ProductRepository.ts => product.repository.ts} | 0 .../routes/{ArticleRouter.ts => article.router.ts} | 2 +- src/api/routes/{AuthRouter.ts => auth.router.ts} | 2 +- .../routes/{CommentRouter.ts => comment.router.ts} | 2 +- src/api/routes/{ImageRouter.ts => image.router.ts} | 2 +- src/api/routes/{LikeRouter.ts => like.router.ts} | 2 +- .../routes/{MypageRouter.ts => mypage.router.ts} | 2 +- .../routes/{ProductRouter.ts => product.router.ts} | 2 +- .../{ArticleService.ts => article.service.ts} | 2 +- .../auth/{AuthService.ts => auth.service.ts} | 2 +- .../{CommentService.ts => comment.service.ts} | 2 +- .../like/{LikeService.ts => like.service.ts} | 2 +- .../mypage/{MypageService.ts => mypage.service.ts} | 4 ++-- .../{ProductService.ts => product.service.ts} | 2 +- src/external/classes/{Article.ts => article.ts} | 0 ...{ElectronicProduct.ts => electronic_product.ts} | 2 +- src/external/classes/{Product.ts => product.ts} | 0 .../{ArticleService.js => article.service.js} | 0 .../{ProductService.js => product.service.js} | 0 .../stores/{ProductStore.js => product_store.js} | 0 ...stArticleService.js => test_article.service.js} | 0 ...stProductService.js => test_product.service.js} | 0 src/main.ts | 14 +++++++------- 35 files changed, 28 insertions(+), 28 deletions(-) rename src/api/controllers/{ArticleController.ts => article.controller.ts} (97%) rename src/api/controllers/{AuthController.ts => auth.controller.ts} (97%) rename src/api/controllers/{CommentController.ts => comment.controller.ts} (97%) rename src/api/controllers/{ImageController.ts => image.controller.ts} (100%) rename src/api/controllers/{LikeController.ts => like.controller.ts} (91%) rename src/api/controllers/{MypageController.ts => mypage.controller.ts} (97%) rename src/api/controllers/{ProductController.ts => product.controller.ts} (97%) rename src/api/repositories/{ArticleRepository.ts => article.repository.ts} (100%) rename src/api/repositories/{AuthRepository.ts => auth.repository.ts} (100%) rename src/api/repositories/{CommentRepository.ts => comment.repository.ts} (100%) rename src/api/repositories/{LikeRepository.ts => like.repository.ts} (100%) rename src/api/repositories/{MypageRepository.ts => mypage.repository.ts} (100%) rename src/api/repositories/{ProductRepository.ts => product.repository.ts} (100%) rename src/api/routes/{ArticleRouter.ts => article.router.ts} (89%) rename src/api/routes/{AuthRouter.ts => auth.router.ts} (89%) rename src/api/routes/{CommentRouter.ts => comment.router.ts} (89%) rename src/api/routes/{ImageRouter.ts => image.router.ts} (76%) rename src/api/routes/{LikeRouter.ts => like.router.ts} (77%) rename src/api/routes/{MypageRouter.ts => mypage.router.ts} (91%) rename src/api/routes/{ProductRouter.ts => product.router.ts} (89%) rename src/api/services/article/{ArticleService.ts => article.service.ts} (96%) rename src/api/services/auth/{AuthService.ts => auth.service.ts} (98%) rename src/api/services/comment/{CommentService.ts => comment.service.ts} (95%) rename src/api/services/like/{LikeService.ts => like.service.ts} (93%) rename src/api/services/mypage/{MypageService.ts => mypage.service.ts} (95%) rename src/api/services/product/{ProductService.ts => product.service.ts} (96%) rename src/external/classes/{Article.ts => article.ts} (100%) rename src/external/classes/{ElectronicProduct.ts => electronic_product.ts} (92%) rename src/external/classes/{Product.ts => product.ts} (100%) rename src/external/services/{ArticleService.js => article.service.js} (100%) rename src/external/services/{ProductService.js => product.service.js} (100%) rename src/external/stores/{ProductStore.js => product_store.js} (100%) rename src/external/tests/{testArticleService.js => test_article.service.js} (100%) rename src/external/tests/{testProductService.js => test_product.service.js} (100%) diff --git a/src/api/controllers/ArticleController.ts b/src/api/controllers/article.controller.ts similarity index 97% rename from src/api/controllers/ArticleController.ts rename to src/api/controllers/article.controller.ts index a2ec941a7..24db81218 100644 --- a/src/api/controllers/ArticleController.ts +++ b/src/api/controllers/article.controller.ts @@ -1,4 +1,4 @@ -import ArticleService from "../services/article/ArticleService.js"; +import ArticleService from "../services/article/article.service.js"; import jwt from "jsonwebtoken"; import type { Request, Response, NextFunction } from "express"; import { ACCESS_TOKEN_SECRET } from "../libs/constants.js"; diff --git a/src/api/controllers/AuthController.ts b/src/api/controllers/auth.controller.ts similarity index 97% rename from src/api/controllers/AuthController.ts rename to src/api/controllers/auth.controller.ts index 397c77734..320d80b64 100644 --- a/src/api/controllers/AuthController.ts +++ b/src/api/controllers/auth.controller.ts @@ -1,4 +1,4 @@ -import AuthService from "../services/auth/AuthService.js"; +import AuthService from "../services/auth/auth.service.js"; import type { Request, Response, NextFunction } from "express"; import type { SignupDto, LoginDto } from "../types/dtos/auth.dto.js"; diff --git a/src/api/controllers/CommentController.ts b/src/api/controllers/comment.controller.ts similarity index 97% rename from src/api/controllers/CommentController.ts rename to src/api/controllers/comment.controller.ts index d6fe04b29..afdce012e 100644 --- a/src/api/controllers/CommentController.ts +++ b/src/api/controllers/comment.controller.ts @@ -1,4 +1,4 @@ -import CommentService from "../services/comment/CommentService.js"; +import CommentService from "../services/comment/comment.service.js"; import type { Request, Response, NextFunction } from "express"; import type { CommentDto } from "../types/dtos/comment.dto.js"; diff --git a/src/api/controllers/ImageController.ts b/src/api/controllers/image.controller.ts similarity index 100% rename from src/api/controllers/ImageController.ts rename to src/api/controllers/image.controller.ts diff --git a/src/api/controllers/LikeController.ts b/src/api/controllers/like.controller.ts similarity index 91% rename from src/api/controllers/LikeController.ts rename to src/api/controllers/like.controller.ts index 8341420df..48b8934ad 100644 --- a/src/api/controllers/LikeController.ts +++ b/src/api/controllers/like.controller.ts @@ -1,4 +1,4 @@ -import LikeService from "../services/like/LikeService.js"; +import LikeService from "../services/like/like.service.js"; import type { Request, Response, NextFunction } from "express"; const LikeController = { diff --git a/src/api/controllers/MypageController.ts b/src/api/controllers/mypage.controller.ts similarity index 97% rename from src/api/controllers/MypageController.ts rename to src/api/controllers/mypage.controller.ts index 413da9fc7..f355e5574 100644 --- a/src/api/controllers/MypageController.ts +++ b/src/api/controllers/mypage.controller.ts @@ -1,4 +1,4 @@ -import MypageService from "../services/mypage/MypageService.js"; +import MypageService from "../services/mypage/mypage.service.js"; import type { Request, Response, NextFunction } from "express"; import type { UpdateUserDTO, UpdatePasswordDTO } from "../types/dtos/mypage.dto.js"; diff --git a/src/api/controllers/ProductController.ts b/src/api/controllers/product.controller.ts similarity index 97% rename from src/api/controllers/ProductController.ts rename to src/api/controllers/product.controller.ts index 5e3596637..a03a9bf43 100644 --- a/src/api/controllers/ProductController.ts +++ b/src/api/controllers/product.controller.ts @@ -1,4 +1,4 @@ -import ProductService from "../services/product/ProductService.js"; +import ProductService from "../services/product/product.service.js"; import jwt from "jsonwebtoken"; import { ACCESS_TOKEN_SECRET } from "../libs/constants.js"; import type { Request, Response, NextFunction } from "express"; diff --git a/src/api/repositories/ArticleRepository.ts b/src/api/repositories/article.repository.ts similarity index 100% rename from src/api/repositories/ArticleRepository.ts rename to src/api/repositories/article.repository.ts diff --git a/src/api/repositories/AuthRepository.ts b/src/api/repositories/auth.repository.ts similarity index 100% rename from src/api/repositories/AuthRepository.ts rename to src/api/repositories/auth.repository.ts diff --git a/src/api/repositories/CommentRepository.ts b/src/api/repositories/comment.repository.ts similarity index 100% rename from src/api/repositories/CommentRepository.ts rename to src/api/repositories/comment.repository.ts diff --git a/src/api/repositories/LikeRepository.ts b/src/api/repositories/like.repository.ts similarity index 100% rename from src/api/repositories/LikeRepository.ts rename to src/api/repositories/like.repository.ts diff --git a/src/api/repositories/MypageRepository.ts b/src/api/repositories/mypage.repository.ts similarity index 100% rename from src/api/repositories/MypageRepository.ts rename to src/api/repositories/mypage.repository.ts diff --git a/src/api/repositories/ProductRepository.ts b/src/api/repositories/product.repository.ts similarity index 100% rename from src/api/repositories/ProductRepository.ts rename to src/api/repositories/product.repository.ts diff --git a/src/api/routes/ArticleRouter.ts b/src/api/routes/article.router.ts similarity index 89% rename from src/api/routes/ArticleRouter.ts rename to src/api/routes/article.router.ts index 6636cfdc4..21ebfa91f 100644 --- a/src/api/routes/ArticleRouter.ts +++ b/src/api/routes/article.router.ts @@ -1,5 +1,5 @@ import express from "express"; -import ArticleController from "../controllers/ArticleController.js"; +import ArticleController from "../controllers/article.controller.js"; import authenticate from "../middlewares/authenticate.js"; import validate from "../middlewares/validate.js"; import { ArticleSchema } from "../types/dtos/article.dto.js"; diff --git a/src/api/routes/AuthRouter.ts b/src/api/routes/auth.router.ts similarity index 89% rename from src/api/routes/AuthRouter.ts rename to src/api/routes/auth.router.ts index 42fa161b5..c091f0b22 100644 --- a/src/api/routes/AuthRouter.ts +++ b/src/api/routes/auth.router.ts @@ -1,5 +1,5 @@ import express from "express"; -import AuthController from "../controllers/AuthController.js"; +import AuthController from "../controllers/auth.controller.js"; import validate from "../middlewares/validate.js"; import { loginSchema, signupSchema } from "../types/dtos/auth.dto.js"; import authenticate from "../middlewares/authenticate.js"; diff --git a/src/api/routes/CommentRouter.ts b/src/api/routes/comment.router.ts similarity index 89% rename from src/api/routes/CommentRouter.ts rename to src/api/routes/comment.router.ts index c74e27528..ab5d35f94 100644 --- a/src/api/routes/CommentRouter.ts +++ b/src/api/routes/comment.router.ts @@ -1,5 +1,5 @@ import express from "express"; -import CommentController from "../controllers/CommentController.js"; +import CommentController from "../controllers/comment.controller.js"; import authenticate from "../middlewares/authenticate.js"; import validate from "../middlewares/validate.js"; import { commentSchema } from "../types/dtos/comment.dto.js"; diff --git a/src/api/routes/ImageRouter.ts b/src/api/routes/image.router.ts similarity index 76% rename from src/api/routes/ImageRouter.ts rename to src/api/routes/image.router.ts index 2bcf206fa..27eac09de 100644 --- a/src/api/routes/ImageRouter.ts +++ b/src/api/routes/image.router.ts @@ -1,6 +1,6 @@ import express from "express"; import upload from "../middlewares/upload.js"; -import ImageController from "../controllers/ImageController.js"; +import ImageController from "../controllers/image.controller.js"; const router = express.Router(); diff --git a/src/api/routes/LikeRouter.ts b/src/api/routes/like.router.ts similarity index 77% rename from src/api/routes/LikeRouter.ts rename to src/api/routes/like.router.ts index e2f6fb7c7..f39744e3c 100644 --- a/src/api/routes/LikeRouter.ts +++ b/src/api/routes/like.router.ts @@ -1,6 +1,6 @@ import express from "express"; import authenticate from "../middlewares/authenticate.js"; -import LikeController from "../controllers/LikeController.js"; +import LikeController from "../controllers/like.controller.js"; const router = express.Router(); diff --git a/src/api/routes/MypageRouter.ts b/src/api/routes/mypage.router.ts similarity index 91% rename from src/api/routes/MypageRouter.ts rename to src/api/routes/mypage.router.ts index 5017f892b..05fd3a785 100644 --- a/src/api/routes/MypageRouter.ts +++ b/src/api/routes/mypage.router.ts @@ -1,6 +1,6 @@ import express from "express"; import authenticate from "../middlewares/authenticate.js"; -import MypageController from "../controllers/MypageController.js"; +import MypageController from "../controllers/mypage.controller.js"; import validate from "../middlewares/validate.js"; import { updateUserSchema, updatePasswordSchema } from "../types/dtos/mypage.dto.js"; diff --git a/src/api/routes/ProductRouter.ts b/src/api/routes/product.router.ts similarity index 89% rename from src/api/routes/ProductRouter.ts rename to src/api/routes/product.router.ts index 1fee957d8..8952e7268 100644 --- a/src/api/routes/ProductRouter.ts +++ b/src/api/routes/product.router.ts @@ -1,5 +1,5 @@ import express from "express"; -import ProductController from "../controllers/ProductController.js"; +import ProductController from "../controllers/product.controller.js"; import authenticate from "../middlewares/authenticate.js"; import validate from "../middlewares/validate.js"; import { ProductSchema } from "../types/dtos/product.dto.js"; diff --git a/src/api/services/article/ArticleService.ts b/src/api/services/article/article.service.ts similarity index 96% rename from src/api/services/article/ArticleService.ts rename to src/api/services/article/article.service.ts index 13f0df2cf..66fd8a58e 100644 --- a/src/api/services/article/ArticleService.ts +++ b/src/api/services/article/article.service.ts @@ -1,6 +1,6 @@ import type { CreateArticleData, UpdateArticleData, FindManyArticleParams } from "../../types/article.js"; import type { CustomError } from "../../types/error.js"; -import * as ArticleRepository from "../../repositories/ArticleRepository.js"; +import * as ArticleRepository from "../../repositories/article.repository.js"; import type { ArticleDto } from "../../types/dtos/article.dto.js"; const ArticleService = { diff --git a/src/api/services/auth/AuthService.ts b/src/api/services/auth/auth.service.ts similarity index 98% rename from src/api/services/auth/AuthService.ts rename to src/api/services/auth/auth.service.ts index f658426e1..45cd7c2c6 100644 --- a/src/api/services/auth/AuthService.ts +++ b/src/api/services/auth/auth.service.ts @@ -3,7 +3,7 @@ import { hashing, compareWords } from "../../libs/hashing.js"; import { generateTokens } from "../../libs/token.js"; import { REFRESH_TOKEN_SECRET } from "../../libs/constants.js"; import type { CustomError } from "src/api/types/error.js"; -import * as AuthRepository from "../../repositories/AuthRepository.js"; +import * as AuthRepository from "../../repositories/auth.repository.js"; import type { SignupDto, LoginDto } from "../../types/dtos/auth.dto.js"; const AuthService = { diff --git a/src/api/services/comment/CommentService.ts b/src/api/services/comment/comment.service.ts similarity index 95% rename from src/api/services/comment/CommentService.ts rename to src/api/services/comment/comment.service.ts index a91225f63..5618b7901 100644 --- a/src/api/services/comment/CommentService.ts +++ b/src/api/services/comment/comment.service.ts @@ -1,6 +1,6 @@ import type { CustomError } from "../../types/error.js"; import type { FindManyCommentParams } from "../../types/comment.js"; -import * as CommentRepository from "../../repositories/CommentRepository.js"; +import * as CommentRepository from "../../repositories/comment.repository.js"; import type { CommentDto } from "../../types/dtos/comment.dto.js"; const CommentService = { diff --git a/src/api/services/like/LikeService.ts b/src/api/services/like/like.service.ts similarity index 93% rename from src/api/services/like/LikeService.ts rename to src/api/services/like/like.service.ts index 67ec0ae73..87a40c4c4 100644 --- a/src/api/services/like/LikeService.ts +++ b/src/api/services/like/like.service.ts @@ -1,5 +1,5 @@ import type { LikeData } from "../../types/like.js"; -import * as LikeRepository from "../../repositories/LikeRepository.js"; +import * as LikeRepository from "../../repositories/like.repository.js"; import type { Prisma } from "@prisma/client"; const LikeService = { diff --git a/src/api/services/mypage/MypageService.ts b/src/api/services/mypage/mypage.service.ts similarity index 95% rename from src/api/services/mypage/MypageService.ts rename to src/api/services/mypage/mypage.service.ts index b429b0de3..4e9f23d66 100644 --- a/src/api/services/mypage/MypageService.ts +++ b/src/api/services/mypage/mypage.service.ts @@ -1,11 +1,11 @@ import bcrypt from "bcrypt"; import type { CustomError } from "../../types/error.js"; -import * as MypageRepository from "../../repositories/MypageRepository.js"; +import * as MypageRepository from "../../repositories/mypage.repository.js"; import type { UpdateUserDTO, UpdatePasswordDTO } from "../../types/dtos/mypage.dto.js"; import type { Prisma } from "@prisma/client"; import { generateTokens } from "../../libs/token.js"; import { hashing } from "../../libs/hashing.js"; -import * as AuthRepository from "../../repositories/AuthRepository.js"; +import * as AuthRepository from "../../repositories/auth.repository.js"; import { log } from "console"; const MypageService = { diff --git a/src/api/services/product/ProductService.ts b/src/api/services/product/product.service.ts similarity index 96% rename from src/api/services/product/ProductService.ts rename to src/api/services/product/product.service.ts index 8596af28e..90174004d 100644 --- a/src/api/services/product/ProductService.ts +++ b/src/api/services/product/product.service.ts @@ -1,6 +1,6 @@ import type { CustomError } from "../../types/error.js"; import type { CreateProductData, UpdateProductData, FindManyProductParams } from "../../types/product.js"; -import * as ProductRepository from "../../repositories/ProductRepository.js"; +import * as ProductRepository from "../../repositories/product.repository.js"; import type { ProductDto } from "../../types/dtos/product.dto.js"; const ProductService = { diff --git a/src/external/classes/Article.ts b/src/external/classes/article.ts similarity index 100% rename from src/external/classes/Article.ts rename to src/external/classes/article.ts diff --git a/src/external/classes/ElectronicProduct.ts b/src/external/classes/electronic_product.ts similarity index 92% rename from src/external/classes/ElectronicProduct.ts rename to src/external/classes/electronic_product.ts index 36620c01c..6fd7b93ab 100644 --- a/src/external/classes/ElectronicProduct.ts +++ b/src/external/classes/electronic_product.ts @@ -1,4 +1,4 @@ -import { Product } from "./Product.js"; +import { Product } from "./product.js"; export class ElectronicProduct extends Product { manufacturer: string; diff --git a/src/external/classes/Product.ts b/src/external/classes/product.ts similarity index 100% rename from src/external/classes/Product.ts rename to src/external/classes/product.ts diff --git a/src/external/services/ArticleService.js b/src/external/services/article.service.js similarity index 100% rename from src/external/services/ArticleService.js rename to src/external/services/article.service.js diff --git a/src/external/services/ProductService.js b/src/external/services/product.service.js similarity index 100% rename from src/external/services/ProductService.js rename to src/external/services/product.service.js diff --git a/src/external/stores/ProductStore.js b/src/external/stores/product_store.js similarity index 100% rename from src/external/stores/ProductStore.js rename to src/external/stores/product_store.js diff --git a/src/external/tests/testArticleService.js b/src/external/tests/test_article.service.js similarity index 100% rename from src/external/tests/testArticleService.js rename to src/external/tests/test_article.service.js diff --git a/src/external/tests/testProductService.js b/src/external/tests/test_product.service.js similarity index 100% rename from src/external/tests/testProductService.js rename to src/external/tests/test_product.service.js diff --git a/src/main.ts b/src/main.ts index c86d33fcc..00fd955e7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,12 @@ import express from "express"; -import ProductRouter from "./api/routes/ProductRouter.js"; -import ArticleRouter from "./api/routes/ArticleRouter.js"; -import CommentRouter from "./api/routes/CommentRouter.js"; +import ProductRouter from "./api/routes/product.router.js"; +import ArticleRouter from "./api/routes/article.router.js"; +import CommentRouter from "./api/routes/comment.router.js"; import errorHandler from "./api/middlewares/errorHandler.js"; -import imageRouter from "./api/routes/ImageRouter.js"; -import AuthRouter from "./api/routes/AuthRouter.js"; -import MypageRouter from "./api/routes/MypageRouter.js"; -import LikeRouter from "./api/routes/LikeRouter.js"; +import imageRouter from "./api/routes/image.router.js"; +import AuthRouter from "./api/routes/auth.router.js"; +import MypageRouter from "./api/routes/mypage.router.js"; +import LikeRouter from "./api/routes/like.router.js"; import cookieParser from "cookie-parser"; // import { testAllArticleService } from "./external/tests/testArticleService.js"; From 786a69f5cf62d5beeb6aa9f25d2c9f6c33d39f14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Fri, 19 Sep 2025 10:30:53 +0900 Subject: [PATCH 49/54] =?UTF-8?q?refactor:=20validator=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20service=20=ED=8F=B4=EB=8D=94=20=EB=82=B4=20?= =?UTF-8?q?=EA=B0=81=20=EC=97=AD=ED=95=A0=20=ED=8F=B4=EB=8D=94=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/middlewares/validate.ts | 4 ++-- src/api/routes/article.router.ts | 2 +- src/api/routes/auth.router.ts | 2 +- src/api/routes/comment.router.ts | 2 +- src/api/routes/mypage.router.ts | 2 +- src/api/routes/product.router.ts | 2 +- src/api/services/article/article.validator.ts | 6 +++++ src/api/services/auth/auth.validator.ts | 12 ++++++++++ src/api/services/comment/comment.validator.ts | 23 +++++++++++++++++++ src/api/services/mypage/mypage.validator.ts | 18 +++++++++++++++ src/api/services/product/product.validator.ts | 7 ++++++ src/api/types/dtos/auth.dto.ts | 2 -- 12 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 src/api/services/article/article.validator.ts create mode 100644 src/api/services/auth/auth.validator.ts create mode 100644 src/api/services/comment/comment.validator.ts create mode 100644 src/api/services/mypage/mypage.validator.ts create mode 100644 src/api/services/product/product.validator.ts diff --git a/src/api/middlewares/validate.ts b/src/api/middlewares/validate.ts index 23bc012b5..4192ef3e4 100644 --- a/src/api/middlewares/validate.ts +++ b/src/api/middlewares/validate.ts @@ -1,7 +1,7 @@ -import { ZodError } from "zod"; +import z, { ZodError } from "zod"; import type { Request, Response, NextFunction } from "express"; -const validate = (schema: any) => (req: Request, res: Response, next: NextFunction) => { +const validate = (schema: z.ZodSchema) => (req: Request, res: Response, next: NextFunction) => { try { schema.parse(req.body); next(); diff --git a/src/api/routes/article.router.ts b/src/api/routes/article.router.ts index 21ebfa91f..7914c8993 100644 --- a/src/api/routes/article.router.ts +++ b/src/api/routes/article.router.ts @@ -2,7 +2,7 @@ import express from "express"; import ArticleController from "../controllers/article.controller.js"; import authenticate from "../middlewares/authenticate.js"; import validate from "../middlewares/validate.js"; -import { ArticleSchema } from "../types/dtos/article.dto.js"; +import { ArticleSchema } from "../services/article/article.validator.js"; const router = express.Router(); diff --git a/src/api/routes/auth.router.ts b/src/api/routes/auth.router.ts index c091f0b22..824bc6e30 100644 --- a/src/api/routes/auth.router.ts +++ b/src/api/routes/auth.router.ts @@ -1,7 +1,7 @@ import express from "express"; import AuthController from "../controllers/auth.controller.js"; import validate from "../middlewares/validate.js"; -import { loginSchema, signupSchema } from "../types/dtos/auth.dto.js"; +import { loginSchema, signupSchema } from "../services/auth/auth.validator.js"; import authenticate from "../middlewares/authenticate.js"; const router = express.Router(); diff --git a/src/api/routes/comment.router.ts b/src/api/routes/comment.router.ts index ab5d35f94..2a540f6bc 100644 --- a/src/api/routes/comment.router.ts +++ b/src/api/routes/comment.router.ts @@ -2,7 +2,7 @@ import express from "express"; import CommentController from "../controllers/comment.controller.js"; import authenticate from "../middlewares/authenticate.js"; import validate from "../middlewares/validate.js"; -import { commentSchema } from "../types/dtos/comment.dto.js"; +import { commentSchema } from "../services/comment/comment.validator.js"; const router = express.Router(); diff --git a/src/api/routes/mypage.router.ts b/src/api/routes/mypage.router.ts index 05fd3a785..6271e6f8e 100644 --- a/src/api/routes/mypage.router.ts +++ b/src/api/routes/mypage.router.ts @@ -2,7 +2,7 @@ import express from "express"; import authenticate from "../middlewares/authenticate.js"; import MypageController from "../controllers/mypage.controller.js"; import validate from "../middlewares/validate.js"; -import { updateUserSchema, updatePasswordSchema } from "../types/dtos/mypage.dto.js"; +import { updateUserSchema, updatePasswordSchema } from "../services/mypage/mypage.validator.js"; const router = express.Router(); diff --git a/src/api/routes/product.router.ts b/src/api/routes/product.router.ts index 8952e7268..31e00f7c2 100644 --- a/src/api/routes/product.router.ts +++ b/src/api/routes/product.router.ts @@ -2,7 +2,7 @@ import express from "express"; import ProductController from "../controllers/product.controller.js"; import authenticate from "../middlewares/authenticate.js"; import validate from "../middlewares/validate.js"; -import { ProductSchema } from "../types/dtos/product.dto.js"; +import { ProductSchema } from "../services/product/product.validator.js"; const router = express.Router(); diff --git a/src/api/services/article/article.validator.ts b/src/api/services/article/article.validator.ts new file mode 100644 index 000000000..7c89643b0 --- /dev/null +++ b/src/api/services/article/article.validator.ts @@ -0,0 +1,6 @@ +import z from "zod"; + +export const ArticleSchema = z.object({ + title: z.string().min(1, { message: "제목을 입력하세요." }), + content: z.string().min(1, { message: "내용을 입력하세요." }), +}); diff --git a/src/api/services/auth/auth.validator.ts b/src/api/services/auth/auth.validator.ts new file mode 100644 index 000000000..23fc34150 --- /dev/null +++ b/src/api/services/auth/auth.validator.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +export const signupSchema = z.object({ + email: z.email({ message: "유효한 이메일 형식이 아닙니다." }), + password: z.string().min(8, { message: "비밀번호는 최소 8자 이상이어야합니다." }), + nickname: z.string().min(2, { message: "닉네임은 최소 2자 이상이어야 합니다." }), +}); + +export const loginSchema = z.object({ + email: z.email({ message: "유효한 이메일 형식이 아닙니다." }), + password: z.string().min(1, { message: "비밀번호를 입력해주세요." }), +}); diff --git a/src/api/services/comment/comment.validator.ts b/src/api/services/comment/comment.validator.ts new file mode 100644 index 000000000..78766ece3 --- /dev/null +++ b/src/api/services/comment/comment.validator.ts @@ -0,0 +1,23 @@ +import { z } from "zod"; + +export const commentSchema = z + .object({ + content: z + .string() + .min(1, { message: "댓글 내용은 최소 1자 이상이어야 합니다." }) + .max(500, { message: "댓글 내용은 최대 500자 이하여야 합니다." }), + productId: z.number().optional(), + articleId: z.number().optional(), + }) + .refine( + (data) => { + return ( + (data.productId !== undefined && data.articleId === undefined) || + (data.productId == undefined && data.articleId != undefined) + ); + }, + { + message: "productId와 articleId 중 하나만 제공되어야 합니다.", + path: ["productId", "articleId"], + } + ); diff --git a/src/api/services/mypage/mypage.validator.ts b/src/api/services/mypage/mypage.validator.ts new file mode 100644 index 000000000..d7d6adbbb --- /dev/null +++ b/src/api/services/mypage/mypage.validator.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +export const updateUserSchema = z + .object({ + nickname: z.string().min(2, { message: "닉네임은 최소 2자 이상이어야 합니다." }).optional(), + image: z.url({ message: "유효한 URL 형식이 아닙니다." }).optional(), + }) + .refine( + (data) => { + return Object.keys(data).length > 0; + }, + { message: "수정할 내용을 하나 이상 입력해주세요." } + ); + +export const updatePasswordSchema = z.object({ + oldPassword: z.string().min(1, { message: "기존 비밀번호를 입력해주세요." }), + newPassword: z.string().min(8, { message: "새로운 비밀번호는 최소 8자 이상이어야합니다." }), +}); diff --git a/src/api/services/product/product.validator.ts b/src/api/services/product/product.validator.ts new file mode 100644 index 000000000..c4ff9d33a --- /dev/null +++ b/src/api/services/product/product.validator.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const ProductSchema = z.object({ + name: z.string().min(1, "상품 이름을 입력하세요"), + price: z.number().positive("상품 가격은 0보다 커야 합니다."), + tags: z.array(z.string(), "태그는 문자열 배열이어야 합니다"), +}); diff --git a/src/api/types/dtos/auth.dto.ts b/src/api/types/dtos/auth.dto.ts index 01c518c2e..b4e8b2394 100644 --- a/src/api/types/dtos/auth.dto.ts +++ b/src/api/types/dtos/auth.dto.ts @@ -1,7 +1,5 @@ import { z } from "zod"; -// User 유효성 검사 미들웨어는 zod 활용해서 구현해봄 -// 이후에 시간 남으면 다른 유효성 검사도 zod로 바꾸기 export const signupSchema = z.object({ email: z.email({ message: "유효한 이메일 형식이 아닙니다." }), password: z.string().min(8, { message: "비밀번호는 최소 8자 이상이어야합니다." }), From 7d56e9230e6968f79d096107d3fe173a486061ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Fri, 19 Sep 2025 14:02:21 +0900 Subject: [PATCH 50/54] =?UTF-8?q?refactor:=20article=20&=20auth=20dto=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존 zod와 type으로 분리해 관리하던 데이터 타입을 class-validator를 사용해 하나로 관리 --- package-lock.json | 33 ++++++++++++++ package.json | 1 + src/api/controllers/article.controller.ts | 31 ++++++------- src/api/controllers/auth.controller.ts | 16 ++++--- src/api/middlewares/validator.ts | 43 +++++++++++++++++++ src/api/repositories/article.repository.ts | 4 +- src/api/routes/article.router.ts | 19 +++++--- src/api/routes/auth.router.ts | 8 ++-- src/api/services/article/article-param.dto.ts | 12 ++++++ src/api/services/article/article.dto.ts | 21 +++++++++ src/api/services/article/article.service.ts | 6 +-- src/api/services/article/article.validator.ts | 6 --- src/api/services/auth/auth.dto.ts | 35 +++++++++++++++ src/api/services/auth/auth.service.ts | 2 +- src/api/services/auth/auth.validator.ts | 12 ------ src/api/types/article.ts | 16 ------- src/api/types/dto.ts | 25 +++++++++++ src/api/types/dtos/article.dto.ts | 8 ---- src/api/types/dtos/auth.dto.ts | 16 ------- src/api/types/express.d.ts | 17 ++++++++ src/api/types/login.ts | 4 -- src/api/types/signup.ts | 5 --- 22 files changed, 231 insertions(+), 109 deletions(-) create mode 100644 src/api/middlewares/validator.ts create mode 100644 src/api/services/article/article-param.dto.ts create mode 100644 src/api/services/article/article.dto.ts delete mode 100644 src/api/services/article/article.validator.ts create mode 100644 src/api/services/auth/auth.dto.ts delete mode 100644 src/api/services/auth/auth.validator.ts delete mode 100644 src/api/types/article.ts create mode 100644 src/api/types/dto.ts delete mode 100644 src/api/types/dtos/article.dto.ts delete mode 100644 src/api/types/dtos/auth.dto.ts delete mode 100644 src/api/types/login.ts delete mode 100644 src/api/types/signup.ts diff --git a/package-lock.json b/package-lock.json index 0e60cf6ae..016412f5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@types/multer": "^2.0.0", "axios": "^1.10.0", "bcrypt": "^6.0.0", + "class-validator": "^0.14.2", "cloudinary": "^1.41.3", "cookie-parser": "^1.4.7", "express": "^5.1.0", @@ -608,6 +609,12 @@ "@types/send": "*" } }, + "node_modules/@types/validator": { + "version": "13.15.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", + "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.41.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz", @@ -1413,6 +1420,17 @@ "consola": "^3.2.3" } }, + "node_modules/class-validator": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", + "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.11.1", + "validator": "^13.9.0" + } + }, "node_modules/cloudinary": { "version": "1.41.3", "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-1.41.3.tgz", @@ -3512,6 +3530,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.17", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.17.tgz", + "integrity": "sha512-bsxi8FoceAYR/bjHcLYc2ShJ/aVAzo5jaxAYiMHF0BD+NTp47405CGuPNKYpw+lHadN9k/ClFGc9X5vaZswIrA==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5422,6 +5446,15 @@ "dev": true, "license": "MIT" }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index cece911e6..18e8f66af 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@types/multer": "^2.0.0", "axios": "^1.10.0", "bcrypt": "^6.0.0", + "class-validator": "^0.14.2", "cloudinary": "^1.41.3", "cookie-parser": "^1.4.7", "express": "^5.1.0", diff --git a/src/api/controllers/article.controller.ts b/src/api/controllers/article.controller.ts index 24db81218..aa1f40559 100644 --- a/src/api/controllers/article.controller.ts +++ b/src/api/controllers/article.controller.ts @@ -3,14 +3,16 @@ import jwt from "jsonwebtoken"; import type { Request, Response, NextFunction } from "express"; import { ACCESS_TOKEN_SECRET } from "../libs/constants.js"; import type { CustomError } from "src/api/types/error.js"; -import type { ArticleDto } from "../types/dtos/article.dto.js"; +import type { RequestWithDto } from "../types/express.d.ts"; +import type { ArticleDto } from "../services/article/article.dto.js"; +import { FindManyParamsDto } from "../types/dto.js"; const ArticleController = { - async createArticle(req: Request, res: Response, next: NextFunction) { + async createArticle(req: RequestWithDto, res: Response, next: NextFunction) { try { const { id: userId } = req.user; - const articleData: ArticleDto = req.body; - const newArticle = await ArticleService.createArticle(articleData, userId); + const articleDto = req.body; + const newArticle = await ArticleService.createArticle(articleDto, userId); res.status(201).json(newArticle); } catch (err) { @@ -46,12 +48,13 @@ const ArticleController = { } }, - async updateArticle(req: Request, res: Response, next: NextFunction) { + async updateArticle(req: RequestWithDto, res: Response, next: NextFunction) { try { const { id: userId } = req.user; const { id } = req.params; - const updateData: ArticleDto = req.body; - const article = await ArticleService.updateArticle(Number(id), updateData, userId); + const articleDto = req.body; + + const article = await ArticleService.updateArticle(Number(id), articleDto, userId); res.status(200).json(article); } catch (err) { next(err); @@ -71,19 +74,9 @@ const ArticleController = { async findManyArticle(req: Request, res: Response, next: NextFunction) { try { - const { offset = 0, limit = 10, order, keyword } = req.query; - - const finalOffset = Number(offset) || 0; - const finalLimit = Number(limit) || 10; - const finalOrder = typeof order === "string" ? order : "recent"; - const finalKeyword = typeof keyword === "string" ? keyword : undefined; + const params = FindManyParamsDto.from(req.query); - const articles = await ArticleService.findManyArticle({ - offset: finalOffset, - limit: finalLimit, - order: finalOrder, - ...(finalKeyword && { keyword: finalKeyword }), - }); + const articles = await ArticleService.findManyArticle(params); res.status(200).json(articles); } catch (err) { next(err); diff --git a/src/api/controllers/auth.controller.ts b/src/api/controllers/auth.controller.ts index 320d80b64..2b2bc2143 100644 --- a/src/api/controllers/auth.controller.ts +++ b/src/api/controllers/auth.controller.ts @@ -1,22 +1,24 @@ import AuthService from "../services/auth/auth.service.js"; import type { Request, Response, NextFunction } from "express"; -import type { SignupDto, LoginDto } from "../types/dtos/auth.dto.js"; +import type { SignupDto, LoginDto } from "../services/auth/auth.dto.js"; +import type { RequestWithDto } from "../types/express.d.ts"; const AuthController = { - async signup(req: Request, res: Response, next: NextFunction) { + async signup(req: RequestWithDto, res: Response, next: NextFunction) { try { - const signupData: SignupDto = req.body; - const newUser = await AuthService.signup(signupData); + const signupDto = req.body; + const newUser = await AuthService.signup(signupDto); + res.status(201).json(newUser); } catch (err) { next(err); } }, - async login(req: Request, res: Response, next: NextFunction) { + async login(req: RequestWithDto, res: Response, next: NextFunction) { try { - const loginData: LoginDto = req.body; - const { userWithoutPassword: user, accessToken, refreshToken } = await AuthService.login(loginData); + const loginDto = req.body; + const { userWithoutPassword: user, accessToken, refreshToken } = await AuthService.login(loginDto); res.cookie("refreshToken", refreshToken, { httpOnly: true, diff --git a/src/api/middlewares/validator.ts b/src/api/middlewares/validator.ts new file mode 100644 index 000000000..d037b1cdd --- /dev/null +++ b/src/api/middlewares/validator.ts @@ -0,0 +1,43 @@ +import { validate } from "class-validator"; +import type { Request, Response, NextFunction } from "express"; +import type { DtoClass } from "../types/express.d.ts"; + +// req.body 검증 및 DTO 생성 +export const validateDto = (dtoClass: DtoClass) => { + return async (req: Request, res: Response, next: NextFunction) => { + // 1. req.body를 DTO 클래스의 인스턴스로 변환 + const dtoObject = dtoClass.from(req.body); + + // 2. DTO 인스턴스의 유효성 검사 + const errors = await validate(dtoObject); + + if (errors.length > 0) { + const errorMessages = errors.map((error) => { + return Object.values(error.constraints || {}).join(", "); + }); + return res.status(400).json({ errors: errorMessages }); + } + + // 3. 완성된 DTO 객체를 req.body에 할당 + req.body = dtoObject; + next(); + }; +}; + +// req.params 검증 +export const validateParams = (dtoClass: DtoClass) => { + return async (req: Request, res: Response, next: NextFunction) => { + const dtoObject = dtoClass.from(req.params); + + const errors = await validate(dtoObject); + + if (errors.length > 0) { + const errorMessages = errors.map((error) => { + return Object.values(error.constraints || {}).join(", "); + }); + return res.status(400).json({ errors: errorMessages }); + } + + next(); + }; +}; diff --git a/src/api/repositories/article.repository.ts b/src/api/repositories/article.repository.ts index 46100d0e3..25489581f 100644 --- a/src/api/repositories/article.repository.ts +++ b/src/api/repositories/article.repository.ts @@ -1,6 +1,6 @@ import prisma from "../libs/prismaClient.js"; import { Prisma } from "@prisma/client"; -import type { FindManyArticleParams } from "../types/article.js"; +import type { FindManyParams } from "../types/express.d.ts"; // 게시글 생성 export const create = async (data: Prisma.ArticleCreateInput) => { @@ -13,7 +13,7 @@ export const findById = async (id: number) => { }; // 여러 게시글 조회 -export const findMany = async ({ offset, limit, order, keyword }: FindManyArticleParams) => { +export const findMany = async ({ offset = 0, limit = 10, order = "recent", keyword }: FindManyParams) => { let orderBy: Prisma.ArticleOrderByWithRelationInput; switch (order) { case "oldest": diff --git a/src/api/routes/article.router.ts b/src/api/routes/article.router.ts index 7914c8993..d262bc577 100644 --- a/src/api/routes/article.router.ts +++ b/src/api/routes/article.router.ts @@ -1,15 +1,22 @@ import express from "express"; import ArticleController from "../controllers/article.controller.js"; import authenticate from "../middlewares/authenticate.js"; -import validate from "../middlewares/validate.js"; -import { ArticleSchema } from "../services/article/article.validator.js"; +import { validateDto, validateParams } from "../middlewares/validator.js"; +import { ArticleDto } from "../services/article/article.dto.js"; +import { ArticleIdParamDto } from "../services/article/article-param.dto.js"; const router = express.Router(); -router.post("/", authenticate, validate(ArticleSchema), ArticleController.createArticle); -router.get("/:id", ArticleController.findUniqueArticle); -router.patch("/:id", authenticate, validate(ArticleSchema), ArticleController.updateArticle); -router.delete("/:id", authenticate, ArticleController.deleteArticle); +router.post("/", authenticate, validateDto(ArticleDto), ArticleController.createArticle); +router.get("/:id", validateParams(ArticleIdParamDto), ArticleController.findUniqueArticle); +router.patch( + "/:id", + authenticate, + validateParams(ArticleIdParamDto), + validateDto(ArticleDto), + ArticleController.updateArticle +); +router.delete("/:id", authenticate, validateParams(ArticleIdParamDto), ArticleController.deleteArticle); router.get("/", ArticleController.findManyArticle); export default router; diff --git a/src/api/routes/auth.router.ts b/src/api/routes/auth.router.ts index 824bc6e30..d643ac491 100644 --- a/src/api/routes/auth.router.ts +++ b/src/api/routes/auth.router.ts @@ -1,12 +1,12 @@ import express from "express"; import AuthController from "../controllers/auth.controller.js"; -import validate from "../middlewares/validate.js"; -import { loginSchema, signupSchema } from "../services/auth/auth.validator.js"; +import { validateDto } from "../middlewares/validator.js"; +import { SignupDto, LoginDto } from "../services/auth/auth.dto.js"; import authenticate from "../middlewares/authenticate.js"; const router = express.Router(); -router.post("/signup", validate(signupSchema), AuthController.signup); -router.post("/login", validate(loginSchema), AuthController.login); +router.post("/signup", validateDto(SignupDto), AuthController.signup); +router.post("/login", validateDto(LoginDto), AuthController.login); router.post("/logout", authenticate, AuthController.logout); router.post("/refresh-token", AuthController.refreshToken); export default router; diff --git a/src/api/services/article/article-param.dto.ts b/src/api/services/article/article-param.dto.ts new file mode 100644 index 000000000..f2acefb39 --- /dev/null +++ b/src/api/services/article/article-param.dto.ts @@ -0,0 +1,12 @@ +import { IsNumberString } from "class-validator"; + +export class ArticleIdParamDto { + @IsNumberString({}, { message: "게시글 ID는 숫자 형태여야 합니다." }) + id: string; + + public static from(data: { [key: string]: any }): ArticleIdParamDto { + const dto = new ArticleIdParamDto(); + dto.id = data.id; + return dto; + } +} diff --git a/src/api/services/article/article.dto.ts b/src/api/services/article/article.dto.ts new file mode 100644 index 000000000..6faaac150 --- /dev/null +++ b/src/api/services/article/article.dto.ts @@ -0,0 +1,21 @@ +import { IsString, IsNotEmpty } from "class-validator"; + +export class ArticleDto { + @IsString({ message: "제목은 문자열이어야 합니다." }) + @IsNotEmpty({ message: "제목을 입력하세요." }) + title: string; + + @IsString({ message: "내용은 문자열이어야 합니다." }) + @IsNotEmpty({ message: "내용을 입력하세요." }) + content: string; + + // DTO 생성 메서드 + public static from(data: { [key: string]: any }): ArticleDto { + const dto = new ArticleDto(); + + dto.title = data.title; + dto.content = data.content; + + return dto; + } +} diff --git a/src/api/services/article/article.service.ts b/src/api/services/article/article.service.ts index 66fd8a58e..50faf4014 100644 --- a/src/api/services/article/article.service.ts +++ b/src/api/services/article/article.service.ts @@ -1,7 +1,7 @@ -import type { CreateArticleData, UpdateArticleData, FindManyArticleParams } from "../../types/article.js"; import type { CustomError } from "../../types/error.js"; import * as ArticleRepository from "../../repositories/article.repository.js"; -import type { ArticleDto } from "../../types/dtos/article.dto.js"; +import type { ArticleDto } from "./article.dto.js"; +import type { FindManyParams } from "../../types/express.d.ts"; const ArticleService = { async createArticle(articleData: ArticleDto, userId: number) { @@ -65,7 +65,7 @@ const ArticleService = { await ArticleRepository.remove(id); }, - async findManyArticle(params: FindManyArticleParams) { + async findManyArticle(params: FindManyParams) { const articles = await ArticleRepository.findMany(params); return articles; }, diff --git a/src/api/services/article/article.validator.ts b/src/api/services/article/article.validator.ts deleted file mode 100644 index 7c89643b0..000000000 --- a/src/api/services/article/article.validator.ts +++ /dev/null @@ -1,6 +0,0 @@ -import z from "zod"; - -export const ArticleSchema = z.object({ - title: z.string().min(1, { message: "제목을 입력하세요." }), - content: z.string().min(1, { message: "내용을 입력하세요." }), -}); diff --git a/src/api/services/auth/auth.dto.ts b/src/api/services/auth/auth.dto.ts new file mode 100644 index 000000000..f1372b64d --- /dev/null +++ b/src/api/services/auth/auth.dto.ts @@ -0,0 +1,35 @@ +import { IsEmail, IsNotEmpty, MinLength } from "class-validator"; + +export class SignupDto { + @IsEmail({}, { message: "유효한 이메일 형식이 아닙니다." }) + email: string; + + @MinLength(8, { message: "비밀번호는 최소 8자 이상이어야합니다." }) + password: string; + + @MinLength(2, { message: "닉네임은 최소 2자 이상이어야 합니다." }) + nickname: string; + + public static from(data: { [key: string]: any }): SignupDto { + const dto = new SignupDto(); + dto.email = data.email; + dto.password = data.password; + dto.nickname = data.nickname; + return dto; + } +} + +export class LoginDto { + @IsEmail({}, { message: "유효한 이메일 형식이 아닙니다." }) + email: string; + + @IsNotEmpty({ message: "비밀번호를 입력해주세요." }) + password: string; + + public static from(data: { [key: string]: any }): LoginDto { + const dto = new LoginDto(); + dto.email = data.email; + dto.password = data.password; + return dto; + } +} diff --git a/src/api/services/auth/auth.service.ts b/src/api/services/auth/auth.service.ts index 45cd7c2c6..605e36e6d 100644 --- a/src/api/services/auth/auth.service.ts +++ b/src/api/services/auth/auth.service.ts @@ -4,7 +4,7 @@ import { generateTokens } from "../../libs/token.js"; import { REFRESH_TOKEN_SECRET } from "../../libs/constants.js"; import type { CustomError } from "src/api/types/error.js"; import * as AuthRepository from "../../repositories/auth.repository.js"; -import type { SignupDto, LoginDto } from "../../types/dtos/auth.dto.js"; +import type { SignupDto, LoginDto } from "./auth.dto.js"; const AuthService = { async signup(signupData: SignupDto) { diff --git a/src/api/services/auth/auth.validator.ts b/src/api/services/auth/auth.validator.ts deleted file mode 100644 index 23fc34150..000000000 --- a/src/api/services/auth/auth.validator.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from "zod"; - -export const signupSchema = z.object({ - email: z.email({ message: "유효한 이메일 형식이 아닙니다." }), - password: z.string().min(8, { message: "비밀번호는 최소 8자 이상이어야합니다." }), - nickname: z.string().min(2, { message: "닉네임은 최소 2자 이상이어야 합니다." }), -}); - -export const loginSchema = z.object({ - email: z.email({ message: "유효한 이메일 형식이 아닙니다." }), - password: z.string().min(1, { message: "비밀번호를 입력해주세요." }), -}); diff --git a/src/api/types/article.ts b/src/api/types/article.ts deleted file mode 100644 index 509713098..000000000 --- a/src/api/types/article.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface CreateArticleData { - title: string; - content: string; -} - -export interface UpdateArticleData { - title?: string; - content?: string; -} - -export type FindManyArticleParams = { - offset: number; - limit: number; - order?: string; - keyword?: string; -}; diff --git a/src/api/types/dto.ts b/src/api/types/dto.ts new file mode 100644 index 000000000..7e683f8ab --- /dev/null +++ b/src/api/types/dto.ts @@ -0,0 +1,25 @@ +import type { ParsedQs } from "qs"; +import type { FindManyParams } from "../types/express.d.ts"; + +export class FindManyParamsDto { + public static from(query: ParsedQs): FindManyParams { + const { offset, limit, order, keyword } = query; + + const params: FindManyParams = {}; + + // 값이 존재할 경우에만 객체에 속성을 추가 + if (offset) { + params.offset = Number(offset); + } + if (limit) { + params.limit = Number(limit); + } + if (typeof order === "string") { + params.order = order; + } + if (typeof keyword === "string") { + params.keyword = keyword; + } + return params; + } +} diff --git a/src/api/types/dtos/article.dto.ts b/src/api/types/dtos/article.dto.ts deleted file mode 100644 index d003fc83a..000000000 --- a/src/api/types/dtos/article.dto.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { z } from "zod"; - -export const ArticleSchema = z.object({ - title: z.string().min(1, { message: "제목을 입력하세요" }), - content: z.string().min(1, { message: "내용을 입력하세요." }), -}); - -export type ArticleDto = z.infer; diff --git a/src/api/types/dtos/auth.dto.ts b/src/api/types/dtos/auth.dto.ts deleted file mode 100644 index b4e8b2394..000000000 --- a/src/api/types/dtos/auth.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { z } from "zod"; - -export const signupSchema = z.object({ - email: z.email({ message: "유효한 이메일 형식이 아닙니다." }), - password: z.string().min(8, { message: "비밀번호는 최소 8자 이상이어야합니다." }), - nickname: z.string().min(2, { message: "닉네임은 최소 2자 이상이어야 합니다." }), -}); - -export type SignupDto = z.infer; - -export const loginSchema = z.object({ - email: z.email({ message: "유효한 이메일 형식이 아닙니다." }), - password: z.string().min(1, { message: "비밀번호를 입력해주세요." }), -}); - -export type LoginDto = z.infer; diff --git a/src/api/types/express.d.ts b/src/api/types/express.d.ts index 92be171b1..d90071e35 100644 --- a/src/api/types/express.d.ts +++ b/src/api/types/express.d.ts @@ -1,5 +1,6 @@ import { Request } from "express"; import { AuthenticatedUser } from "./user"; +import type { number } from "zod"; declare global { namespace Express { @@ -9,3 +10,19 @@ declare global { } } } + +export interface RequestWithDto extends Request { + body: T; +} + +export type DtoClass = { + from(data: any): T; +}; + +// 목록 조회 파라미터 타입 정의 +export interface FindManyParams { + offset?: number; + limit?: number; + order?: string; + keyword?: string; +} diff --git a/src/api/types/login.ts b/src/api/types/login.ts deleted file mode 100644 index c49460f4a..000000000 --- a/src/api/types/login.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface loginData { - email: string; - password: string; -} diff --git a/src/api/types/signup.ts b/src/api/types/signup.ts deleted file mode 100644 index d33ab8ec7..000000000 --- a/src/api/types/signup.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface SignupData { - email: string; - nickname: string; - password: string; -} From 9d5351deb70fd9d04b3317c54fe981888dc5b2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Fri, 19 Sep 2025 15:44:33 +0900 Subject: [PATCH 51/54] =?UTF-8?q?refactor:=20like,=20mypage,=20products=20?= =?UTF-8?q?dto=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/controllers/comment.controller.ts | 48 +++++------------- src/api/controllers/mypage.controller.ts | 15 +++--- src/api/controllers/product.controller.ts | 30 +++++------- src/api/repositories/comment.repository.ts | 4 +- src/api/repositories/product.repository.ts | 4 +- src/api/routes/comment.router.ts | 17 +++++-- src/api/routes/like.router.ts | 4 +- src/api/routes/mypage.router.ts | 8 +-- src/api/routes/product.router.ts | 19 ++++--- .../services/comment/comment-findmany.dto.ts | 22 +++++++++ src/api/services/comment/comment-param.dto.ts | 12 +++++ src/api/services/comment/comment.dto.ts | 39 +++++++++++++++ src/api/services/comment/comment.service.ts | 49 ++++++++++++++----- src/api/services/comment/comment.validator.ts | 23 --------- src/api/services/like/like.dto.ts | 16 ++++++ src/api/services/like/like.service.ts | 23 ++++++--- src/api/services/mypage/mypage.dto.ts | 36 ++++++++++++++ src/api/services/mypage/mypage.service.ts | 24 +++++---- src/api/services/mypage/mypage.validator.ts | 18 ------- .../services/product/product-params.dto.ts | 12 +++++ src/api/services/product/product.dto.ts | 26 ++++++++++ src/api/services/product/product.service.ts | 9 ++-- src/api/services/product/product.validator.ts | 7 --- src/api/types/comment.ts | 17 ------- src/api/types/dtos/comment.dto.ts | 25 ---------- src/api/types/dtos/mypage.dto.ts | 22 --------- src/api/types/dtos/product.dto.ts | 9 ---- src/api/types/like.ts | 5 -- src/api/types/product.ts | 22 --------- src/api/types/user.ts | 5 -- 30 files changed, 301 insertions(+), 269 deletions(-) create mode 100644 src/api/services/comment/comment-findmany.dto.ts create mode 100644 src/api/services/comment/comment-param.dto.ts create mode 100644 src/api/services/comment/comment.dto.ts delete mode 100644 src/api/services/comment/comment.validator.ts create mode 100644 src/api/services/like/like.dto.ts create mode 100644 src/api/services/mypage/mypage.dto.ts delete mode 100644 src/api/services/mypage/mypage.validator.ts create mode 100644 src/api/services/product/product-params.dto.ts create mode 100644 src/api/services/product/product.dto.ts delete mode 100644 src/api/services/product/product.validator.ts delete mode 100644 src/api/types/comment.ts delete mode 100644 src/api/types/dtos/comment.dto.ts delete mode 100644 src/api/types/dtos/mypage.dto.ts delete mode 100644 src/api/types/dtos/product.dto.ts delete mode 100644 src/api/types/like.ts delete mode 100644 src/api/types/product.ts delete mode 100644 src/api/types/user.ts diff --git a/src/api/controllers/comment.controller.ts b/src/api/controllers/comment.controller.ts index afdce012e..a6017af93 100644 --- a/src/api/controllers/comment.controller.ts +++ b/src/api/controllers/comment.controller.ts @@ -1,39 +1,31 @@ import CommentService from "../services/comment/comment.service.js"; import type { Request, Response, NextFunction } from "express"; -import type { CommentDto } from "../types/dtos/comment.dto.js"; +import type { RequestWithDto } from "../types/express.d.ts"; +import { CreateCommentDto, UpdateCommentDto } from "../services/comment/comment.dto.js"; +import { FindManyCommentsQueryDto } from "../services/comment/comment-findmany.dto.js"; const CommentController = { - async createComment(req: Request, res: Response, next: NextFunction) { + async createComment(req: RequestWithDto, res: Response, next: NextFunction) { try { const { id: userId } = req.user; - const commentData: CommentDto = req.body; + const commentDto = req.body; - const newComment = await CommentService.createComment({ - ...commentData, - userId, - }); + const newComment = await CommentService.createComment(commentDto, userId); res.status(201).json(newComment); } catch (err) { - if (err.code === "P2003") { - const target = req.body.productId ? "상품" : "게시글"; - return res.status(404).json({ error: `존재하지 않는 ${target}` }); - } next(err); } }, - async updateComment(req: Request, res: Response, next: NextFunction) { + async updateComment(req: RequestWithDto, res: Response, next: NextFunction) { try { const { id: userId } = req.user; const { id } = req.params; - const updateData: CommentDto = req.body; + const commentDto = req.body; - const comment = await CommentService.updateComment(Number(id), updateData, userId); + const comment = await CommentService.updateComment(Number(id), commentDto, userId); res.status(200).json(comment); } catch (err) { - if (err.code === "P2025") { - res.status(404).json({ error: "해당 댓글이 존재하지 않음" }); - } next(err); } }, @@ -45,32 +37,14 @@ const CommentController = { await CommentService.deleteComment(Number(id), userId); res.status(204).json({ success: "댓글 삭제" }); } catch (err) { - if (err.code === "P2025") { - res.status(404).json({ error: "해당 댓글이 존재하지 않음" }); - } next(err); } }, async findManyComment(req: Request, res: Response, next: NextFunction) { try { - const q = req.query; - - const productId = typeof q.productId === "string" ? Number(q.productId) : undefined; - const articleId = typeof q.articleId === "string" ? Number(q.articleId) : undefined; - const cursor = typeof q.cursor === "string" ? q.cursor : undefined; - const limit = typeof q.limit === "string" ? parseInt(q.limit, 10) : 10; - - if (productId !== undefined && articleId !== undefined) { - return res.status(400).json({ error: "productId 혹은 articleId 둘 중 하나만 있어야 함" }); - } - - const comments = await CommentService.findManyComment({ - ...(productId !== undefined && { productId }), - ...(articleId !== undefined && { articleId }), - ...(cursor !== undefined && { cursor }), - limit, - }); + const params = FindManyCommentsQueryDto.from(req.query); + const comments = await CommentService.findManyComment(params); res.status(200).json(comments); } catch (err) { diff --git a/src/api/controllers/mypage.controller.ts b/src/api/controllers/mypage.controller.ts index f355e5574..5bc841655 100644 --- a/src/api/controllers/mypage.controller.ts +++ b/src/api/controllers/mypage.controller.ts @@ -1,6 +1,7 @@ import MypageService from "../services/mypage/mypage.service.js"; import type { Request, Response, NextFunction } from "express"; -import type { UpdateUserDTO, UpdatePasswordDTO } from "../types/dtos/mypage.dto.js"; +import type { RequestWithDto } from "../types/express.d.ts"; +import type { UpdateUserDto, UpdatePasswordDto } from "../services/mypage/mypage.dto.js"; const MypageController = { async getUser(req: Request, res: Response, next: NextFunction) { @@ -14,23 +15,23 @@ const MypageController = { } }, - async updateUser(req: Request, res: Response, next: NextFunction) { + async updateUser(req: RequestWithDto, res: Response, next: NextFunction) { try { const { id: userId } = req.user; - const updateData: UpdateUserDTO = req.body; - const updatedUser = await MypageService.updateUser(userId, updateData); + const updateUserDTO = req.body; + const updatedUser = await MypageService.updateUser(userId, updateUserDTO); res.status(200).json(updatedUser); } catch (err) { next(err); } }, - async updatePassword(req: Request, res: Response, next: NextFunction) { + async updatePassword(req: RequestWithDto, res: Response, next: NextFunction) { try { const { id: userId } = req.user; - const updatePasswordData: UpdatePasswordDTO = req.body; + const updatePasswordDTO = req.body; - const { accessToken, refreshToken } = await MypageService.updatePassword(userId, updatePasswordData); + const { accessToken, refreshToken } = await MypageService.updatePassword(userId, updatePasswordDTO); res.cookie("accessToken", accessToken, { httpOnly: true, maxAge: 1 * 60 * 60 * 1000, diff --git a/src/api/controllers/product.controller.ts b/src/api/controllers/product.controller.ts index a03a9bf43..b263b6924 100644 --- a/src/api/controllers/product.controller.ts +++ b/src/api/controllers/product.controller.ts @@ -2,15 +2,17 @@ import ProductService from "../services/product/product.service.js"; import jwt from "jsonwebtoken"; import { ACCESS_TOKEN_SECRET } from "../libs/constants.js"; import type { Request, Response, NextFunction } from "express"; +import type { RequestWithDto } from "../types/express.d.ts"; +import { ProductDto } from "../services/product/product.dto.js"; +import { FindManyParamsDto } from "../types/dto.js"; import type { CustomError } from "src/api/types/error.js"; -import type { ProductDto } from "../types/dtos/product.dto.js"; const ProductController = { - async createProduct(req: Request, res: Response, next: NextFunction) { + async createProduct(req: RequestWithDto, res: Response, next: NextFunction) { try { const { id: userId } = req.user; - const productData: ProductDto = req.body; - const newProduct = await ProductService.createProduct(productData, userId); + const productDto = req.body; + const newProduct = await ProductService.createProduct(productDto, userId); res.status(201).json(newProduct); } catch (err) { @@ -47,13 +49,13 @@ const ProductController = { } }, - async patchProduct(req: Request, res: Response, next: NextFunction) { + async patchProduct(req: RequestWithDto, res: Response, next: NextFunction) { try { const { id } = req.params; const { id: userId } = req.user; - const updateData: ProductDto = req.body; + const productDto = req.body; - const product = await ProductService.patchProduct(Number(id), updateData, userId); + const product = await ProductService.patchProduct(Number(id), productDto, userId); if (!product) { return res.status(404).json({ error: "수정할 상품이 없음" }); } @@ -77,19 +79,9 @@ const ProductController = { async findManyProduct(req: Request, res: Response, next: NextFunction) { try { - const { offset = 0, limit = 10, order = "recent", keyword } = req.query; + const params = FindManyParamsDto.from(req.query); - const finalOffset = Number(offset) || 0; - const finalLimit = Number(limit) || 10; - const finalOrder = typeof order === "string" ? order : "recent"; - const finalKeyword = typeof keyword === "string" ? keyword : undefined; - - const products = await ProductService.findManyProduct({ - offset: finalOffset, - limit: finalLimit, - order: finalOrder, - ...(finalKeyword && { keyword: finalKeyword }), - }); + const products = await ProductService.findManyProduct(params); res.status(200).json(products); } catch (err) { next(err); diff --git a/src/api/repositories/comment.repository.ts b/src/api/repositories/comment.repository.ts index 6c22b09e5..c06f79280 100644 --- a/src/api/repositories/comment.repository.ts +++ b/src/api/repositories/comment.repository.ts @@ -1,6 +1,6 @@ import prisma from "../libs/prismaClient.js"; -import type { FindManyCommentParams } from "../types/comment.js"; import { Prisma } from "@prisma/client"; +import type { FindManyCommentsQuery } from "../services/comment/comment-findmany.dto.js"; export const create = async (data: Prisma.CommentCreateInput) => { return await prisma.comment.create({ data }); @@ -23,7 +23,7 @@ export const remove = async (id: number) => { return await prisma.comment.delete({ where: { id } }); }; -export const findMany = async ({ productId, articleId, cursor, limit }: FindManyCommentParams) => { +export const findMany = async ({ productId, articleId, cursor, limit = 10 }: FindManyCommentsQuery) => { let where: Prisma.CommentWhereInput = {}; if (productId) { diff --git a/src/api/repositories/product.repository.ts b/src/api/repositories/product.repository.ts index 29f53c897..1da2c20b2 100644 --- a/src/api/repositories/product.repository.ts +++ b/src/api/repositories/product.repository.ts @@ -1,5 +1,5 @@ import prisma from "../libs/prismaClient.js"; -import type { FindManyProductParams } from "../types/product.js"; +import type { FindManyParams } from "../types/express.d.ts"; import { Prisma } from "@prisma/client"; export const create = async (data: Prisma.ProductCreateInput) => { @@ -31,7 +31,7 @@ export const remove = async (id: number) => { }); }; -export const findMany = async ({ offset, limit, order, keyword }: FindManyProductParams) => { +export const findMany = async ({ offset = 0, limit = 10, order = "recent", keyword }: FindManyParams) => { let orderBy: Prisma.ProductOrderByWithRelationInput; switch (order) { case "oldest": diff --git a/src/api/routes/comment.router.ts b/src/api/routes/comment.router.ts index 2a540f6bc..9bdf8254c 100644 --- a/src/api/routes/comment.router.ts +++ b/src/api/routes/comment.router.ts @@ -1,14 +1,21 @@ import express from "express"; import CommentController from "../controllers/comment.controller.js"; import authenticate from "../middlewares/authenticate.js"; -import validate from "../middlewares/validate.js"; -import { commentSchema } from "../services/comment/comment.validator.js"; +import { validateDto, validateParams } from "../middlewares/validator.js"; +import { CreateCommentDto, UpdateCommentDto } from "../services/comment/comment.dto.js"; +import { CommentIdParamDto } from "../services/comment/comment-param.dto.js"; const router = express.Router(); -router.post("/", authenticate, validate(commentSchema), CommentController.createComment); -router.patch("/:id", authenticate, validate(commentSchema), CommentController.updateComment); -router.delete("/:id", authenticate, CommentController.deleteComment); +router.post("/", authenticate, validateDto(CreateCommentDto), CommentController.createComment); +router.patch( + "/:id", + authenticate, + validateParams(CommentIdParamDto), + validateDto(CreateCommentDto), + CommentController.updateComment +); +router.delete("/:id", authenticate, validateParams(CommentIdParamDto), CommentController.deleteComment); router.get("/", CommentController.findManyComment); export default router; diff --git a/src/api/routes/like.router.ts b/src/api/routes/like.router.ts index f39744e3c..9f53270bf 100644 --- a/src/api/routes/like.router.ts +++ b/src/api/routes/like.router.ts @@ -1,9 +1,11 @@ import express from "express"; import authenticate from "../middlewares/authenticate.js"; import LikeController from "../controllers/like.controller.js"; +import { validateParams } from "../middlewares/validator.js"; +import { ToggleLikeParamDto } from "../services/like/like.dto.js"; const router = express.Router(); -router.post("/:type/:id", authenticate, LikeController.toggleLike); +router.post("/:type/:id", authenticate, validateParams(ToggleLikeParamDto), LikeController.toggleLike); export default router; diff --git a/src/api/routes/mypage.router.ts b/src/api/routes/mypage.router.ts index 6271e6f8e..556b6e64a 100644 --- a/src/api/routes/mypage.router.ts +++ b/src/api/routes/mypage.router.ts @@ -1,14 +1,14 @@ import express from "express"; import authenticate from "../middlewares/authenticate.js"; import MypageController from "../controllers/mypage.controller.js"; -import validate from "../middlewares/validate.js"; -import { updateUserSchema, updatePasswordSchema } from "../services/mypage/mypage.validator.js"; +import { validateDto } from "../middlewares/validator.js"; +import { UpdateUserDto, UpdatePasswordDto } from "../services/mypage/mypage.dto.js"; const router = express.Router(); router.get("/", authenticate, MypageController.getUser); -router.patch("/", authenticate, validate(updateUserSchema), MypageController.updateUser); -router.patch("/password", authenticate, validate(updatePasswordSchema), MypageController.updatePassword); +router.patch("/", authenticate, validateDto(UpdateUserDto), MypageController.updateUser); +router.patch("/password", authenticate, validateDto(UpdatePasswordDto), MypageController.updatePassword); router.delete("/", authenticate, MypageController.deleteUser); router.get("/products", authenticate, MypageController.getProducts); router.get("/like-products", authenticate, MypageController.getLikeProducts); diff --git a/src/api/routes/product.router.ts b/src/api/routes/product.router.ts index 31e00f7c2..5003e8be2 100644 --- a/src/api/routes/product.router.ts +++ b/src/api/routes/product.router.ts @@ -1,15 +1,22 @@ import express from "express"; import ProductController from "../controllers/product.controller.js"; import authenticate from "../middlewares/authenticate.js"; -import validate from "../middlewares/validate.js"; -import { ProductSchema } from "../services/product/product.validator.js"; +import { validateDto, validateParams } from "../middlewares/validator.js"; +import { ProductDto } from "../services/product/product.dto.js"; +import { ProductIdParamDto } from "../services/product/product-params.dto.js"; const router = express.Router(); -router.post("/", authenticate, validate(ProductSchema), ProductController.createProduct); -router.get("/:id", ProductController.findUniqueProduct); -router.patch("/:id", authenticate, validate(ProductSchema), ProductController.patchProduct); -router.delete("/:id", authenticate, ProductController.deleteProduct); +router.post("/", authenticate, validateDto(ProductDto), ProductController.createProduct); +router.get("/:id", validateParams(ProductIdParamDto), ProductController.findUniqueProduct); +router.patch( + "/:id", + authenticate, + validateParams(ProductIdParamDto), + validateDto(ProductDto), + ProductController.patchProduct +); +router.delete("/:id", authenticate, validateParams(ProductIdParamDto), ProductController.deleteProduct); router.get("/", ProductController.findManyProduct); export default router; diff --git a/src/api/services/comment/comment-findmany.dto.ts b/src/api/services/comment/comment-findmany.dto.ts new file mode 100644 index 000000000..52dcddded --- /dev/null +++ b/src/api/services/comment/comment-findmany.dto.ts @@ -0,0 +1,22 @@ +import type { ParsedQs } from "qs"; + +export interface FindManyCommentsQuery { + productId?: number; + articleId?: number; + cursor?: string; + limit?: number; +} + +export class FindManyCommentsQueryDto { + public static from(query: ParsedQs): FindManyCommentsQuery { + const { productId, articleId, cursor, limit } = query; + const params: FindManyCommentsQuery = {}; + + if (productId) params.productId = Number(productId); + if (articleId) params.articleId = Number(articleId); + if (typeof cursor === "string") params.cursor = cursor; + if (limit) params.limit = Number(limit); + + return params; + } +} diff --git a/src/api/services/comment/comment-param.dto.ts b/src/api/services/comment/comment-param.dto.ts new file mode 100644 index 000000000..663caaebf --- /dev/null +++ b/src/api/services/comment/comment-param.dto.ts @@ -0,0 +1,12 @@ +import { IsNumberString, MinLength, MaxLength, IsOptional, IsNumber } from "class-validator"; + +export class CommentIdParamDto { + @IsNumberString({}, { message: "댓글 ID는 숫자 형태여야 합니다." }) + id: string; + + public static from(data: { [key: string]: any }): CommentIdParamDto { + const dto = new CommentIdParamDto(); + dto.id = data.id; + return dto; + } +} diff --git a/src/api/services/comment/comment.dto.ts b/src/api/services/comment/comment.dto.ts new file mode 100644 index 000000000..1d09e2d7d --- /dev/null +++ b/src/api/services/comment/comment.dto.ts @@ -0,0 +1,39 @@ +import { IsString, MinLength, MaxLength, IsOptional, IsNumber } from "class-validator"; + +// 댓글 생성용 DTO +export class CreateCommentDto { + @IsString() + @MinLength(1, { message: "댓글 내용은 최소 1자 이상이어야 합니다." }) + @MaxLength(500, { message: "댓글 내용은 최대 500자 이하여야 합니다." }) + content: string; + + @IsNumber() + @IsOptional() + productId?: number; + + @IsNumber() + @IsOptional() + articleId?: number; + + public static from(data: { [key: string]: any }): CreateCommentDto { + const dto = new CreateCommentDto(); + dto.content = data.content; + dto.productId = data.productId; + dto.articleId = data.articleId; + return dto; + } +} + +// 댓글 수정용 DTO +export class UpdateCommentDto { + @IsString() + @MinLength(1, { message: "댓글 내용은 최소 1자 이상이어야 합니다." }) + @MaxLength(500, { message: "댓글 내용은 최대 500자 이하여야 합니다." }) + content: string; + + public static from(data: { [key: string]: any }): UpdateCommentDto { + const dto = new UpdateCommentDto(); + dto.content = data.content; + return dto; + } +} diff --git a/src/api/services/comment/comment.service.ts b/src/api/services/comment/comment.service.ts index 5618b7901..646a295fb 100644 --- a/src/api/services/comment/comment.service.ts +++ b/src/api/services/comment/comment.service.ts @@ -1,21 +1,44 @@ import type { CustomError } from "../../types/error.js"; -import type { FindManyCommentParams } from "../../types/comment.js"; import * as CommentRepository from "../../repositories/comment.repository.js"; -import type { CommentDto } from "../../types/dtos/comment.dto.js"; +import type { CreateCommentDto, UpdateCommentDto } from "./comment.dto.js"; +import type { FindManyCommentsQuery } from "./comment-findmany.dto.js"; const CommentService = { - async createComment(data: CommentDto & { userId: number }) { - const { content, productId, articleId, userId } = data; - const newComment = await CommentRepository.create({ - content, - ...(productId && { product: { connect: { id: productId } } }), - ...(articleId && { article: { connect: { id: articleId } } }), - user: { connect: { id: userId } }, - }); - return newComment; + async createComment(commentDto: CreateCommentDto, userId: number) { + const { content, productId, articleId } = commentDto; + + if (productId && articleId) { + const error: CustomError = new Error("productId와 articleId 중 하나만 제공되어야 합니다."); + error.statusCode = 400; + throw error; + } + + if (!productId && !articleId) { + const error: CustomError = new Error("productId 또는 articleId는 필수입니다."); + error.statusCode = 400; + throw error; + } + + try { + const newComment = await CommentRepository.create({ + content, + ...(productId && { product: { connect: { id: productId } } }), + ...(articleId && { article: { connect: { id: articleId } } }), + user: { connect: { id: userId } }, + }); + return newComment; + } catch (err) { + if (err.code === "P2003") { + const target = productId ? "상품" : "게시글"; + const error: CustomError = new Error(`존재하지 않는 ${target}입니다.`); + error.statusCode = 404; + throw error; + } + throw err; + } }, - async updateComment(id: number, updateData: CommentDto, userId: number) { + async updateComment(id: number, updateData: UpdateCommentDto, userId: number) { const comment = await CommentRepository.findById(id); if (!comment) { @@ -51,7 +74,7 @@ const CommentService = { return await CommentRepository.remove(id); }, - async findManyComment(params: FindManyCommentParams) { + async findManyComment(params: FindManyCommentsQuery) { const comments = await CommentRepository.findMany(params); return comments; }, diff --git a/src/api/services/comment/comment.validator.ts b/src/api/services/comment/comment.validator.ts deleted file mode 100644 index 78766ece3..000000000 --- a/src/api/services/comment/comment.validator.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { z } from "zod"; - -export const commentSchema = z - .object({ - content: z - .string() - .min(1, { message: "댓글 내용은 최소 1자 이상이어야 합니다." }) - .max(500, { message: "댓글 내용은 최대 500자 이하여야 합니다." }), - productId: z.number().optional(), - articleId: z.number().optional(), - }) - .refine( - (data) => { - return ( - (data.productId !== undefined && data.articleId === undefined) || - (data.productId == undefined && data.articleId != undefined) - ); - }, - { - message: "productId와 articleId 중 하나만 제공되어야 합니다.", - path: ["productId", "articleId"], - } - ); diff --git a/src/api/services/like/like.dto.ts b/src/api/services/like/like.dto.ts new file mode 100644 index 000000000..fb002f455 --- /dev/null +++ b/src/api/services/like/like.dto.ts @@ -0,0 +1,16 @@ +import { IsIn, IsNumberString } from "class-validator"; + +export class ToggleLikeParamDto { + @IsIn(["product", "article"], { message: 'type은 "product" 또는 "article"이어야 합니다.' }) + type: "product" | "article"; + + @IsNumberString({}, { message: "ID는 숫자 형태여야 합니다." }) + id: string; + + public static from(data: { [key: string]: any }): ToggleLikeParamDto { + const dto = new ToggleLikeParamDto(); + dto.type = data.type; + dto.id = data.id; + return dto; + } +} diff --git a/src/api/services/like/like.service.ts b/src/api/services/like/like.service.ts index 87a40c4c4..e75065d68 100644 --- a/src/api/services/like/like.service.ts +++ b/src/api/services/like/like.service.ts @@ -1,17 +1,28 @@ -import type { LikeData } from "../../types/like.js"; import * as LikeRepository from "../../repositories/like.repository.js"; +import * as ArticleRepository from "../../repositories/article.repository.js"; +import * as ProductRepository from "../../repositories/product.repository.js"; import type { Prisma } from "@prisma/client"; +import type { CustomError } from "../../types/error.js"; const LikeService = { async toggleLike(userId: number, type: string, contentId: number) { - const user: LikeData = { userId }; - if (type === "article") { - user.articleId = contentId; + const article = await ArticleRepository.findById(contentId); + + if (!article) { + const error: CustomError = new Error("존재하지 않는 게시글입니다."); + error.statusCode = 404; + throw error; } else { - user.productId = contentId; + const product = await ProductRepository.findById(contentId); + if (!product) { + const error: CustomError = new Error("존재하지 않는 상품입니다."); + error.statusCode = 404; + throw error; + } } - const existingLike = await LikeRepository.findFirst(user); + const where = type === "article" ? { userId, articleId: contentId } : { userId, productId: contentId }; + const existingLike = await LikeRepository.findFirst(where); if (existingLike) { await LikeRepository.remove(existingLike.id); diff --git a/src/api/services/mypage/mypage.dto.ts b/src/api/services/mypage/mypage.dto.ts new file mode 100644 index 000000000..d08b1f951 --- /dev/null +++ b/src/api/services/mypage/mypage.dto.ts @@ -0,0 +1,36 @@ +import { IsString, MinLength, IsUrl, IsOptional } from "class-validator"; + +export class UpdateUserDto { + @IsOptional() + @IsOptional() + @MinLength(2, { message: "닉네임은 최소 2자 이상이어야 합니다." }) + nickname?: string; + + @IsOptional() + @IsUrl({}, { message: "유효한 URL 형식이 아닙니다." }) + image?: string; + + public static from(data: { [key: string]: any }): UpdateUserDto { + const dto = new UpdateUserDto(); + dto.nickname = data.nickname; + dto.image = data.image; + return dto; + } +} + +export class UpdatePasswordDto { + @IsString() + @MinLength(1, { message: "기존 비밀번호를 입력해주세요." }) + oldPassword: string; + + @IsString() + @MinLength(8, { message: "새로운 비밀번호는 최소 8자 이상이어야합니다." }) + newPassword: string; + + public static from(data: { [key: string]: any }): UpdatePasswordDto { + const dto = new UpdatePasswordDto(); + dto.oldPassword = data.oldPassword; + dto.newPassword = data.newPassword; + return dto; + } +} diff --git a/src/api/services/mypage/mypage.service.ts b/src/api/services/mypage/mypage.service.ts index 4e9f23d66..5aa40efb8 100644 --- a/src/api/services/mypage/mypage.service.ts +++ b/src/api/services/mypage/mypage.service.ts @@ -1,12 +1,11 @@ import bcrypt from "bcrypt"; import type { CustomError } from "../../types/error.js"; import * as MypageRepository from "../../repositories/mypage.repository.js"; -import type { UpdateUserDTO, UpdatePasswordDTO } from "../../types/dtos/mypage.dto.js"; import type { Prisma } from "@prisma/client"; import { generateTokens } from "../../libs/token.js"; import { hashing } from "../../libs/hashing.js"; import * as AuthRepository from "../../repositories/auth.repository.js"; -import { log } from "console"; +import { UpdateUserDto, UpdatePasswordDto } from "./mypage.dto.js"; const MypageService = { async getUser(userId: number) { @@ -21,24 +20,29 @@ const MypageService = { return user; }, - async updateUser(userId: number, updateData: UpdateUserDTO) { - const dataToUpdate: Prisma.UserUpdateInput = {}; + async updateUser(userId: number, updateData: UpdateUserDto) { + const { nickname, image } = updateData; - if (updateData.nickname !== undefined) { - dataToUpdate.nickname = updateData.nickname; + if (!nickname && !image) { + const error: CustomError = new Error("수정할 내용을 하나 이상 입력해주세요."); + error.statusCode = 400; + throw error; } - if (updateData.image !== undefined) { - dataToUpdate.nickname = updateData.image; + const dataToUpdate: Prisma.UserUpdateInput = {}; + if (nickname) { + dataToUpdate.nickname = nickname; + } + if (image) { + dataToUpdate.image = image; } const updatedUser = await MypageRepository.update(userId, dataToUpdate); - const { password, refreshToken, ...UserData } = updatedUser; return UserData; }, - async updatePassword(userId: number, updatePasswordDTO: UpdatePasswordDTO) { + async updatePassword(userId: number, updatePasswordDTO: UpdatePasswordDto) { const { oldPassword, newPassword } = updatePasswordDTO; const user = await MypageRepository.findUserForAuth(userId); diff --git a/src/api/services/mypage/mypage.validator.ts b/src/api/services/mypage/mypage.validator.ts deleted file mode 100644 index d7d6adbbb..000000000 --- a/src/api/services/mypage/mypage.validator.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { z } from "zod"; - -export const updateUserSchema = z - .object({ - nickname: z.string().min(2, { message: "닉네임은 최소 2자 이상이어야 합니다." }).optional(), - image: z.url({ message: "유효한 URL 형식이 아닙니다." }).optional(), - }) - .refine( - (data) => { - return Object.keys(data).length > 0; - }, - { message: "수정할 내용을 하나 이상 입력해주세요." } - ); - -export const updatePasswordSchema = z.object({ - oldPassword: z.string().min(1, { message: "기존 비밀번호를 입력해주세요." }), - newPassword: z.string().min(8, { message: "새로운 비밀번호는 최소 8자 이상이어야합니다." }), -}); diff --git a/src/api/services/product/product-params.dto.ts b/src/api/services/product/product-params.dto.ts new file mode 100644 index 000000000..b41e33496 --- /dev/null +++ b/src/api/services/product/product-params.dto.ts @@ -0,0 +1,12 @@ +import { IsNumberString } from "class-validator"; + +export class ProductIdParamDto { + @IsNumberString({}, { message: "상품 ID는 숫자 형태여야 합니다." }) + id: string; + + public static from(data: { [key: string]: any }): ProductIdParamDto { + const dto = new ProductIdParamDto(); + dto.id = data.id; + return dto; + } +} diff --git a/src/api/services/product/product.dto.ts b/src/api/services/product/product.dto.ts new file mode 100644 index 000000000..142afcdf4 --- /dev/null +++ b/src/api/services/product/product.dto.ts @@ -0,0 +1,26 @@ +import { IsString, IsNotEmpty, IsNumber, IsPositive, IsArray } from "class-validator"; + +export class ProductDto { + @IsString({ message: "제목은 문자열이어야 합니다." }) + @IsNotEmpty({ message: "제목을 입력하세요." }) + name: string; + + @IsNumber({}, { message: "상품 가격은 숫자여야 합니다." }) + @IsPositive({ message: "상품 가격은 0보다 커야 합니다." }) + price: number; + + @IsArray({ message: "태그는 배열이어야 합니다." }) + @IsString({ each: true, message: "각 태그는 문자열이어야 합니다." }) + tags: string[]; + + // DTO 생성 메서드 + public static from(data: { [key: string]: any }): ProductDto { + const dto = new ProductDto(); + + dto.name = data.name; + dto.price = data.price; + dto.tags = dto.tags; + + return dto; + } +} diff --git a/src/api/services/product/product.service.ts b/src/api/services/product/product.service.ts index 90174004d..c81f3aea7 100644 --- a/src/api/services/product/product.service.ts +++ b/src/api/services/product/product.service.ts @@ -1,7 +1,7 @@ import type { CustomError } from "../../types/error.js"; -import type { CreateProductData, UpdateProductData, FindManyProductParams } from "../../types/product.js"; +import type { FindManyParams } from "../../types/express.d.ts"; import * as ProductRepository from "../../repositories/product.repository.js"; -import type { ProductDto } from "../../types/dtos/product.dto.js"; +import type { ProductDto } from "./product.dto.js"; const ProductService = { async createProduct(productData: ProductDto, userId: number) { @@ -57,8 +57,9 @@ const ProductService = { return await ProductRepository.remove(id); }, - async findManyProduct(params: FindManyProductParams) { - return await ProductRepository.findMany(params); + async findManyProduct(params: FindManyParams) { + const products = await ProductRepository.findMany(params); + return products; }, }; diff --git a/src/api/services/product/product.validator.ts b/src/api/services/product/product.validator.ts deleted file mode 100644 index c4ff9d33a..000000000 --- a/src/api/services/product/product.validator.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from "zod"; - -export const ProductSchema = z.object({ - name: z.string().min(1, "상품 이름을 입력하세요"), - price: z.number().positive("상품 가격은 0보다 커야 합니다."), - tags: z.array(z.string(), "태그는 문자열 배열이어야 합니다"), -}); diff --git a/src/api/types/comment.ts b/src/api/types/comment.ts deleted file mode 100644 index 92220b155..000000000 --- a/src/api/types/comment.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface CreateCommentData { - content: string; - productId?: number; - articleId?: number; - userId: number; -} - -export interface UpdateCommentData { - content?: string; -} - -export type FindManyCommentParams = { - productId?: number; - articleId?: number; - cursor?: string; - limit: number; -}; diff --git a/src/api/types/dtos/comment.dto.ts b/src/api/types/dtos/comment.dto.ts deleted file mode 100644 index 1eae6b375..000000000 --- a/src/api/types/dtos/comment.dto.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { z } from "zod"; - -export const commentSchema = z - .object({ - content: z - .string() - .min(1, { message: "댓글 내용은 최소 1자 이상이어야 합니다." }) - .max(500, { message: "댓글 내용은 최대 500자 이하여야 합니다." }), - productId: z.number().optional(), - articleId: z.number().optional(), - }) - .refine( - (data) => { - return ( - (data.productId !== undefined && data.articleId === undefined) || - (data.productId == undefined && data.articleId != undefined) - ); - }, - { - message: "productId와 articleId 중 하나만 제공되어야 합니다.", - path: ["productId", "articleId"], - } - ); - -export type CommentDto = z.infer; diff --git a/src/api/types/dtos/mypage.dto.ts b/src/api/types/dtos/mypage.dto.ts deleted file mode 100644 index 2a50ac7cf..000000000 --- a/src/api/types/dtos/mypage.dto.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { z } from "zod"; - -export const updateUserSchema = z - .object({ - nickname: z.string().min(2, { message: "닉네임은 최소 2자 이상이어야 합니다." }).optional(), - image: z.url({ message: "유효한 URL 형식이 아닙니다." }).optional(), - }) - .refine( - (data) => { - return Object.keys(data).length > 0; - }, - { message: "수정할 내용을 하나 이상 입력해주세요." } - ); - -export type UpdateUserDTO = z.infer; - -export const updatePasswordSchema = z.object({ - oldPassword: z.string().min(1, { message: "기존 비밀번호를 입력해주세요." }), - newPassword: z.string().min(8, { message: "새로운 비밀번호는 최소 8자 이상이어야합니다." }), -}); - -export type UpdatePasswordDTO = z.infer; diff --git a/src/api/types/dtos/product.dto.ts b/src/api/types/dtos/product.dto.ts deleted file mode 100644 index baf226f17..000000000 --- a/src/api/types/dtos/product.dto.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { z } from "zod"; - -export const ProductSchema = z.object({ - name: z.string().min(1, "상품 이름을 입력하세요"), - price: z.number().positive("상품 가격은 0보다 커야 합니다."), - tags: z.array(z.string(), "태그는 문자열 배열이어야 합니다"), -}); - -export type ProductDto = z.infer; diff --git a/src/api/types/like.ts b/src/api/types/like.ts deleted file mode 100644 index 858071e94..000000000 --- a/src/api/types/like.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface LikeData { - userId: number; - articleId?: number; - productId?: number; -} diff --git a/src/api/types/product.ts b/src/api/types/product.ts deleted file mode 100644 index 51225af21..000000000 --- a/src/api/types/product.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface CreateProductData { - name: string; - description?: string; - price: number; - tags: string[]; -} - -export interface UpdateProductData { - name?: string; - description?: string; - price?: number; - tags?: string[]; -} - -export type ProductOrder = "oldest" | "recent"; - -export interface FindManyProductParams { - offset: number; - limit: number; - order?: string; - keyword?: string; -} diff --git a/src/api/types/user.ts b/src/api/types/user.ts deleted file mode 100644 index 4deaf23db..000000000 --- a/src/api/types/user.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface UserData { - email: string; - nickname?: string; - image?: string; -} From 4e9759600e00d850a57a01ed7b57fabbada67c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 24 Sep 2025 11:23:39 +0900 Subject: [PATCH 52/54] =?UTF-8?q?refactor:=20=EB=82=A8=EC=95=84=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20js=20=ED=8C=8C=EC=9D=BC=20ts=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 --- src/external/services/article/article.dto.ts | 43 +++++++++++++++ .../article.service.ts} | 24 +++++---- src/external/services/product/product.dto.ts | 52 +++++++++++++++++++ .../product.service.ts} | 15 +++--- src/external/stores/product_store.js | 41 --------------- src/external/stores/product_store.ts | 47 +++++++++++++++++ ...cle.service.js => test_article.service.ts} | 29 +++++++---- ...uct.service.js => test_product.service.ts} | 7 +-- 8 files changed, 186 insertions(+), 72 deletions(-) create mode 100644 src/external/services/article/article.dto.ts rename src/external/services/{article.service.js => article/article.service.ts} (76%) create mode 100644 src/external/services/product/product.dto.ts rename src/external/services/{product.service.js => product/product.service.ts} (76%) delete mode 100644 src/external/stores/product_store.js create mode 100644 src/external/stores/product_store.ts rename src/external/tests/{test_article.service.js => test_article.service.ts} (65%) rename src/external/tests/{test_product.service.js => test_product.service.ts} (91%) diff --git a/src/external/services/article/article.dto.ts b/src/external/services/article/article.dto.ts new file mode 100644 index 000000000..a6d54be7e --- /dev/null +++ b/src/external/services/article/article.dto.ts @@ -0,0 +1,43 @@ +export interface ArticleListQuery { + page?: number; + limit?: number; + sort?: string; + order?: "asc" | "desc"; + search?: string; + [key: string]: any; +} + +export interface ArticleData { + id: string; + title: string; + content: string; + likeCount: number; + createdAt: number; +} + +export interface CreateArticleDto { + title: string; + content: string; +} + +export interface PatchArticleDto { + title: string; + content: string; +} + +export interface ArticleListResponse { + list: ArticleData[]; +} + +export function isArticleData(item: unknown): item is ArticleData { + if (typeof item !== "object" || item === null) return false; + const article = item as Record; + return ( + typeof article.id === "string" && + typeof article.title === "string" && + typeof article.content === "string" && + typeof article.writer === "string" && + typeof article.likeCount === "number" && + typeof article.createdAt === "number" + ); +} diff --git a/src/external/services/article.service.js b/src/external/services/article/article.service.ts similarity index 76% rename from src/external/services/article.service.js rename to src/external/services/article/article.service.ts index d79af3608..511d7d3ec 100644 --- a/src/external/services/article.service.js +++ b/src/external/services/article/article.service.ts @@ -1,20 +1,24 @@ +import type { ArticleListQuery, ArticleListResponse, CreateArticleDto, PatchArticleDto } from "./article.dto.js"; + const BASE_URL = "https://panda-market-api-crud.vercel.app/articles"; const ArticleService = { // 기사 목록 조회 - getArticleList(query) { + getArticleList(query?: ArticleListQuery): Promise { const url = new URL(BASE_URL); - for (const [key, value] of Object.entries(query)) { - if (value !== undefined) { - url.searchParams.append(key, value); + if (query) { + for (const key in query) { + if (query[key] !== undefined) { + url.searchParams.append(key, String(query[key])); + } } } - return fetch(url) + return fetch(url.toString()) .then((res) => { if (!res.ok) throw new Error(`[error] 상태 코드: ${res.status}`); - return res.json(); + return res.json() as Promise; // 이후에 수정.. }) .catch((err) => { console.error("[error] 요청 실패:", err.message); @@ -23,7 +27,7 @@ const ArticleService = { }, // 특정 기사 조회 - getArticle(id) { + getArticle(id: string) { return fetch(`${BASE_URL}/${id}`) .then((res) => { if (!res.ok) throw new Error(`[error] 상태 코드: ${res.status}`); @@ -36,7 +40,7 @@ const ArticleService = { }, // 기사 생성 - createArticle(articleData) { + createArticle(articleData: CreateArticleDto) { return fetch(BASE_URL, { method: "POST", headers: { @@ -55,7 +59,7 @@ const ArticleService = { }, // 기사 수정 - patchArticle(id, articlePatchData) { + patchArticle(id: string, articlePatchData: PatchArticleDto) { return fetch(`${BASE_URL}/${id}`, { method: "PATCH", headers: { @@ -74,7 +78,7 @@ const ArticleService = { }, // 기사 삭제 - deleteArticle(id) { + deleteArticle(id: string) { return fetch(`${BASE_URL}/${id}`, { method: "DELETE", }) diff --git a/src/external/services/product/product.dto.ts b/src/external/services/product/product.dto.ts new file mode 100644 index 000000000..902407471 --- /dev/null +++ b/src/external/services/product/product.dto.ts @@ -0,0 +1,52 @@ +// getProductList의 쿼리 파라미터 타입 +export interface ProductListQuery { + page?: number; + limit?: number; + sort?: string; + order?: "asc" | "desc"; + search?: string; + [key: string]: any; +} + +// API로부터 받는 상품 데이터의 타입 +export interface ProductData { + id: string; + name: string; + description: string; + price: number; + tags: string[]; + images: string[]; + favoriteCount: number; +} + +// API로부터 받는 전자상품 데이터의 타입 +export interface ElectronicProductData { + id: string; + manufacturer: string; + name: string; + description: string; + price: number; + tags: string[]; + images: string[]; + favoriteCount: number; +} + +// 상품 생성을 위한 데이터 타입 +export interface CreateProductDto { + name: string; + price: number; + tags?: string[]; + image?: string[]; + description: string; +} + +// 상품 수정을 위한 데이터 타입 +export interface PatchProductDto { + name?: string; + price?: number; + tags?: string[]; +} + +export interface ProductListResponse { + list: ProductData[]; +} diff --git a/src/external/services/product.service.js b/src/external/services/product/product.service.ts similarity index 76% rename from src/external/services/product.service.js rename to src/external/services/product/product.service.ts index de597d9a5..c22da324c 100644 --- a/src/external/services/product.service.js +++ b/src/external/services/product/product.service.ts @@ -1,10 +1,11 @@ -import axios from "axios"; +import axios, { type AxiosResponse } from "axios"; +import type { CreateProductDto, PatchProductDto, ProductListQuery, ProductListResponse } from "./product.dto.js"; const BASE_URL = "https://panda-market-api-crud.vercel.app/products"; const ProductService = { // 상품 리스트 조회 - async getProductList(query) { + async getProductList(query?: ProductListQuery): Promise { const url = new URL(BASE_URL); for (let key in query) { @@ -14,7 +15,7 @@ const ProductService = { } try { - const res = await axios.get(url); + const res = await axios.get(url.toString()); const data = await res.data; return data; } catch (err) { @@ -24,7 +25,7 @@ const ProductService = { }, // 개별 상품 조회 - async getProduct(id) { + async getProduct(id: string) { try { const res = await axios.get(`${BASE_URL}/${id}`); const data = await res.data; @@ -36,7 +37,7 @@ const ProductService = { }, // 상품 등록 - async createProduct(productData) { + async createProduct(productData: CreateProductDto) { try { const res = await axios.post(BASE_URL, productData); const data = await res.data; @@ -48,7 +49,7 @@ const ProductService = { }, // 상품 정보 수정 - async patchProduct(id, productPatchData) { + async patchProduct(id: string, productPatchData: PatchProductDto) { try { const res = await axios.patch(`${BASE_URL}/${id}`, productPatchData); const data = res.data; @@ -60,7 +61,7 @@ const ProductService = { }, // 상품 삭제 - async deleteProduct(id) { + async deleteProduct(id: string) { try { const res = await axios.delete(`${BASE_URL}/${id}`); const data = res.data; diff --git a/src/external/stores/product_store.js b/src/external/stores/product_store.js deleted file mode 100644 index 122a63054..000000000 --- a/src/external/stores/product_store.js +++ /dev/null @@ -1,41 +0,0 @@ -import ProductService from "../services/ProductService.js"; -import { Product } from "../classes/Product.js"; -import { ElectronicProduct } from "../classes/ElectronicProduct.js"; - -const products = []; - -// const result = await ProductService.getProductList(); -// const data = result.list; -// console.log(data); - -async function loadProducts(query) { - try { - const result = await ProductService.getProductList(query); - const data = result.list; - - const electronicProducts = data - .filter((ele) => ele.tags.includes("전자제품")) - .map((ele) => new ElectronicProduct(ele)); - - const normalProducts = data - .filter((ele) => !ele.tags.includes("전자제품")) - .map((ele) => new Product(ele)); - - products.splice(0, products.length, ...electronicProducts, normalProducts); - - // for (let ele of data) { - // const isElectronic = ele.tags.includes("전자제품"); - // let product; - // if (isElectronic) { - // product = new ElectronicProduct(ele); - // } else { - // product = new Product(ele); - // } - - // products.push(product); - // } - } catch (err) { - console.log("[store error] 상품을 불러오지 못했습니다."); - throw err; - } -} diff --git a/src/external/stores/product_store.ts b/src/external/stores/product_store.ts new file mode 100644 index 000000000..719adc1f2 --- /dev/null +++ b/src/external/stores/product_store.ts @@ -0,0 +1,47 @@ +import ProductService from "../services/product/product.service.js"; +import { Product } from "../classes/product.js"; +import { ElectronicProduct } from "../classes/electronic_product.js"; +import type { ProductData, ProductListQuery, ElectronicProductData } from "../services/product/product.dto.js"; + +const products: (Product | ElectronicProduct)[] = []; + +async function loadProducts(query?: ProductListQuery): Promise { + try { + const result = await ProductService.getProductList(query); + const data = result.list; + + const electronicProducts = data + .filter((ele: ElectronicProductData) => ele.tags.includes("전자제품")) + .map( + (ele: ElectronicProductData) => + new ElectronicProduct({ + manufacturer: ele.manufacturer || "Unknown", + name: ele.name, + description: ele.description || "No description", + price: ele.price, + tags: ele.tags, + images: ele.images || [], + favoriteCount: ele.favoriteCount || 0, + }) + ); + + const normalProducts = data + .filter((ele: ProductData) => !ele.tags.includes("전자제품")) + .map( + (ele: ProductData) => + new Product( + ele.name, + ele.description || "No description", + ele.price, + ele.tags, + ele.images || [], + ele.favoriteCount || 0 + ) + ); + + products.splice(0, products.length, ...electronicProducts, ...normalProducts); + } catch (err) { + console.log("[store error] 상품을 불러오지 못했습니다."); + throw err; + } +} diff --git a/src/external/tests/test_article.service.js b/src/external/tests/test_article.service.ts similarity index 65% rename from src/external/tests/test_article.service.js rename to src/external/tests/test_article.service.ts index 993237f78..f32a28759 100644 --- a/src/external/tests/test_article.service.js +++ b/src/external/tests/test_article.service.ts @@ -1,4 +1,6 @@ -import ArticleService from "../services/ArticleService.js"; +import { create } from "node_modules/axios/index.cjs"; +import ArticleService from "../services/article/article.service.js"; +import { isArticleData } from "../services/article/article.dto.js"; // const res = await ArticleService.getArticleList(); // console.log(res.list.length); @@ -26,7 +28,10 @@ export async function testAllArticleService() { image: "https://example.com/...", }; const created = await ArticleService.createArticle(newArticle); - createdId = created.id; + + if (isArticleData(created)) { + createdId = created.id; + } console.log(`생성한 기사 id : ${createdId}`); } catch (err) { console.log("❌", err); @@ -34,25 +39,31 @@ export async function testAllArticleService() { console.log("--------특정 기사 조회---------"); try { - const article = await ArticleService.getArticle(createdId); - console.log(`[생성한 기사]`, article); + if (createdId) { + const article = await ArticleService.getArticle(createdId); + console.log(`[생성한 기사]`, article); + } } catch (err) { console.log("❌", err); } console.log("--------기사 수정---------"); try { - const patchData = { title: "제목 수정했음" }; - const updated = await ArticleService.patchArticle(createdId, patchData); - console.log(`[기사 정보 수정 성공]`, updated); + const patchData = { title: "제목 수정했음", content: "수정된 내용" }; + if (createdId) { + const updated = await ArticleService.patchArticle(createdId, patchData); + console.log(`[기사 정보 수정 성공]`, updated); + } } catch (err) { console.log("❌", err); } console.log("--------기사 삭제---------"); try { - const deleted = await ArticleService.deleteArticle(createdId); - console.log(`[기사 삭제 성공]`, deleted); + if (createdId) { + const deleted = await ArticleService.deleteArticle(createdId); + console.log(`[기사 삭제 성공]`, deleted); + } } catch (err) { console.log("❌", err); } diff --git a/src/external/tests/test_product.service.js b/src/external/tests/test_product.service.ts similarity index 91% rename from src/external/tests/test_product.service.js rename to src/external/tests/test_product.service.ts index ff2303eec..a2359ac9f 100644 --- a/src/external/tests/test_product.service.js +++ b/src/external/tests/test_product.service.ts @@ -1,4 +1,4 @@ -import ProductService from "../services/ProductService.js"; +import ProductService from "../services/product/product.service.js"; // const res = await ProductService.getProductList(); // console.log(res); @@ -42,10 +42,7 @@ export async function testAllProductService() { console.log("-------개별 상품 수정--------"); try { const patchProductData = { name: "이름을 수정함" }; - const updated = await ProductService.patchProduct( - createdId, - patchProductData - ); + const updated = await ProductService.patchProduct(createdId, patchProductData); console.log(`[상품 정보 수정 성공]`, updated); } catch (err) { console.log(`❌`, err); From ec7e0863a0e2b18c708e13f582c80e7f2d27b22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 24 Sep 2025 12:04:42 +0900 Subject: [PATCH 53/54] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98=EC=98=811(?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/controllers/article.controller.ts | 2 +- src/api/controllers/comment.controller.ts | 2 +- src/api/controllers/product.controller.ts | 2 +- src/api/middlewares/errorHandler.ts | 5 +++-- src/api/routes/comment.router.ts | 2 +- src/api/services/product/product.dto.ts | 2 +- src/api/services/product/product.service.ts | 5 +++++ src/api/types/express.d.ts | 1 - 8 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/api/controllers/article.controller.ts b/src/api/controllers/article.controller.ts index aa1f40559..7219716b7 100644 --- a/src/api/controllers/article.controller.ts +++ b/src/api/controllers/article.controller.ts @@ -36,7 +36,7 @@ const ArticleController = { error.statusCode = 403; throw error; } - userId = decoded.userId; + userId = decoded.id; } catch (err) { console.error("토큰 검증 오류:", err); } diff --git a/src/api/controllers/comment.controller.ts b/src/api/controllers/comment.controller.ts index a6017af93..bec8d87e8 100644 --- a/src/api/controllers/comment.controller.ts +++ b/src/api/controllers/comment.controller.ts @@ -17,7 +17,7 @@ const CommentController = { } }, - async updateComment(req: RequestWithDto, res: Response, next: NextFunction) { + async updateComment(req: RequestWithDto, res: Response, next: NextFunction) { try { const { id: userId } = req.user; const { id } = req.params; diff --git a/src/api/controllers/product.controller.ts b/src/api/controllers/product.controller.ts index b263b6924..87750d241 100644 --- a/src/api/controllers/product.controller.ts +++ b/src/api/controllers/product.controller.ts @@ -36,7 +36,7 @@ const ProductController = { error.statusCode = 403; throw error; } - userId = decoded.userId; + userId = decoded.id; } catch (err) { console.error("토큰 검증 오류:", err); } diff --git a/src/api/middlewares/errorHandler.ts b/src/api/middlewares/errorHandler.ts index 74588fd24..5950267e4 100644 --- a/src/api/middlewares/errorHandler.ts +++ b/src/api/middlewares/errorHandler.ts @@ -6,8 +6,9 @@ export default function errorHandler(err: CustomError, req: Request, res: Respon if (res.headersSent) return next(err); - if (err.status) { - res.status(err.status).json({ error: err.message }); + const code = err.statusCode ?? err.status; + if (code) { + res.status(code).json({ error: err.message }); } else { res.status(500).json({ error: "서버 오류" }); } diff --git a/src/api/routes/comment.router.ts b/src/api/routes/comment.router.ts index 9bdf8254c..0f7a3fa6d 100644 --- a/src/api/routes/comment.router.ts +++ b/src/api/routes/comment.router.ts @@ -12,7 +12,7 @@ router.patch( "/:id", authenticate, validateParams(CommentIdParamDto), - validateDto(CreateCommentDto), + validateDto(UpdateCommentDto), CommentController.updateComment ); router.delete("/:id", authenticate, validateParams(CommentIdParamDto), CommentController.deleteComment); diff --git a/src/api/services/product/product.dto.ts b/src/api/services/product/product.dto.ts index 142afcdf4..d252f705c 100644 --- a/src/api/services/product/product.dto.ts +++ b/src/api/services/product/product.dto.ts @@ -19,7 +19,7 @@ export class ProductDto { dto.name = data.name; dto.price = data.price; - dto.tags = dto.tags; + dto.tags = data.tags; return dto; } diff --git a/src/api/services/product/product.service.ts b/src/api/services/product/product.service.ts index c81f3aea7..90aaa8bec 100644 --- a/src/api/services/product/product.service.ts +++ b/src/api/services/product/product.service.ts @@ -15,6 +15,11 @@ const ProductService = { async findUniqueProduct(productId: number, userId: number) { const product = await ProductRepository.findById(productId); + if (!product) { + const error: CustomError = new Error("존재하지 않는 상품입니다."); + error.statusCode = 404; + throw error; + } if (!userId) { return { ...product, isLiked: false }; } diff --git a/src/api/types/express.d.ts b/src/api/types/express.d.ts index d90071e35..e9a0c1b97 100644 --- a/src/api/types/express.d.ts +++ b/src/api/types/express.d.ts @@ -1,6 +1,5 @@ import { Request } from "express"; import { AuthenticatedUser } from "./user"; -import type { number } from "zod"; declare global { namespace Express { From 3676f254cb4fc7664b06f64230af085eb57f74f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=98=A4=EC=97=B0=EC=A7=84?= Date: Wed, 24 Sep 2025 13:59:26 +0900 Subject: [PATCH 54/54] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit like type 파라미터 union타입 사용 req.user 값에 모든 데이터를 넣지 않고 id, email, nickname만 넣기 중복된 데코레이터 제거 상태코드 204 -> 200 strict equal로 변경 --- src/api/controllers/article.controller.ts | 17 ++++++++++- src/api/controllers/comment.controller.ts | 18 ++++++++++- src/api/controllers/like.controller.ts | 6 ++++ src/api/controllers/mypage.controller.ts | 33 ++++++++++++++++++++- src/api/controllers/product.controller.ts | 17 ++++++++++- src/api/middlewares/authenticate.ts | 6 +++- src/api/services/article/article.service.ts | 4 +-- src/api/services/comment/comment.service.ts | 4 +-- src/api/services/like/like.service.ts | 15 +++++----- src/api/services/mypage/mypage.dto.ts | 1 - src/api/services/product/product.service.ts | 2 +- src/api/types/express.d.ts | 2 +- src/api/types/user.d.ts | 5 ++++ 13 files changed, 111 insertions(+), 19 deletions(-) create mode 100644 src/api/types/user.d.ts diff --git a/src/api/controllers/article.controller.ts b/src/api/controllers/article.controller.ts index 7219716b7..4d0406c56 100644 --- a/src/api/controllers/article.controller.ts +++ b/src/api/controllers/article.controller.ts @@ -10,6 +10,11 @@ import { FindManyParamsDto } from "../types/dto.js"; const ArticleController = { async createArticle(req: RequestWithDto, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id: userId } = req.user; const articleDto = req.body; const newArticle = await ArticleService.createArticle(articleDto, userId); @@ -50,6 +55,11 @@ const ArticleController = { async updateArticle(req: RequestWithDto, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id: userId } = req.user; const { id } = req.params; const articleDto = req.body; @@ -63,10 +73,15 @@ const ArticleController = { async deleteArticle(req: Request, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id: userId } = req.user; const { id } = req.params; await ArticleService.deleteArticle(Number(id), userId); - res.status(204).json({ success: "상품 삭제 성공" }); + res.status(200).json({ success: "상품 삭제 성공" }); } catch (err) { next(err); } diff --git a/src/api/controllers/comment.controller.ts b/src/api/controllers/comment.controller.ts index bec8d87e8..517d56e33 100644 --- a/src/api/controllers/comment.controller.ts +++ b/src/api/controllers/comment.controller.ts @@ -3,10 +3,16 @@ import type { Request, Response, NextFunction } from "express"; import type { RequestWithDto } from "../types/express.d.ts"; import { CreateCommentDto, UpdateCommentDto } from "../services/comment/comment.dto.js"; import { FindManyCommentsQueryDto } from "../services/comment/comment-findmany.dto.js"; +import type { CustomError } from "../types/error.js"; const CommentController = { async createComment(req: RequestWithDto, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id: userId } = req.user; const commentDto = req.body; @@ -19,6 +25,11 @@ const CommentController = { async updateComment(req: RequestWithDto, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id: userId } = req.user; const { id } = req.params; const commentDto = req.body; @@ -32,10 +43,15 @@ const CommentController = { async deleteComment(req: Request, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id: userId } = req.user; const { id } = req.params; await CommentService.deleteComment(Number(id), userId); - res.status(204).json({ success: "댓글 삭제" }); + res.status(200).json({ success: "댓글 삭제" }); } catch (err) { next(err); } diff --git a/src/api/controllers/like.controller.ts b/src/api/controllers/like.controller.ts index 48b8934ad..382ad5104 100644 --- a/src/api/controllers/like.controller.ts +++ b/src/api/controllers/like.controller.ts @@ -1,9 +1,15 @@ import LikeService from "../services/like/like.service.js"; import type { Request, Response, NextFunction } from "express"; +import type { CustomError } from "../types/error.js"; const LikeController = { async toggleLike(req: Request, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id: userId } = req.user; const { type, id } = req.params; diff --git a/src/api/controllers/mypage.controller.ts b/src/api/controllers/mypage.controller.ts index 5bc841655..1d4836259 100644 --- a/src/api/controllers/mypage.controller.ts +++ b/src/api/controllers/mypage.controller.ts @@ -2,10 +2,16 @@ import MypageService from "../services/mypage/mypage.service.js"; import type { Request, Response, NextFunction } from "express"; import type { RequestWithDto } from "../types/express.d.ts"; import type { UpdateUserDto, UpdatePasswordDto } from "../services/mypage/mypage.dto.js"; +import type { CustomError } from "../types/error.js"; const MypageController = { async getUser(req: Request, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id: userId } = req.user; const user = await MypageService.getUser(userId); @@ -17,6 +23,11 @@ const MypageController = { async updateUser(req: RequestWithDto, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id: userId } = req.user; const updateUserDTO = req.body; const updatedUser = await MypageService.updateUser(userId, updateUserDTO); @@ -28,6 +39,11 @@ const MypageController = { async updatePassword(req: RequestWithDto, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id: userId } = req.user; const updatePasswordDTO = req.body; @@ -50,12 +66,17 @@ const MypageController = { async deleteUser(req: Request, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id: userId } = req.user; await MypageService.deleteUser(userId); res.clearCookie("accessToken"); res.clearCookie("refreshToken"); - res.status(204).json("회원 탈퇴가 완료되었습니다."); + res.status(200).json("회원 탈퇴가 완료되었습니다."); } catch (err) { next(err); } @@ -63,6 +84,11 @@ const MypageController = { async getProducts(req: Request, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id: userId } = req.user; const products = await MypageService.getProducts(userId); @@ -74,6 +100,11 @@ const MypageController = { async getLikeProducts(req: Request, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id: userId } = req.user; const products = await MypageService.getLikeProducts(userId); diff --git a/src/api/controllers/product.controller.ts b/src/api/controllers/product.controller.ts index 87750d241..7821aec35 100644 --- a/src/api/controllers/product.controller.ts +++ b/src/api/controllers/product.controller.ts @@ -10,6 +10,11 @@ import type { CustomError } from "src/api/types/error.js"; const ProductController = { async createProduct(req: RequestWithDto, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id: userId } = req.user; const productDto = req.body; const newProduct = await ProductService.createProduct(productDto, userId); @@ -51,6 +56,11 @@ const ProductController = { async patchProduct(req: RequestWithDto, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id } = req.params; const { id: userId } = req.user; const productDto = req.body; @@ -67,11 +77,16 @@ const ProductController = { async deleteProduct(req: Request, res: Response, next: NextFunction) { try { + if (!req.user) { + const error: CustomError = new Error("인증이 필요합니다."); + error.statusCode = 401; + throw error; + } const { id } = req.params; const { id: userId } = req.user; await ProductService.deleteProduct(Number(id), userId); - res.status(204).json({ success: "상품 삭제 성공" }); + res.status(200).json({ success: "상품 삭제 성공" }); } catch (err) { next(err); } diff --git a/src/api/middlewares/authenticate.ts b/src/api/middlewares/authenticate.ts index fd89ef6f7..66c44be6b 100644 --- a/src/api/middlewares/authenticate.ts +++ b/src/api/middlewares/authenticate.ts @@ -32,7 +32,11 @@ export default async function authenticate(req: Request, res: Response, next: Ne return next(error); } - req.user = user; + req.user = { + id: user.id, + email: user.email, + nickname: user.nickname, + }; next(); } catch (err) { let message; diff --git a/src/api/services/article/article.service.ts b/src/api/services/article/article.service.ts index 50faf4014..cf827aad5 100644 --- a/src/api/services/article/article.service.ts +++ b/src/api/services/article/article.service.ts @@ -38,7 +38,7 @@ const ArticleService = { throw error; } - if (article.userId != userId) { + if (article.userId !== userId) { const error: CustomError = new Error("게시글을 수정할 권한이 없습니다."); error.statusCode = 403; throw error; @@ -56,7 +56,7 @@ const ArticleService = { throw error; } - if (article.userId != userId) { + if (article.userId !== userId) { const error: CustomError = new Error("게시글을 삭제할 권한이 없습니다."); error.statusCode = 403; throw error; diff --git a/src/api/services/comment/comment.service.ts b/src/api/services/comment/comment.service.ts index 646a295fb..59a1c1a8f 100644 --- a/src/api/services/comment/comment.service.ts +++ b/src/api/services/comment/comment.service.ts @@ -47,7 +47,7 @@ const CommentService = { throw error; } - if (comment.userId != userId) { + if (comment.userId !== userId) { const error: CustomError = new Error("댓글을 수정할 권한이 없습니다."); error.statusCode = 403; throw error; @@ -65,7 +65,7 @@ const CommentService = { throw error; } - if (comment.userId != userId) { + if (comment.userId !== userId) { const error: CustomError = new Error("댓글을 삭제할 권한이 없습니다."); error.statusCode = 403; throw error; diff --git a/src/api/services/like/like.service.ts b/src/api/services/like/like.service.ts index e75065d68..30a9899ad 100644 --- a/src/api/services/like/like.service.ts +++ b/src/api/services/like/like.service.ts @@ -5,13 +5,14 @@ import type { Prisma } from "@prisma/client"; import type { CustomError } from "../../types/error.js"; const LikeService = { - async toggleLike(userId: number, type: string, contentId: number) { - const article = await ArticleRepository.findById(contentId); - - if (!article) { - const error: CustomError = new Error("존재하지 않는 게시글입니다."); - error.statusCode = 404; - throw error; + async toggleLike(userId: number, type: "article" | "product", contentId: number) { + if (type === "article") { + const article = await ArticleRepository.findById(contentId); + if (!article) { + const error: CustomError = new Error("존재하지 않는 게시글입니다."); + error.statusCode = 404; + throw error; + } } else { const product = await ProductRepository.findById(contentId); if (!product) { diff --git a/src/api/services/mypage/mypage.dto.ts b/src/api/services/mypage/mypage.dto.ts index d08b1f951..9bb28b762 100644 --- a/src/api/services/mypage/mypage.dto.ts +++ b/src/api/services/mypage/mypage.dto.ts @@ -1,7 +1,6 @@ import { IsString, MinLength, IsUrl, IsOptional } from "class-validator"; export class UpdateUserDto { - @IsOptional() @IsOptional() @MinLength(2, { message: "닉네임은 최소 2자 이상이어야 합니다." }) nickname?: string; diff --git a/src/api/services/product/product.service.ts b/src/api/services/product/product.service.ts index 90aaa8bec..c94e316b7 100644 --- a/src/api/services/product/product.service.ts +++ b/src/api/services/product/product.service.ts @@ -37,7 +37,7 @@ const ProductService = { throw error; } - if (product.userId != userId) { + if (product.userId !== userId) { const error: CustomError = new Error("상품을 수정할 권한이 없습니다."); error.statusCode = 403; throw error; diff --git a/src/api/types/express.d.ts b/src/api/types/express.d.ts index e9a0c1b97..7f40889dd 100644 --- a/src/api/types/express.d.ts +++ b/src/api/types/express.d.ts @@ -1,5 +1,5 @@ import { Request } from "express"; -import { AuthenticatedUser } from "./user"; +import { AuthenticatedUser } from "./user.d.js"; declare global { namespace Express { diff --git a/src/api/types/user.d.ts b/src/api/types/user.d.ts new file mode 100644 index 000000000..0078c8a8a --- /dev/null +++ b/src/api/types/user.d.ts @@ -0,0 +1,5 @@ +export interface AuthenticatedUser { + id: number; + email: string; + nickname: string; +}