From 8aeca641f9747815556f8714abb4773dfaaca14c Mon Sep 17 00:00:00 2001 From: Jinho Hyeon Date: Wed, 14 Jun 2023 13:52:31 +0900 Subject: [PATCH 1/9] =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=20=EC=9C=84?= =?UTF-8?q?=EC=B9=98=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 41299adf..5dfc6661 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "license": "GNU", "private": true, "dependencies": { + "@babel/plugin-proposal-decorators": "^7.22.3", "@types/swagger-ui-express": "^4.1.3", "@types/winston": "^2.4.4", "aws-sdk": "^2.1101.0", @@ -56,7 +57,6 @@ "@babel/core": "^7.17.8", "@babel/node": "^7.16.8", "@babel/plugin-proposal-class-properties": "^7.16.7", - "@babel/plugin-proposal-decorators": "^7.22.3", "@babel/plugin-proposal-object-rest-spread": "^7.17.3", "@babel/plugin-transform-runtime": "^7.17.0", "@babel/preset-env": "^7.16.11", From fc03c9363acf7ff842a0a941a8d5c86de723abc2 Mon Sep 17 00:00:00 2001 From: Jinho Hyeon Date: Fri, 16 Jun 2023 14:55:49 +0900 Subject: [PATCH 2/9] =?UTF-8?q?PerfumeService=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81=20(#511)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 불용 setter 삭제 * NoteService 분리 * LikePerfumeService 분리 * LikePerfume 관련 함수를 서비스로 분리 --- src/controllers/Perfume.ts | 4 +- src/service/LikePerfumeService.ts | 63 ++++++ src/service/NoteService.ts | 36 ++++ src/service/PerfumeService.ts | 185 +++++------------- .../service/LikePerfumeService.spec.ts | 44 +++++ .../test_unit/service/PerfumeService.spec.ts | 61 ++---- 6 files changed, 209 insertions(+), 184 deletions(-) create mode 100644 src/service/LikePerfumeService.ts create mode 100644 src/service/NoteService.ts create mode 100644 tests/test_unit/service/LikePerfumeService.spec.ts diff --git a/src/controllers/Perfume.ts b/src/controllers/Perfume.ts index b7a67a82..ba1fa750 100644 --- a/src/controllers/Perfume.ts +++ b/src/controllers/Perfume.ts @@ -51,11 +51,13 @@ import { DEFAULT_NEW_PERFUME_REQUEST_SIZE, } from '@utils/constants'; import _ from 'lodash'; +import { LikePerfumeService } from '@src/service/LikePerfumeService'; const LOG_TAG: string = '[Perfume/Controller]'; let Perfume: PerfumeService = new PerfumeService(); let SearchHistory: SearchHistoryService = new SearchHistoryService(); +const LikePerfume: LikePerfumeService = new LikePerfumeService(); /** * @swagger @@ -343,7 +345,7 @@ const likePerfume: RequestHandler = ( req.params )})` ); - Perfume.likePerfume(loginUserIdx, perfumeIdx) + LikePerfume.likePerfume(loginUserIdx, perfumeIdx) .then((result: boolean) => { LoggerHelper.logTruncated( logger.debug, diff --git a/src/service/LikePerfumeService.ts b/src/service/LikePerfumeService.ts new file mode 100644 index 00000000..75077cef --- /dev/null +++ b/src/service/LikePerfumeService.ts @@ -0,0 +1,63 @@ +import LikePerfumeDao from '@src/dao/LikePerfumeDao'; +import { FailedToCreateError, NotMatchedError } from '@src/utils/errors/errors'; +import _ from 'lodash'; + +let likePerfumeDao: LikePerfumeDao = new LikePerfumeDao(); + +export class LikePerfumeService { + likePerfumeDao: LikePerfumeDao; + + constructor(likePerfumeDao?: LikePerfumeDao) { + this.likePerfumeDao = likePerfumeDao ?? new LikePerfumeDao(); + } + /** + * 향수 좋아요 + * + * @param {number} userIdx + * @param {number} perfumeIdx + * @returns {Promise} + * @throws {FailedToCreateError} if failed to create likePerfume + **/ + async likePerfume(userIdx: number, perfumeIdx: number): Promise { + try { + await likePerfumeDao.read(userIdx, perfumeIdx); + await likePerfumeDao.delete(userIdx, perfumeIdx); + return false; + } catch (err) { + if (err instanceof NotMatchedError) { + await likePerfumeDao.create(userIdx, perfumeIdx); + return true; + } + throw new FailedToCreateError(); + } + } + + async isLike(userIdx: number, perfumeIdx: number): Promise { + try { + await this.likePerfumeDao.read(userIdx, perfumeIdx); + return true; + } catch (err) { + if (err instanceof NotMatchedError) { + return false; + } + throw err; + } + } + + isLikeJob(likePerfumeList: any[]): (obj: any) => any { + const likeMap: { [key: string]: boolean } = _.chain(likePerfumeList) + .keyBy('perfumeIdx') + .mapValues(() => true) + .value(); + + return (obj: any) => { + const ret: any = Object.assign({}, obj); + ret.isLiked = likeMap[obj.perfumeIdx] ? true : false; + return ret; + }; + } + + async readLikeInfo(userIdx: number, perfumeIdxList: number[]) { + return await likePerfumeDao.readLikeInfo(userIdx, perfumeIdxList); + } +} diff --git a/src/service/NoteService.ts b/src/service/NoteService.ts new file mode 100644 index 00000000..a7dab087 --- /dev/null +++ b/src/service/NoteService.ts @@ -0,0 +1,36 @@ +import NoteDao from '@src/dao/NoteDao'; +import { NoteDictDTO } from '@src/data/dto'; +import { + PERFUME_NOTE_TYPE_NORMAL, + PERFUME_NOTE_TYPE_SINGLE, +} from '@src/utils/constants'; + +export class NoteService { + noteDao: NoteDao; + constructor(noteDao?: NoteDao) { + this.noteDao = noteDao ?? new NoteDao(); + } + + async generateNote(perfumeIdx: number): Promise<{ + noteType: number; + noteDictDTO: { + top: string; + middle: string; + base: string; + single: string; + }; + }> { + const noteList: any[] = await this.noteDao.readByPerfumeIdx(perfumeIdx); + const noteDictDTO: { + top: string; + middle: string; + base: string; + single: string; + } = NoteDictDTO.createByNoteList(noteList); + const noteType: number = + noteDictDTO.single.length > 0 + ? PERFUME_NOTE_TYPE_SINGLE + : PERFUME_NOTE_TYPE_NORMAL; + return { noteType, noteDictDTO }; + } +} diff --git a/src/service/PerfumeService.ts b/src/service/PerfumeService.ts index 6fe467e7..021b5ee1 100644 --- a/src/service/PerfumeService.ts +++ b/src/service/PerfumeService.ts @@ -1,40 +1,35 @@ import { logger } from '@modules/winston'; -import { NotMatchedError, FailedToCreateError } from '@errors'; +import { NotMatchedError } from '@errors'; -import { removeKeyJob, flatJob } from '@utils/func'; -import { - PERFUME_NOTE_TYPE_SINGLE, - PERFUME_NOTE_TYPE_NORMAL, -} from '@utils/constants'; +import { flatJob, removeKeyJob } from '@utils/func'; -import UserDao from '@dao/UserDao'; +import KeywordDao from '@dao/KeywordDao'; import PerfumeDao from '@dao/PerfumeDao'; -import NoteDao from '@dao/NoteDao'; -import LikePerfumeDao from '@dao/LikePerfumeDao'; -import S3FileDao from '@dao/S3FileDao'; import ReviewDao from '@dao/ReviewDao'; -import KeywordDao from '@dao/KeywordDao'; +import S3FileDao from '@dao/S3FileDao'; +import UserDao from '@dao/UserDao'; import { - PagingDTO, ListAndCountDTO, - PerfumeThumbDTO, - PerfumeThumbKeywordDTO, - PerfumeSummaryDTO, - PerfumeSearchDTO, - PerfumeIntegralDTO, + PagingDTO, PerfumeDTO, + PerfumeIntegralDTO, + PerfumeSearchDTO, PerfumeSearchResultDTO, - UserDTO, - NoteDictDTO, + PerfumeSummaryDTO, + PerfumeThumbDTO, + PerfumeThumbKeywordDTO, PerfumeThumbWithReviewDTO, + UserDTO, } from '@dto/index'; -import fp from 'lodash/fp'; -import _ from 'lodash'; -import IngredientDao from '@src/dao/IngredientDao'; import { Ingredient } from '@sequelize'; +import IngredientDao from '@src/dao/IngredientDao'; +import _ from 'lodash'; +import fp from 'lodash/fp'; import { Op } from 'sequelize'; +import { NoteService } from './NoteService'; +import { LikePerfumeService } from './LikePerfumeService'; const LOG_TAG: string = '[Perfume/Service]'; const DEFAULT_VALUE_OF_INDEX = 0; @@ -42,8 +37,6 @@ const DEFAULT_VALUE_OF_INDEX = 0; let perfumeDao: PerfumeDao = new PerfumeDao(); let ingredientDao: IngredientDao = new IngredientDao(); let reviewDao: ReviewDao = new ReviewDao(); -let noteDao: NoteDao = new NoteDao(); -let likePerfumeDao: LikePerfumeDao = new LikePerfumeDao(); let keywordDao: KeywordDao = new KeywordDao(); let s3FileDao: S3FileDao = new S3FileDao(); let userDao: UserDao = new UserDao(); @@ -58,6 +51,11 @@ const commonJob = [ ), ]; class PerfumeService { + likePerfumeService: LikePerfumeService; + constructor(likePerfumeService?: LikePerfumeService) { + this.likePerfumeService = + likePerfumeService ?? new LikePerfumeService(); + } /** * 향수 세부 정보 조회 * @@ -81,7 +79,10 @@ class PerfumeService { flatJob('PerfumeDetail') )(_perfume); - perfume.isLiked = await this.isLike(userIdx, perfumeIdx); + perfume.isLiked = await this.likePerfumeService.isLike( + userIdx, + perfumeIdx + ); const keywordList: string[] = [ ...new Set( ( @@ -96,7 +97,9 @@ class PerfumeService { perfume.imageUrl ); - const { noteType, noteDictDTO } = await this.generateNote(perfumeIdx); + const { noteType, noteDictDTO } = await new NoteService().generateNote( + perfumeIdx + ); const perfumeSummaryDTO: PerfumeSummaryDTO = await this.generateSummary( perfumeIdx ); @@ -156,7 +159,7 @@ class PerfumeService { (it) => it.perfumeIdx ); const likePerfumeList: any[] = - await likePerfumeDao.readLikeInfo( + await this.likePerfumeService.readLikeInfo( perfumeSearchDTO.userIdx, perfumeIdxList ); @@ -166,7 +169,9 @@ class PerfumeService { ): PerfumeSearchResultDTO => { return fp.compose( ...commonJob, - this.isLikeJob(likePerfumeList), + this.likePerfumeService.isLikeJob( + likePerfumeList + ), PerfumeSearchResultDTO.createByJson )(item); } @@ -195,11 +200,14 @@ class PerfumeService { (it: PerfumeThumbDTO) => it.perfumeIdx ); const likePerfumeList: any[] = - await likePerfumeDao.readLikeInfo(userIdx, perfumeIdxList); + await this.likePerfumeService.readLikeInfo( + userIdx, + perfumeIdxList + ); return result.convertType((item: PerfumeThumbDTO) => { return fp.compose( ...commonJob, - this.isLikeJob(likePerfumeList), + this.likePerfumeService.isLikeJob(likePerfumeList), PerfumeThumbDTO.createByJson )(item); }); @@ -221,41 +229,6 @@ class PerfumeService { return await perfumeDao.updateSimilarPerfumes(perfumeSimilarRequest); } - /** - * 향수 좋아요 - * - * @param {number} userIdx - * @param {number} perfumeIdx - * @returns {Promise} - * @throws {FailedToCreateError} if failed to create likePerfume - **/ - likePerfume(userIdx: number, perfumeIdx: number): Promise { - logger.debug( - `${LOG_TAG} likePerfume(userIdx = ${userIdx}, perfumeIdx = ${perfumeIdx})` - ); - return likePerfumeDao - .read(userIdx, perfumeIdx) - .then((_: any) => { - return likePerfumeDao - .delete(userIdx, perfumeIdx) - .then((_: number) => true); - }) - .catch((err: Error) => { - if (err instanceof NotMatchedError) { - return likePerfumeDao - .create(userIdx, perfumeIdx) - .then(() => false); - } - throw new FailedToCreateError(); - }) - .then((exist: boolean) => { - return !exist; - }) - .catch((err: Error) => { - throw err; - }); - } - /** * 유저의 최근 검색한 향수 조회 * @@ -277,11 +250,14 @@ class PerfumeService { (it) => it.perfumeIdx ); const likePerfumeList: any[] = - await likePerfumeDao.readLikeInfo(userIdx, perfumeIdxList); + await this.likePerfumeService.readLikeInfo( + userIdx, + perfumeIdxList + ); return result.convertType((item: PerfumeThumbDTO) => { return fp.compose( ...commonJob, - this.isLikeJob(likePerfumeList), + this.likePerfumeService.isLikeJob(likePerfumeList), PerfumeThumbDTO.createByJson )(item); }); @@ -315,14 +291,14 @@ class PerfumeService { (it: PerfumeThumbDTO) => it.perfumeIdx ); const likePerfumeList: any[] = - await likePerfumeDao.readLikeInfo( + await this.likePerfumeService.readLikeInfo( userIdx, perfumeIdxList ); return result.convertType((item: PerfumeThumbDTO) => { return fp.compose( ...commonJob, - this.isLikeJob(likePerfumeList), + this.likePerfumeService.isLikeJob(likePerfumeList), PerfumeThumbDTO.createByJson )(item); }); @@ -351,7 +327,10 @@ class PerfumeService { (it: any) => it.perfumeIdx ); const likePerfumeList: any[] = - await likePerfumeDao.readLikeInfo(userIdx, perfumeIdxList); + await this.likePerfumeService.readLikeInfo( + userIdx, + perfumeIdxList + ); const perfumeReviewList: any[] = await reviewDao.readAllMineOfPerfumes( userIdx, @@ -360,7 +339,7 @@ class PerfumeService { return result.convertType((item: any) => { return fp.compose( ...commonJob, - this.isLikeJob(likePerfumeList), + this.likePerfumeService.isLikeJob(likePerfumeList), this.matchReviewsWithPerfumesJob(perfumeReviewList), PerfumeThumbWithReviewDTO.createByJson )(item); @@ -448,22 +427,10 @@ class PerfumeService { }); } - setPerfumeDao(dao: PerfumeDao) { - perfumeDao = dao; - } - setReviewDao(dao: any) { reviewDao = dao; } - setNoteDao(dao: NoteDao) { - noteDao = dao; - } - - setLikePerfumeDao(dao: LikePerfumeDao) { - likePerfumeDao = dao; - } - setKeywordDao(dao: any) { keywordDao = dao; } @@ -476,29 +443,6 @@ class PerfumeService { s3FileDao = dao; } - private async generateNote(perfumeIdx: number): Promise<{ - noteType: number; - noteDictDTO: { - top: string; - middle: string; - base: string; - single: string; - }; - }> { - const noteList: any[] = await noteDao.readByPerfumeIdx(perfumeIdx); - const noteDictDTO: { - top: string; - middle: string; - base: string; - single: string; - } = NoteDictDTO.createByNoteList(noteList); - const noteType: number = - noteDictDTO.single.length > 0 - ? PERFUME_NOTE_TYPE_SINGLE - : PERFUME_NOTE_TYPE_NORMAL; - return { noteType, noteDictDTO }; - } - private async generateSummary( perfumeIdx: number ): Promise { @@ -507,31 +451,6 @@ class PerfumeService { return userSummary; } - private isLike(userIdx: number, perfumeIdx: number): Promise { - return likePerfumeDao - .read(userIdx, perfumeIdx) - .then((_: any) => true) - .catch((err: Error) => { - if (err instanceof NotMatchedError) { - return false; - } - throw err; - }); - } - - private isLikeJob(likePerfumeList: any[]): (obj: any) => any { - const likeMap: { [key: string]: boolean } = _.chain(likePerfumeList) - .keyBy('perfumeIdx') - .mapValues(() => true) - .value(); - - return (obj: any) => { - const ret: any = Object.assign({}, obj); - ret.isLiked = likeMap[obj.perfumeIdx] ? true : false; - return ret; - }; - } - private matchReviewsWithPerfumesJob( perfumeReviewList: any[] ): (obj: any) => any { @@ -583,7 +502,7 @@ class PerfumeService { ): Promise<(item: PerfumeThumbDTO) => PerfumeThumbKeywordDTO> { let likePerfumeList: any[] = []; if (userIdx > -1) { - likePerfumeList = await likePerfumeDao.readLikeInfo( + likePerfumeList = await this.likePerfumeService.readLikeInfo( userIdx, perfumeIdxList ); @@ -601,7 +520,7 @@ class PerfumeService { return (item: PerfumeThumbDTO): PerfumeThumbKeywordDTO => { return fp.compose( ...commonJob, - this.isLikeJob(likePerfumeList), + this.likePerfumeService.isLikeJob(likePerfumeList), this.addKeyword(joinKeywordList), PerfumeThumbKeywordDTO.createByJson )(item); diff --git a/tests/test_unit/service/LikePerfumeService.spec.ts b/tests/test_unit/service/LikePerfumeService.spec.ts new file mode 100644 index 00000000..b2a3a3d0 --- /dev/null +++ b/tests/test_unit/service/LikePerfumeService.spec.ts @@ -0,0 +1,44 @@ +import { expect } from 'chai'; + +import LikePerfumeDao from '@src/dao/LikePerfumeDao'; +import { LikePerfumeService } from '@src/service/LikePerfumeService'; +import { NotMatchedError } from '@src/utils/errors/errors'; + +const mockLikePerfumeDao = {} as LikePerfumeDao; +const LikePerfume: LikePerfumeService = new LikePerfumeService( + mockLikePerfumeDao +); + +describe('# Perfume Service Test', () => { + describe('# like Test', () => { + it('# likePerfume Test (좋아요)', () => { + mockLikePerfumeDao.read = async (_: number, __: number) => { + throw new NotMatchedError(); + }; + mockLikePerfumeDao.delete = async (_: number, __: number) => { + return 0; + }; + mockLikePerfumeDao.create = async (_: number, __: number) => { + return { userIdx: -1, perfumeIdx: -1 }; + }; + LikePerfume.likePerfume(1, 1).then((result: boolean) => { + expect(result).to.be.true; + }); + }); + + it('# likePerfume Test (좋아요 취소)', () => { + mockLikePerfumeDao.read = async (_: number, __: number) => { + return { userIdx: 1, perfumeIdx: 1 }; + }; + mockLikePerfumeDao.delete = async (_: number, __: number) => { + return 0; + }; + mockLikePerfumeDao.create = async (_: number, __: number) => { + return { userIdx: 1, perfumeIdx: 1 }; + }; + LikePerfume.likePerfume(1, 1).then((result: boolean) => { + expect(result).to.be.false; + }); + }); + }); +}); diff --git a/tests/test_unit/service/PerfumeService.spec.ts b/tests/test_unit/service/PerfumeService.spec.ts index 6831e5f5..fb695208 100644 --- a/tests/test_unit/service/PerfumeService.spec.ts +++ b/tests/test_unit/service/PerfumeService.spec.ts @@ -1,50 +1,48 @@ -import dotenv from 'dotenv'; import { expect } from 'chai'; +import dotenv from 'dotenv'; import { Done } from 'mocha'; dotenv.config(); import PerfumeService from '@services/PerfumeService'; -import { NotMatchedError } from '@errors'; - import { + ACCESS_PRIVATE, + ACCESS_PUBLIC, GENDER_MAN, - GRADE_USER, GENDER_WOMAN, - ACCESS_PUBLIC, - ACCESS_PRIVATE, + GRADE_USER, } from '@utils/constants'; import { ListAndCountDTO, PagingDTO, PerfumeIntegralDTO, - PerfumeSearchResultDTO, PerfumeSearchDTO, + PerfumeSearchResultDTO, PerfumeThumbDTO, PerfumeThumbWithReviewDTO, } from '@dto/index'; import { - LongevityProperty, - SillageProperty, GenderProperty, + LongevityProperty, SeasonalProperty, + SillageProperty, } from '@vo/ReviewProperty'; import PerfumeIntegralMockHelper from '../mock_helper/PerfumeIntegralMockHelper'; +import { LikePerfumeService } from '@src/service/LikePerfumeService'; -const Perfume: PerfumeService = new PerfumeService(); +const mockLikePerfumeDao: any = {}; +const LikePerfume = new LikePerfumeService(mockLikePerfumeDao); +const Perfume: PerfumeService = new PerfumeService(LikePerfume); const defaultPagingDTO: PagingDTO = PagingDTO.createByJson({}); const mockS3FileDao: any = {}; Perfume.setS3FileDao(mockS3FileDao); -const mockLikePerfumeDao: any = {}; -Perfume.setLikePerfumeDao(mockLikePerfumeDao); - const mockUserDao: any = {}; Perfume.setUserDao(mockUserDao); @@ -423,41 +421,4 @@ describe('# Perfume Service Test', () => { expect(result.rows.length).to.be.eq(2); }); }); - describe('# like Test', () => { - it('# likePerfume Test (좋아요)', (done: Done) => { - mockLikePerfumeDao.read = async (_: number, __: number) => { - throw new NotMatchedError(); - }; - mockLikePerfumeDao.delete = async (_: number, __: number) => { - return; - }; - mockLikePerfumeDao.create = async (_: number, __: number) => { - return; - }; - Perfume.likePerfume(1, 1) - .then((result: boolean) => { - expect(result).to.be.true; - done(); - }) - .catch((err: Error) => done(err)); - }); - - it('# likePerfume Test (좋아요 취소)', (done: Done) => { - mockLikePerfumeDao.read = async (_: number, __: number) => { - return true; - }; - mockLikePerfumeDao.delete = async (_: number, __: number) => { - return; - }; - mockLikePerfumeDao.create = async (_: number, __: number) => { - return; - }; - Perfume.likePerfume(1, 1) - .then((result: boolean) => { - expect(result).to.be.false; - done(); - }) - .catch((err: Error) => done(err)); - }); - }); }); From 6c54950fb4ef649f47b65cb35cf54d7e20476071 Mon Sep 17 00:00:00 2001 From: Jinho Hyeon Date: Fri, 16 Jun 2023 15:25:59 +0900 Subject: [PATCH 3/9] =?UTF-8?q?js=20=EB=A5=BC=20ts=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20-=201=20(#512)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * controllers Keyword * KeywordService * controllers Review * LikeReviewDao --- src/controllers/Keyword.js | 31 ---- src/controllers/Keyword.ts | 45 +++++ src/controllers/Review.js | 186 --------------------- src/controllers/Review.ts | 195 ++++++++++++++++++++++ src/dao/LikeReviewDao.js | 93 ----------- src/dao/LikeReviewDao.ts | 97 +++++++++++ src/service/KeywordService.js | 32 ---- src/service/KeywordService.ts | 40 +++++ src/service/ReviewService.js | 4 +- tests/test_unit/dao/LikeReviewDao.spec.js | 3 +- 10 files changed, 382 insertions(+), 344 deletions(-) delete mode 100644 src/controllers/Keyword.js create mode 100644 src/controllers/Keyword.ts delete mode 100644 src/controllers/Review.js create mode 100644 src/controllers/Review.ts delete mode 100644 src/dao/LikeReviewDao.js create mode 100644 src/dao/LikeReviewDao.ts delete mode 100644 src/service/KeywordService.js create mode 100644 src/service/KeywordService.ts diff --git a/src/controllers/Keyword.js b/src/controllers/Keyword.js deleted file mode 100644 index b1414afd..00000000 --- a/src/controllers/Keyword.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const Keyword = require('../service/KeywordService'); -import { DEFAULT_PAGE_SIZE } from '@src/utils/constants'; -import StatusCode from '../utils/statusCode'; - -module.exports.getKeywordAll = (req, res, next) => { - let { pagingIndex, pagingSize } = req.query; - pagingIndex = parseInt(pagingIndex) || 1; - pagingSize = parseInt(pagingSize) || DEFAULT_PAGE_SIZE; - Keyword.getKeywordAll(pagingIndex, pagingSize) - .then((response) => { - res.status(StatusCode.OK).json({ - message: '키워드 목록 전체 조회 성공', - data: response, - }); - }) - .catch((err) => next(err)); -}; - -module.exports.getKeywordOfPerfume = (req, res, next) => { - let perfumeIdx = req.params['perfumeIdx']; - Keyword.getKeywordOfPerfume(perfumeIdx) - .then((response) => { - res.status(StatusCode.OK).json({ - message: '향수별 키워드 전체 조회 성공', - data: response, - }); - }) - .catch((err) => next(err)); -}; diff --git a/src/controllers/Keyword.ts b/src/controllers/Keyword.ts new file mode 100644 index 00000000..2b39080a --- /dev/null +++ b/src/controllers/Keyword.ts @@ -0,0 +1,45 @@ +'use strict'; + +import KeywordService from '../service/KeywordService'; +import { DEFAULT_PAGE_SIZE } from '@src/utils/constants'; +import StatusCode from '../utils/statusCode'; +import { NextFunction, Request, Response } from 'express'; + +const Keyword = new KeywordService(); + +export const getKeywordAll = async ( + req: Request, + res: Response, + next: NextFunction +) => { + const pagingIndex = parseInt(req.query.pagingIndex?.toString() ?? '1'); + const pagingSize = parseInt( + req.query.pagingSize?.toString() ?? `${DEFAULT_PAGE_SIZE}` + ); + try { + const response = await Keyword.getKeywordAll(pagingIndex, pagingSize); + res.status(StatusCode.OK).json({ + message: '키워드 목록 전체 조회 성공', + data: response, + }); + } catch (err) { + next(err); + } +}; + +export const getKeywordOfPerfume = async ( + req: Request, + res: Response, + next: NextFunction +) => { + const perfumeIdx = parseInt(req.params['perfumeIdx']); + try { + const response = await Keyword.getKeywordOfPerfume(perfumeIdx); + res.status(StatusCode.OK).json({ + message: '향수별 키워드 전체 조회 성공', + data: response, + }); + } catch (err) { + next(err); + } +}; diff --git a/src/controllers/Review.js b/src/controllers/Review.js deleted file mode 100644 index 00495d1a..00000000 --- a/src/controllers/Review.js +++ /dev/null @@ -1,186 +0,0 @@ -'use strict'; - -var Review = require('../service/ReviewService'); -import StatusCode from '../utils/statusCode'; - -module.exports.postReview = function postReview(req, res, next) { - const perfumeIdx = req.params['perfumeIdx']; - const userIdx = req.middlewareToken.loginUserIdx; - const { - score, - longevity, - sillage, - seasonal, - gender, - access, - content, - keywordList, - } = req.body; - Review.postReview({ - perfumeIdx, - userIdx, - score, - longevity, - sillage, - seasonal, - gender, - access, - content, - keywordList, - }) - .then((response) => { - res.status(StatusCode.OK).json({ - message: '시향노트 추가 성공', - data: { - reviewIdx: response, - }, - }); - }) - .catch((err) => next(err)); -}; - -module.exports.getReviewByIdx = function getReviewByIdx(req, res, next) { - var reviewIdx = req.params['reviewIdx']; - Review.getReviewByIdx(reviewIdx) - .then((response) => { - res.status(StatusCode.OK).json({ - message: '시향노트 조회 성공', - data: response, - }); - }) - .catch((err) => next(err)); -}; - -module.exports.getPerfumeReview = function getPerfumeReview(req, res, next) { - var perfumeIdx = req.params['perfumeIdx']; - const userIdx = req.middlewareToken.loginUserIdx; - Review.getReviewOfPerfumeByLike({ perfumeIdx, userIdx }) - .then((response) => { - res.status(StatusCode.OK).json({ - message: '특정 향수의 시향노트 목록 인기순 조회 성공', - data: response, - }); - }) - .catch((err) => next(err)); -}; - -module.exports.getReviewOfUser = function getReviewOfUser(req, res, next) { - const userIdx = req.middlewareToken.loginUserIdx; - Review.getReviewOfUser(userIdx) - .then((response) => { - res.status(StatusCode.OK).json({ - message: '마이퍼퓸 조회 성공', - data: response, - }); - }) - .catch((err) => next(err)); -}; - -module.exports.putReview = (req, res, next) => { - var reviewIdx = req.params['reviewIdx']; - const userIdx = req.middlewareToken.loginUserIdx; - var { - score, - longevity, - sillage, - seasonal, - gender, - access, - content, - keywordList, - } = req.body; - Review.updateReview({ - reviewIdx, - userIdx, - score, - longevity, - sillage, - seasonal, - gender, - access, - content, - userIdx, - keywordList, - }) - .then(() => { - res.status(StatusCode.OK).json({ - message: '시향노트 수정 성공', - }); - }) - .catch((err) => next(err)); -}; - -module.exports.deleteReview = (req, res, next) => { - const reviewIdx = req.params['reviewIdx']; - const userIdx = req.middlewareToken.loginUserIdx; - Review.deleteReview({ reviewIdx, userIdx }) - .then(() => { - res.status(StatusCode.OK).json({ - message: '시향노트 삭제 성공', - }); - }) - .catch((err) => next(err)); -}; - -module.exports.likeReview = (req, res, next) => { - const reviewIdx = req.params['reviewIdx']; - const userIdx = req.middlewareToken.loginUserIdx; - Review.likeReview(reviewIdx, userIdx) - .then((result) => { - res.status(StatusCode.OK).json({ - message: '시향노트 좋아요 상태 변경 성공', - data: result, - }); - }) - .catch((err) => next(err)); -}; - -module.exports.reportReview = (req, res, next) => { - const reviewIdx = req.params['reviewIdx']; - const userIdx = req.middlewareToken.loginUserIdx; - var { - reason - } = req.body; - Review.reportReview({ - userIdx, - reviewIdx, - reason - }).then(() => { - res.status(StatusCode.OK).json({ - message: '시향노트 신고 성공', - }); - }).catch((err) => next(err)); -}; - - -// module.exports.getReviewOfPerfumeByScore = function getReviewOfPerfumeByScore( -// req, -// res, -// next -// ) { -// var perfumeIdx = req.swagger.params['perfumeIdx'].value; -// Review.getReviewOfPerfumeByScore(perfumeIdx) -// .then((response) => { -// res.status(OK).json({ -// message: '특정 향수의 시향노트 목록 별점순 조회 성공', -// data: response, -// }); -// }) -// .catch((err) => next(err)); -// }; - -// module.exports.getReviewOfPerfumeByRecent = function getReviewOfPerfumeByRecent( -// req, -// res, -// next -// ) { -// var perfumeIdx = req.swagger.params['perfumeIdx'].value; -// Review.getReviewOfPerfumeByRecent(perfumeIdx) -// .then((response) => { -// res.status(OK).json({ -// message: '특정 향수의 시향노트 목록 최신순 조회 성공', -// data: response, -// }); -// }) -// .catch((err) => next(err)); -// }; diff --git a/src/controllers/Review.ts b/src/controllers/Review.ts new file mode 100644 index 00000000..becdc7b3 --- /dev/null +++ b/src/controllers/Review.ts @@ -0,0 +1,195 @@ +var Review = require('../service/ReviewService'); +import StatusCode from '../utils/statusCode'; + +import { NextFunction, Request, Response } from 'express'; + +export const postReview = async ( + req: Request | any, + res: Response, + next: NextFunction +) => { + const perfumeIdx = req.params['perfumeIdx']; + const userIdx = req.middlewareToken.loginUserIdx; + const { + score, + longevity, + sillage, + seasonal, + gender, + access, + content, + keywordList, + } = req.body; + try { + const response = await Review.postReview({ + perfumeIdx, + userIdx, + score, + longevity, + sillage, + seasonal, + gender, + access, + content, + keywordList, + }); + res.status(StatusCode.OK).json({ + message: '시향노트 추가 성공', + data: { + reviewIdx: response, + }, + }); + } catch (err) { + next(err); + } +}; + +export const getReviewByIdx = async ( + req: Request, + res: Response, + next: NextFunction +) => { + var reviewIdx = req.params['reviewIdx']; + try { + const response = await Review.getReviewByIdx(reviewIdx); + res.status(StatusCode.OK).json({ + message: '시향노트 조회 성공', + data: response, + }); + } catch (err) { + next(err); + } +}; + +export const getPerfumeReview = async ( + req: Request | any, + res: Response, + next: NextFunction +) => { + var perfumeIdx = req.params['perfumeIdx']; + const userIdx = req.middlewareToken.loginUserIdx; + try { + const response = await Review.getReviewOfPerfumeByLike({ + perfumeIdx, + userIdx, + }); + res.status(StatusCode.OK).json({ + message: '특정 향수의 시향노트 목록 인기순 조회 성공', + data: response, + }); + } catch (err) { + next(err); + } +}; + +export const getReviewOfUser = async ( + req: Request | any, + res: Response, + next: NextFunction +) => { + const userIdx = req.middlewareToken.loginUserIdx; + try { + const response = await Review.getReviewOfUser(userIdx); + res.status(StatusCode.OK).json({ + message: '마이퍼퓸 조회 성공', + data: response, + }); + } catch (err) { + next(err); + } +}; + +export const putReview = async ( + req: Request | any, + res: Response, + next: NextFunction +) => { + var reviewIdx = req.params['reviewIdx']; + const userIdx = req.middlewareToken.loginUserIdx; + var { + score, + longevity, + sillage, + seasonal, + gender, + access, + content, + keywordList, + } = req.body; + try { + await Review.updateReview({ + reviewIdx, + userIdx, + score, + longevity, + sillage, + seasonal, + gender, + access, + content, + keywordList, + }); + res.status(StatusCode.OK).json({ + message: '시향노트 수정 성공', + }); + } catch (err) { + next(err); + } +}; + +export const deleteReview = async ( + req: Request | any, + res: Response, + next: NextFunction +) => { + const reviewIdx = req.params['reviewIdx']; + const userIdx = req.middlewareToken.loginUserIdx; + try { + await Review.deleteReview({ reviewIdx, userIdx }); + res.status(StatusCode.OK).json({ + message: '시향노트 삭제 성공', + }); + } catch (err) { + next(err); + } +}; + +export const likeReview = async ( + req: Request | any, + res: Response, + next: NextFunction +) => { + const reviewIdx = req.params['reviewIdx']; + const userIdx = req.middlewareToken.loginUserIdx; + try { + const result = await Review.likeReview(reviewIdx, userIdx); + res.status(StatusCode.OK).json({ + message: '시향노트 좋아요 상태 변경 성공', + data: result, + }); + } catch (err) { + next(err); + } +}; + +export const reportReview = async ( + req: Request | any, + res: Response, + next: NextFunction +) => { + const reviewIdx = req.params['reviewIdx']; + const userIdx = req.middlewareToken.loginUserIdx; + var { reason } = req.body; + try { + await Review.reportReview({ + userIdx, + reviewIdx, + reason, + }); + res.status(StatusCode.OK).json({ + message: '시향노트 신고 성공', + }); + } catch (err) { + next(err); + } +}; diff --git a/src/dao/LikeReviewDao.js b/src/dao/LikeReviewDao.js deleted file mode 100644 index 3346b9e4..00000000 --- a/src/dao/LikeReviewDao.js +++ /dev/null @@ -1,93 +0,0 @@ -import { - NotMatchedError, - DuplicatedEntryError, - FailedToCreateError, -} from '../utils/errors/errors'; -const { sequelize, LikeReview, Review } = require('../models'); - -/** - * 시향노트 좋아요 생성 - * - * @param {Object} LikeReview - * @returns {Promise} - */ - -module.exports.create = (userIdx, reviewIdx) => { - return sequelize.transaction((t) => { - const createLike = LikeReview.create( - { userIdx, reviewIdx }, - { transaction: t } - ).catch((err) => { - if ( - err.original.code === 'ER_DUP_ENTRY' || - err.parent.errno === 1062 - ) { - throw new DuplicatedEntryError(); - } - if ( - err.original.code === 'ER_NO_REFERENCED_ROW_2' || - err.original.errno === 1452 - ) { - throw new NotMatchedError(); - } - throw new FailedToCreateError(); - }); - - const updateLikeCnt = Review.update( - { likeCnt: sequelize.literal('like_cnt + 1') }, - { - where: { id: reviewIdx }, - transaction: t, - } - ); - - return Promise.all([createLike, updateLikeCnt]); - }); -}; - -/** - * 시향노트 좋아요 조회 - * - * @param {number} userIdx - * @param {number} reviewIdx - * @returns {Promise} || null - */ - -module.exports.read = async (userIdx, reviewIdx) => { - return await LikeReview.findOne({ - where: { userIdx, reviewIdx }, - raw: true, - nest: true, - }); -}; - -/** - * 시향노트 좋아요 취소 - * - * @param {Object} whereObj - * @returns Boolean - */ - -module.exports.delete = async (userIdx, reviewIdx) => { - return sequelize.transaction((t) => { - const deleteLike = LikeReview.destroy({ - where: { userIdx, reviewIdx }, - transaction: t, - }).then((it) => { - if (it == 0) throw new NotMatchedError(); - return it; - }); - - const updateLikeCnt = Review.update( - { likeCnt: sequelize.literal('like_cnt - 1') }, - { - where: { id: reviewIdx }, - transaction: t, - } - ); - - return Promise.all([deleteLike, updateLikeCnt]).then((it) => { - return it; - }); - }); -}; diff --git a/src/dao/LikeReviewDao.ts b/src/dao/LikeReviewDao.ts new file mode 100644 index 00000000..34f80184 --- /dev/null +++ b/src/dao/LikeReviewDao.ts @@ -0,0 +1,97 @@ +import { + NotMatchedError, + DuplicatedEntryError, + FailedToCreateError, +} from '../utils/errors/errors'; +import { sequelize, LikeReview, Review } from '@src/models'; + +class LikeReviewDao { + /** + * 시향노트 좋아요 생성 + * + * @param {Object} LikeReview + * @returns {Promise} + */ + + async create(userIdx: number, reviewIdx: number) { + return sequelize.transaction((t) => { + const createLike = LikeReview.create( + { userIdx, reviewIdx }, + { transaction: t } + ).catch((err) => { + if ( + err.original.code === 'ER_DUP_ENTRY' || + err.parent.errno === 1062 + ) { + throw new DuplicatedEntryError(); + } + if ( + err.original.code === 'ER_NO_REFERENCED_ROW_2' || + err.original.errno === 1452 + ) { + throw new NotMatchedError(); + } + throw new FailedToCreateError(); + }); + + const updateLikeCnt = Review.update( + { likeCnt: sequelize.literal('like_cnt + 1') }, + { + where: { id: reviewIdx }, + transaction: t, + } + ); + + return Promise.all([createLike, updateLikeCnt]); + }); + } + + /** + * 시향노트 좋아요 조회 + * + * @param {number} userIdx + * @param {number} reviewIdx + * @returns {Promise} || null + */ + + async read(userIdx: number, reviewIdx: number) { + return await LikeReview.findOne({ + where: { userIdx, reviewIdx }, + raw: true, + nest: true, + }); + } + + /** + * 시향노트 좋아요 취소 + * + * @param {Object} whereObj + * @returns Boolean + */ + + async delete(userIdx: number, reviewIdx: number) { + return sequelize.transaction((t) => { + const deleteLike = LikeReview.destroy({ + where: { userIdx, reviewIdx }, + transaction: t, + }).then((it) => { + if (it == 0) throw new NotMatchedError(); + return it; + }); + + const updateLikeCnt = Review.update( + { likeCnt: sequelize.literal('like_cnt - 1') }, + { + where: { id: reviewIdx }, + transaction: t, + } + ); + + return Promise.all([deleteLike, updateLikeCnt]).then((it) => { + return it; + }); + }); + } +} + +export default LikeReviewDao; diff --git a/src/service/KeywordService.js b/src/service/KeywordService.js deleted file mode 100644 index dc7134b8..00000000 --- a/src/service/KeywordService.js +++ /dev/null @@ -1,32 +0,0 @@ -import KeywordDao from '@dao/KeywordDao'; - -const keywordDao = new KeywordDao(); - -/** - * 키워드 전체 조회 - * - * @returns {Promise} - **/ -exports.getKeywordAll = async (pagingIndex, pagingSize) => { - const result = await keywordDao.readAll(pagingIndex, pagingSize); - result.rows = await result.rows.map((it) => { - it.keywordIdx = it.id; - delete it.id; - return it; - }); - return result; -}; - -/** - * 특정 향수의 키워드 목록 조회 - * - * @param {number} perfumeIdx - * @returns {Promise} - */ -exports.getKeywordOfPerfume = async (perfumeIdx) => { - return (await keywordDao.readAllOfPerfume(perfumeIdx)).map((it) => { - it.keywordIdx = it.id; - delete it.id; - return it; - }); -}; diff --git a/src/service/KeywordService.ts b/src/service/KeywordService.ts new file mode 100644 index 00000000..2563d3a3 --- /dev/null +++ b/src/service/KeywordService.ts @@ -0,0 +1,40 @@ +import KeywordDao from '@dao/KeywordDao'; +import { Keyword } from '@src/models'; + +const keywordDao = new KeywordDao(); + +class KeywordService { + /** + * 키워드 전체 조회 + * + * @returns {Promise} + **/ + async getKeywordAll(pagingIndex: number, pagingSize: number) { + const result = await keywordDao.readAll(pagingIndex, pagingSize); + return { + ...result, + rows: result.rows.map(this.transform), + }; + } + + /** + * 특정 향수의 키워드 목록 조회 + * + * @param {number} perfumeIdx + * @returns {Promise} + */ + async getKeywordOfPerfume(perfumeIdx: number) { + const keywords = await keywordDao.readAllOfPerfume(perfumeIdx); + return keywords.map(this.transform); + } + + private transform(it: Keyword) { + return { + ...it, + id: undefined, + keywordIdx: it.id, + }; + } +} + +export default KeywordService; diff --git a/src/service/ReviewService.js b/src/service/ReviewService.js index a4d90a1c..1d25f121 100644 --- a/src/service/ReviewService.js +++ b/src/service/ReviewService.js @@ -1,6 +1,8 @@ import { NotMatchedError, UnAuthorizedError } from '../utils/errors/errors'; -const likeReviewDao = require('../dao/LikeReviewDao'); +import LikeReviewDao from '@dao/LikeReviewDao'; +const likeReviewDao = new LikeReviewDao(); + const reportReviewDao = require('../dao/ReportReviewDao'); const { InputIntToDBIntOfReview, diff --git a/tests/test_unit/dao/LikeReviewDao.spec.js b/tests/test_unit/dao/LikeReviewDao.spec.js index 687660e4..6a999c67 100644 --- a/tests/test_unit/dao/LikeReviewDao.spec.js +++ b/tests/test_unit/dao/LikeReviewDao.spec.js @@ -9,7 +9,8 @@ import { const chai = require('chai'); const { expect } = chai; -const likeReviewDao = require('@dao/LikeReviewDao.js'); +import LikeReviewDao from '@dao/LikeReviewDao'; +const likeReviewDao = new LikeReviewDao(); import { LikeReview } from '@sequelize'; From f09a943ba64c2274acef68fce18f4c220e363566 Mon Sep 17 00:00:00 2001 From: Jinho Hyeon Date: Fri, 16 Jun 2023 15:58:32 +0900 Subject: [PATCH 4/9] =?UTF-8?q?js=20=EB=A5=BC=20ts=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98=20-=202=20(#513)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * converter * converter.spec * KeywordDao.spec * LikeReviewDao.spec --- src/dao/KeywordDao.ts | 6 +- src/dao/ReviewDao.ts | 6 +- src/utils/{converter.js => converter.ts} | 28 ++-- tests/test_unit/dao/KeywordDao.spec.js | 72 ----------- tests/test_unit/dao/KeywordDao.spec.ts | 65 ++++++++++ tests/test_unit/dao/LikeReviewDao.spec.js | 129 ------------------- tests/test_unit/dao/LikeReviewDao.spec.ts | 90 +++++++++++++ tests/test_unit/utils/converter.spec.js | 150 ---------------------- tests/test_unit/utils/converter.spec.ts | 133 +++++++++++++++++++ 9 files changed, 312 insertions(+), 367 deletions(-) rename src/utils/{converter.js => converter.ts} (86%) delete mode 100644 tests/test_unit/dao/KeywordDao.spec.js create mode 100644 tests/test_unit/dao/KeywordDao.spec.ts delete mode 100644 tests/test_unit/dao/LikeReviewDao.spec.js create mode 100644 tests/test_unit/dao/LikeReviewDao.spec.ts delete mode 100644 tests/test_unit/utils/converter.spec.js create mode 100644 tests/test_unit/utils/converter.spec.ts diff --git a/src/dao/KeywordDao.ts b/src/dao/KeywordDao.ts index ac17991a..bbbf363d 100644 --- a/src/dao/KeywordDao.ts +++ b/src/dao/KeywordDao.ts @@ -196,8 +196,8 @@ class KeywordDao { sort: Order = [['count', 'desc']], condition: any = {}, limitSize: number = 2 - ): Promise { - const result = await JoinPerfumeKeyword.findAll({ + ): Promise> { + const result = (await JoinPerfumeKeyword.findAll({ attributes: { exclude: ['createdAt', 'updatedAt'], }, @@ -220,7 +220,7 @@ class KeywordDao { limit: limitSize, raw: true, // To receive a plain response instead, pass { raw: true } as an option to the finder method. nest: true, - }); + })) as Array; if (result.length === 0) { throw new NotMatchedError(); diff --git a/src/dao/ReviewDao.ts b/src/dao/ReviewDao.ts index f054c105..07841803 100644 --- a/src/dao/ReviewDao.ts +++ b/src/dao/ReviewDao.ts @@ -62,9 +62,9 @@ class ReviewDao { score: number; longevity: number; sillage: number; - seasonal: string[]; + seasonal: number; gender: number; - access: boolean; + access: number; content: string; }): Promise { try { @@ -185,7 +185,7 @@ class ReviewDao { readAllOfPerfume( perfumeIdx: number, includePrivate: boolean = false - ): Promise { + ): Promise { return sequelize.query(SQL_READ_ALL_OF_PERFUME, { bind: [perfumeIdx, includePrivate ? ACCESS_PRIVATE : ACCESS_PUBLIC], nest: true, diff --git a/src/utils/converter.js b/src/utils/converter.ts similarity index 86% rename from src/utils/converter.js rename to src/utils/converter.ts index 5f0e9ebf..951d230d 100644 --- a/src/utils/converter.js +++ b/src/utils/converter.ts @@ -3,9 +3,6 @@ import { logger } from '@modules/winston'; import { InvalidValueError } from '@errors'; const seasonalTypeArr = ['봄', '여름', '가을', '겨울']; -const sillageTypeArr = ['가벼움', '보통', '무거움']; -const longevityTypeArr = ['매우 약함', '약함', '보통', '강함', '매우 강함']; -const genderTypeArr = ['남성', '중성', '여성']; // TODO converter가 특정 dao에 의존하는 것은 불필요한 의존성을 만드는 거 같네요. import KeywordDao from '@dao/KeywordDao'; @@ -26,12 +23,18 @@ const INSTEAD_NULL_VALUE = -1; * @param {string[]|integer[]} Review.keywordList * @returns {Promise} */ -module.exports.InputIntToDBIntOfReview = async ({ +export const InputIntToDBIntOfReview = async ({ longevity, sillage, seasonalList, gender, keywordList, +}: { + longevity: number; + sillage: number; + seasonalList: string[]; + gender: number; + keywordList: (string | number)[]; }) => { try { // seasonalList 변환하기 (비트연산) @@ -44,7 +47,7 @@ module.exports.InputIntToDBIntOfReview = async ({ } // keywordList 변환하기 - let keywordIdxList; + let keywordIdxList = []; if (keywordList) { keywordIdxList = await Promise.all( keywordList.map((it) => { @@ -62,9 +65,9 @@ module.exports.InputIntToDBIntOfReview = async ({ return { longevity: longevity + 1 ? longevity + 1 : null, sillage: sillage + 1 ? sillage + 1 : null, - sumOfBitSeasonal: seasonalList && sum > 0 ? sum : null, + sumOfBitSeasonal: seasonalList && sum && sum > 0 ? sum : null, gender: gender + 1 ? gender + 1 : null, - keywordList: keywordList ? keywordIdxList : [], + keywordList: keywordIdxList, }; } catch (err) { logger.error(err); @@ -84,11 +87,16 @@ module.exports.InputIntToDBIntOfReview = async ({ * @param {number} Review.sumOfBitSeasonal * @returns {Promise} */ -module.exports.DBIntToOutputIntOfReview = async ({ +export const DBIntToOutputIntOfReview = ({ longevity, sillage, sumOfBitSeasonal, gender, +}: { + longevity: number | null; + sillage: number | null; + sumOfBitSeasonal: number | null; + gender: number | null; }) => { try { // seasonalList 변환하기 (비트연산) @@ -121,10 +129,10 @@ module.exports.DBIntToOutputIntOfReview = async ({ * getApproxAge(1999) * @returns {string} approxAge */ -module.exports.getApproxAge = (birthYear) => { +export const getApproxAge = (birthYear: number) => { const thisYear = new Date().getFullYear(); const exactAge = thisYear - birthYear + 1; - const exactAgeTens = parseInt(exactAge / 10) * 10; + const exactAgeTens = parseInt(String(exactAge / 10)) * 10; const exactAgeUnit = exactAge % 10; let section; diff --git a/tests/test_unit/dao/KeywordDao.spec.js b/tests/test_unit/dao/KeywordDao.spec.js deleted file mode 100644 index f9529aaf..00000000 --- a/tests/test_unit/dao/KeywordDao.spec.js +++ /dev/null @@ -1,72 +0,0 @@ -const dotenv = require('dotenv'); -dotenv.config(); - -import KeywordDao from '@dao/KeywordDao'; - -const keywordDao = new KeywordDao(); - -const { expect } = require('chai'); -import { Op } from 'sequelize'; - -describe('# KeywordDao Test', () => { - before(async function () { - await require('./common/presets.js')(this); - }); - - describe('# readAll Test', () => { - it('# success case', (done) => { - keywordDao - .readAll() - .then((result) => { - expect(result.count).to.be.eq(5); - expect(result.rows.length).to.be.eq(5); - for (const keyword of result.rows) { - expect(keyword.id).to.be.ok; - expect(keyword.name).to.be.ok; - } - done(); - }) - .catch((err) => done(err)); - }); - }); - - describe('# readAllOfPerfume Test', () => { - it('# success case', (done) => { - keywordDao - .readAllOfPerfume(1, [['count', 'desc']], { - count: { [Op.gte]: 1 }, - }) - .then((result) => { - expect(result.length).to.be.gte(2); - for (const keyword of result) { - expect(keyword.id).to.be.ok; - expect(keyword.name).to.be.ok; - } - done(); - }) - .catch((err) => done(err)); - }); - }); - - describe('# readAllOfPerfume Test', () => { - it('# success case', (done) => { - keywordDao - .readAllOfPerfumeIdxList([1], null, { count: { [Op.gte]: 1 } }) - .then((result) => { - expect(result.length).gte(2); - for (const keyword of result) { - expect(keyword.perfumeIdx).to.be.eq(1); - expect(keyword.keywordIdx).to.be.ok; - expect(keyword.count).to.be.ok; - expect(keyword.Keyword.id).to.eq(keyword.keywordIdx); - expect(keyword.Keyword.name).to.be.ok; - } - expect( - new Set(result.map((it) => it.keywordIdx)) - ).to.have.property('size', result.length); - done(); - }) - .catch((err) => done(err)); - }); - }); -}); diff --git a/tests/test_unit/dao/KeywordDao.spec.ts b/tests/test_unit/dao/KeywordDao.spec.ts new file mode 100644 index 00000000..36ad375e --- /dev/null +++ b/tests/test_unit/dao/KeywordDao.spec.ts @@ -0,0 +1,65 @@ +import { expect } from 'chai'; +import { Op } from 'sequelize'; + +import KeywordDao from '@dao/KeywordDao'; + +const keywordDao = new KeywordDao(); + +describe('# KeywordDao Test', () => { + before(async function () { + await require('./common/presets.js')(this); + }); + + describe('# readAll Test', () => { + it('# success case', async () => { + const result = await keywordDao.readAll(); + expect(result.count).to.be.eq(5); + expect(result.rows.length).to.be.eq(5); + for (const keyword of result.rows) { + expect(keyword.id).to.be.ok; + expect(keyword.name).to.be.ok; + } + }); + }); + + describe('# readAllOfPerfume Test', () => { + it('# success case', async () => { + const result = await keywordDao.readAllOfPerfume( + 1, + [['count', 'desc']], + { + count: { [Op.gte]: 1 }, + } + ); + expect(result.length).to.be.gte(2); + for (const keyword of result) { + expect(keyword.id).to.be.ok; + expect(keyword.name).to.be.ok; + } + }); + }); + + describe('# readAllOfPerfume Test', () => { + it('# success case', async () => { + const result = await keywordDao.readAllOfPerfumeIdxList( + [1], + undefined, + { + count: { [Op.gte]: 1 }, + } + ); + expect(result.length).gte(2); + for (const keyword of result) { + expect(keyword.perfumeIdx).to.be.eq(1); + expect(keyword.keywordIdx).to.be.ok; + expect(keyword.count).to.be.ok; + expect(keyword.Keyword.id).to.eq(keyword.keywordIdx); + expect(keyword.Keyword.name).to.be.ok; + } + expect(new Set(result.map((it) => it.keywordIdx))).to.have.property( + 'size', + result.length + ); + }); + }); +}); diff --git a/tests/test_unit/dao/LikeReviewDao.spec.js b/tests/test_unit/dao/LikeReviewDao.spec.js deleted file mode 100644 index 6a999c67..00000000 --- a/tests/test_unit/dao/LikeReviewDao.spec.js +++ /dev/null @@ -1,129 +0,0 @@ -const dotenv = require('dotenv'); -dotenv.config(); - -import { - DuplicatedEntryError, - NotMatchedError, - UnExpectedError, -} from '@errors'; - -const chai = require('chai'); -const { expect } = chai; -import LikeReviewDao from '@dao/LikeReviewDao'; -const likeReviewDao = new LikeReviewDao(); - -import { LikeReview } from '@sequelize'; - -describe('# LikeReviewDao Test', () => { - before(async function () { - await require('./common/presets.js')(this); - }); - - describe('# create Test', () => { - before(async () => { - // const before = await LikeReview.findOne({ - // where: { userIdx: 1, reviewIdx: 2 }, - // raw: true, - // nest: true, - // }); - // console.log('likeReview create before result: ', before) - await LikeReview.destroy({ where: { userIdx: 1, reviewIdx: 2 } }); - // const after = await LikeReview.findOne({ - // where: { userIdx: 1, reviewIdx: 2 }, - // raw: true, - // nest: true, - // }); - // console.log('likeReview create after result: ', after) - }); - it('# success case', (done) => { - likeReviewDao - .create(1, 2) - .then((result) => { - const likeReview = result[0]; - const updateLikeCnt = result[1]; - expect(likeReview.userIdx).eq(1); - expect(likeReview.reviewIdx).eq(2); - expect(updateLikeCnt[0]).eq(1); - done(); - }) - .catch((err) => done(err)); - }); - - it('# fail case (duplicated)', (done) => { - likeReviewDao - .create(1, 2) - .then(() => { - done(new UnExpectedError(DuplicatedEntryError)); - }) - .catch((err) => { - expect(err).instanceOf(DuplicatedEntryError); - done(); - }) - .catch((err) => done(err)); - }); - - it('# fail case (invalid userIdx)', (done) => { - likeReviewDao - .create(-1, 2) - .then(() => { - done(new UnExpectedError(NotMatchedError)); - }) - .catch((err) => { - expect(err).instanceOf(NotMatchedError); - done(); - }) - .catch((err) => done(err)); - }); - - it('# fail case (invalid reviewIdx)', (done) => { - likeReviewDao - .create(1, -2) - .then(() => { - done(new UnExpectedError(NotMatchedError)); - }) - .catch((err) => { - expect(err).instanceOf(NotMatchedError); - done(); - }) - .catch((err) => done(err)); - }); - - after(async () => { - await LikeReview.destroy({ where: { userIdx: 1, reviewIdx: 2 } }); - await LikeReview.destroy({ where: { userIdx: -1, reviewIdx: 2 } }); - await LikeReview.destroy({ where: { userIdx: -1, reviewIdx: -2 } }); - }); - }); - - describe('# read case', () => { - it('# success case', (done) => { - likeReviewDao - .read(1, 1) - .then((result) => { - expect(result.userIdx).eq(1); - expect(result.reviewIdx).eq(1); - done(); - }) - .catch((err) => done(err)); - }); - }); - - describe('# delete Test', () => { - before(async () => { - await likeReviewDao.create(3, 1); - }); - - it('# success case', (done) => { - likeReviewDao - .delete(3, 1) - .then((result) => { - expect(result[0]).eq(1); - expect(result[1][0]).eq(1); - done(); - }) - .catch((err) => { - done(err); - }); - }); - }); -}); diff --git a/tests/test_unit/dao/LikeReviewDao.spec.ts b/tests/test_unit/dao/LikeReviewDao.spec.ts new file mode 100644 index 00000000..193c81eb --- /dev/null +++ b/tests/test_unit/dao/LikeReviewDao.spec.ts @@ -0,0 +1,90 @@ +import { expect } from 'chai'; + +import { DuplicatedEntryError, NotMatchedError } from '@errors'; + +import LikeReviewDao from '@dao/LikeReviewDao'; +const likeReviewDao = new LikeReviewDao(); + +import { LikeReview } from '@sequelize'; + +describe('# LikeReviewDao Test', () => { + before(async function () { + await require('./common/presets.js')(this); + }); + + describe('# create Test', () => { + before(async () => { + // const before = await LikeReview.findOne({ + // where: { userIdx: 1, reviewIdx: 2 }, + // raw: true, + // nest: true, + // }); + // console.log('likeReview create before result: ', before) + await LikeReview.destroy({ where: { userIdx: 1, reviewIdx: 2 } }); + // const after = await LikeReview.findOne({ + // where: { userIdx: 1, reviewIdx: 2 }, + // raw: true, + // nest: true, + // }); + // console.log('likeReview create after result: ', after) + }); + it('# success case', async () => { + const result = await likeReviewDao.create(1, 2); + const likeReview = result[0]; + const updateLikeCnt = result[1]; + expect(likeReview.userIdx).eq(1); + expect(likeReview.reviewIdx).eq(2); + expect(updateLikeCnt[0]).eq(1); + }); + + it('# fail case (duplicated)', async () => { + try { + await likeReviewDao.create(1, 2); + } catch (err) { + expect(err).instanceOf(DuplicatedEntryError); + } + }); + + it('# fail case (invalid userIdx)', async () => { + try { + await likeReviewDao.create(-1, 2); + } catch (err) { + expect(err).instanceOf(NotMatchedError); + } + }); + + it('# fail case (invalid reviewIdx)', async () => { + try { + await likeReviewDao.create(1, -2); + } catch (err) { + expect(err).instanceOf(NotMatchedError); + } + }); + + after(async () => { + await LikeReview.destroy({ where: { userIdx: 1, reviewIdx: 2 } }); + await LikeReview.destroy({ where: { userIdx: -1, reviewIdx: 2 } }); + await LikeReview.destroy({ where: { userIdx: -1, reviewIdx: -2 } }); + }); + }); + + describe('# read case', () => { + it('# success case', async () => { + const result = await likeReviewDao.read(1, 1); + expect(result?.userIdx).eq(1); + expect(result?.reviewIdx).eq(1); + }); + }); + + describe('# delete Test', () => { + before(async () => { + await likeReviewDao.create(3, 1); + }); + + it('# success case', async () => { + const result = await likeReviewDao.delete(3, 1); + expect(result[0]).eq(1); + expect(result[1][0]).eq(1); + }); + }); +}); diff --git a/tests/test_unit/utils/converter.spec.js b/tests/test_unit/utils/converter.spec.js deleted file mode 100644 index 0f60eac8..00000000 --- a/tests/test_unit/utils/converter.spec.js +++ /dev/null @@ -1,150 +0,0 @@ -const dotenv = require('dotenv'); -dotenv.config(); - -const chai = require('chai'); -const { expect } = chai; - -const converter = require('@utils/converter.js'); - -describe('# converter Test', () => { - it(' # Input Int < - > DB Int Test', (done) => { - Promise.all( - [0, 1, 2, 3, 4, 5].map((idx) => { - const longevity = Math.min(idx, 5); - const sillage = Math.min(idx, 3); - const seasonal = parseInt(Math.random() * 100) % 16; - const gender = Math.min(idx, 3); - const keywordList = [1, 2, 3, 4, 5]; - converter - .DBIntToOutputIntOfReview({ - longevity, - sillage, - sumOfBitSeasonal: seasonal, - gender, - }) - .then((result) => { - expect(result.longevity).to.be.not.null; - expect(result.sillage).to.be.not.null; - expect(result.seasonalList).to.be.instanceof(Array); - expect(result.gender).to.be.not.null; - expect(result.keywordList).to.be.not.null; - return converter.InputIntToDBIntOfReview({ - ...result, - keywordList, - }); - }) - .then((recover) => { - expect(recover.longevity).to.be.eq(longevity); - expect(recover.sillage).to.be.eq(sillage); - expect(recover.sumOfBitSeasonal).to.be.eq(seasonal); - expect(recover.gender).to.be.eq(gender); - expect(recover.keywordList).to.be.deep.eq(keywordList); - }); - }) - ) - .then((_) => { - done(); - }) - .catch((err) => done(err)); - }); - - it(' # Input Int < - > DB Int Zero Test', (done) => { - const longevity = null; - const sillage = null; - const seasonal = null; - const gender = null; - const keywordList = []; - converter - .DBIntToOutputIntOfReview({ - longevity, - sillage, - sumOfBitSeasonal: seasonal, - gender, - }) - .then((result) => { - expect(result.longevity).to.be.not.null; - expect(result.sillage).to.be.not.null; - expect(result.seasonalList).to.be.instanceof(Array); - expect(result.gender).to.be.not.null; - return converter.InputIntToDBIntOfReview({ - ...result, - keywordList, - }); - }) - .then((recover) => { - expect(recover.longevity).to.be.eq(longevity); - expect(recover.sillage).to.be.eq(sillage); - expect(recover.sumOfBitSeasonal).to.be.eq(seasonal); - expect(recover.gender).to.be.eq(gender); - expect(recover.keywordList).to.be.deep.eq(keywordList); - done(); - }) - .catch((err) => done(err)); - }); - describe(' # ApproxAge Test', () => { - it(' # 20대 테스트', async () => { - const thisYear = new Date().getFullYear(); - for (let age = 20; age <= 23; age++) { - const it = await converter.getApproxAge(thisYear - age + 1); - expect(it).to.be.eq('20대 초반'); - } - - for (let age = 24; age <= 26; age++) { - const it = await converter.getApproxAge(thisYear - age + 1); - expect(it).to.be.eq('20대 중반'); - } - for (let age = 27; age <= 29; age++) { - const it = await converter.getApproxAge(thisYear - age + 1); - expect(it).to.be.eq('20대 후반'); - } - }); - - it(' # 30대 테스트', async () => { - const thisYear = new Date().getFullYear(); - for (let age = 37; age <= 39; age++) { - const it = await converter.getApproxAge(thisYear - age + 1); - expect(it).to.be.eq('30대 후반'); - } - }); - - it(' # 50대 테스트', async () => { - const thisYear = new Date().getFullYear(); - for (let age = 50; age <= 53; age++) { - const it = await converter.getApproxAge(thisYear - age + 1); - expect(it).to.be.eq('50대 초반'); - } - }); - - it(' # 0대 테스트', async () => { - const thisYear = new Date().getFullYear(); - for (let age = 0; age <= 3; age++) { - const it = await converter.getApproxAge(thisYear - age + 1); - expect(it).to.be.eq('0대 초반'); - } - for (let age = 4; age <= 6; age++) { - const it = await converter.getApproxAge(thisYear - age + 1); - expect(it).to.be.eq('0대 중반'); - } - for (let age = 7; age <= 9; age++) { - const it = await converter.getApproxAge(thisYear - age + 1); - expect(it).to.be.eq('0대 후반'); - } - }); - - it(' # 100대 테스트', async () => { - const thisYear = new Date().getFullYear(); - for (let age = 100; age <= 103; age++) { - const it = await converter.getApproxAge(thisYear - age + 1); - expect(it).to.be.eq('100대 초반'); - } - for (let age = 104; age <= 106; age++) { - const it = await converter.getApproxAge(thisYear - age + 1); - expect(it).to.be.eq('100대 중반'); - } - for (let age = 107; age <= 109; age++) { - const it = await converter.getApproxAge(thisYear - age + 1); - expect(it).to.be.eq('100대 후반'); - } - }); - }); -}); diff --git a/tests/test_unit/utils/converter.spec.ts b/tests/test_unit/utils/converter.spec.ts new file mode 100644 index 00000000..3392d335 --- /dev/null +++ b/tests/test_unit/utils/converter.spec.ts @@ -0,0 +1,133 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +import { expect } from 'chai'; + +import * as converter from '@utils/converter'; + +describe('# converter Test', () => { + it(' # Input Int < - > DB Int Test', () => { + Promise.all( + [0, 1, 2, 3, 4, 5].map(async (idx) => { + const longevity = Math.min(idx, 5); + const sillage = Math.min(idx, 3); + const seasonal = parseInt(String(Math.random() * 100)) % 16; + const gender = Math.min(idx, 3); + const keywordList = [1, 2, 3, 4, 5]; + const result = converter.DBIntToOutputIntOfReview({ + longevity, + sillage, + sumOfBitSeasonal: seasonal, + gender, + }); + expect(result.longevity).to.be.not.null; + expect(result.sillage).to.be.not.null; + expect(result.seasonalList).to.be.instanceof(Array); + expect(result.gender).to.be.not.null; + + const recover = await converter.InputIntToDBIntOfReview({ + ...result, + keywordList, + }); + expect(recover.longevity).to.be.eq(longevity); + expect(recover.sillage).to.be.eq(sillage); + expect(recover.sumOfBitSeasonal).to.be.eq(seasonal); + expect(recover.gender).to.be.eq(gender); + expect(recover.keywordList).to.be.deep.eq(keywordList); + }) + ); + }); + + it(' # Input Int < - > DB Int Zero Test', async () => { + const longevity = null; + const sillage = null; + const seasonal = null; + const gender = null; + const keywordList: number[] = []; + const result = converter.DBIntToOutputIntOfReview({ + longevity, + sillage, + sumOfBitSeasonal: seasonal, + gender, + }); + expect(result.longevity).to.be.not.null; + expect(result.sillage).to.be.not.null; + expect(result.seasonalList).to.be.instanceof(Array); + expect(result.gender).to.be.not.null; + const recover = await converter.InputIntToDBIntOfReview({ + ...result, + keywordList, + }); + expect(recover.longevity).to.be.eq(longevity); + expect(recover.sillage).to.be.eq(sillage); + expect(recover.sumOfBitSeasonal).to.be.eq(seasonal); + expect(recover.gender).to.be.eq(gender); + expect(recover.keywordList).to.be.deep.eq(keywordList); + }); + describe(' # ApproxAge Test', () => { + it(' # 20대 테스트', () => { + const thisYear = new Date().getFullYear(); + for (let age = 20; age <= 23; age++) { + const it = converter.getApproxAge(thisYear - age + 1); + expect(it).to.be.eq('20대 초반'); + } + + for (let age = 24; age <= 26; age++) { + const it = converter.getApproxAge(thisYear - age + 1); + expect(it).to.be.eq('20대 중반'); + } + for (let age = 27; age <= 29; age++) { + const it = converter.getApproxAge(thisYear - age + 1); + expect(it).to.be.eq('20대 후반'); + } + }); + + it(' # 30대 테스트', () => { + const thisYear = new Date().getFullYear(); + for (let age = 37; age <= 39; age++) { + const it = converter.getApproxAge(thisYear - age + 1); + expect(it).to.be.eq('30대 후반'); + } + }); + + it(' # 50대 테스트', () => { + const thisYear = new Date().getFullYear(); + for (let age = 50; age <= 53; age++) { + const it = converter.getApproxAge(thisYear - age + 1); + expect(it).to.be.eq('50대 초반'); + } + }); + + it(' # 0대 테스트', () => { + const thisYear = new Date().getFullYear(); + for (let age = 0; age <= 3; age++) { + const it = converter.getApproxAge(thisYear - age + 1); + expect(it).to.be.eq('0대 초반'); + } + for (let age = 4; age <= 6; age++) { + const it = converter.getApproxAge(thisYear - age + 1); + expect(it).to.be.eq('0대 중반'); + } + for (let age = 7; age <= 9; age++) { + const it = converter.getApproxAge(thisYear - age + 1); + expect(it).to.be.eq('0대 후반'); + } + }); + + it(' # 100대 테스트', () => { + const thisYear = new Date().getFullYear(); + for (let age = 100; age <= 103; age++) { + const it = converter.getApproxAge(thisYear - age + 1); + expect(it).to.be.eq('100대 초반'); + } + for (let age = 104; age <= 106; age++) { + const it = converter.getApproxAge(thisYear - age + 1); + expect(it).to.be.eq('100대 중반'); + } + for (let age = 107; age <= 109; age++) { + const it = converter.getApproxAge(thisYear - age + 1); + expect(it).to.be.eq('100대 후반'); + } + }); + }); +}); From 142e71f49e45a2e04ec576ddecb78a1d644883dd Mon Sep 17 00:00:00 2001 From: Jinho Hyeon Date: Fri, 23 Jun 2023 21:33:19 +0900 Subject: [PATCH 5/9] =?UTF-8?q?=EC=96=B4=EB=93=9C=EB=AF=BC=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=ED=95=A8=EC=88=98=EC=97=90=20findAndCountAll=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20(#514)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 어드민 목록 함수에 findAndCountAll 적용 * readPage 함수에서 최신순 정렬 적용 --- src/controllers/Admin.ts | 8 ++++---- src/dao/IngredientDao.ts | 3 ++- src/dao/PerfumeDao.ts | 3 ++- src/service/IngredientService.ts | 9 +++------ src/service/PerfumeService.ts | 10 +++++++--- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/controllers/Admin.ts b/src/controllers/Admin.ts index 7221342d..56baac85 100644 --- a/src/controllers/Admin.ts +++ b/src/controllers/Admin.ts @@ -160,10 +160,10 @@ export const getPerfume: RequestHandler = async ( * in: query * required: false * type: string - # enum: - # - id - # - name - # - englishName + * enum: + * - id + * - name + * - englishName * - name: keyword * in: query * required: false diff --git a/src/dao/IngredientDao.ts b/src/dao/IngredientDao.ts index da9566f5..cde4dd23 100644 --- a/src/dao/IngredientDao.ts +++ b/src/dao/IngredientDao.ts @@ -92,7 +92,7 @@ class IngredientDao { */ async readPage(offset: number, limit: number, where?: WhereOptions) { logger.debug(`${LOG_TAG} readAll()`); - return Ingredient.findAll({ + return Ingredient.findAndCountAll({ offset, limit, include: [ @@ -102,6 +102,7 @@ class IngredientDao { where, raw: true, nest: true, + order: [['createdAt', 'desc']], }); } } diff --git a/src/dao/PerfumeDao.ts b/src/dao/PerfumeDao.ts index 2f609067..c01c5a07 100644 --- a/src/dao/PerfumeDao.ts +++ b/src/dao/PerfumeDao.ts @@ -570,13 +570,14 @@ class PerfumeDao { */ async readPage(offset: number, limit: number, where?: WhereOptions) { logger.debug(`${LOG_TAG} readAll()`); - return Perfume.findAll({ + return Perfume.findAndCountAll({ offset, limit, include: [{ model: Brand, as: 'Brand' }], where, raw: true, nest: true, + order: [['createdAt', 'desc']], }); } } diff --git a/src/service/IngredientService.ts b/src/service/IngredientService.ts index f65c85b4..4a3c2ec6 100644 --- a/src/service/IngredientService.ts +++ b/src/service/IngredientService.ts @@ -67,21 +67,18 @@ class IngredientService { } } - const perfumes = await this.ingredientDao.readPage( + const { rows, count } = await this.ingredientDao.readPage( offset, limit, whereOptions ); - const perfumesWithCategory = perfumes.map((perfume) => { + const perfumesWithCategory = rows.map((perfume) => { return { ...perfume, IngredientCategory: IngredientCategoryDTO.createByJson(perfume), }; }); - return new ListAndCountDTO( - perfumesWithCategory.length, - perfumesWithCategory - ); + return new ListAndCountDTO(count, perfumesWithCategory); } } diff --git a/src/service/PerfumeService.ts b/src/service/PerfumeService.ts index 021b5ee1..ba0be19c 100644 --- a/src/service/PerfumeService.ts +++ b/src/service/PerfumeService.ts @@ -563,9 +563,13 @@ class PerfumeService { } } - const perfumes = await perfumeDao.readPage(offset, limit, whereOptions); - const list = perfumes.map((c) => PerfumeThumbDTO.createByJson(c)); - return new ListAndCountDTO(list.length, list); + const { rows, count } = await perfumeDao.readPage( + offset, + limit, + whereOptions + ); + const list = rows.map((c) => PerfumeThumbDTO.createByJson(c)); + return new ListAndCountDTO(count, list); } } From 704c8adb6103d0a4dbb73b92322d5b8e93407d83 Mon Sep 17 00:00:00 2001 From: Hanyi SEO <122385460+hanyiseo2@users.noreply.github.com> Date: Fri, 23 Jun 2023 21:57:45 +0900 Subject: [PATCH 6/9] Dev admin category (#515) * GET /admin/ingredientCategories * GET /admin/ingredientCategories * GET /admin/ingredientCategories * GET /admin/ingredientCategories --- src/controllers/Admin.ts | 85 +++++++++++++++++++ .../definitions/response/ingredient.ts | 10 ++- .../definitions/response/series.ts | 19 +++-- src/dao/IngredientCategoryDao.ts | 19 +++++ src/data/dto/IngredientCategoryDTO.ts | 12 +-- src/data/dto/PerfumeThumbDTO.ts | 2 +- src/models/tables/IngredientCategories.ts | 8 +- src/service/IngredientCategoryService.ts | 41 +++++++++ src/service/IngredientService.ts | 1 + 9 files changed, 174 insertions(+), 23 deletions(-) create mode 100644 src/service/IngredientCategoryService.ts diff --git a/src/controllers/Admin.ts b/src/controllers/Admin.ts index 56baac85..a47ead1b 100644 --- a/src/controllers/Admin.ts +++ b/src/controllers/Admin.ts @@ -10,17 +10,22 @@ import { } from '@src/utils/strings'; import { NextFunction, Request, RequestHandler, Response } from 'express'; import { + IngredientCategoryResponse, IngredientFullResponse, + // IngredientResponse, LoginResponse, PerfumeDetailResponse, PerfumeResponse, ResponseDTO, } from './definitions/response'; import { ListAndCountDTO } from '@src/data/dto'; +import IngredientCategoryService from '@src/service/IngredientCategoryService'; let Admin: AdminService = new AdminService(); let Perfume: PerfumeService = new PerfumeService(); let Ingredient: IngredientService = new IngredientService(); +let IngredientCategory: IngredientCategoryService = + new IngredientCategoryService(); /** * @swagger @@ -290,3 +295,83 @@ export const getIngredientAll: RequestHandler = async ( ) ); }; + +/** + * + * @swagger + * /admin/ingredientCategories: + * get: + * tags: + * - admin + * summary: 재료 카테고리 목록 조회 + * description: 재료 카테고리 리스트 조회
반환 되는 정보 [재료] + * operationId: getIngredientCategoryList + * produces: + * - application/json + * parameters: + * - name: page + * in: query + * required: true + * type: integer + * format: int64 + * - name: target + * in: query + * required: false + * type: string + * enum: + * - id + * - name + * - name: keyword + * in: query + * required: false + * type: string + * responses: + * 200: + * description: 성공 + * schema: + * type: object + * properties: + * message: + * type: string + * example: Ingredient Category 목록 조회 성공 + * data: + * type: object + * properties: + * count: + * type: integer + * example: 1 + * rows: + * type: array + * items: + * allOf: + * - $ref: '#/definitions/IngredientResponse' + * 401: + * description: Token is missing or invalid + * x-swagger-router-controller: Admin + */ +export const getIngredientCategoryList: RequestHandler = async ( + req: Request, + res: Response, + next: NextFunction +) => { + const page: number = Number(req.query.page); + if (isNaN(page)) { + next(); + return; + } + const limit = 20; + const offset = (page - 1) * limit; + + const categories = await IngredientCategory.readPage( + offset, + limit, + req.query + ); + + res.status(StatusCode.OK).json( + new ResponseDTO>( + MSG_GET_SEARCH_INGREDIENT_SUCCESS, + categories.convertType(IngredientCategoryResponse.create) + ) + ); +}; diff --git a/src/controllers/definitions/response/ingredient.ts b/src/controllers/definitions/response/ingredient.ts index 70ff76c8..55c4feb9 100644 --- a/src/controllers/definitions/response/ingredient.ts +++ b/src/controllers/definitions/response/ingredient.ts @@ -13,7 +13,7 @@ import { IngredientCategoryResponse, SeriesResponse } from './series'; * type: string * example: * ingredientIdx: 1 - * name: 씨쏠트 + * name: 베르가못 * */ class IngredientResponse { readonly ingredientIdx: number; @@ -54,11 +54,17 @@ class IngredientResponse { * ingredientCategory: * - $ref: '#/definitions/IngredientCategoryResponse' * - * example: + * examples: * ingredientIdx: 1 * name: 씨쏠트 * englishName: seesalt * description: 설명 + * Series: + * seriesIdx: 2 + * name: 시트러스 + * IngredientCategory: + * ingredientIdx: 5 + * name: 버터오렌지 * */ class IngredientFullResponse { readonly ingredientIdx: number; diff --git a/src/controllers/definitions/response/series.ts b/src/controllers/definitions/response/series.ts index 49c4ed9f..fcbab168 100644 --- a/src/controllers/definitions/response/series.ts +++ b/src/controllers/definitions/response/series.ts @@ -13,9 +13,10 @@ import { SeriesFilterDTO, SeriesDTO, IngredientCategoryDTO } from '@dto/index'; * imageUrl: * type: string * example: - * seriesIdx: 1 - * name: 꿀 + * seriesIdx: 2 + * name: 시트러스 * imageUrl: http:// + * * */ class SeriesResponse { readonly seriesIdx: number; @@ -46,19 +47,19 @@ class SeriesResponse { * IngredientCategory: * type: object * properties: - * ingredientIdx: + * id: * type: number * name: * type: string * example: - * ingredientIdx: 1 - * name: 꿀 + * id: 5 + * name: 버터오렌지 * */ class IngredientCategoryResponse { - readonly ingredientIdx: number; + readonly id: number; readonly name: string; - constructor(ingredientIdx: number, name: string) { - this.ingredientIdx = ingredientIdx; + constructor(id: number, name: string) { + this.id = id; this.name = name; } @@ -69,7 +70,7 @@ class IngredientCategoryResponse { ingredientCategoryDTO: IngredientCategoryDTO ): IngredientCategoryResponse { return new IngredientCategoryResponse( - ingredientCategoryDTO.ingredientIdx, + ingredientCategoryDTO.id, ingredientCategoryDTO.name ); } diff --git a/src/dao/IngredientCategoryDao.ts b/src/dao/IngredientCategoryDao.ts index 5aa9c07f..34a267c5 100644 --- a/src/dao/IngredientCategoryDao.ts +++ b/src/dao/IngredientCategoryDao.ts @@ -3,6 +3,7 @@ import { logger } from '@modules/winston'; import { IngredientCategoryDTO } from '@src/data/dto'; import { IngredientCategories } from '@sequelize'; +import { WhereOptions } from 'sequelize'; const LOG_TAG: string = '[IngredientCategory/DAO]'; @@ -23,6 +24,24 @@ class IngredientCategoryDao { }); return result.map((it: any) => IngredientCategoryDTO.createByJson(it)); } + + /** + * 향료 카테고리 조회 + * + * @returns {Promise} + */ + + async readPage(offset: number, limit: number, where?: WhereOptions) { + return IngredientCategories.findAndCountAll({ + offset, + limit, + where, + + raw: true, + nest: true, + order: [['createdAt', 'desc']], + }); + } } export default IngredientCategoryDao; diff --git a/src/data/dto/IngredientCategoryDTO.ts b/src/data/dto/IngredientCategoryDTO.ts index b8292ba7..9dd0c376 100644 --- a/src/data/dto/IngredientCategoryDTO.ts +++ b/src/data/dto/IngredientCategoryDTO.ts @@ -2,17 +2,10 @@ class IngredientCategoryDTO { readonly id: number; readonly name: string; readonly usedCountOnPerfume: number; - readonly ingredientIdx: number; - constructor( - id: number, - name: string, - usedCountOnPerfume: number, - ingredientIdx: number - ) { + constructor(id: number, name: string, usedCountOnPerfume: number) { this.id = id; this.name = name; this.usedCountOnPerfume = usedCountOnPerfume; - this.ingredientIdx = ingredientIdx; } public toString(): string { @@ -22,8 +15,7 @@ class IngredientCategoryDTO { return new IngredientCategoryDTO( json.id, json.name, - json.usedCountOnPerfume, - json.ingredientIdx + json.usedCountOnPerfume ); } } diff --git a/src/data/dto/PerfumeThumbDTO.ts b/src/data/dto/PerfumeThumbDTO.ts index 11f944bc..bb79b8d1 100644 --- a/src/data/dto/PerfumeThumbDTO.ts +++ b/src/data/dto/PerfumeThumbDTO.ts @@ -20,7 +20,7 @@ class PerfumeThumbDTO { ) { this.perfumeIdx = perfumeIdx; this.name = name; - this.brandName = Brand.name; + this.brandName = Brand?.name; this.isLiked = isLiked || false; this.imageUrl = imageUrl; this.createdAt = createdAt; diff --git a/src/models/tables/IngredientCategories.ts b/src/models/tables/IngredientCategories.ts index 20cb72ec..933c1453 100644 --- a/src/models/tables/IngredientCategories.ts +++ b/src/models/tables/IngredientCategories.ts @@ -12,7 +12,13 @@ export class IngredientCategories extends Model { unique: true, }) name: string; - + @Column({ + type: DataType.INTEGER, + autoIncrement: true, + allowNull: false, + primaryKey: true, + }) + id: number; @Column({ type: DataType.INTEGER, allowNull: false, diff --git a/src/service/IngredientCategoryService.ts b/src/service/IngredientCategoryService.ts new file mode 100644 index 00000000..0233a019 --- /dev/null +++ b/src/service/IngredientCategoryService.ts @@ -0,0 +1,41 @@ +import IngredientCategoryDao from '@dao/IngredientCategoryDao'; + +import { IngredientCategoryDTO, ListAndCountDTO } from '@dto/index'; +import { Op } from 'sequelize'; + +class IngredientCategoryService { + ingredientCategoryDao: IngredientCategoryDao; + + constructor(ingredientCategoryDao?: IngredientCategoryDao) { + this.ingredientCategoryDao = + ingredientCategoryDao ?? new IngredientCategoryDao(); + } + + async readPage(offset: number, limit: number, query: any) { + const { target, keyword } = query; + const whereOptions = {} as any; + if (target && keyword) { + switch (target) { + case 'id': + whereOptions.id = keyword; + break; + case 'name': + whereOptions.name = { [Op.startsWith]: keyword }; + break; + } + } + + const { rows, count } = await this.ingredientCategoryDao.readPage( + offset, + limit, + whereOptions + ); + + const perfumesWithCategory = rows.map((c) => + IngredientCategoryDTO.createByJson(c) + ); + return new ListAndCountDTO(count, perfumesWithCategory); + } +} + +export default IngredientCategoryService; diff --git a/src/service/IngredientService.ts b/src/service/IngredientService.ts index 4a3c2ec6..3fa56af7 100644 --- a/src/service/IngredientService.ts +++ b/src/service/IngredientService.ts @@ -79,6 +79,7 @@ class IngredientService { }; }); return new ListAndCountDTO(count, perfumesWithCategory); + } } From 86b94d29fa29c6de3c41d3476ecbb104e5d683fb Mon Sep 17 00:00:00 2001 From: Jinho Hyeon Date: Fri, 23 Jun 2023 22:27:17 +0900 Subject: [PATCH 7/9] =?UTF-8?q?=ED=96=A5=EB=A3=8C=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=B6=94=EA=B0=80=20api=20(#516)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 향료카테고리 추가 api * 불용 함수 제거 --- src/controllers/Admin.ts | 62 ++++++++++++++++++++++++ src/dao/IngredientCategoryDao.ts | 6 +++ src/service/IngredientCategoryService.ts | 15 ++++++ 3 files changed, 83 insertions(+) diff --git a/src/controllers/Admin.ts b/src/controllers/Admin.ts index a47ead1b..b82496ee 100644 --- a/src/controllers/Admin.ts +++ b/src/controllers/Admin.ts @@ -3,6 +3,7 @@ import { AdminService } from '@src/service/AdminService'; import PerfumeService from '@src/service/PerfumeService'; import StatusCode from '@src/utils/statusCode'; import { + MSG_EXIST_DUPLICATE_ENTRY, MSG_GET_ADDED_PERFUME_RECENT_SUCCESS, MSG_GET_PERFUME_DETAIL_SUCCESS, MSG_GET_SEARCH_INGREDIENT_SUCCESS, @@ -17,9 +18,11 @@ import { PerfumeDetailResponse, PerfumeResponse, ResponseDTO, + SimpleResponseDTO, } from './definitions/response'; import { ListAndCountDTO } from '@src/data/dto'; import IngredientCategoryService from '@src/service/IngredientCategoryService'; +import { DuplicatedEntryError } from '@src/utils/errors/errors'; let Admin: AdminService = new AdminService(); let Perfume: PerfumeService = new PerfumeService(); @@ -375,3 +378,62 @@ export const getIngredientCategoryList: RequestHandler = async ( ) ); }; + +/** + * @swagger + * /admin/ingredientCategories: + * post: + * tags: + * - admin + * summary: 재료 카테고리 추가 + * description: 재료 카테고리 추가 + * operationId: createIngredientCategory + * produces: + * - application/json + * parameters: + * - name: body + * in: body + * required: true + * schema: + * type: object + * properties: + * name: + * type: string + * responses: + * 200: + * description: success + * schema: + * type: object + * properties: + * message: + * type: string + * 400: + * description: 요청 실패 + * 409: + * description: 같은 이름의 카테고리가 존재할 때 + * schema: + * type: object + * x-swagger-router-controller: Admin + */ +export const createIngredientCategory: RequestHandler = async ( + req: Request, + res: Response +) => { + const { name } = req.body; + try { + await IngredientCategory.create(name); + res.status(StatusCode.OK).json({ + message: '성공', + }); + } catch (e: any) { + if (e instanceof DuplicatedEntryError) { + res.status(StatusCode.CONFLICT).json( + new ResponseDTO(MSG_EXIST_DUPLICATE_ENTRY, false) + ); + } else { + res.status(StatusCode.BAD_REQUEST).json( + new SimpleResponseDTO(e.message) + ); + } + } +}; diff --git a/src/dao/IngredientCategoryDao.ts b/src/dao/IngredientCategoryDao.ts index 34a267c5..9ef71ac7 100644 --- a/src/dao/IngredientCategoryDao.ts +++ b/src/dao/IngredientCategoryDao.ts @@ -42,6 +42,12 @@ class IngredientCategoryDao { order: [['createdAt', 'desc']], }); } + + async create(name: string) { + return IngredientCategories.create({ + name, + }); + } } export default IngredientCategoryDao; diff --git a/src/service/IngredientCategoryService.ts b/src/service/IngredientCategoryService.ts index 0233a019..fca9ae05 100644 --- a/src/service/IngredientCategoryService.ts +++ b/src/service/IngredientCategoryService.ts @@ -1,6 +1,10 @@ import IngredientCategoryDao from '@dao/IngredientCategoryDao'; import { IngredientCategoryDTO, ListAndCountDTO } from '@dto/index'; +import { + DuplicatedEntryError, + FailedToCreateError, +} from '@src/utils/errors/errors'; import { Op } from 'sequelize'; class IngredientCategoryService { @@ -36,6 +40,17 @@ class IngredientCategoryService { ); return new ListAndCountDTO(count, perfumesWithCategory); } + + async create(name: string) { + try { + return await this.ingredientCategoryDao.create(name); + } catch (err: Error | any) { + if (err.parent.errno === 1062) { + throw new DuplicatedEntryError(); + } + throw new FailedToCreateError(); + } + } } export default IngredientCategoryService; From 15659abe0da0effe7a2bf02830e878d859b8a2f9 Mon Sep 17 00:00:00 2001 From: Jinho Hyeon Date: Sat, 24 Jun 2023 00:45:29 +0900 Subject: [PATCH 8/9] =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC=20(?= =?UTF-8?q?#517)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * NoteDictDTO * TokenGroupDTO * constants 정리 * 테스트오류 수정 --- src/controllers/User.ts | 10 +-- src/dao/ReviewDao.ts | 4 +- src/data/dto/NoteDictDTO.ts | 66 ------------------- src/data/dto/TokenGroupDTO.ts | 20 ------ src/data/dto/index.ts | 2 - src/service/NoteService.ts | 59 +++++++++++++---- src/service/SeriesService.ts | 3 +- src/service/UserService.ts | 8 +-- src/utils/constants.ts | 34 ---------- tests/test_integral/perfume.spec.ts | 16 ++--- tests/test_unit/controller/Perfume.spec.ts | 7 +- tests/test_unit/controller/User.spec.ts | 19 +++--- tests/test_unit/dao/ReviewDao.spec.js | 10 +-- .../test_unit/service/PerfumeService.spec.ts | 16 ++--- tests/test_unit/service/UserService.spec.ts | 9 ++- 15 files changed, 86 insertions(+), 197 deletions(-) delete mode 100644 src/data/dto/NoteDictDTO.ts delete mode 100644 src/data/dto/TokenGroupDTO.ts diff --git a/src/controllers/User.ts b/src/controllers/User.ts index 6c1e1d71..907b61e7 100644 --- a/src/controllers/User.ts +++ b/src/controllers/User.ts @@ -36,13 +36,7 @@ import { LoginResponse, } from '@response/user'; -import { - UserAuthDTO, - UserInputDTO, - LoginInfoDTO, - SurveyDTO, - UserDTO, -} from '@dto/index'; +import { UserAuthDTO, LoginInfoDTO, SurveyDTO, UserDTO } from '@dto/index'; const LOG_TAG: string = '[User/Controller]'; @@ -99,7 +93,7 @@ const registerUser: RequestHandler = ( return; } User.createUser(userRegisterRequest.toUserInputDTO()) - .then((result: UserInputDTO) => { + .then((result) => { return UserRegisterResponse.createByJson(result); }) .then((response: UserRegisterResponse) => { diff --git a/src/dao/ReviewDao.ts b/src/dao/ReviewDao.ts index 07841803..6e1c68cd 100644 --- a/src/dao/ReviewDao.ts +++ b/src/dao/ReviewDao.ts @@ -1,5 +1,4 @@ import { NotMatchedError, DuplicatedEntryError } from '@errors'; -import { ACCESS_PUBLIC, ACCESS_PRIVATE } from '@utils/constants'; import { sequelize, @@ -11,6 +10,9 @@ import { } from '@sequelize'; import { Op, Order, QueryTypes } from 'sequelize'; +const ACCESS_PUBLIC: number = 1; +const ACCESS_PRIVATE: number = 0; + const SQL_READ_ALL_OF_PERFUME = ` SELECT r.id as reviewIdx, diff --git a/src/data/dto/NoteDictDTO.ts b/src/data/dto/NoteDictDTO.ts deleted file mode 100644 index 45e6f316..00000000 --- a/src/data/dto/NoteDictDTO.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - NOTE_TYPE_TOP, - NOTE_TYPE_MIDDLE, - NOTE_TYPE_BASE, - NOTE_TYPE_SINGLE, -} from '@utils/constants'; - -import { NoteDTO } from '@dto/NoteDTO'; - -class NoteDictDTO { - readonly top: string; - readonly middle: string; - readonly base: string; - readonly single: string; - constructor(top: string, middle: string, base: string, single: string) { - this.top = top; - this.middle = middle; - this.base = base; - this.single = single; - } - - public toString(): string { - return `${this.constructor.name} (${JSON.stringify(this)})`; - } - - static createByNoteList(noteList: NoteDTO[]): NoteDictDTO { - const topList: string[] = []; - const middleList: string[] = []; - const baseList: string[] = []; - const singleList: string[] = []; - - noteList.forEach((it: NoteDTO) => { - switch (it.type) { - case NOTE_TYPE_TOP: - topList.push(it.ingredientName); - break; - case NOTE_TYPE_MIDDLE: - middleList.push(it.ingredientName); - break; - case NOTE_TYPE_BASE: - baseList.push(it.ingredientName); - break; - case NOTE_TYPE_SINGLE: - singleList.push(it.ingredientName); - break; - } - }); - - return new NoteDictDTO( - topList.join(', '), - middleList.join(', '), - baseList.join(', '), - singleList.join(', ') - ); - } - static createByJson(json: { - top: string; - middle: string; - base: string; - single: string; - }) { - return new NoteDictDTO(json.top, json.middle, json.base, json.single); - } -} - -export { NoteDictDTO }; diff --git a/src/data/dto/TokenGroupDTO.ts b/src/data/dto/TokenGroupDTO.ts deleted file mode 100644 index 4f85c195..00000000 --- a/src/data/dto/TokenGroupDTO.ts +++ /dev/null @@ -1,20 +0,0 @@ -class TokenGroupDTO { - readonly userIdx: number; - readonly token: string; - readonly refreshToken: string; - constructor(userIdx: number, token: string, refreshToken: string) { - this.userIdx = userIdx; - this.token = token; - this.refreshToken = refreshToken; - } - - public toString(): string { - return `${this.constructor.name} (${JSON.stringify(this)})`; - } - - static createByJSON(json: any): TokenGroupDTO { - return new TokenGroupDTO(json.userIdx, json.token, json.refreshToken); - } -} - -export { TokenGroupDTO }; diff --git a/src/data/dto/index.ts b/src/data/dto/index.ts index 541d367c..6bf4a425 100644 --- a/src/data/dto/index.ts +++ b/src/data/dto/index.ts @@ -5,7 +5,6 @@ export * from './IngredientCategoryDTO'; export * from './IngredientDTO'; export * from './ListAndCountDTO'; export * from './LoginInfoDTO'; -export * from './NoteDictDTO'; export * from './NoteDTO'; export * from './PagingDTO'; export * from './PerfumeDTO'; @@ -22,7 +21,6 @@ export * from './ReportUserInquirePerfumeDTO'; export * from './SeriesDTO'; export * from './SeriesFilterDTO'; export * from './SurveyDTO'; -export * from './TokenGroupDTO'; export * from './TokenPayloadDTO'; export * from './UserAuthDTO'; export * from './UserDTO'; diff --git a/src/service/NoteService.ts b/src/service/NoteService.ts index a7dab087..3baf0fb7 100644 --- a/src/service/NoteService.ts +++ b/src/service/NoteService.ts @@ -1,9 +1,20 @@ import NoteDao from '@src/dao/NoteDao'; -import { NoteDictDTO } from '@src/data/dto'; -import { - PERFUME_NOTE_TYPE_NORMAL, - PERFUME_NOTE_TYPE_SINGLE, -} from '@src/utils/constants'; +import { NoteDTO } from '@src/data/dto'; + +interface NoteDict { + top: string; + middle: string; + base: string; + single: string; +} + +const NOTE_TYPE_TOP: number = 1; +const NOTE_TYPE_MIDDLE: number = 2; +const NOTE_TYPE_BASE: number = 3; +const NOTE_TYPE_SINGLE: number = 4; + +const PERFUME_NOTE_TYPE_SINGLE: number = 1; +const PERFUME_NOTE_TYPE_NORMAL: number = 0; export class NoteService { noteDao: NoteDao; @@ -21,16 +32,42 @@ export class NoteService { }; }> { const noteList: any[] = await this.noteDao.readByPerfumeIdx(perfumeIdx); - const noteDictDTO: { - top: string; - middle: string; - base: string; - single: string; - } = NoteDictDTO.createByNoteList(noteList); + const noteDictDTO = this.createByNoteList(noteList); const noteType: number = noteDictDTO.single.length > 0 ? PERFUME_NOTE_TYPE_SINGLE : PERFUME_NOTE_TYPE_NORMAL; return { noteType, noteDictDTO }; } + + private createByNoteList(noteList: NoteDTO[]): NoteDict { + const topList: string[] = []; + const middleList: string[] = []; + const baseList: string[] = []; + const singleList: string[] = []; + + noteList.forEach((it: NoteDTO) => { + switch (it.type) { + case NOTE_TYPE_TOP: + topList.push(it.ingredientName); + break; + case NOTE_TYPE_MIDDLE: + middleList.push(it.ingredientName); + break; + case NOTE_TYPE_BASE: + baseList.push(it.ingredientName); + break; + case NOTE_TYPE_SINGLE: + singleList.push(it.ingredientName); + break; + } + }); + + return { + top: topList.join(', '), + middle: middleList.join(', '), + base: baseList.join(', '), + single: singleList.join(', '), + }; + } } diff --git a/src/service/SeriesService.ts b/src/service/SeriesService.ts index 8b903ee3..889d618b 100644 --- a/src/service/SeriesService.ts +++ b/src/service/SeriesService.ts @@ -13,12 +13,13 @@ import { SeriesFilterDTO, IngredientCategoryDTO, } from '@dto/index'; -import { THRESHOLD_CATEGORY } from '@src/utils/constants'; import { Op } from 'sequelize'; import { ETC } from '@src/utils/strings'; const LOG_TAG: string = '[Series/Service]'; +const THRESHOLD_CATEGORY: number = 10; + class SeriesService { seriesDao: SeriesDao; ingredientDao: IngredientDao; diff --git a/src/service/UserService.ts b/src/service/UserService.ts index 6aa25a1e..368e8719 100644 --- a/src/service/UserService.ts +++ b/src/service/UserService.ts @@ -17,7 +17,6 @@ import JwtController from '@libs/JwtController'; import { TokenPayloadDTO, LoginInfoDTO, - TokenGroupDTO, UserAuthDTO, UserInputDTO, UserDTO, @@ -52,10 +51,9 @@ class UserService { * 유저 회원 가입 * * @param {UserInputDTO} UserInputDTO - * @returns {Promise} * @throws {FailedToCreateError} if failed to create user **/ - async createUser(userInputDTO: UserInputDTO): Promise { + async createUser(userInputDTO: UserInputDTO) { logger.debug(`${LOG_TAG} createUser(userInputDTO = ${userInputDTO})`); return this.userDao .create(userInputDTO) @@ -68,11 +66,11 @@ class UserService { const { userIdx } = user; const { token, refreshToken } = this.jwt.publish(payload); - return TokenGroupDTO.createByJSON({ + return { userIdx, token, refreshToken, - }); + }; }) .catch((error: Error) => { if (error instanceof DuplicatedEntryError) { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 86a28fc4..9b29691e 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,14 +1,9 @@ import { - NONE, EAU_DE_COLOGNE, EAU_DE_TOILETTE, EAU_DE_PERFUME, PERFUME, ETC, - TOP, - MIDDLE, - BASE, - SINGLE, } from '@utils/strings'; const GENDER_MAN: number = 1; @@ -31,29 +26,14 @@ const ABUNDANCE_RATE_STR_DICT: { [key: string]: string } = new Proxy<{ } ); -const NOTE_TYPE_TOP: number = 1; -const NOTE_TYPE_MIDDLE: number = 2; -const NOTE_TYPE_BASE: number = 3; -const NOTE_TYPE_SINGLE: number = 4; -const NOTE_TYPE_LIST: string[] = [NONE, TOP, MIDDLE, BASE, SINGLE]; -const PERFUME_NOTE_TYPE_SINGLE: number = 1; -const PERFUME_NOTE_TYPE_NORMAL: number = 0; -const MIN_SCORE: number = 0; -const MAX_SCORE: number = 10; const DEFAULT_PAGE_SIZE: number = 100; -const THRESHOLD_CATEGORY: number = 10; const DEFAULT_RECOMMEND_REQUEST_SIZE: number = 7; const DEFAULT_NEW_PERFUME_REQUEST_SIZE: number = 50; const DEFAULT_SIMILAR_PERFUMES_REQUEST_SIZE: number = 20; const DEFAULT_RECENT_ADDED_PERFUME_REQUEST_SIZE: number = 7; -const DEFAULT_RECOMMEND_COMMON_REQUEST_SIZE: number = 15; const DEFAULT_BRAND_REQUEST_SIZE: number = 1000; const DEFAULT_INGREDIENT_REQUEST_SIZE: number = 2000; -const DEFAULT_OP_CODE: number = 0; - -const ACCESS_PUBLIC: number = 1; -const ACCESS_PRIVATE: number = 0; export { GENDER_MAN, @@ -62,25 +42,11 @@ export { GRADE_MANAGER, GRADE_SYSTEM_ADMIN, ABUNDANCE_RATE_STR_DICT, - NOTE_TYPE_TOP, - NOTE_TYPE_MIDDLE, - NOTE_TYPE_BASE, - NOTE_TYPE_SINGLE, - NOTE_TYPE_LIST, - PERFUME_NOTE_TYPE_SINGLE, - PERFUME_NOTE_TYPE_NORMAL, - MIN_SCORE, - MAX_SCORE, DEFAULT_PAGE_SIZE, - THRESHOLD_CATEGORY, DEFAULT_RECOMMEND_REQUEST_SIZE, DEFAULT_NEW_PERFUME_REQUEST_SIZE, DEFAULT_RECENT_ADDED_PERFUME_REQUEST_SIZE, DEFAULT_SIMILAR_PERFUMES_REQUEST_SIZE, - DEFAULT_RECOMMEND_COMMON_REQUEST_SIZE, DEFAULT_BRAND_REQUEST_SIZE, DEFAULT_INGREDIENT_REQUEST_SIZE, - ACCESS_PUBLIC, - ACCESS_PRIVATE, - DEFAULT_OP_CODE, }; diff --git a/tests/test_integral/perfume.spec.ts b/tests/test_integral/perfume.spec.ts index 7af12889..ecc86d9d 100644 --- a/tests/test_integral/perfume.spec.ts +++ b/tests/test_integral/perfume.spec.ts @@ -26,11 +26,9 @@ import { import { DEFAULT_RECOMMEND_REQUEST_SIZE, DEFAULT_RECENT_ADDED_PERFUME_REQUEST_SIZE, - DEFAULT_RECOMMEND_COMMON_REQUEST_SIZE, - DEFAULT_OP_CODE, } from '@utils/constants'; -import { ResponseDTO, SimpleResponseDTO } from '@response/common'; +import { OpCode, ResponseDTO, SimpleResponseDTO } from '@response/common'; import { PerfumeResponse, PerfumeDetailResponse, @@ -180,12 +178,8 @@ describe('# Perfume Integral Test', () => { expect(responseDTO.message).to.be.eq( MSG_GET_RECOMMEND_PERFUME_BY_AGE_AND_GENDER ); - expect(responseDTO.data.count).to.be.gte( - DEFAULT_RECOMMEND_COMMON_REQUEST_SIZE - ); - expect(responseDTO.data.rows.length).to.be.eq( - DEFAULT_RECOMMEND_COMMON_REQUEST_SIZE - ); + expect(responseDTO.data.count).to.be.gte(15); + expect(responseDTO.data.rows.length).to.be.eq(15); done(); }) .catch((err: Error) => done(err)); @@ -356,7 +350,7 @@ describe('# Perfume Integral Test', () => { expect(responseDTO.message).to.be.eq( MSG_POST_PERFUME_RECOMMEND_SIMMILAR_SUCCESS ); - expect(responseDTO.opcode).to.be.eq(DEFAULT_OP_CODE); + expect(responseDTO.opcode).to.be.eq(OpCode.NONE); }); it('read success case', async () => { @@ -373,7 +367,7 @@ describe('# Perfume Integral Test', () => { MSG_GET_RECOMMEND_SIMILAR_PERFUMES ); expect(responseDTO.data.count).to.be.eq(3); - expect(responseDTO.opcode).to.be.eq(DEFAULT_OP_CODE); + expect(responseDTO.opcode).to.be.eq(OpCode.NONE); }); }); }); diff --git a/tests/test_unit/controller/Perfume.spec.ts b/tests/test_unit/controller/Perfume.spec.ts index f076431d..03c5d9bd 100644 --- a/tests/test_unit/controller/Perfume.spec.ts +++ b/tests/test_unit/controller/Perfume.spec.ts @@ -27,12 +27,11 @@ import { import { DEFAULT_RECOMMEND_REQUEST_SIZE, DEFAULT_RECENT_ADDED_PERFUME_REQUEST_SIZE, - DEFAULT_OP_CODE, } from '@utils/constants'; import JwtController from '@libs/JwtController'; -import { ResponseDTO, SimpleResponseDTO } from '@response/common'; +import { OpCode, ResponseDTO, SimpleResponseDTO } from '@response/common'; import { PerfumeResponse, PerfumeDetailResponse, @@ -487,7 +486,7 @@ describe('# Perfume Controller Test', () => { expect(responseDTO.message).to.be.eq( MSG_POST_PERFUME_RECOMMEND_SIMMILAR_SUCCESS ); - expect(responseDTO.opcode).to.be.eq(DEFAULT_OP_CODE); + expect(responseDTO.opcode).to.be.eq(OpCode.NONE); }); it('read success case', async () => { @@ -515,7 +514,7 @@ describe('# Perfume Controller Test', () => { expect(responseDTO.data.count).to.be.eq( DEFAULT_RECOMMEND_REQUEST_SIZE ); - expect(responseDTO.opcode).to.be.eq(DEFAULT_OP_CODE); + expect(responseDTO.opcode).to.be.eq(OpCode.NONE); }); }); }); diff --git a/tests/test_unit/controller/User.spec.ts b/tests/test_unit/controller/User.spec.ts index 68712811..4d0a009c 100644 --- a/tests/test_unit/controller/User.spec.ts +++ b/tests/test_unit/controller/User.spec.ts @@ -30,7 +30,6 @@ import { TokenPayloadDTO, UserAuthDTO, UserDTO, - TokenGroupDTO, LoginInfoDTO, } from '@dto/index'; @@ -54,16 +53,14 @@ const invalidToken = describe('# User Controller Test', () => { describe('# registerUser Test', () => { - mockUserService.createUser = async (): Promise => - TokenGroupDTO.createByJSON( - Object.assign( - { - userIdx: 1, - token: 'token', - refreshToken: 'refreshToken', - }, - {} - ) + mockUserService.createUser = async (): Promise => + Object.assign( + { + userIdx: 1, + token: 'token', + refreshToken: 'refreshToken', + }, + {} ); it('success case', (done: Done) => { request(app) diff --git a/tests/test_unit/dao/ReviewDao.spec.js b/tests/test_unit/dao/ReviewDao.spec.js index 82c9dc0b..5e84faad 100644 --- a/tests/test_unit/dao/ReviewDao.spec.js +++ b/tests/test_unit/dao/ReviewDao.spec.js @@ -3,7 +3,6 @@ dotenv.config(); import ReviewDao from '@dao/ReviewDao'; import KeywordDao from '@dao/KeywordDao'; -import { ACCESS_PUBLIC, ACCESS_PRIVATE } from '@utils/constants'; const chai = require('chai'); const { expect } = chai; @@ -117,10 +116,7 @@ describe('# reviewDao Test', () => { expect(review.sillage).to.be.ok; expect(review.seasonal).to.be.ok; expect(review.gender).to.be.ok; - expect(review.access).to.be.oneOf([ - ACCESS_PRIVATE, - ACCESS_PUBLIC, - ]); + expect(review.access).to.be.oneOf([0, 1]); expect(review.content).to.be.ok; expect(review.likeCnt).to.be.ok; expect(review.perfumeIdx).to.be.ok; @@ -167,7 +163,7 @@ describe('# reviewDao Test', () => { expect(review.sillage).to.be.not.undefined; expect(review.seasonal).to.be.not.undefined; expect(review.gender).to.be.not.undefined; - expect(review.access).to.be.eq(ACCESS_PUBLIC); + expect(review.access).to.be.eq(1); expect(review.content).to.be.ok; expect(review.User).to.be.ok; @@ -208,7 +204,7 @@ describe('# reviewDao Test', () => { expect(result).to.be.ok; expect(result.length).to.be.eq(5); result.forEach((it) => { - expect(it.access).to.be.gte(ACCESS_PRIVATE); + expect(it.access).to.be.gte(0); }); done(); }) diff --git a/tests/test_unit/service/PerfumeService.spec.ts b/tests/test_unit/service/PerfumeService.spec.ts index fb695208..70ce698d 100644 --- a/tests/test_unit/service/PerfumeService.spec.ts +++ b/tests/test_unit/service/PerfumeService.spec.ts @@ -6,13 +6,7 @@ dotenv.config(); import PerfumeService from '@services/PerfumeService'; -import { - ACCESS_PRIVATE, - ACCESS_PUBLIC, - GENDER_MAN, - GENDER_WOMAN, - GRADE_USER, -} from '@utils/constants'; +import { GENDER_MAN, GENDER_WOMAN, GRADE_USER } from '@utils/constants'; import { ListAndCountDTO, @@ -81,7 +75,7 @@ describe('# Perfume Service Test', () => { sillage: SillageProperty.light.value, seasonal: SeasonalProperty.fall.value, gender: GenderProperty.male.value, - access: ACCESS_PUBLIC, + access: 1, content: '시향노트1', createdAt: '2021-09-26T08:38:33.000Z', User: { @@ -103,7 +97,7 @@ describe('# Perfume Service Test', () => { sillage: SillageProperty.light.value, seasonal: SeasonalProperty.fall.value, gender: GenderProperty.male.value, - access: ACCESS_PRIVATE, + access: 0, content: '시향노트1', createdAt: '2021-09-26T08:38:33.000Z', User: { @@ -179,7 +173,7 @@ describe('# Perfume Service Test', () => { sillage: SillageProperty.light.value, seasonal: SeasonalProperty.fall.value, gender: GenderProperty.male.value, - access: ACCESS_PUBLIC, + access: 1, content: '시향노트1', createdAt: '2021-09-26T08:38:33.000Z', User: { @@ -201,7 +195,7 @@ describe('# Perfume Service Test', () => { sillage: SillageProperty.light.value, seasonal: SeasonalProperty.fall.value, gender: GenderProperty.male.value, - access: ACCESS_PRIVATE, + access: 1, content: '시향노트1', createdAt: '2021-09-26T08:38:33.000Z', User: { diff --git a/tests/test_unit/service/UserService.spec.ts b/tests/test_unit/service/UserService.spec.ts index d843c6d0..38bdad07 100644 --- a/tests/test_unit/service/UserService.spec.ts +++ b/tests/test_unit/service/UserService.spec.ts @@ -4,7 +4,7 @@ dotenv.config(); import { WrongPasswordError, PasswordPolicyError } from '@errors'; -import { TokenGroupDTO, LoginInfoDTO } from '@dto/index'; +import { LoginInfoDTO } from '@dto/index'; import UserService from '@services/UserService'; @@ -49,16 +49,15 @@ class MockGenerator { describe('▣ UserService', () => { describe('▶ create Test', () => { describe('# method: createUser', () => { - new TestSingle( + new TestSingle( 'common', userService.createUser, [{}], - (result: TokenGroupDTO | null, err: any, _: any[]) => { + (result, err: any, _: any[]) => { expect(result).to.be.not.null; expect(err).to.be.eq(null); - const token: TokenGroupDTO = result!!; - expect(token).to.be.instanceOf(TokenGroupDTO); + const token = result!!; expect(token.userIdx).to.be.ok; expect(token.token).to.be.ok; expect(token.refreshToken).to.be.ok; From 03516baa0e566888a462d8cc5271b23dd2237b9c Mon Sep 17 00:00:00 2001 From: Jinho Hyeon Date: Sat, 24 Jun 2023 01:31:40 +0900 Subject: [PATCH 9/9] Js to ts3 (#518) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ReportReviewDao * 테이블에 필요한 fk 필드 추가 * dao 타입 일부 수정 * converter 반환타입 수정 * ReviewService * LikeReviewService 분리 * LikeReviewService.read * ImageService * getPerfumeThumbKeywordConverter * readAllOfPerfume --- src/controllers/Review.ts | 9 +- src/dao/ReportReviewDao.js | 45 --- src/dao/ReportReviewDao.ts | 59 +++ src/dao/ReviewDao.ts | 10 +- src/models/tables/Perfume.ts | 6 + src/models/tables/Review.ts | 16 +- src/service/ImageService.ts | 26 ++ src/service/KeywordService.ts | 71 +++- src/service/LikeReviewService.ts | 31 ++ src/service/PerfumeService.ts | 102 ++--- src/service/ReviewService.js | 338 ---------------- src/service/ReviewService.ts | 365 ++++++++++++++++++ src/utils/converter.ts | 8 +- .../test_unit/service/PerfumeService.spec.ts | 14 +- tests/test_unit/utils/converter.spec.ts | 8 +- 15 files changed, 616 insertions(+), 492 deletions(-) delete mode 100644 src/dao/ReportReviewDao.js create mode 100644 src/dao/ReportReviewDao.ts create mode 100644 src/service/ImageService.ts create mode 100644 src/service/LikeReviewService.ts delete mode 100644 src/service/ReviewService.js create mode 100644 src/service/ReviewService.ts diff --git a/src/controllers/Review.ts b/src/controllers/Review.ts index becdc7b3..1d12ab29 100644 --- a/src/controllers/Review.ts +++ b/src/controllers/Review.ts @@ -1,4 +1,7 @@ -var Review = require('../service/ReviewService'); +import ReviewService from '@services/ReviewService'; +import LikeReviewService from '@services/LikeReviewService'; +const Review = new ReviewService(); +const LikeReview = new LikeReviewService(); import StatusCode from '../utils/statusCode'; import { NextFunction, Request, Response } from 'express'; @@ -51,7 +54,7 @@ export const getReviewByIdx = async ( ) => { var reviewIdx = req.params['reviewIdx']; try { - const response = await Review.getReviewByIdx(reviewIdx); + const response = await Review.getReviewByIdx(Number(reviewIdx)); res.status(StatusCode.OK).json({ message: '시향노트 조회 성공', data: response, @@ -162,7 +165,7 @@ export const likeReview = async ( const reviewIdx = req.params['reviewIdx']; const userIdx = req.middlewareToken.loginUserIdx; try { - const result = await Review.likeReview(reviewIdx, userIdx); + const result = await LikeReview.likeReview(reviewIdx, userIdx); res.status(StatusCode.OK).json({ message: '시향노트 좋아요 상태 변경 성공', data: result, diff --git a/src/dao/ReportReviewDao.js b/src/dao/ReportReviewDao.js deleted file mode 100644 index 1aa640c1..00000000 --- a/src/dao/ReportReviewDao.js +++ /dev/null @@ -1,45 +0,0 @@ -import { - NotMatchedError, - DuplicatedEntryError, - FailedToCreateError, -} from '../utils/errors/errors'; -const { sequelize, ReportReview, Review, User } = require('../models'); - -/** - * 시향노트 신고 생성 - * - * @param {Object} ReportReview - * @returns {Promise} - */ - -module.exports.create = async ({ reporterIdx, reviewIdx, reason }) => { - try { - const createLike = await ReportReview.create({ - reporterIdx: Number(reporterIdx), - reviewIdx: Number(reviewIdx), - reason, - }); - return createLike; - } catch (err) { - if (err.original.code === 'ER_DUP_ENTRY' || err.parent.errno === 1062) { - throw new DuplicatedEntryError(); - } - throw new FailedToCreateError(); - } -}; - -/** - * 내가 신고한 시향노트 목록 조회 - * - * @param {number} userIdx - * @returns {Promise} ReportReviewObj List - */ - -module.exports.readAllReportedReviewByUser = async (userIdx) => { - const result = await ReportReview.findAll({ - where: { reporterIdx: userIdx }, - raw: true, - nest: true, - }); - return result; -}; diff --git a/src/dao/ReportReviewDao.ts b/src/dao/ReportReviewDao.ts new file mode 100644 index 00000000..ddf18170 --- /dev/null +++ b/src/dao/ReportReviewDao.ts @@ -0,0 +1,59 @@ +import { ReportReview } from '../models'; +import { + DuplicatedEntryError, + FailedToCreateError, +} from '../utils/errors/errors'; + +class ReportReviewDao { + /** + * 시향노트 신고 생성 + * + * @param {Object} ReportReview + * @returns {Promise} + */ + + create = async ({ + reporterIdx, + reviewIdx, + reason, + }: { + reporterIdx: number; + reviewIdx: number; + reason: string; + }) => { + try { + const createLike = await ReportReview.create({ + reporterIdx: Number(reporterIdx), + reviewIdx: Number(reviewIdx), + reason, + }); + return createLike; + } catch (err: any) { + if ( + err.original.code === 'ER_DUP_ENTRY' || + err.parent.errno === 1062 + ) { + throw new DuplicatedEntryError(); + } + throw new FailedToCreateError(); + } + }; + + /** + * 내가 신고한 시향노트 목록 조회 + * + * @param {number} userIdx + * @returns {Promise} ReportReviewObj List + */ + + readAllReportedReviewByUser = async (userIdx: number) => { + const result = await ReportReview.findAll({ + where: { reporterIdx: userIdx }, + raw: true, + nest: true, + }); + return result; + }; +} + +export default ReportReviewDao; diff --git a/src/dao/ReviewDao.ts b/src/dao/ReviewDao.ts index 6e1c68cd..38e73336 100644 --- a/src/dao/ReviewDao.ts +++ b/src/dao/ReviewDao.ts @@ -97,7 +97,7 @@ class ReviewDao { */ // const SQL_REVIEW_SELECT_BY_IDX = `SELECT p.image_thumbnail_url as imageUrl, b.english_name as brandName, p.name, rv.score, rv.content, rv.longevity, rv.sillage, rv.seasonal, rv.gender, rv.access, rv.create_time as createTime, u.user_idx as userIdx, u.nickname FROM review rv NATURAL JOIN perfume p JOIN brand b ON p.brand_idx = b.brand_idx JOIN user u ON rv.user_idx = u.user_idx WHERE review_idx = ?`; - async read(reviewIdx: number): Promise { + async read(reviewIdx: number) { const readReviewResult = await Review.findByPk(reviewIdx, { include: [ { @@ -155,7 +155,7 @@ class ReviewDao { readAllOfUser( userIdx: number, sort: Order = [['createdAt', 'desc']] - ): Promise { + ): Promise { return Review.findAll({ where: { userIdx }, include: { @@ -187,7 +187,7 @@ class ReviewDao { readAllOfPerfume( perfumeIdx: number, includePrivate: boolean = false - ): Promise { + ): Promise> { return sequelize.query(SQL_READ_ALL_OF_PERFUME, { bind: [perfumeIdx, includePrivate ? ACCESS_PRIVATE : ACCESS_PUBLIC], nest: true, @@ -218,9 +218,9 @@ class ReviewDao { score: number; longevity: number; sillage: number; - seasonal: string[]; + seasonal: number; gender: number; - access: boolean; + access: number; content: string; reviewIdx: number; }): Promise { diff --git a/src/models/tables/Perfume.ts b/src/models/tables/Perfume.ts index a481a0bb..35e533fb 100644 --- a/src/models/tables/Perfume.ts +++ b/src/models/tables/Perfume.ts @@ -71,6 +71,12 @@ export class Perfume extends Model { }) volumeAndPrice: string; + @Column({ + type: DataType.INTEGER, + allowNull: false, + }) + brandIdx: number; + @BelongsTo(() => Brand, { foreignKey: { name: 'brandIdx', diff --git a/src/models/tables/Review.ts b/src/models/tables/Review.ts index c0a1d1e2..2daac4d0 100644 --- a/src/models/tables/Review.ts +++ b/src/models/tables/Review.ts @@ -72,6 +72,12 @@ export class Review extends Model { }) likeCnt: number; + @Column({ + type: DataType.INTEGER, + allowNull: false, + }) + perfumeIdx: number; + @BelongsTo(() => Perfume, { foreignKey: { name: 'perfumeIdx', @@ -81,7 +87,13 @@ export class Review extends Model { onDelete: 'CASCADE', as: 'Perfume', }) - perfume: Perfume; + Perfume: Perfume; + + @Column({ + type: DataType.INTEGER, + allowNull: false, + }) + userIdx: number; @BelongsTo(() => User, { foreignKey: { @@ -91,7 +103,7 @@ export class Review extends Model { onUpdate: 'CASCADE', onDelete: 'CASCADE', }) - user: User; + User: User; @BelongsToMany(() => User, { through: { diff --git a/src/service/ImageService.ts b/src/service/ImageService.ts new file mode 100644 index 00000000..4bcbcc76 --- /dev/null +++ b/src/service/ImageService.ts @@ -0,0 +1,26 @@ +import S3FileDao from '@src/dao/S3FileDao'; + +class ImageService { + s3FileDao: S3FileDao; + + constructor(s3FileDao?: S3FileDao) { + this.s3FileDao = s3FileDao ?? new S3FileDao(); + } + + async getImageList( + perfumeIdx: number, + defaultImage: string + ): Promise { + const imageFromS3: string[] = await this.s3FileDao + .getS3ImageList(perfumeIdx) + .catch((_: any) => []); + + if (imageFromS3.length > 0) { + return imageFromS3; + } + + return [defaultImage]; + } +} + +export default ImageService; diff --git a/src/service/KeywordService.ts b/src/service/KeywordService.ts index 2563d3a3..8b74204a 100644 --- a/src/service/KeywordService.ts +++ b/src/service/KeywordService.ts @@ -1,16 +1,29 @@ import KeywordDao from '@dao/KeywordDao'; +import { PerfumeThumbDTO, PerfumeThumbKeywordDTO } from '@src/data/dto'; import { Keyword } from '@src/models'; - -const keywordDao = new KeywordDao(); - +import { LikePerfumeService } from './LikePerfumeService'; +import { NotMatchedError } from '@src/utils/errors/errors'; +import fp from 'lodash/fp'; +import { commonJob } from './PerfumeService'; +import _ from 'lodash'; class KeywordService { + keywordDao: KeywordDao; + likePerfumeService: LikePerfumeService; + constructor( + keywordDao?: KeywordDao, + likePerfumeService?: LikePerfumeService + ) { + this.keywordDao = keywordDao ?? new KeywordDao(); + this.likePerfumeService = + likePerfumeService ?? new LikePerfumeService(); + } /** * 키워드 전체 조회 * * @returns {Promise} **/ async getKeywordAll(pagingIndex: number, pagingSize: number) { - const result = await keywordDao.readAll(pagingIndex, pagingSize); + const result = await this.keywordDao.readAll(pagingIndex, pagingSize); return { ...result, rows: result.rows.map(this.transform), @@ -24,7 +37,7 @@ class KeywordService { * @returns {Promise} */ async getKeywordOfPerfume(perfumeIdx: number) { - const keywords = await keywordDao.readAllOfPerfume(perfumeIdx); + const keywords = await this.keywordDao.readAllOfPerfume(perfumeIdx); return keywords.map(this.transform); } @@ -35,6 +48,54 @@ class KeywordService { keywordIdx: it.id, }; } + + async getPerfumeThumbKeywordConverter( + perfumeIdxList: number[], + userIdx: number = -1 + ): Promise<(item: PerfumeThumbDTO) => PerfumeThumbKeywordDTO> { + let likePerfumeList: any[] = []; + if (userIdx > -1) { + likePerfumeList = await this.likePerfumeService.readLikeInfo( + userIdx, + perfumeIdxList + ); + } + + const joinKeywordList: any[] = await this.keywordDao + .readAllOfPerfumeIdxList(perfumeIdxList) + .catch((err: Error) => { + if (err instanceof NotMatchedError) { + return []; + } + throw err; + }); + + return (item: PerfumeThumbDTO): PerfumeThumbKeywordDTO => { + return fp.compose( + ...commonJob, + this.likePerfumeService.isLikeJob(likePerfumeList), + this.addKeyword(joinKeywordList), + PerfumeThumbKeywordDTO.createByJson + )(item); + }; + } + + private addKeyword(joinKeywordList: any[]): (obj: any) => any { + const keywordMap: { [key: number]: string[] } = _.chain(joinKeywordList) + .groupBy('perfumeIdx') + .mapValues((arr) => arr.map((it) => it.Keyword.name)) + .value(); + + return (obj: any) => { + const ret: any = Object.assign({}, obj); + ret.keywordList = keywordMap[obj.perfumeIdx] || []; + return ret; + }; + } + + async readAllOfPerfume(perfumeIdx: number) { + return this.keywordDao.readAllOfPerfume(perfumeIdx); + } } export default KeywordService; diff --git a/src/service/LikeReviewService.ts b/src/service/LikeReviewService.ts new file mode 100644 index 00000000..45166abc --- /dev/null +++ b/src/service/LikeReviewService.ts @@ -0,0 +1,31 @@ +import LikeReviewDao from '@dao/LikeReviewDao'; +const likeReviewDao = new LikeReviewDao(); + +class LikeReviewService { + /** + * 시향노트 좋아요 생성/취소 + * + * @param {number} reviewIdx + * @param {number} userIdx + * @returns {boolean} isLiked + * reviewIdx Long 시향노트 Idx + * returns Boolean + **/ + async likeReview(reviewIdx: number, userIdx: number) { + const resultOfReadLike = await likeReviewDao.read(userIdx, reviewIdx); + let isLiked = resultOfReadLike ? true : false; + if (!isLiked) { + await likeReviewDao.create(userIdx, reviewIdx); + } + if (isLiked) { + await likeReviewDao.delete(userIdx, reviewIdx); + } + return !isLiked; + } + + async read(userIdx: number, reviewIdx: number) { + return await likeReviewDao.read(userIdx, reviewIdx); + } +} + +export default LikeReviewService; diff --git a/src/service/PerfumeService.ts b/src/service/PerfumeService.ts index ba0be19c..72268af4 100644 --- a/src/service/PerfumeService.ts +++ b/src/service/PerfumeService.ts @@ -1,13 +1,9 @@ import { logger } from '@modules/winston'; -import { NotMatchedError } from '@errors'; - import { flatJob, removeKeyJob } from '@utils/func'; -import KeywordDao from '@dao/KeywordDao'; import PerfumeDao from '@dao/PerfumeDao'; import ReviewDao from '@dao/ReviewDao'; -import S3FileDao from '@dao/S3FileDao'; import UserDao from '@dao/UserDao'; import { @@ -30,6 +26,8 @@ import fp from 'lodash/fp'; import { Op } from 'sequelize'; import { NoteService } from './NoteService'; import { LikePerfumeService } from './LikePerfumeService'; +import ImageService from './ImageService'; +import KeywordService from './KeywordService'; const LOG_TAG: string = '[Perfume/Service]'; const DEFAULT_VALUE_OF_INDEX = 0; @@ -37,11 +35,9 @@ const DEFAULT_VALUE_OF_INDEX = 0; let perfumeDao: PerfumeDao = new PerfumeDao(); let ingredientDao: IngredientDao = new IngredientDao(); let reviewDao: ReviewDao = new ReviewDao(); -let keywordDao: KeywordDao = new KeywordDao(); -let s3FileDao: S3FileDao = new S3FileDao(); let userDao: UserDao = new UserDao(); -const commonJob = [ +export const commonJob = [ removeKeyJob( 'perfume_idx', 'englishName', @@ -52,9 +48,18 @@ const commonJob = [ ]; class PerfumeService { likePerfumeService: LikePerfumeService; - constructor(likePerfumeService?: LikePerfumeService) { + imageService: ImageService; + keywordService: KeywordService; + + constructor( + likePerfumeService?: LikePerfumeService, + imageService?: ImageService, + keywordService?: KeywordService + ) { this.likePerfumeService = likePerfumeService ?? new LikePerfumeService(); + this.imageService = imageService ?? new ImageService(); + this.keywordService = keywordService ?? new KeywordService(); } /** * 향수 세부 정보 조회 @@ -86,13 +91,13 @@ class PerfumeService { const keywordList: string[] = [ ...new Set( ( - await keywordDao + await this.keywordService .readAllOfPerfume(perfumeIdx) .catch((_: Error) => []) ).map((it: any) => it.name) ), ]; - const imageUrls: string[] = await this.getImageList( + const imageUrls: string[] = await this.imageService.getImageList( perfumeIdx, perfume.imageUrl ); @@ -419,7 +424,10 @@ class PerfumeService { userIdx: number ): Promise { const converter: (item: PerfumeThumbDTO) => PerfumeThumbKeywordDTO = - await this.getPerfumeThumbKeywordConverter(perfumeIdxList, userIdx); + await this.keywordService.getPerfumeThumbKeywordConverter( + perfumeIdxList, + userIdx + ); return perfumeDao .getPerfumesByIdxList(perfumeIdxList) .then((result: PerfumeThumbDTO[]): PerfumeThumbKeywordDTO[] => { @@ -431,18 +439,10 @@ class PerfumeService { reviewDao = dao; } - setKeywordDao(dao: any) { - keywordDao = dao; - } - setUserDao(dao: UserDao) { userDao = dao; } - setS3FileDao(dao: S3FileDao) { - s3FileDao = dao; - } - private async generateSummary( perfumeIdx: number ): Promise { @@ -468,19 +468,6 @@ class PerfumeService { }; } - private addKeyword(joinKeywordList: any[]): (obj: any) => any { - const keywordMap: { [key: number]: string[] } = _.chain(joinKeywordList) - .groupBy('perfumeIdx') - .mapValues((arr) => arr.map((it) => it.Keyword.name)) - .value(); - - return (obj: any) => { - const ret: any = Object.assign({}, obj); - ret.keywordList = keywordMap[obj.perfumeIdx] || []; - return ret; - }; - } - private async convertToThumbKeyword( result: ListAndCountDTO, userIdx: number = -1 @@ -489,59 +476,16 @@ class PerfumeService { (it: PerfumeThumbDTO) => it.perfumeIdx ); const converter: (item: PerfumeThumbDTO) => PerfumeThumbKeywordDTO = - await this.getPerfumeThumbKeywordConverter(perfumeIdxList, userIdx); + await this.keywordService.getPerfumeThumbKeywordConverter( + perfumeIdxList, + userIdx + ); return result.convertType((item: PerfumeThumbDTO) => { return converter(item); }); } - private async getPerfumeThumbKeywordConverter( - perfumeIdxList: number[], - userIdx: number = -1 - ): Promise<(item: PerfumeThumbDTO) => PerfumeThumbKeywordDTO> { - let likePerfumeList: any[] = []; - if (userIdx > -1) { - likePerfumeList = await this.likePerfumeService.readLikeInfo( - userIdx, - perfumeIdxList - ); - } - - const joinKeywordList: any[] = await keywordDao - .readAllOfPerfumeIdxList(perfumeIdxList) - .catch((err: Error) => { - if (err instanceof NotMatchedError) { - return []; - } - throw err; - }); - - return (item: PerfumeThumbDTO): PerfumeThumbKeywordDTO => { - return fp.compose( - ...commonJob, - this.likePerfumeService.isLikeJob(likePerfumeList), - this.addKeyword(joinKeywordList), - PerfumeThumbKeywordDTO.createByJson - )(item); - }; - } - - private async getImageList( - perfumeIdx: number, - defaultImage: string - ): Promise { - const imageFromS3: string[] = await s3FileDao - .getS3ImageList(perfumeIdx) - .catch((_: any) => []); - - if (imageFromS3.length > 0) { - return imageFromS3; - } - - return [defaultImage]; - } - async readPage( offset: number, limit: number, diff --git a/src/service/ReviewService.js b/src/service/ReviewService.js deleted file mode 100644 index 1d25f121..00000000 --- a/src/service/ReviewService.js +++ /dev/null @@ -1,338 +0,0 @@ -import { NotMatchedError, UnAuthorizedError } from '../utils/errors/errors'; - -import LikeReviewDao from '@dao/LikeReviewDao'; -const likeReviewDao = new LikeReviewDao(); - -const reportReviewDao = require('../dao/ReportReviewDao'); -const { - InputIntToDBIntOfReview, - DBIntToOutputIntOfReview, - getApproxAge, -} = require('../utils/converter'); - -import UserDao from '@dao/UserDao'; -import LikePerfumeDao from '@dao/LikePerfumeDao'; -import ReviewDao from '@dao/ReviewDao'; -import KeywordDao from '../dao/KeywordDao'; -import { PRIVATE } from '@src/utils/strings'; - -const userDao = new UserDao(); -const likePerfumeDao = new LikePerfumeDao(); -const reviewDao = new ReviewDao(); -const keywordDao = new KeywordDao(); - -const discordHook = - require('../utils/discordHook').discordManager.getReportReviewHook(); - -/** - * 시향노트 작성 - * - * @param {Object} Review - * @returns {Promise} - **/ -exports.postReview = async ({ - perfumeIdx, - userIdx, - score, - longevity, - sillage, - seasonal, - gender, - access, - content, - keywordList, -}) => { - try { - // 데이터 변환 - const translationResult = await InputIntToDBIntOfReview({ - longevity, - sillage, - seasonalList: seasonal, - gender, - keywordList, - }); - - const createReview = await reviewDao.create({ - perfumeIdx, - userIdx, - score, - longevity: translationResult.longevity, - sillage: translationResult.sillage, - seasonal: translationResult.sumOfBitSeasonal, - gender: translationResult.gender, - access: access ? 1 : 0, - content, - }); - - const reviewIdx = createReview.dataValues.id; - const KeywordIdxList = translationResult.keywordList; - const createReviewKeyword = await Promise.all( - KeywordIdxList.map((it) => { - keywordDao.create({ reviewIdx, keywordIdx: it, perfumeIdx }); - }) - ); - try { - await likePerfumeDao.delete(userIdx, perfumeIdx); - } catch (err) { - if (err instanceof NotMatchedError) { - } else throw err; - } - return reviewIdx; - } catch (err) { - console.log(err); - throw err; - } -}; - -/** - * 시향노트 삭제 - * 시향노트 삭제하기 - * - * @param {Object} Review - * @returns {Promise} - **/ -exports.deleteReview = async ({ reviewIdx, userIdx }) => { - const readReviewResult = await reviewDao.read(reviewIdx); - if (readReviewResult.userIdx != userIdx) { - throw new UnAuthorizedError(); - } - const perfumeIdx = readReviewResult.perfumeIdx; - const deleteReviewKeyword = await keywordDao.deleteReviewKeyword({ - reviewIdx, - perfumeIdx, - }); - const deleteOnlyReview = await reviewDao.delete(reviewIdx); - - return deleteOnlyReview; -}; - -/** - * 시향노트 수정 - * - * @param {Object} Review - * @returns {Promise} - **/ -exports.updateReview = async ({ - score, - longevity, - sillage, - seasonal, - gender, - access, - content, - keywordList, - reviewIdx, - userIdx, -}) => { - // 데이터 변환 - const translationResult = await InputIntToDBIntOfReview({ - longevity, - sillage, - seasonalList: seasonal, - gender, - keywordList, - }); - - // 권환 확인 - const readReviewResult = await reviewDao.read(reviewIdx); - if (readReviewResult.userIdx != userIdx) { - throw new UnAuthorizedError(); - } - - const updateReviewResult = await reviewDao.update({ - score, - longevity: translationResult.longevity, - sillage: translationResult.sillage, - seasonal: translationResult.sumOfBitSeasonal, - gender: translationResult.gender, - access: access ? 1 : 0, - content, - reviewIdx, - }); - const deleteReviewKeyword = await keywordDao.deleteReviewKeyword({ - reviewIdx, - perfumeIdx: readReviewResult.perfumeIdx, - }); - - const KeywordIdxList = translationResult.keywordList; - const createReviewKeyword = await Promise.all( - KeywordIdxList.map((it) => { - keywordDao.create({ - reviewIdx, - keywordIdx: it, - perfumeIdx: readReviewResult.perfumeIdx, - }); - }) - ); - - return reviewIdx; -}; - -/** - * 시향노트 조회 - * - * @param {Object} whereObj - * @returns {Promise} - **/ -exports.getReviewByIdx = async (reviewIdx) => { - const result = await reviewDao.read(reviewIdx); - const translationResult = await DBIntToOutputIntOfReview({ - longevity: result.longevity, - sillage: result.sillage, - sumOfBitSeasonal: result.seasonal, - gender: result.gender, - }); - return { - reviewIdx: result.id, - score: result.score, - longevity: translationResult.longevity, - sillage: translationResult.sillage, - seasonal: translationResult.seasonalList - ? translationResult.seasonalList - : [], - gender: translationResult.gender, - access: result.access == 1 ? true : false, - content: result.content, - Perfume: { - perfumeIdx: result.Perfume.perfumeIdx, - perfumeName: result.Perfume.name, - imageUrl: result.Perfume.imageUrl, - }, - KeywordList: result.keywordList.map((it) => { - return { - keywordIdx: it.keywordIdx, - name: it.keyword, - }; - }), - Brand: { - brandIdx: result.Perfume.Brand.brandIdx, - brandName: result.Perfume.Brand.name, - }, - }; -}; - -/** - * 내가 쓴 시향기 전체 조회 - * = 마이퍼퓸 조회 - * - * @param {number} userIdx - * @returns {Promise} reviewList - **/ -exports.getReviewOfUser = async (userIdx) => { - return (await reviewDao.readAllOfUser(userIdx)).map((it) => { - return { - reviewIdx: it.id, - score: it.score, - perfumeIdx: it.perfumeIdx, - perfumeName: it.Perfume.name, - imageUrl: it.Perfume.imageUrl, - brandIdx: it.Perfume.brandIdx, - brandName: it.Perfume.Brand.englishName, - }; - }); -}; - -/** - * 전체 시향노트 반환(인기순) - * 특정 향수에 달린 전체 시향노트 인기순으로 가져오기 - * - * @param {number} perfumeIdx - * @returns {Promise} reviewList - **/ -exports.getReviewOfPerfumeByLike = async ({ perfumeIdx, userIdx }) => { - try { - const reviewList = await reviewDao.readAllOfPerfume(perfumeIdx); - - // 유저가 신고한 시향노트 인덱스 목록 조회 - const allReportedReviewByUser = - await reportReviewDao.readAllReportedReviewByUser(userIdx); - const reportedReviewIdxList = allReportedReviewByUser.map((it) => { - return it.reviewIdx; - }); - - const result = await reviewList.reduce(async (prevPromise, it) => { - let prevResult = await prevPromise.then(); - const approxAge = it.User.birth - ? getApproxAge(it.User.birth) - : PRIVATE; - const readLikeResult = await likeReviewDao.read( - userIdx, - it.reviewIdx - ); - const currentResult = { - reviewIdx: it.reviewIdx, - score: it.score, - access: it.access == 1 ? true : false, - content: it.content, - likeCount: it.LikeReview.likeCount, - isLiked: readLikeResult ? true : false, - userGender: it.User.gender || PRIVATE, - age: approxAge, - nickname: it.User.nickname, - createTime: it.createdAt, - isReported: reportedReviewIdxList.includes(it.reviewIdx), - }; - prevResult.push(currentResult); - return Promise.resolve(prevResult); - }, Promise.resolve([])); - return result; - } catch (err) { - console.log(err); - throw err; - } -}; - -/** - * 시향노트 좋아요 생성/취소 - * - * @param {number} reviewIdx - * @param {number} userIdx - * @returns {boolean} isLiked - * reviewIdx Long 시향노트 Idx - * returns Boolean - **/ -exports.likeReview = async (reviewIdx, userIdx) => { - const resultOfReadLike = await likeReviewDao.read(userIdx, reviewIdx); - let isLiked = resultOfReadLike ? true : false; - if (!isLiked) { - await likeReviewDao.create(userIdx, reviewIdx); - } - if (isLiked) { - await likeReviewDao.delete(userIdx, reviewIdx); - } - return !isLiked; -}; - -/** - * 시향노트 신고 - * - * @param {String} reason - * @param {Number} userIdx - * @returns {Promise} - **/ -exports.reportReview = async ({ userIdx, reviewIdx, reason }) => { - try { - const userInfo = await userDao.readByIdx(userIdx); - const userNickname = userInfo.nickname; - const reviewData = await reviewDao.read(reviewIdx); - const perfumeName = reviewData.Perfume.name; - const reviewContent = reviewData.content; - - // 신고 정보 저장 - await reportReviewDao.create({ - reporterIdx: userIdx, - reviewIdx, - reason, - }); - - // 디스코드로 신고 알림 전송 - await discordHook.send( - `시향노트 신고가 들어왔습니다.\n\n신고 사유 : ${reason} \n향수명 : ${perfumeName} \n시향노트 내용 : ${reviewContent} \n신고자 : ${userNickname} \n시향노트 Idx : ${reviewIdx} ` - ); - - return true; - } catch (err) { - console.log(err); - throw err; - } -}; diff --git a/src/service/ReviewService.ts b/src/service/ReviewService.ts new file mode 100644 index 00000000..24a00eba --- /dev/null +++ b/src/service/ReviewService.ts @@ -0,0 +1,365 @@ +import { NotMatchedError, UnAuthorizedError } from '../utils/errors/errors'; + +import ReportReviewDao from '@dao/ReportReviewDao'; + +const reportReviewDao = new ReportReviewDao(); + +import { + InputIntToDBIntOfReview, + DBIntToOutputIntOfReview, + getApproxAge, +} from '../utils/converter'; +import { discordManager } from '../utils/discordHook'; + +import UserDao from '@dao/UserDao'; +import LikePerfumeDao from '@dao/LikePerfumeDao'; +import ReviewDao from '@dao/ReviewDao'; +import KeywordDao from '../dao/KeywordDao'; +import { PRIVATE } from '@src/utils/strings'; +import LikeReviewService from './LikeReviewService'; + +const userDao = new UserDao(); +const likePerfumeDao = new LikePerfumeDao(); +const reviewDao = new ReviewDao(); +const keywordDao = new KeywordDao(); + +const discordHook = discordManager.getReportReviewHook(); + +interface ReviewVO { + userIdx: number; + score: number; + longevity: number; + sillage: number; + seasonal: string[]; + gender: number; + access: number; + content: string; + keywordList: string[]; +} + +export class ReviewService { + likeReviewService: LikeReviewService; + + constructor() { + this.likeReviewService = new LikeReviewService(); + } + + /** + * 시향노트 작성 + * + * @param {Object} Review + * @returns {Promise} + **/ + postReview = async ({ + perfumeIdx, + userIdx, + score, + longevity, + sillage, + seasonal, + gender, + access, + content, + keywordList, + }: ReviewVO & { perfumeIdx: number }) => { + try { + // 데이터 변환 + const translationResult = await InputIntToDBIntOfReview({ + longevity, + sillage, + seasonalList: seasonal, + gender, + keywordList, + }); + + const createReview = await reviewDao.create({ + perfumeIdx, + userIdx, + score, + longevity: translationResult.longevity, + sillage: translationResult.sillage, + seasonal: translationResult.sumOfBitSeasonal, + gender: translationResult.gender, + access: access ? 1 : 0, + content, + }); + + const reviewIdx = createReview.dataValues.id; + const KeywordIdxList = translationResult.keywordList; + await Promise.all( + KeywordIdxList.map((it) => { + keywordDao.create({ + reviewIdx, + keywordIdx: it, + perfumeIdx, + }); + }) + ); + try { + await likePerfumeDao.delete(userIdx, perfumeIdx); + } catch (err) { + if (err instanceof NotMatchedError) { + } else throw err; + } + return reviewIdx; + } catch (err) { + console.log(err); + throw err; + } + }; + + /** + * 시향노트 삭제 + * 시향노트 삭제하기 + * + * @param {Object} Review + * @returns {Promise} + **/ + deleteReview = async ({ + reviewIdx, + userIdx, + }: { + reviewIdx: number; + userIdx: number; + }) => { + const readReviewResult = await reviewDao.read(reviewIdx); + if (readReviewResult.userIdx != userIdx) { + throw new UnAuthorizedError(); + } + const perfumeIdx = readReviewResult.perfumeIdx; + await keywordDao.deleteReviewKeyword({ + reviewIdx, + perfumeIdx, + }); + const deleteOnlyReview = await reviewDao.delete(reviewIdx); + + return deleteOnlyReview; + }; + + /** + * 시향노트 수정 + * + * @param {Object} Review + * @returns {Promise} + **/ + updateReview = async ({ + score, + longevity, + sillage, + seasonal, + gender, + access, + content, + keywordList, + reviewIdx, + userIdx, + }: ReviewVO & { reviewIdx: number }) => { + // 데이터 변환 + const translationResult = await InputIntToDBIntOfReview({ + longevity, + sillage, + seasonalList: seasonal, + gender, + keywordList, + }); + + // 권환 확인 + const readReviewResult = await reviewDao.read(reviewIdx); + if (readReviewResult.userIdx != userIdx) { + throw new UnAuthorizedError(); + } + + await reviewDao.update({ + score, + longevity: translationResult.longevity, + sillage: translationResult.sillage, + seasonal: translationResult.sumOfBitSeasonal, + gender: translationResult.gender, + access: access ? 1 : 0, + content, + reviewIdx, + }); + await keywordDao.deleteReviewKeyword({ + reviewIdx, + perfumeIdx: readReviewResult.perfumeIdx, + }); + + const KeywordIdxList = translationResult.keywordList; + await Promise.all( + KeywordIdxList.map((it) => { + keywordDao.create({ + reviewIdx, + keywordIdx: it, + perfumeIdx: readReviewResult.perfumeIdx, + }); + }) + ); + + return reviewIdx; + }; + + /** + * 시향노트 조회 + * + * @param {Object} whereObj + * @returns {Promise} + **/ + getReviewByIdx = async (reviewIdx: number) => { + const result = await reviewDao.read(reviewIdx); + const translationResult = await DBIntToOutputIntOfReview({ + longevity: result.longevity, + sillage: result.sillage, + sumOfBitSeasonal: result.seasonal, + gender: result.gender, + }); + return { + reviewIdx: result.id, + score: result.score, + longevity: translationResult.longevity, + sillage: translationResult.sillage, + seasonal: translationResult.seasonalList + ? translationResult.seasonalList + : [], + gender: translationResult.gender, + access: result.access == 1 ? true : false, + content: result.content, + Perfume: { + perfumeIdx: result.Perfume.perfumeIdx, + perfumeName: result.Perfume.name, + imageUrl: result.Perfume.imageUrl, + }, + KeywordList: result.keywordList.map((it) => { + return { + keywordIdx: it.keywordIdx, + name: it.keyword, + }; + }), + Brand: { + brandIdx: result.Perfume.Brand.brandIdx, + brandName: result.Perfume.Brand.name, + }, + }; + }; + + /** + * 내가 쓴 시향기 전체 조회 + * = 마이퍼퓸 조회 + * + * @param {number} userIdx + * @returns {Promise} reviewList + **/ + getReviewOfUser = async (userIdx: number) => { + const result = await reviewDao.readAllOfUser(userIdx); + return result.map((it) => { + return { + reviewIdx: it.id, + score: it.score, + perfumeIdx: it.perfumeIdx, + perfumeName: it.Perfume.name, + imageUrl: it.Perfume.imageUrl, + brandIdx: it.Perfume.brandIdx, + brandName: it.Perfume.Brand.englishName, + }; + }); + }; + + /** + * 전체 시향노트 반환(인기순) + * 특정 향수에 달린 전체 시향노트 인기순으로 가져오기 + * + * @param {number} perfumeIdx + * @returns {Promise} reviewList + **/ + getReviewOfPerfumeByLike = async ({ + perfumeIdx, + userIdx, + }: { + perfumeIdx: number; + userIdx: number; + }) => { + try { + const reviewList = await reviewDao.readAllOfPerfume(perfumeIdx); + + // 유저가 신고한 시향노트 인덱스 목록 조회 + const allReportedReviewByUser = + await reportReviewDao.readAllReportedReviewByUser(userIdx); + const reportedReviewIdxList = allReportedReviewByUser.map((it) => { + return it.reviewIdx; + }); + + const result = await reviewList.reduce(async (prevPromise, it) => { + let prevResult = await prevPromise.then(); + const approxAge = it.User.birth + ? getApproxAge(it.User.birth) + : PRIVATE; + const readLikeResult = await this.likeReviewService.read( + userIdx, + it.reviewIdx + ); + const currentResult = { + reviewIdx: it.reviewIdx, + score: it.score, + access: it.access == 1 ? true : false, + content: it.content, + likeCount: it.LikeReview.likeCount, + isLiked: readLikeResult ? true : false, + userGender: it.User.gender || PRIVATE, + age: approxAge, + nickname: it.User.nickname, + createTime: it.createdAt, + isReported: reportedReviewIdxList.includes(it.reviewIdx), + }; + prevResult.push(currentResult); + return Promise.resolve(prevResult); + }, Promise.resolve([])); + return result; + } catch (err) { + console.log(err); + throw err; + } + }; + + /** + * 시향노트 신고 + * + * @param {String} reason + * @param {Number} userIdx + * @returns {Promise} + **/ + reportReview = async ({ + userIdx, + reviewIdx, + reason, + }: { + userIdx: number; + reviewIdx: number; + reason: string; + }) => { + try { + const userInfo = await userDao.readByIdx(userIdx); + const userNickname = userInfo.nickname; + const reviewData = await reviewDao.read(reviewIdx); + const perfumeName = reviewData.Perfume.name; + const reviewContent = reviewData.content; + + // 신고 정보 저장 + await reportReviewDao.create({ + reporterIdx: userIdx, + reviewIdx, + reason, + }); + + // 디스코드로 신고 알림 전송 + await discordHook?.send( + `시향노트 신고가 들어왔습니다.\n\n신고 사유 : ${reason} \n향수명 : ${perfumeName} \n시향노트 내용 : ${reviewContent} \n신고자 : ${userNickname} \n시향노트 Idx : ${reviewIdx} ` + ); + + return true; + } catch (err) { + console.log(err); + throw err; + } + }; +} + +export default ReviewService; diff --git a/src/utils/converter.ts b/src/utils/converter.ts index 951d230d..fb70fcc5 100644 --- a/src/utils/converter.ts +++ b/src/utils/converter.ts @@ -63,10 +63,10 @@ export const InputIntToDBIntOfReview = async ({ } return { - longevity: longevity + 1 ? longevity + 1 : null, - sillage: sillage + 1 ? sillage + 1 : null, - sumOfBitSeasonal: seasonalList && sum && sum > 0 ? sum : null, - gender: gender + 1 ? gender + 1 : null, + longevity: longevity + 1 ? longevity + 1 : 0, + sillage: sillage + 1 ? sillage + 1 : 0, + sumOfBitSeasonal: seasonalList && sum && sum > 0 ? sum : 0, + gender: gender + 1 ? gender + 1 : 0, keywordList: keywordIdxList, }; } catch (err) { diff --git a/tests/test_unit/service/PerfumeService.spec.ts b/tests/test_unit/service/PerfumeService.spec.ts index 70ce698d..8c36e79c 100644 --- a/tests/test_unit/service/PerfumeService.spec.ts +++ b/tests/test_unit/service/PerfumeService.spec.ts @@ -27,22 +27,22 @@ import { import PerfumeIntegralMockHelper from '../mock_helper/PerfumeIntegralMockHelper'; import { LikePerfumeService } from '@src/service/LikePerfumeService'; +import ImageService from '@src/service/ImageService'; +import KeywordService from '@src/service/KeywordService'; const mockLikePerfumeDao: any = {}; +const mockS3FileDao: any = {}; +const mockKeywordDao: any = {}; const LikePerfume = new LikePerfumeService(mockLikePerfumeDao); -const Perfume: PerfumeService = new PerfumeService(LikePerfume); +const Image = new ImageService(mockS3FileDao); +const Keyword = new KeywordService(mockKeywordDao, LikePerfume); +const Perfume: PerfumeService = new PerfumeService(LikePerfume, Image, Keyword); const defaultPagingDTO: PagingDTO = PagingDTO.createByJson({}); -const mockS3FileDao: any = {}; -Perfume.setS3FileDao(mockS3FileDao); - const mockUserDao: any = {}; Perfume.setUserDao(mockUserDao); -const mockKeywordDao: any = {}; -Perfume.setKeywordDao(mockKeywordDao); - const mockReviewDao: any = {}; Perfume.setReviewDao(mockReviewDao); diff --git a/tests/test_unit/utils/converter.spec.ts b/tests/test_unit/utils/converter.spec.ts index 3392d335..702a9731 100644 --- a/tests/test_unit/utils/converter.spec.ts +++ b/tests/test_unit/utils/converter.spec.ts @@ -58,10 +58,10 @@ describe('# converter Test', () => { ...result, keywordList, }); - expect(recover.longevity).to.be.eq(longevity); - expect(recover.sillage).to.be.eq(sillage); - expect(recover.sumOfBitSeasonal).to.be.eq(seasonal); - expect(recover.gender).to.be.eq(gender); + expect(recover.longevity).to.be.eq(0); + expect(recover.sillage).to.be.eq(0); + expect(recover.sumOfBitSeasonal).to.be.eq(0); + expect(recover.gender).to.be.eq(0); expect(recover.keywordList).to.be.deep.eq(keywordList); }); describe(' # ApproxAge Test', () => {