diff --git a/.sql/.sql b/.sql/.sql index 7c04285..a96e77a 100644 --- a/.sql/.sql +++ b/.sql/.sql @@ -26,7 +26,6 @@ CREATE TABLE "mock" ( "title" TEXT NOT NULL, "description" TEXT NOT NULL, "quiz_count" INTEGER NOT NULL, - "like_count" INTEGER DEFAULT 0 NOT NULL, "created_at" TIMESTAMP NOT NULL, "updated_at" TIMESTAMP DEFAULT NOW() NOT NULL, "is_deleted" BOOLEAN DEFAULT FALSE NOT NULL diff --git a/index.ts b/index.ts index 966512c..921829e 100644 --- a/index.ts +++ b/index.ts @@ -13,6 +13,7 @@ import express from 'express' configDotenv() const app = express() + app.use( cors({ origin: process.env.FRONTEND_SERVER_URL, diff --git a/package-lock.json b/package-lock.json index 1632935..2c7995b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,10 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@types/crypto-js": "^4.2.2", "@upstash/redis": "1.34.4", "cookie-parser": "1.4.7", "cors": "2.8.5", - "crypto-js": "^4.2.0", - "crypto-ts": "^1.0.2", + "crypto-js": "4.2.0", "dotenv": "16.4.7", "express": "4.21.2", "jsonwebtoken": "9.0.2", @@ -26,6 +24,7 @@ "devDependencies": { "@types/cookie-parser": "1.4.8", "@types/cors": "2.8.17", + "@types/crypto-js": "^4.2.2", "@types/express": "5.0.0", "@types/jsonwebtoken": "9.0.8", "@types/multer": "1.4.12", @@ -44,38 +43,6 @@ "typescript": "5.7.3" } }, - "node_modules/@angular/common": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.0.tgz", - "integrity": "sha512-dm8PR94QY3DucXxltdV5p2Yxyr5bfPlmjOElwLhiTvxWbwCZJTVhPc8dw0TCKzCEu+tKafT48u4BLIB34a0A/g==", - "peer": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/core": "19.2.0", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, - "node_modules/@angular/core": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.0.tgz", - "integrity": "sha512-WKTRltOt3MMWWuhRX7Y9RonKxIYjZeBDE6XRwceHMgaEDS2d8I2D3AIuqizRsgHpJqDPnQnH+vxcek4FivcSGA==", - "peer": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.15.0" - } - }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -1900,7 +1867,9 @@ "node_modules/@types/crypto-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", - "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==" + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true, + "license": "MIT" }, "node_modules/@types/express": { "version": "5.0.0", @@ -2784,23 +2753,6 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, - "node_modules/crypto-ts": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/crypto-ts/-/crypto-ts-1.0.2.tgz", - "integrity": "sha512-TcBWwF8ghYhVd/qPSwvY4nsbDZRN/PVxQ1Uc8ryRLiX4M4C5XSPyIhVgR4M5mIhrQEnWIktLcrv+FIqhKk2t3g==", - "dependencies": { - "tslib": "^1.7.1" - }, - "peerDependencies": { - "@angular/common": ">= 5.0.0", - "@angular/core": ">= 5.0.0" - } - }, - "node_modules/crypto-ts/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -5765,15 +5717,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "peer": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -6822,12 +6765,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zone.js": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.0.tgz", - "integrity": "sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==", - "peer": true } } } diff --git a/package.json b/package.json index 49bd55e..14e7247 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,10 @@ "#error/*": "./dist/src/core/error/*.js" }, "dependencies": { - "@types/crypto-js": "4.2.2", "@upstash/redis": "1.34.4", "cookie-parser": "1.4.7", "cors": "2.8.5", "crypto-js": "4.2.0", - "crypto-ts": "1.0.2", "dotenv": "16.4.7", "express": "4.21.2", "jsonwebtoken": "9.0.2", @@ -40,6 +38,7 @@ "devDependencies": { "@types/cookie-parser": "1.4.8", "@types/cors": "2.8.17", + "@types/crypto-js": "^4.2.2", "@types/express": "5.0.0", "@types/jsonwebtoken": "9.0.8", "@types/multer": "1.4.12", diff --git a/src/core/error/ErrorRegistry.ts b/src/core/error/ErrorRegistry.ts index 9438a35..46cdc06 100644 --- a/src/core/error/ErrorRegistry.ts +++ b/src/core/error/ErrorRegistry.ts @@ -98,6 +98,11 @@ export default class ErrorRegistry { 'MU001', '지원하지 않는 파일 형식입니다', ) + static readonly OUT_OF_UPLOAD_LIMIT = new CustomError( + 400, + 'MU002', + '파일 업로드 개수를 초과했습니다', + ) // Like(LI) static readonly DUPLICATE_LIKE = new CustomError( diff --git a/src/core/util/Regex/index.ts b/src/core/util/Regex/index.ts index 47d6f41..273e26e 100644 --- a/src/core/util/Regex/index.ts +++ b/src/core/util/Regex/index.ts @@ -1,22 +1,17 @@ export default class Regex { public static readonly ID = /^(?=.*[a-zA-Z])[a-zA-Z0-9]{5,16}$/ - public static readonly EMAIL = - /^[a-zA-Z0-9._%+-]{1,20}@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ - public static readonly NICKNAME = - /^(?=.*[가-힣a-zA-Z])[가-힣a-zA-Z0-9]{2,12}$/ - public static readonly TITLE = /^[가-힣a-zA-Z0-9]{2,50}$/ - public static readonly DESCRIPTION = /^[가-힣a-zA-Z0-9]{2,500}$/ - public static readonly SUBJECT = /^(?=.*[가-힣a-zA-Z])[가-힣a-zA-Z0-9]{2,50}$/ - public static readonly QUIZ_COUNT = /^(10|[1-9])$/ - public static readonly CONTENT = /^[가-힣a-zA-Z0-9]{2,500}$/ - public static readonly TEXT_ANSWER = - /^[가-힣a-zA-Z0-9!@#$%^&*()_+={}\[\]:;"'<>,.?~`-]{1,100}$/ - public static readonly SEARCH = /^[가-힣a-zA-Z0-9]{2,100}$/ - public static readonly TIER = /^(DIAMOND|PLATINUM|GOLD|SILVER|BRONZE)$/ - public static readonly FILE = - /^(?:[^\s]+\.jpg|[^\s]+\.png)(,(?:[^\s]+\.jpg|[^\s]+\.png)){0,4}$/ public static readonly PASSWORD = /^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z0-9!@#$%^&*()_+={}\[\]:;"'<>,.?~`-]{8,16}$/ + + public static readonly EMAIL = + /^[a-zA-Z0-9._%+-]{1,20}@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,100}$/ + + public static readonly NICKNAME = /^.{2,12}$/ + public static readonly SUBJECT = /^.{1,50}$/ + public static readonly TITLE = /^.{1,50}$/ + public static readonly DESCRIPTION = /^.{0,1000}$/ + public static readonly CONTENT = /^.{0,10000}$/ + public static readonly TIER = /^(DIAMOND|PLATINUM|GOLD|SILVER|BRONZE)$/ public static readonly UUID = - /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/ } diff --git a/src/core/util/multipartParser/index.ts b/src/core/util/multipartParser/index.ts index 2d9f6c5..619b94a 100644 --- a/src/core/util/multipartParser/index.ts +++ b/src/core/util/multipartParser/index.ts @@ -40,10 +40,26 @@ const multipartParser = (contentType: string, limit: number) => { } } - const uploadUrls = req.files.map((file) => { - const multerFile = file as Express.MulterS3.File - return replaceBaseUrl(multerFile.location) - }) + const uploadUrls = [] + if (req.body.existingUrls) { + const existingUrls = req.body.existingUrls.split(',') + existingUrls.forEach((url: string) => { + if (!url.startsWith(newBaseUrl)) { + return next(ErrorRegistry.INVALID_INPUT_FORMAT) + } + }) + if (existingUrls.length + req.files.length > limit) { + return next(ErrorRegistry.OUT_OF_UPLOAD_LIMIT) + } + uploadUrls.push(...existingUrls) + } + + uploadUrls.push( + ...req.files.map((file) => { + const multerFile = file as Express.MulterS3.File + return replaceBaseUrl(multerFile.location) + }), + ) req.body = { ...req.body, uploadUrls } next() diff --git a/src/like/entity/dao/frontend/request/path/ArticleIdxPath.ts b/src/like/entity/dao/frontend/request/path/ArticleIdxPath.ts index bf6d771..dd80f48 100644 --- a/src/like/entity/dao/frontend/request/path/ArticleIdxPath.ts +++ b/src/like/entity/dao/frontend/request/path/ArticleIdxPath.ts @@ -10,7 +10,7 @@ export default class ArticleIdxPath { public idx: UUID constructor(params: ArticleIdxPathParams) { - if (!new RegExp(Regex.UUID).test(params.idx)) { + if (!Regex.UUID.test(params.idx)) { throw ErrorRegistry.INVALID_INPUT_FORMAT } this.idx = params.idx diff --git a/src/like/router/addRouter.ts b/src/like/router/addRouter.ts deleted file mode 100644 index 5b264f0..0000000 --- a/src/like/router/addRouter.ts +++ /dev/null @@ -1,21 +0,0 @@ -import controller from '#controller' -import express from 'express' -import ArticleIdxPath from '../entity/dao/frontend/request/path/ArticleIdxPath.js' -import LikeService from '../service/LikeService.js' - -export const addRouter = express.Router() - -addRouter.post( - '/:idx', - controller( - 'login', - null, - ArticleIdxPath, - null, - null, - )(async (req, res) => { - return res.send( - await LikeService.addLikeToArticle(req.memberIdx, req.params.idx), - ) - }), -) diff --git a/src/like/router/index.ts b/src/like/router/index.ts index 0f17186..036f347 100644 --- a/src/like/router/index.ts +++ b/src/like/router/index.ts @@ -1,8 +1,6 @@ import express from 'express' -import { addRouter } from './addRouter.js' -import { deleteRouter } from './deleteRouter.js' +import { likeItemRouter } from './likeItemRouter.js' export const likeRouter = express.Router() -likeRouter.use('/add', addRouter) -likeRouter.use('/delete', deleteRouter) +likeRouter.use('/', likeItemRouter) diff --git a/src/like/router/deleteRouter.ts b/src/like/router/likeItemRouter.ts similarity index 57% rename from src/like/router/deleteRouter.ts rename to src/like/router/likeItemRouter.ts index 392d093..612a0b1 100644 --- a/src/like/router/deleteRouter.ts +++ b/src/like/router/likeItemRouter.ts @@ -3,9 +3,24 @@ import express from 'express' import ArticleIdxPath from '../entity/dao/frontend/request/path/ArticleIdxPath.js' import LikeService from '../service/LikeService.js' -export const deleteRouter = express.Router() +export const likeItemRouter = express.Router() -deleteRouter.delete( +likeItemRouter.post( + '/:idx', + controller( + 'login', + null, + ArticleIdxPath, + null, + null, + )(async (req, res) => { + return res.send( + await LikeService.addLikeToArticle(req.memberIdx, req.params.idx), + ) + }), +) + +likeItemRouter.delete( '/:idx', controller( 'login', diff --git a/src/member/entity/dao/db/ProfileResultFromDB.ts b/src/member/entity/dao/db/ProfileResultFromDB.ts index c1cc1e6..ba82154 100644 --- a/src/member/entity/dao/db/ProfileResultFromDB.ts +++ b/src/member/entity/dao/db/ProfileResultFromDB.ts @@ -1,3 +1,5 @@ export default interface ProfileResultFromDB { nickname: string + role: string + idx: number } diff --git a/src/member/entity/dao/frontend/request/FindIdRequest.ts b/src/member/entity/dao/frontend/request/FindIdRequest.ts index 057ac37..b797625 100644 --- a/src/member/entity/dao/frontend/request/FindIdRequest.ts +++ b/src/member/entity/dao/frontend/request/FindIdRequest.ts @@ -1,4 +1,5 @@ import ErrorRegistry from '#error/ErrorRegistry' +import Regex from '#util/Regex' interface FindIdRequestParams { email: string } @@ -7,10 +8,9 @@ export default class FindIdRequest { public email: string constructor(params: FindIdRequestParams) { - if (!params.email || params.email.length > 100) { + if (!Regex.EMAIL.test(params.email)) { throw ErrorRegistry.INVALID_INPUT_FORMAT } this.email = params.email } } -;`` diff --git a/src/member/entity/dao/frontend/request/FindPasswordRequest.ts b/src/member/entity/dao/frontend/request/FindPasswordRequest.ts index 813d58f..c939c13 100644 --- a/src/member/entity/dao/frontend/request/FindPasswordRequest.ts +++ b/src/member/entity/dao/frontend/request/FindPasswordRequest.ts @@ -1,4 +1,5 @@ import ErrorRegistry from '#error/ErrorRegistry' +import Regex from '#util/Regex' interface FindPasswordRequestParams { id: string email: string @@ -9,15 +10,17 @@ export default class FindPasswordRequest { public email: string constructor(params: FindPasswordRequestParams) { - if ( - !params.id || - !params.email || - params.id.length > 100 || - params.email.length > 100 - ) { + if (!params.id) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } + if (params.id.length > 16) { throw ErrorRegistry.INVALID_INPUT_FORMAT } this.id = params.id + + if (!Regex.EMAIL.test(params.email)) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } this.email = params.email } } diff --git a/src/member/entity/dao/frontend/request/NicknameChangeRequest.ts b/src/member/entity/dao/frontend/request/NicknameChangeRequest.ts index 23031ec..53e827f 100644 --- a/src/member/entity/dao/frontend/request/NicknameChangeRequest.ts +++ b/src/member/entity/dao/frontend/request/NicknameChangeRequest.ts @@ -9,7 +9,7 @@ export default class NicknameChangeRequest { public nickname: string constructor(params: NicknameChangeRequestParams) { - if (!new RegExp(Regex.NICKNAME).test(params.nickname)) { + if (!Regex.NICKNAME.test(params.nickname)) { throw ErrorRegistry.INVALID_INPUT_FORMAT } this.nickname = params.nickname diff --git a/src/member/entity/dao/frontend/request/NormalLoginRequest.ts b/src/member/entity/dao/frontend/request/NormalLoginRequest.ts index 30efefa..b5119f2 100644 --- a/src/member/entity/dao/frontend/request/NormalLoginRequest.ts +++ b/src/member/entity/dao/frontend/request/NormalLoginRequest.ts @@ -9,16 +9,20 @@ export default class NormalLoginRequest { public id: string public password: string constructor(params: NormalLoginRequestParams) { - if ( - !params.id || - !params.password || - params.id.length > 100 || - params.password.length > 100 - ) { + if (!params.id) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } + if (params.id.length > 16) { throw ErrorRegistry.INVALID_INPUT_FORMAT } - this.id = params.id + + if (!params.password) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } + if (params.password.length > 16) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } this.password = params.password } } diff --git a/src/member/entity/dao/frontend/request/PasswordChangeRequest.ts b/src/member/entity/dao/frontend/request/PasswordChangeRequest.ts index bc4b306..125a9d1 100644 --- a/src/member/entity/dao/frontend/request/PasswordChangeRequest.ts +++ b/src/member/entity/dao/frontend/request/PasswordChangeRequest.ts @@ -12,7 +12,7 @@ export default class PasswordChangeRequest { public token: string constructor(params: PasswordChangeRequestParams) { - if (!new RegExp(Regex.PASSWORD).test(params.password)) { + if (!Regex.PASSWORD.test(params.password)) { throw ErrorRegistry.INVALID_INPUT_FORMAT } if (params.password != params.passwordCheck) { diff --git a/src/member/entity/dao/frontend/request/SendFindPasswordEmailRequest.ts b/src/member/entity/dao/frontend/request/SendFindPasswordEmailRequest.ts index 0224081..d01d2c7 100644 --- a/src/member/entity/dao/frontend/request/SendFindPasswordEmailRequest.ts +++ b/src/member/entity/dao/frontend/request/SendFindPasswordEmailRequest.ts @@ -11,10 +11,7 @@ export default class SendFindPasswordEmailRequest { public email: string constructor(params: SendFindPasswordEmailRequestParams) { - if ( - !new RegExp(Regex.ID).test(params.id) || - !new RegExp(Regex.EMAIL).test(params.email) - ) { + if (!Regex.ID.test(params.id) || !Regex.EMAIL.test(params.email)) { throw ErrorRegistry.INVALID_INPUT_FORMAT } this.id = params.id diff --git a/src/member/entity/dao/frontend/request/SendVerifyEmailRequest.ts b/src/member/entity/dao/frontend/request/SendVerifyEmailRequest.ts index be467e2..9780acc 100644 --- a/src/member/entity/dao/frontend/request/SendVerifyEmailRequest.ts +++ b/src/member/entity/dao/frontend/request/SendVerifyEmailRequest.ts @@ -9,7 +9,7 @@ export default class SendVerifyEmailRequest { public email: string constructor(params: SendVerifyEmailRequestParams) { - if (!new RegExp(Regex.EMAIL).test(params.email)) { + if (!Regex.EMAIL.test(params.email)) { throw ErrorRegistry.INVALID_INPUT_FORMAT } this.email = params.email diff --git a/src/member/entity/dao/frontend/request/SignupRequest.ts b/src/member/entity/dao/frontend/request/SignupRequest.ts index a5b1641..9ed1907 100644 --- a/src/member/entity/dao/frontend/request/SignupRequest.ts +++ b/src/member/entity/dao/frontend/request/SignupRequest.ts @@ -16,10 +16,10 @@ export default class SignupRequest { constructor(params: SignupRequestParams) { if ( - !new RegExp(Regex.ID).test(params.id) || - !new RegExp(Regex.PASSWORD).test(params.password) || - !new RegExp(Regex.PASSWORD).test(params.passwordCheck) || - !new RegExp(Regex.EMAIL).test(params.email) + !Regex.ID.test(params.id) || + !Regex.PASSWORD.test(params.password) || + !Regex.PASSWORD.test(params.passwordCheck) || + !Regex.EMAIL.test(params.email) ) { throw ErrorRegistry.INVALID_INPUT_FORMAT } diff --git a/src/member/entity/dao/frontend/response/ProfileResponse.ts b/src/member/entity/dao/frontend/response/ProfileResponse.ts index 8d5c8e8..0547935 100644 --- a/src/member/entity/dao/frontend/response/ProfileResponse.ts +++ b/src/member/entity/dao/frontend/response/ProfileResponse.ts @@ -1,7 +1,11 @@ export default class ProfileResponse { public nickname: string + public role: string + public idx: number - constructor(nickname: string) { + constructor(nickname: string, role: string, idx: number) { this.nickname = nickname + this.role = role + this.idx = idx } } diff --git a/src/member/repository/MemberInfoRepository.ts b/src/member/repository/MemberInfoRepository.ts index 83086f6..9b5b2c9 100644 --- a/src/member/repository/MemberInfoRepository.ts +++ b/src/member/repository/MemberInfoRepository.ts @@ -5,9 +5,10 @@ export default class MemberInfoRepository { /** 내 정보 가져오기 */ static async getProfile(memberIdx: number) { return ( - await postgres.query('SELECT nickname FROM member WHERE idx = $1', [ - memberIdx, - ]) + await postgres.query( + 'SELECT nickname, role, idx FROM member WHERE idx = $1', + [memberIdx], + ) ).rows[0] as ProfileResultFromDB } } diff --git a/src/member/repository/MemberLoginRepository.ts b/src/member/repository/MemberLoginRepository.ts index 283ea71..d28b033 100644 --- a/src/member/repository/MemberLoginRepository.ts +++ b/src/member/repository/MemberLoginRepository.ts @@ -17,10 +17,22 @@ export default class MemberLoginRepository { */ static async checkMemberDataFromDb(socialId: string) { return ( - await postgres.query('SELECT * FROM member WHERE id = $1', [socialId]) + await postgres.query('SELECT * FROM member WHERE id =$1', [socialId]) ).rows[0] } - + static async checkGoogleMailAvailable(socialId: string, email: string) { + return ( + await postgres.query( + `SELECT + CASE + WHEN EXISTS (SELECT 1 FROM member WHERE id = $1 AND email = $2) THEN true + WHEN EXISTS (SELECT 1 FROM member WHERE email = $2) THEN false + ELSE NULL + END AS result`, + [socialId, email], + ) + ).rows[0].result + } /** 카카오 소셜로그인 회원가입 */ static async insertKakaoLoginMemberData(socialId: string, nickname: string) { let dateTime = new Date() diff --git a/src/member/repository/RedisEmailSignupRepository.ts b/src/member/repository/RedisEmailSignupRepository.ts index 4761dc0..e69c26b 100644 --- a/src/member/repository/RedisEmailSignupRepository.ts +++ b/src/member/repository/RedisEmailSignupRepository.ts @@ -50,4 +50,10 @@ export default class RedisSignupRepository { } await redis.hincrby(email, 'send_count', 1) } + + /** 가입이 완료되고 나면 redis 데이터를 삭제합니다. + */ + static async resetEmailDataFromRedis(email: string) { + await redis.del(email) + } } diff --git a/src/member/service/ChangeService.ts b/src/member/service/ChangeService.ts index f956710..6a5a8b2 100644 --- a/src/member/service/ChangeService.ts +++ b/src/member/service/ChangeService.ts @@ -1,6 +1,7 @@ import ErrorRegistry from '#error/ErrorRegistry' import EmailSender from '#util/EmailSender' import Token from '#util/Token' +import CryptoJS from 'crypto-js' import { Request } from 'express' import jwt from 'jsonwebtoken' import NicknameChangeRequest from '../entity/dao/frontend/request/NicknameChangeRequest.js' @@ -30,7 +31,12 @@ export default class ChangeService { } const secretKey = process.env.JWT_SIGNATURE_KEY const data: any = jwt.verify(token, secretKey) - await MemberChangeRepository.updateMemberPassword(password, data.userId) + const salt = process.env.ENCRYPT_SALT_STRING + const changePassword = CryptoJS.SHA256(password + salt).toString() + await MemberChangeRepository.updateMemberPassword( + changePassword, + data.userId, + ) await RedisEmailChangeRepository.resetFindPasswordEmailDataFromRedis( data.email, ) diff --git a/src/member/service/LoginService.ts b/src/member/service/LoginService.ts index 38fe524..e092def 100644 --- a/src/member/service/LoginService.ts +++ b/src/member/service/LoginService.ts @@ -45,6 +45,7 @@ export default class LoginService { kakaoToken, kakaoOauthUserInfoUrl, ) + // const kakaoEmail = `${userData.id}@kakao.com` const checkResult: any = await MemberLoginRepository.checkMemberDataFromDb( userData.id, ) @@ -87,7 +88,14 @@ export default class LoginService { const checkResult: any = await MemberLoginRepository.checkMemberDataFromDb( userData.id, ) - + const checkEmail: any = + await MemberLoginRepository.checkGoogleMailAvailable( + userData.id, + userData.email, + ) + if (checkEmail == false) { + throw ErrorRegistry.DUPLICATED_EMAIL + } if (!checkResult) { await MemberLoginRepository.insertGoogleLoginMemberData( userData.id, @@ -112,6 +120,6 @@ export default class LoginService { ) Token.generateCookie('loginToken', token, res) - return signupRedirectUrl + return loginRedirectUrl } } diff --git a/src/member/service/MeService.ts b/src/member/service/MeService.ts index ceaecef..68921fb 100644 --- a/src/member/service/MeService.ts +++ b/src/member/service/MeService.ts @@ -5,6 +5,6 @@ export default class MeService { /** 로그아웃 */ static async getProfile(memberIdx: number) { const result = await MemberInfoRepository.getProfile(memberIdx) - return new ProfileResponse(result.nickname) + return new ProfileResponse(result.nickname, result.role, result.idx) } } diff --git a/src/member/service/SignupService.ts b/src/member/service/SignupService.ts index 15a93cb..1cc7b1e 100644 --- a/src/member/service/SignupService.ts +++ b/src/member/service/SignupService.ts @@ -58,6 +58,7 @@ export default class SignupService { result.email, result.role, ) + await RedisEmailSignupRepository.resetEmailDataFromRedis(data.email) Token.generateCookie('loginToken', token, res) } /** 회원가입 인증 이메일 재전송 서비스 로직 */ diff --git a/src/mock/entity/dao/frontend/request/body/MockEditRequest.ts b/src/mock/entity/dao/frontend/request/body/MockEditRequest.ts index 5303b0a..d2e9957 100644 --- a/src/mock/entity/dao/frontend/request/body/MockEditRequest.ts +++ b/src/mock/entity/dao/frontend/request/body/MockEditRequest.ts @@ -13,14 +13,16 @@ export default class MockEditRequest { uploadUrls: string[] constructor(params: MockEditRequestParams) { - if ( - !new RegExp(Regex.TITLE).test(params.title) || - !new RegExp(Regex.DESCRIPTION).test(params.description) - ) { + if (!Regex.TITLE.test(params.title)) { throw ErrorRegistry.INVALID_INPUT_FORMAT } this.title = params.title + + if (params.description && !Regex.DESCRIPTION.test(params.description)) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } this.description = params.description + this.uploadUrls = params.uploadUrls } } diff --git a/src/mock/entity/dao/frontend/request/body/QuizRequest.ts b/src/mock/entity/dao/frontend/request/body/QuizRequest.ts index 5cedc12..9a0b1f8 100644 --- a/src/mock/entity/dao/frontend/request/body/QuizRequest.ts +++ b/src/mock/entity/dao/frontend/request/body/QuizRequest.ts @@ -10,7 +10,7 @@ export default class QuizRequest { public idx: UUID constructor(params: QuizRequestParams) { - if (!new RegExp(Regex.UUID).test(params.idx)) { + if (!Regex.UUID.test(params.idx)) { throw ErrorRegistry.INVALID_INPUT_FORMAT } this.idx = params.idx diff --git a/src/mock/entity/dao/frontend/request/body/SolveRequest.ts b/src/mock/entity/dao/frontend/request/body/SolveRequest.ts index 4630f93..16d9a05 100644 --- a/src/mock/entity/dao/frontend/request/body/SolveRequest.ts +++ b/src/mock/entity/dao/frontend/request/body/SolveRequest.ts @@ -1,5 +1,4 @@ import ErrorRegistry from '#error/ErrorRegistry' -import Regex from '#util/Regex' interface SolveRequestParams { singleChoiceAnswer?: number @@ -10,17 +9,22 @@ export default class SolveRequest { singleChoiceAnswer?: number textAnswer?: string - constructor(public params: SolveRequestParams) { + constructor(params: SolveRequestParams) { if ( - (typeof params.textAnswer === 'string' && - !new RegExp(Regex.TEXT_ANSWER).test(params.textAnswer)) || - (typeof params.singleChoiceAnswer === 'number' && - (params.singleChoiceAnswer > 3 || params.singleChoiceAnswer < 0)) + params.singleChoiceAnswer && + (params.singleChoiceAnswer > 3 || params.singleChoiceAnswer < 0) ) { throw ErrorRegistry.INVALID_INPUT_FORMAT } + this.singleChoiceAnswer = Number(params.singleChoiceAnswer) - this.singleChoiceAnswer = params.singleChoiceAnswer + if (params.textAnswer && params.textAnswer.length > 100) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } this.textAnswer = params.textAnswer + + if (params.singleChoiceAnswer === null && params.textAnswer === null) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } } } diff --git a/src/mock/entity/dao/frontend/request/body/WriteRequest.ts b/src/mock/entity/dao/frontend/request/body/WriteRequest.ts index 6d59cf9..4d5f9c4 100644 --- a/src/mock/entity/dao/frontend/request/body/WriteRequest.ts +++ b/src/mock/entity/dao/frontend/request/body/WriteRequest.ts @@ -17,19 +17,26 @@ export default class WriteRequest { public uploadUrls: string[] constructor(params: WriteRequestParams) { - if ( - !new RegExp(Regex.SUBJECT).test(params.subject) || - !new RegExp(Regex.TITLE).test(params.title) || - !new RegExp(Regex.DESCRIPTION).test(params.description) || - params.quizCount > 10 || - params.quizCount < 0 - ) { + if (!Regex.SUBJECT.test(params.subject)) { throw ErrorRegistry.INVALID_INPUT_FORMAT } this.subject = params.subject - this.quizCount = params.quizCount + + if (!Regex.TITLE.test(params.title)) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } this.title = params.title + + if (params.description && !Regex.DESCRIPTION.test(params.description)) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } this.description = params.description + + if (params.quizCount > 10 || params.quizCount < 1) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } + this.quizCount = params.quizCount + this.uploadUrls = params.uploadUrls } } diff --git a/src/mock/entity/dao/frontend/request/path/MockIdxPath.ts b/src/mock/entity/dao/frontend/request/path/MockIdxPath.ts index 126e1e1..51ccee3 100644 --- a/src/mock/entity/dao/frontend/request/path/MockIdxPath.ts +++ b/src/mock/entity/dao/frontend/request/path/MockIdxPath.ts @@ -10,7 +10,7 @@ export default class MockIdxPath { public idx: UUID constructor(params: MockIdxPathParams) { - if (!new RegExp(Regex.UUID).test(params.idx)) { + if (!Regex.UUID.test(params.idx)) { throw ErrorRegistry.INVALID_INPUT_FORMAT } this.idx = params.idx diff --git a/src/mock/entity/dao/frontend/request/path/QuizIdxPath.ts b/src/mock/entity/dao/frontend/request/path/QuizIdxPath.ts index fafb431..f25668f 100644 --- a/src/mock/entity/dao/frontend/request/path/QuizIdxPath.ts +++ b/src/mock/entity/dao/frontend/request/path/QuizIdxPath.ts @@ -10,7 +10,7 @@ export default class QuizIdxPath { public idx: UUID constructor(params: QuizIdxPathParams) { - if (!new RegExp(Regex.UUID).test(params.idx)) { + if (!Regex.UUID.test(params.idx)) { throw ErrorRegistry.INVALID_INPUT_FORMAT } this.idx = params.idx diff --git a/src/mock/entity/dao/frontend/request/query/ListRequest.ts b/src/mock/entity/dao/frontend/request/query/ListRequest.ts index 8c9dd1f..a41b8f0 100644 --- a/src/mock/entity/dao/frontend/request/query/ListRequest.ts +++ b/src/mock/entity/dao/frontend/request/query/ListRequest.ts @@ -1,19 +1,36 @@ import ErrorRegistry from '#error/ErrorRegistry' -interface ListQueryParams { +interface ListRequestParams { current: number display: number + sort: 'like' + title: string } -export default class ListQuery { +export default class ListRequest { current: number display: number + sort: 'new' | 'like' + title: string - constructor(params: ListQueryParams) { - if (!params.current || !params.display) { + constructor(params: ListRequestParams) { + if (!params.current) { throw ErrorRegistry.INVALID_INPUT_FORMAT + } else { + this.current = Number(params.current) } - this.current = Number(params.current) - this.display = Number(params.display) + + if (!params.display) { + this.display = 10 + } else { + this.display = Number(params.display) + } + + this.sort = params.sort + + if (params.title && params.title.length > 100) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } + this.title = params.title } } diff --git a/src/mock/entity/dao/frontend/response/DetailResponse.ts b/src/mock/entity/dao/frontend/response/DetailResponse.ts index cfa4832..9179d0a 100644 --- a/src/mock/entity/dao/frontend/response/DetailResponse.ts +++ b/src/mock/entity/dao/frontend/response/DetailResponse.ts @@ -8,6 +8,7 @@ export default class DetailResponse { writerNickname: string images: string[] firstQuizIdx: UUID + likeCount: number ranks: { rank: number nickName: string @@ -22,6 +23,7 @@ export default class DetailResponse { writerNickname: string, images: string[], firstQuizIdx: UUID, + likeCount: number, ranks: { rank: number nickName: string @@ -35,6 +37,7 @@ export default class DetailResponse { this.writerNickname = writerNickname this.images = images this.firstQuizIdx = firstQuizIdx + this.likeCount = likeCount this.ranks = ranks } } diff --git a/src/mock/entity/dao/frontend/response/ListResponse.ts b/src/mock/entity/dao/frontend/response/ListResponse.ts index 6ac205d..58f6a1f 100644 --- a/src/mock/entity/dao/frontend/response/ListResponse.ts +++ b/src/mock/entity/dao/frontend/response/ListResponse.ts @@ -1,5 +1,3 @@ -import MockList from 'src/mock/entity/dto/MockList.js' - export default class ListResponse { firstPageNumber: number currentPageNumber: number @@ -14,16 +12,10 @@ export default class ListResponse { likeCount: number }[] - public static createEmpty(): ListResponse { - return new ListResponse(1, 1, 1, false, false, []) - } - constructor( - firstPageNumber: number, currentPageNumber: number, - lastPageNumber: number, - prevPageExist: boolean, - nextPageExist: boolean, + displayCount: number, + totalCount: number, mocks: { idx: number title: string @@ -32,22 +24,18 @@ export default class ListResponse { likeCount: number }[], ) { - this.firstPageNumber = firstPageNumber this.currentPageNumber = currentPageNumber - this.lastPageNumber = lastPageNumber - this.prevPageExist = prevPageExist - this.nextPageExist = nextPageExist - this.mocks = mocks - } - - static of(mockList: MockList): ListResponse { - return new ListResponse( - mockList.firstPageNumber, - mockList.currentPageNumber, - mockList.lastPageNumber, - mockList.prevPageExist, - mockList.nextPageExist, - mockList.mocks, + this.firstPageNumber = Math.floor((currentPageNumber - 1) / 10) * 10 + 1 + const pageOffset = Math.min( + 9, + Math.floor( + (totalCount - (this.firstPageNumber - 1) * displayCount) / displayCount, + ), ) + this.lastPageNumber = this.firstPageNumber + pageOffset + this.prevPageExist = this.firstPageNumber > 1 + this.nextPageExist = + this.lastPageNumber < Math.floor((totalCount - 1) / displayCount) + 1 + this.mocks = mocks } } diff --git a/src/mock/entity/dao/frontend/response/ResultResponse.ts b/src/mock/entity/dao/frontend/response/ResultResponse.ts index 8fbfc26..a87781b 100644 --- a/src/mock/entity/dao/frontend/response/ResultResponse.ts +++ b/src/mock/entity/dao/frontend/response/ResultResponse.ts @@ -1,11 +1,18 @@ export default class ResultResponse { score: number maxScore: number - topPercentile: number + greaterEqualCandidateCount: number + totalCandidateCount: number - constructor(score: number, maxScore: number, topPercentile: number) { + constructor( + score: number, + maxScore: number, + greaterEqualCandidateCount: number, + totalCandidateCount: number, + ) { this.score = score this.maxScore = maxScore - this.topPercentile = topPercentile + this.greaterEqualCandidateCount = greaterEqualCandidateCount + this.totalCandidateCount = totalCandidateCount } } diff --git a/src/mock/entity/dto/MockList.ts b/src/mock/entity/dto/MockList.ts deleted file mode 100644 index 6b3bd65..0000000 --- a/src/mock/entity/dto/MockList.ts +++ /dev/null @@ -1,41 +0,0 @@ -export default class MockList { - firstPageNumber: number - currentPageNumber: number - lastPageNumber: number - prevPageExist: boolean - nextPageExist: boolean - mocks: { - idx: number - title: string - writerNickname: string - createdAt: string - likeCount: number - }[] - - constructor( - currentPageNumber: number, - displayCount: number, - totalCount: number, - mocks: { - idx: number - title: string - writerNickname: string - createdAt: string - likeCount: number - }[], - ) { - this.currentPageNumber = currentPageNumber - this.firstPageNumber = Math.floor((currentPageNumber - 1) / 10) * 10 + 1 - const pageOffset = Math.min( - 9, - Math.floor( - (totalCount - (this.firstPageNumber - 1) * displayCount) / displayCount, - ), - ) - this.lastPageNumber = this.firstPageNumber + pageOffset - this.prevPageExist = this.firstPageNumber > 1 - this.nextPageExist = - this.lastPageNumber < Math.floor((totalCount - 1) / displayCount) + 1 - this.mocks = mocks - } -} diff --git a/src/mock/repository/MockRepository.ts b/src/mock/repository/MockRepository.ts index ff978aa..cb53b87 100644 --- a/src/mock/repository/MockRepository.ts +++ b/src/mock/repository/MockRepository.ts @@ -28,14 +28,18 @@ export default class MockRepository { FROM first_score ORDER BY score DESC LIMIT 15 + ), + like_count AS ( + SELECT COUNT(*)::integer as count FROM like_history + WHERE article_idx = $1 ) SELECT mock.idx, mock.title, mock.description, - mock.created_at as "createdAt", + TO_CHAR(mock.created_at,'YYYY-MM-DD' )as "createdAt", mock.quiz_count as "quizCount", - mock.like_count as "likeCount", + like_count.count as "likeCount", member.nickname as "writerNickname", image.urls as images, ( @@ -45,19 +49,23 @@ export default class MockRepository { ORDER BY quiz.created_at ASC LIMIT 1 ) AS "firstQuizIdx", - ( - SELECT json_agg( - json_build_object( + COALESCE( + ( + SELECT json_agg( + json_build_object( 'rank', mr.rank, 'nickname', mr.nickname, 'score', mr.score + ) ) - ) - FROM mock_rank mr + FROM mock_rank mr + ), + '[]'::json ) AS "ranks" FROM mock JOIN member ON member.idx = mock.member_idx - JOIN image ON image.article_idx = mock.idx + JOIN image ON image.article_idx = mock.idx, + like_count WHERE mock.idx = $1 AND mock.is_deleted = false `, [idx], @@ -74,8 +82,7 @@ export default class MockRepository { idx, title, member_idx, - created_at, - like_count + created_at FROM mock WHERE is_deleted = false ), @@ -89,9 +96,9 @@ export default class MockRepository { SELECT new_paginated.idx, new_paginated.title, - new_paginated.created_at as "createdAt", - new_paginated.like_count as "likeCount", - member.nickname as "writerNickname" + TO_CHAR(new_paginated.created_at,'YYYY-MM-DD') as "createdAt", + member.nickname as "writerNickname", + (SELECT COUNT(*) FROM like_history lh WHERE lh.article_idx = new_paginated.idx) AS "likeCount" FROM new_paginated JOIN member ON member.idx = new_paginated.member_idx ) @@ -121,24 +128,23 @@ export default class MockRepository { idx, title, member_idx, - created_at, - like_count + created_at FROM mock WHERE title LIKE $1 AND is_deleted = false ), like_count_paginated AS ( SELECT * FROM filtered - ORDER BY like_count DESC, created_at DESC + ORDER BY created_at DESC LIMIT $2 OFFSET $3 ), nickname_added AS ( SELECT like_count_paginated.idx, like_count_paginated.title, - like_count_paginated.like_count as "likeCount", - like_count_paginated.created_at as "createdAt", - member.nickname as "writerNickname" + TO_CHAR(like_count_paginated.created_at,'YYYY-MM-DD') as "createdAt", + member.nickname as "writerNickname", + (SELECT COUNT(*) FROM like_history lc WHERE lc.article_idx = like_count_paginated.idx) AS "likeCount" FROM like_count_paginated JOIN member ON member.idx = like_count_paginated.member_idx ) @@ -168,8 +174,7 @@ export default class MockRepository { idx, title, member_idx, - created_at, - like_count + created_at FROM mock WHERE title LIKE $1 AND is_deleted = false ), @@ -183,9 +188,9 @@ export default class MockRepository { SELECT new_paginated.idx, new_paginated.title, - new_paginated.created_at as "createdAt", - new_paginated.like_count as "likeCount", - member.nickname as "writerNickname" + TO_CHAR(new_paginated.created_at,'YYYY-MM-DD') as "createdAt", + member.nickname as "writerNickname", + (SELECT COUNT(*) FROM like_history lh WHERE lh.article_idx = new_paginated.idx) AS "likeCount" FROM new_paginated JOIN member ON member.idx = new_paginated.member_idx ) diff --git a/src/mock/router/index.ts b/src/mock/router/index.ts index 2b88eb9..652a2de 100644 --- a/src/mock/router/index.ts +++ b/src/mock/router/index.ts @@ -2,11 +2,9 @@ import express from 'express' import { listRouter } from './listRouter.js' import { mockItemRouter } from './mockItemRouter.js' import { quizRouter } from './quizRouter.js' -import { writeRouter } from './writeRouter.js' export const mockRouter = express.Router() mockRouter.use('/list', listRouter) -mockRouter.use('/write', writeRouter) mockRouter.use('/quiz', quizRouter) mockRouter.use('/', mockItemRouter) diff --git a/src/mock/router/listRouter.ts b/src/mock/router/listRouter.ts index 223a943..7723af4 100644 --- a/src/mock/router/listRouter.ts +++ b/src/mock/router/listRouter.ts @@ -1,8 +1,7 @@ import controller from '#controller' import express from 'express' -import ListQuery from '../entity/dao/frontend/request/query/ListRequest.js' -import SearchQuery from '../entity/dao/frontend/request/query/SearchRequest.js' +import ListRequest from '../entity/dao/frontend/request/query/ListRequest.js' import ListResponse from '../entity/dao/frontend/response/ListResponse.js' import ListService from '../service/ListService.js' @@ -12,7 +11,7 @@ listRouter.get( '/', controller( 'login', - ListQuery, + ListRequest, null, null, ListResponse, @@ -20,16 +19,3 @@ listRouter.get( return res.send(await ListService.getMockList(req.query)) }), ) - -listRouter.get( - '/search', - controller( - 'login', - SearchQuery, - null, - null, - ListResponse, - )(async (req, res) => { - return res.send(await ListService.searchMock(req.query)) - }), -) diff --git a/src/mock/router/mockItemRouter.ts b/src/mock/router/mockItemRouter.ts index 2ace21f..7a08a11 100644 --- a/src/mock/router/mockItemRouter.ts +++ b/src/mock/router/mockItemRouter.ts @@ -3,14 +3,31 @@ import express from 'express' import multipartParser from '#util/multipartParser' import MockEditRequest from '../entity/dao/frontend/request/body/MockEditRequest.js' +import WriteRequest from '../entity/dao/frontend/request/body/WriteRequest.js' import MockIdxPath from '../entity/dao/frontend/request/path/MockIdxPath.js' import DetailResponse from '../entity/dao/frontend/response/DetailResponse.js' import IndividualMockInfoResponse from '../entity/dao/frontend/response/IndividualDetailResponse.js' import ResultResponse from '../entity/dao/frontend/response/ResultResponse.js' +import WriteResponse from '../entity/dao/frontend/response/WriteResponse.js' import MockItemService from '../service/MockItemService.js' +import WriteService from '../service/WriteService.js' export const mockItemRouter = express.Router() +mockItemRouter.post( + '/', + multipartParser('image', 1), + controller( + 'login', + null, + null, + WriteRequest, + WriteResponse, + )(async (req, res) => { + return res.send(await WriteService.writeMock(req.memberIdx, req.body)) + }), +) + mockItemRouter.get( '/:idx', controller( @@ -24,55 +41,52 @@ mockItemRouter.get( }), ) -mockItemRouter.get( - '/:idx/result', +mockItemRouter.delete( + '/:idx', controller( 'login', null, MockIdxPath, null, - ResultResponse, + null, )(async (req, res) => { - return res.send( - await MockItemService.getMockResult(req.memberIdx, req.params), - ) + await MockItemService.deleteMock(req.memberIdx, req.params) + res.sendStatus(200) }), ) -mockItemRouter.post( - '/:idx/result', +mockItemRouter.patch( + '/:idx', + multipartParser('image', 1), controller( 'login', null, MockIdxPath, - null, + MockEditRequest, null, )(async (req, res) => { - await MockItemService.saveMockResult(req.memberIdx, req.params) - res.sendStatus(201) + await MockItemService.editMock(req.memberIdx, req.params, req.body) + res.sendStatus(200) }), ) mockItemRouter.get( - '/:idx/individual', + '/:idx/result', controller( 'login', null, MockIdxPath, null, - IndividualMockInfoResponse, + ResultResponse, )(async (req, res) => { return res.send( - await MockItemService.getIndividualMockDetail({ - memberIdx: req.memberIdx, - path: req.params, - }), + await MockItemService.getMockResult(req.memberIdx, req.params), ) }), ) -mockItemRouter.delete( - '/:idx/delete', +mockItemRouter.post( + '/:idx/result', controller( 'login', null, @@ -80,22 +94,25 @@ mockItemRouter.delete( null, null, )(async (req, res) => { - await MockItemService.deleteMock(req.memberIdx, req.params) - res.sendStatus(200) + await MockItemService.saveMockResult(req.memberIdx, req.params) + res.sendStatus(201) }), ) -mockItemRouter.patch( - '/:idx/edit', - multipartParser('image', 1), +mockItemRouter.get( + '/:idx/individual', controller( 'login', null, MockIdxPath, - MockEditRequest, null, + IndividualMockInfoResponse, )(async (req, res) => { - await MockItemService.editMock(req.memberIdx, req.params, req.body) - res.sendStatus(200) + return res.send( + await MockItemService.getIndividualMockDetail({ + memberIdx: req.memberIdx, + path: req.params, + }), + ) }), ) diff --git a/src/mock/router/writeRouter.ts b/src/mock/router/writeRouter.ts deleted file mode 100644 index 37e3cb8..0000000 --- a/src/mock/router/writeRouter.ts +++ /dev/null @@ -1,22 +0,0 @@ -import controller from '#controller' -import multipartParser from '#util/multipartParser' -import express from 'express' -import WriteRequest from '../entity/dao/frontend/request/body/WriteRequest.js' -import WriteResponse from '../entity/dao/frontend/response/WriteResponse.js' -import WriteService from '../service/WriteService.js' - -export const writeRouter = express.Router() - -writeRouter.post( - '/', - multipartParser('image', 1), - controller( - 'login', - null, - null, - WriteRequest, - WriteResponse, - )(async (req, res) => { - return res.send(await WriteService.writeMock(req.memberIdx, req.body)) - }), -) diff --git a/src/mock/service/ListService.ts b/src/mock/service/ListService.ts index 1b23490..821fd23 100644 --- a/src/mock/service/ListService.ts +++ b/src/mock/service/ListService.ts @@ -1,69 +1,56 @@ import ErrorRegistry from '#error/ErrorRegistry' -import ListQuery from '../entity/dao/frontend/request/query/ListRequest.js' -import SearchQuery from '../entity/dao/frontend/request/query/SearchRequest.js' +import ListRequest from '../entity/dao/frontend/request/query/ListRequest.js' import ListResponse from '../entity/dao/frontend/response/ListResponse.js' -import MockList from '../entity/dto/MockList.js' import MockRepository from '../repository/MockRepository.js' export default class ListService { - static async getMockList(query: ListQuery): Promise { - const currentPageNumber = query.current - const displayCount = query.display - const offset = (currentPageNumber - 1) * displayCount - - const dbResult = await MockRepository.getPaginatedMockList( - displayCount, - offset, - ) - - const mockList = new MockList( - currentPageNumber, - displayCount, - dbResult.totalCount, - dbResult.mocks, - ) - - return ListResponse.of(mockList) - } - - static async searchMock(query: SearchQuery): Promise { - const currentPageNumber = query.current - const displayCount = query.display - const offset = (currentPageNumber - 1) * displayCount - const title = '%' + query.title + '%' + static async getMockList(listRequest: ListRequest): Promise { + const { current, display, sort, title } = listRequest + const offset = (current - 1) * display + + let dbResult + if (!listRequest.sort && !listRequest.title) { + dbResult = await MockRepository.getPaginatedMockList(display, offset) + } else { + let titleQuery + if (!title) { + titleQuery = '%%%' + } else { + titleQuery = '%' + title + '%' + } + + if (!sort) { + dbResult = await MockRepository.getFilteredNewMockList( + titleQuery, + display, + offset, + ) + } else if (sort === 'new') { + dbResult = await MockRepository.getFilteredNewMockList( + titleQuery, + display, + offset, + ) + } else if (sort === 'like') { + dbResult = await MockRepository.getFilteredLikeDescMockList( + titleQuery, + display, + offset, + ) + } else { + throw ErrorRegistry.INTERNAL_SERVER_ERROR + } + } - const dbResult = await this.getDBResultBySortType( - query.sort, - title, - displayCount, - offset, - ) + if (dbResult.totalCount === 0) { + return new ListResponse(current, display, 0, []) + } - const mockList = new MockList( - currentPageNumber, - displayCount, + return new ListResponse( + current, + display, dbResult.totalCount, dbResult.mocks, ) - return ListResponse.of(mockList) - } - - private static getDBResultBySortType = ( - sortType: 'new' | 'like', - title: string, - displayCount: number, - offset: number, - ) => { - if (sortType === 'new') { - return MockRepository.getFilteredNewMockList(title, displayCount, offset) - } - if (sortType === 'like') { - return MockRepository.getFilteredLikeDescMockList( - title, - displayCount, - offset, - ) - } - throw ErrorRegistry.INTERNAL_SERVER_ERROR } } diff --git a/src/mock/service/MockItemService.ts b/src/mock/service/MockItemService.ts index 52e512d..e6c4141 100644 --- a/src/mock/service/MockItemService.ts +++ b/src/mock/service/MockItemService.ts @@ -22,6 +22,7 @@ export default class MockItemService { result.writerNickname, result.images, result.firstQuizIdx, + result.likeCount, result.ranks, ) } @@ -32,9 +33,12 @@ export default class MockItemService { ): Promise { const result = await MockScoreRepository.getScore(userIdx, path.idx) - const topPercentile = - (result.greaterEqualCandidateCount / result.totalCandidateCount) * 100 - return new ResultResponse(result.score, result.maxScore, topPercentile) + return new ResultResponse( + result.score, + result.maxScore, + result.greaterEqualCandidateCount, + result.totalCandidateCount, + ) } static async saveMockResult( diff --git a/src/mock/service/WriteService.ts b/src/mock/service/WriteService.ts index 3aa30f6..b7dc9d7 100644 --- a/src/mock/service/WriteService.ts +++ b/src/mock/service/WriteService.ts @@ -10,6 +10,8 @@ export default class WriteService { const singleChoiceQuizCount = Math.floor(quizCount * 0.8) const textQuizCount = Math.ceil(quizCount * 0.2) + console.log(body) + const generatedQuizzes = await Promise.all([ AIAdapter.getSingleChoiceQuizzes( body.title, diff --git a/src/notice/entity/dao/db/NoticeInfoFromDB.ts b/src/notice/entity/dao/db/NoticeInfoFromDB.ts new file mode 100644 index 0000000..8a44c4c --- /dev/null +++ b/src/notice/entity/dao/db/NoticeInfoFromDB.ts @@ -0,0 +1,7 @@ +export interface NoticeInfoFromDB { + title: string + writerNickname: string + content: string + createdAt: string + images: string[] +} diff --git a/src/notice/entity/dao/db/NoticeListFromDB.ts b/src/notice/entity/dao/db/NoticeListFromDB.ts new file mode 100644 index 0000000..4dea803 --- /dev/null +++ b/src/notice/entity/dao/db/NoticeListFromDB.ts @@ -0,0 +1,12 @@ +import { UUID } from 'crypto' + +export interface NoticeListFromDB { + totalCount: number + list: { + idx: UUID + title: string + writerNickname: string + content: string + createdAt: string + }[] +} diff --git a/src/notice/entity/dao/frontend/request/ListRequest.ts b/src/notice/entity/dao/frontend/request/ListRequest.ts index 3b7e363..0b428cc 100644 --- a/src/notice/entity/dao/frontend/request/ListRequest.ts +++ b/src/notice/entity/dao/frontend/request/ListRequest.ts @@ -1,24 +1,29 @@ import ErrorRegistry from '#error/ErrorRegistry' interface ListRequestParams { - current: number + title: string display: number + current: number } export default class ListRequest { + public title: string public current: number public display: number constructor(params: ListRequestParams) { + if (params.title && params.title.length > 100) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } + this.title = params.title + if (!params.current) { throw ErrorRegistry.INVALID_INPUT_FORMAT - } else { - this.current = Number(params.current) } + this.display = Number(params.display) if (!params.display) { throw ErrorRegistry.INVALID_INPUT_FORMAT - } else { - this.display = Number(params.display) } + this.current = Number(params.current) } } diff --git a/src/notice/entity/dao/frontend/request/NoticeEditBodyRequest.ts b/src/notice/entity/dao/frontend/request/NoticeEditBodyRequest.ts index 867a14c..30f2ebb 100644 --- a/src/notice/entity/dao/frontend/request/NoticeEditBodyRequest.ts +++ b/src/notice/entity/dao/frontend/request/NoticeEditBodyRequest.ts @@ -4,6 +4,7 @@ import Regex from '#util/Regex' interface NoticeEditBodyRequestParams { title: string content: string + existingUrls: string[] uploadUrls: string[] } @@ -12,14 +13,16 @@ export default class NoticeEditBodyRequest { public content: string public uploadUrls: string[] constructor(params: NoticeEditBodyRequestParams) { - if ( - !new RegExp(Regex.TITLE).test(params.title) || - !new RegExp(Regex.CONTENT).test(params.content) - ) { + if (!Regex.TITLE.test(params.title)) { throw ErrorRegistry.INVALID_INPUT_FORMAT } this.title = params.title + + if (params.content && !Regex.CONTENT.test(params.content)) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } this.content = params.content + this.uploadUrls = params.uploadUrls } } diff --git a/src/notice/entity/dao/frontend/request/NoticePathRequest.ts b/src/notice/entity/dao/frontend/request/NoticePathRequest.ts index 6339f88..961d46d 100644 --- a/src/notice/entity/dao/frontend/request/NoticePathRequest.ts +++ b/src/notice/entity/dao/frontend/request/NoticePathRequest.ts @@ -9,7 +9,7 @@ interface NoticePathRequestParams { export default class NoticePathRequest { public idx: UUID constructor(params: NoticePathRequestParams) { - if (!new RegExp(Regex.UUID).test(params.idx)) { + if (!Regex.UUID.test(params.idx)) { throw ErrorRegistry.INVALID_INPUT_FORMAT } this.idx = params.idx diff --git a/src/notice/entity/dao/frontend/request/WriteRequest.ts b/src/notice/entity/dao/frontend/request/WriteRequest.ts index 9a5284e..c2421f1 100644 --- a/src/notice/entity/dao/frontend/request/WriteRequest.ts +++ b/src/notice/entity/dao/frontend/request/WriteRequest.ts @@ -12,14 +12,16 @@ export default class WriteRequest { public content: string public uploadUrls: string[] constructor(params: WriteRequestParams) { - if ( - !new RegExp(Regex.TITLE).test(params.title) || - !new RegExp(Regex.CONTENT).test(params.content) - ) { + if (!Regex.TITLE.test(params.title)) { throw ErrorRegistry.INVALID_INPUT_FORMAT } this.title = params.title + + if (params.content && !Regex.CONTENT.test(params.content)) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } this.content = params.content + this.uploadUrls = params.uploadUrls } } diff --git a/src/notice/entity/dao/frontend/response/ListResponse.ts b/src/notice/entity/dao/frontend/response/ListResponse.ts index 787e3e4..b77d96b 100644 --- a/src/notice/entity/dao/frontend/response/ListResponse.ts +++ b/src/notice/entity/dao/frontend/response/ListResponse.ts @@ -1,4 +1,4 @@ -import NoticeList from 'src/notice/entity/dto/NoticeList.js' +import { UUID } from 'crypto' export default class NoticeListResponse { firstPageNumber: number @@ -7,45 +7,36 @@ export default class NoticeListResponse { prevPageExist: boolean nextPageExist: boolean notices: { - idx: number + idx: UUID title: string writerNickname: string createdAt: string }[] - public static of(noticeList: NoticeList): NoticeListResponse { - return new NoticeListResponse( - noticeList.firstPageNumber, - noticeList.currentPageNumber, - noticeList.lastPageNumber, - noticeList.prevPageExist, - noticeList.nextPageExist, - noticeList.notices, - ) - } - - public static createEmpty(): NoticeListResponse { - return new NoticeListResponse(1, 1, 1, false, false, []) - } - constructor( - firstPageNumber: number, currentPageNumber: number, - lastPageNumber: number, - prevPageExist: boolean, - nextPageExist: boolean, + displayCount: number, + totalCount: number, notices: { - idx: number + idx: UUID title: string writerNickname: string + content: string createdAt: string }[], ) { - this.firstPageNumber = firstPageNumber this.currentPageNumber = currentPageNumber - this.lastPageNumber = lastPageNumber - this.prevPageExist = prevPageExist - this.nextPageExist = nextPageExist + this.firstPageNumber = Math.floor((currentPageNumber - 1) / 10) * 10 + 1 + const pageOffset = Math.min( + 9, + Math.floor( + (totalCount - (this.firstPageNumber - 1) * displayCount) / displayCount, + ), + ) + this.lastPageNumber = this.firstPageNumber + pageOffset + this.prevPageExist = this.firstPageNumber > 1 + this.nextPageExist = + this.lastPageNumber < Math.floor((totalCount - 1) / displayCount) + 1 this.notices = notices } } diff --git a/src/notice/entity/dao/frontend/response/ListSearchResponse.ts b/src/notice/entity/dao/frontend/response/ListSearchResponse.ts deleted file mode 100644 index dc088d2..0000000 --- a/src/notice/entity/dao/frontend/response/ListSearchResponse.ts +++ /dev/null @@ -1,50 +0,0 @@ -import NoticeList from 'src/notice/entity/dto/NoticeList.js' -export default class NoticeSearchListResponse { - firstPageNumber: number - currentPageNumber: number - lastPageNumber: number - prevPageExist: boolean - nextPageExist: boolean - notices: { - idx: number - title: string - writerNickname: string - createdAt: string - }[] - - public static of(noticeList: NoticeList): NoticeSearchListResponse { - return new NoticeSearchListResponse( - noticeList.firstPageNumber, - noticeList.currentPageNumber, - noticeList.lastPageNumber, - noticeList.prevPageExist, - noticeList.nextPageExist, - noticeList.notices, - ) - } - - public static createEmpty(): NoticeSearchListResponse { - return new NoticeSearchListResponse(1, 1, 1, false, false, []) - } - - constructor( - firstPageNumber: number, - currentPageNumber: number, - lastPageNumber: number, - prevPageExist: boolean, - nextPageExist: boolean, - notices: { - idx: number - title: string - writerNickname: string - createdAt: string - }[], - ) { - this.firstPageNumber = firstPageNumber - this.currentPageNumber = currentPageNumber - this.lastPageNumber = lastPageNumber - this.prevPageExist = prevPageExist - this.nextPageExist = nextPageExist - this.notices = notices - } -} diff --git a/src/notice/entity/dto/NoticeList.ts b/src/notice/entity/dto/NoticeList.ts deleted file mode 100644 index e58bbfa..0000000 --- a/src/notice/entity/dto/NoticeList.ts +++ /dev/null @@ -1,42 +0,0 @@ -export default class NoticeList { - firstPageNumber: number - currentPageNumber: number - lastPageNumber: number - prevPageExist: boolean - nextPageExist: boolean - totalCount: number - notices: { - idx: number - title: string - writerNickname: string - createdAt: string - }[] - - constructor( - currentPageNumber: number, - displayCount: number, - totalCount: number, - notices: { - idx: number - title: string - writerNickname: string - createdAt: string - totalCount: number - }[], - ) { - this.totalCount = totalCount - this.currentPageNumber = currentPageNumber - this.firstPageNumber = Math.floor((currentPageNumber - 1) / 10) * 10 + 1 - const pageOffset = Math.min( - 9, - Math.floor( - (totalCount - (this.firstPageNumber - 1) * displayCount) / displayCount, - ), - ) - this.lastPageNumber = this.firstPageNumber + pageOffset - this.prevPageExist = this.firstPageNumber > 1 - this.nextPageExist = - this.lastPageNumber < Math.floor((totalCount - 1) / displayCount) + 1 - this.notices = notices - } -} diff --git a/src/notice/repository/NoticeRepository.ts b/src/notice/repository/NoticeRepository.ts index 962bad2..02ce095 100644 --- a/src/notice/repository/NoticeRepository.ts +++ b/src/notice/repository/NoticeRepository.ts @@ -1,6 +1,8 @@ import { postgres } from '#config/postgres' import ErrorRegistry from '#error/ErrorRegistry' import { UUID } from 'crypto' +import { NoticeInfoFromDB } from '../entity/dao/db/NoticeInfoFromDB.js' +import { NoticeListFromDB } from '../entity/dao/db/NoticeListFromDB.js' export default class NoticeRepository { static async getPagedListFromDb(display: number, offset: number) { @@ -10,7 +12,7 @@ export default class NoticeRepository { SELECT notice.idx AS idx, notice.title AS title, mem.nickname AS "writerNickname", - notice.created_at AS "createdAt" + TO_CHAR(notice.created_at,'YYYY-MM-DD') AS "createdAt" FROM notice AS notice LEFT JOIN member AS mem ON notice.member_idx = mem.idx WHERE notice.is_deleted = 'f' @@ -26,10 +28,11 @@ export default class NoticeRepository { ) ).rows[0].totalCount return { - listResult, - totalCountResult, - } + list: listResult, + totalCount: totalCountResult, + } as NoticeListFromDB } + static async getSearchListFromDb( titleToSearch: string, display: number, @@ -41,7 +44,7 @@ export default class NoticeRepository { SELECT notice.idx AS idx, notice.title AS title, mem.nickname AS "writerNickname", - notice.created_at AS "createdAt" + TO_CHAR(notice.created_at,'YYYY-MM-DD') AS "createdAt" FROM notice AS notice LEFT JOIN member AS mem ON notice.member_idx = mem.idx WHERE notice.is_deleted = 'f' AND title LIKE $1 @@ -51,16 +54,18 @@ export default class NoticeRepository { [titleToSearch, display, offset], ) ).rows + const totalCountResult = ( await postgres.query( `SELECT COUNT(*) AS "totalCount" FROM notice WHERE is_deleted = 'f' AND title LIKE $1`, [titleToSearch], ) ).rows[0].totalCount + return { - listResult, - totalCountResult, - } + totalCount: totalCountResult, + list: listResult, + } as NoticeListFromDB } static async insertNoticeToDb( memberIdx: number, @@ -98,7 +103,7 @@ export default class NoticeRepository { `SELECT n.title, mem.nickname AS "writerNickname", n.content, - n.created_at AS "createdAt", + TO_CHAR(n.created_at,'YYYY-MM-DD') AS "createdAt", i.urls AS "images" FROM notice AS n LEFT JOIN image AS i ON n.idx = i.article_idx @@ -106,7 +111,7 @@ export default class NoticeRepository { WHERE n.idx = $1 AND n.is_deleted = 'f'`, [idx], ) - ).rows[0] + ).rows[0] as NoticeInfoFromDB } static async deleteNoticeFromDb(idx: UUID) { return await postgres.query( @@ -119,7 +124,7 @@ export default class NoticeRepository { idx: UUID, title: string, content: string, - uploadUrls: string[], // uploadUrls는 null 또는 undefined일 수 있음 + uploadUrls: string[], ) { await postgres.query( ` diff --git a/src/notice/router/index.ts b/src/notice/router/index.ts index 734f9f7..56a7622 100644 --- a/src/notice/router/index.ts +++ b/src/notice/router/index.ts @@ -1,10 +1,8 @@ import express from 'express' import { listRouter } from './listRouter.js' import { noticeItemRouter } from './noticeItemRouter.js' -import { writeRouter } from './writeRouter.js' export const noticeRouter = express.Router() noticeRouter.use('/list', listRouter) -noticeRouter.use('/write', writeRouter) noticeRouter.use('/', noticeItemRouter) diff --git a/src/notice/router/listRouter.ts b/src/notice/router/listRouter.ts index d7a584d..7e4f29b 100644 --- a/src/notice/router/listRouter.ts +++ b/src/notice/router/listRouter.ts @@ -1,9 +1,7 @@ import controller from '#controller' import express from 'express' import ListRequest from '../entity/dao/frontend/request/ListRequest.js' -import ListSearchRequest from '../entity/dao/frontend/request/ListSearchRequest.js' import ListResponse from '../entity/dao/frontend/response/ListResponse.js' -import ListSearchResponse from '../entity/dao/frontend/response/ListSearchResponse.js' import ListService from '../service/ListService.js' export const listRouter = express.Router() @@ -19,16 +17,3 @@ listRouter.get( return res.send(await ListService.getList(req.query)) }), ) - -listRouter.get( - '/search', - controller( - 'login', - ListSearchRequest, - null, - null, - ListSearchResponse, - )(async (req, res) => { - return res.send(await ListService.searchList(req.query)) - }), -) diff --git a/src/notice/router/noticeItemRouter.ts b/src/notice/router/noticeItemRouter.ts index 61b3bbf..89009f3 100644 --- a/src/notice/router/noticeItemRouter.ts +++ b/src/notice/router/noticeItemRouter.ts @@ -3,10 +3,27 @@ import multipartParser from '#util/multipartParser' import express from 'express' import NoticeEditBodyRequest from '../entity/dao/frontend/request/NoticeEditBodyRequest.js' import NoticePathRequest from '../entity/dao/frontend/request/NoticePathRequest.js' +import WriteRequest from '../entity/dao/frontend/request/WriteRequest.js' import NoticeResponse from '../entity/dao/frontend/response/NoticeResponse.js' +import WriteResponse from '../entity/dao/frontend/response/WriteResponse.js' import NoticeItemService from '../service/NoticeItemService.js' +import WriteService from '../service/WriteService.js' export const noticeItemRouter = express.Router() +noticeItemRouter.post( + '/', + multipartParser('image', 5), + controller( + 'admin', + null, + null, + WriteRequest, + WriteResponse, + )(async (req, res) => { + return res.send(await WriteService.writeNotice(req.memberIdx, req.body)) + }), +) + noticeItemRouter.get( '/:idx', controller( diff --git a/src/notice/router/writeRouter.ts b/src/notice/router/writeRouter.ts deleted file mode 100644 index 3e1788f..0000000 --- a/src/notice/router/writeRouter.ts +++ /dev/null @@ -1,21 +0,0 @@ -import controller from '#controller' -import multipartParser from '#util/multipartParser' -import express from 'express' -import WriteRequest from '../entity/dao/frontend/request/WriteRequest.js' -import WriteResponse from '../entity/dao/frontend/response/WriteResponse.js' -import WriteService from '../service/WriteService.js' -export const writeRouter = express.Router() - -writeRouter.post( - '/', - multipartParser('image', 5), - controller( - 'admin', - null, - null, - WriteRequest, - WriteResponse, - )(async (req, res) => { - return res.send(await WriteService.writeNotice(req.body)) - }), -) diff --git a/src/notice/service/ListService.ts b/src/notice/service/ListService.ts index 2628763..61986ce 100644 --- a/src/notice/service/ListService.ts +++ b/src/notice/service/ListService.ts @@ -1,44 +1,29 @@ import ListRequest from '../entity/dao/frontend/request/ListRequest.js' -import ListSearchRequest from '../entity/dao/frontend/request/ListSearchRequest.js' import ListResponse from '../entity/dao/frontend/response/ListResponse.js' -import ListSearchResponse from '../entity/dao/frontend/response/ListSearchResponse.js' -import NoticeList from '../entity/dto/NoticeList.js' import NoticeRepository from '../repository/NoticeRepository.js' export default class ListService { static async getList(listRequest: ListRequest) { - const { current, display } = listRequest + const { current, display, title } = listRequest const offset = (current - 1) * display - const listData: any = await NoticeRepository.getPagedListFromDb( - display, - offset, - ) - - const noticeList = new NoticeList( - current, - display, - listData.totalCountResult, - listData.listResult, - ) - return ListResponse.of(noticeList) - } + let listData + if (!title) { + listData = await NoticeRepository.getPagedListFromDb(display, offset) + } else { + const titleQuery = '%' + title + '%' + listData = await NoticeRepository.getSearchListFromDb( + titleQuery, + display, + offset, + ) + } - static async searchList(listSearchRequest: ListSearchRequest) { - const { title, display, current } = listSearchRequest - const offset = (current - 1) * display - const titleToSearch = '%' + title + '%' - const searchData: any = await NoticeRepository.getSearchListFromDb( - titleToSearch, - display, - offset, - ) - const noticeList = new NoticeList( + return new ListResponse( current, display, - searchData.totalCountResult, - searchData.listResult, + listData.totalCount, + listData.list, ) - return ListSearchResponse.of(noticeList) } } diff --git a/src/notice/service/NoticeItemService.ts b/src/notice/service/NoticeItemService.ts index 3827039..0c81d50 100644 --- a/src/notice/service/NoticeItemService.ts +++ b/src/notice/service/NoticeItemService.ts @@ -13,16 +13,19 @@ export default class NoticeItemService { } return new NoticeResponse(result) } + static async deleteNotice(noticePathRequest: NoticePathRequest) { const { idx } = noticePathRequest await NoticeRepository.deleteNoticeFromDb(idx) } + static async editNotice( noticePathRequest: NoticePathRequest, noticeEditBodyRequest: NoticeEditBodyRequest, ) { const { idx } = noticePathRequest const { title, content, uploadUrls } = noticeEditBodyRequest + console.log(noticeEditBodyRequest) await NoticeRepository.editNoticeFromDb(idx, title, content, uploadUrls) } } diff --git a/src/notice/service/WriteService.ts b/src/notice/service/WriteService.ts index 5fd7f90..6f7ae07 100644 --- a/src/notice/service/WriteService.ts +++ b/src/notice/service/WriteService.ts @@ -3,12 +3,16 @@ import WriteResponse from '../entity/dao/frontend/response/WriteResponse.js' import NoticeRepository from '../repository/NoticeRepository.js' export default class WriteService { - static async writeNotice(writeRequest: WriteRequest) { + static async writeNotice(memberIdx: number, writeRequest: WriteRequest) { const { title, content, uploadUrls } = writeRequest - // 멤버 인덱스 return new WriteResponse( - await NoticeRepository.insertNoticeToDb(2, title, content, uploadUrls), + await NoticeRepository.insertNoticeToDb( + memberIdx, + title, + content, + uploadUrls, + ), ) } } diff --git a/src/rank/entity/dao/request/FilteredRankListRequest.ts b/src/rank/entity/dao/request/FilteredRankListRequest.ts deleted file mode 100644 index 342e604..0000000 --- a/src/rank/entity/dao/request/FilteredRankListRequest.ts +++ /dev/null @@ -1,26 +0,0 @@ -import ErrorRegistry from '#error/ErrorRegistry' -import Regex from '#util/Regex' - -interface FilteredRankListRequestParams { - tier: string - current: number - nickname: string -} - -export default class FilteredRankListRequest { - public tier: string - public current: number - public nickname: string - constructor(params: FilteredRankListRequestParams) { - if ( - !new RegExp(Regex.TIER).test(params.tier) || - !params.nickname || - params.nickname.length > 100 - ) { - throw ErrorRegistry.INVALID_INPUT_FORMAT - } - this.tier = params.tier - this.current = Number(params.current) - this.nickname = params.nickname - } -} diff --git a/src/rank/entity/dao/request/RankListRequest.ts b/src/rank/entity/dao/request/RankListRequest.ts index d10da31..e242e77 100644 --- a/src/rank/entity/dao/request/RankListRequest.ts +++ b/src/rank/entity/dao/request/RankListRequest.ts @@ -1,11 +1,30 @@ +import ErrorRegistry from '#error/ErrorRegistry' +import Regex from '#util/Regex' + interface RankListRequestParams { + tier: string current: number + nickname: string } export default class RankListRequest { + public tier: string public current: number + public nickname: string constructor(params: RankListRequestParams) { + if (!params.current) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } this.current = Number(params.current) + + if (params.tier && !Regex.TIER.test(params.tier)) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } + this.tier = params.tier + + if (params.nickname && params.nickname.length > 12) { + throw ErrorRegistry.INVALID_INPUT_FORMAT + } + this.nickname = params.nickname } } -;`` diff --git a/src/rank/repository/rankListRepository.ts b/src/rank/repository/rankListRepository.ts index 9ce9ece..ff282fd 100644 --- a/src/rank/repository/rankListRepository.ts +++ b/src/rank/repository/rankListRepository.ts @@ -72,7 +72,7 @@ export default class RankListRepository { WHEN rank BETWEEN 3 AND 5 THEN 'GOLD' WHEN rank BETWEEN 6 AND 8 THEN 'SILVER' ELSE 'BRONZE' - END + END WHEN score = 0 THEN 'BRONZE' WHEN rank BETWEEN 1 AND total_count * 0.06 THEN 'DIAMOND' WHEN rank BETWEEN total_count * 0.06 + 1 AND total_count * 0.12 THEN 'PLATINUM' @@ -86,17 +86,17 @@ export default class RankListRepository { ), ranked_tiers AS ( SELECT - ROW_NUMBER() OVER (ORDER BY score DESC) AS new_rank, - tier, + ROW_NUMBER() OVER (ORDER BY score DESC) AS rank, + tier, nickname, score FROM ranked_with_tiers - WHERE tier LIKE $1 + WHERE tier LIKE $1 AND nickname LIKE $3 ) SELECT * FROM ranked_tiers - WHERE new_rank > $2 AND nickname LIKE $3 - ORDER BY new_rank + WHERE rank > $2 + ORDER BY rank LIMIT 10; `, [tier, current, nickname], diff --git a/src/rank/router/rankListRouter.ts b/src/rank/router/rankListRouter.ts index b9e2813..625e2e0 100644 --- a/src/rank/router/rankListRouter.ts +++ b/src/rank/router/rankListRouter.ts @@ -1,6 +1,5 @@ import controller from '#controller' import express from 'express' -import FilteredRankListRequest from '../entity/dao/request/FilteredRankListRequest.js' import RankListRequest from '../entity/dao/request/RankListRequest.js' import RankListResponse from '../entity/dao/response/RankListResponse.js' import RankListService from '../service/rankListService.js' @@ -19,16 +18,3 @@ rankListRouter.get( return res.send(await RankListService.getRankList(req.query)) }), ) - -rankListRouter.get( - '/search', - controller( - 'login', - FilteredRankListRequest, - null, - null, - RankListResponse, - )(async (req, res) => { - return res.send(await RankListService.getFilteredRankList(req.query)) - }), -) diff --git a/src/rank/service/rankListService.ts b/src/rank/service/rankListService.ts index 1f69f25..a6daa7b 100644 --- a/src/rank/service/rankListService.ts +++ b/src/rank/service/rankListService.ts @@ -1,40 +1,36 @@ -import FilteredRankListRequest from '../entity/dao/request/FilteredRankListRequest.js' import RankListRequest from '../entity/dao/request/RankListRequest.js' import RankListResponse from '../entity/dao/response/RankListResponse.js' import RankListRepository from '../repository/rankListRepository.js' export default class RankListService { static async getRankList(rankListRequest: RankListRequest) { - const { current } = rankListRequest - const result: any = await RankListRepository.getRankListFromDb(current) - return new RankListResponse(result) - } - static async getFilteredRankList( - filteredRankListRequest: FilteredRankListRequest, - ) { - const { tier, current, nickname } = filteredRankListRequest - if (!nickname) { - const result: any = await RankListRepository.getFilteredRankListFromDb( - tier, - current, - '%', - ) - return new RankListResponse(result) - } + const { current, tier, nickname } = rankListRequest + + let result + if (!tier && !nickname) { + result = await RankListRepository.getRankListFromDb(current) + } else { + let tierQuery: string + if (!tier) { + tierQuery = '%%%' + } else { + tierQuery = tier + } - if (!tier) { - const result: any = await RankListRepository.getFilteredRankListFromDb( - '%', + let nicknameQuery: string + if (!nickname) { + nicknameQuery = '%%%' + } else { + nicknameQuery = '%' + nickname + '%' + } + + result = await RankListRepository.getFilteredRankListFromDb( + tierQuery, current, - nickname, + nicknameQuery, ) - return new RankListResponse(result) } - const result: any = await RankListRepository.getFilteredRankListFromDb( - tier, - current, - nickname, - ) + return new RankListResponse(result) } }