From a8eb6d0595ab967d2bd4b73de840715d4b9d33b0 Mon Sep 17 00:00:00 2001 From: Hyun-git Date: Mon, 8 Jul 2024 11:21:54 +0900 Subject: [PATCH 01/19] fix: add memeRecommendCout ad getUSer --- example.env | 3 -- src/controller/user.controller.ts | 29 ++------------- src/service/user.service.ts | 61 ++++++++++++++++++------------- 3 files changed, 38 insertions(+), 55 deletions(-) delete mode 100644 example.env diff --git a/example.env b/example.env deleted file mode 100644 index 5e9ba1e..0000000 --- a/example.env +++ /dev/null @@ -1,3 +0,0 @@ -DB_URL=mongodb 주소 - -PORT=포트번호 diff --git a/src/controller/user.controller.ts b/src/controller/user.controller.ts index 69ad3f7..aa207af 100644 --- a/src/controller/user.controller.ts +++ b/src/controller/user.controller.ts @@ -4,7 +4,6 @@ import _ from 'lodash'; import CustomError from '../errors/CustomError'; import { HttpCode } from '../errors/HttpCode'; import { CustomRequest } from '../middleware/requestedInfo'; -import { InteractionType, MemeInteractionModel } from '../model/memeInteraction'; import * as UserService from '../service/user.service'; import { createSuccessResponse } from '../util/response'; @@ -26,31 +25,9 @@ const getUser = async (req: CustomRequest, res: Response, next: NextFunction) => const user = req.requestedUser; try { - const countInteractionType = (type: InteractionType) => - MemeInteractionModel.countDocuments({ - deviceId: user.deviceId, - interactionType: type, - }); - - const [watch, reaction, share, save] = await Promise.all([ - countInteractionType(InteractionType.WATCH), - countInteractionType(InteractionType.REACTION), - countInteractionType(InteractionType.SHARE), - countInteractionType(InteractionType.SAVE), - ]); - - const level = getLevel(watch, reaction, share); - - return res.json( - createSuccessResponse(HttpCode.OK, 'Get User', { - ...user, - watch, - reaction, - share, - save, - level, - }), - ); + const userInfos = await UserService.makeUserInfos(user.deviceId); + const level = getLevel(userInfos.watch, userInfos.reaction, userInfos.share); + return res.json(createSuccessResponse(HttpCode.OK, 'Get User', { ...userInfos, level })); } catch (err) { return next(new CustomError(err.message, err.status)); } diff --git a/src/service/user.service.ts b/src/service/user.service.ts index 9de751e..f91cec1 100644 --- a/src/service/user.service.ts +++ b/src/service/user.service.ts @@ -33,6 +33,38 @@ async function getUser(deviceId: string): Promise { } } +async function makeUserInfos(deviceId: string): Promise { + const user = await UserModel.findOne({ deviceId, isDeleted: false }); + const countInteractionType = (type: InteractionType) => + MemeInteractionModel.countDocuments({ + deviceId: user.deviceId, + interactionType: type, + }); + + const [watch, reaction, share, save] = await Promise.all([ + countInteractionType(InteractionType.WATCH), + countInteractionType(InteractionType.REACTION), + countInteractionType(InteractionType.SHARE), + countInteractionType(InteractionType.SAVE), + ]); + + const todayWeekStart = startOfWeek(new Date(), { weekStartsOn: 1 }); + const memeRecommendWatchCount = await MemeRecommendWatchModel.countDocuments({ + startDate: todayWeekStart, + deviceId: user.deviceId, + isDeleted: false, + }); + + return { + ...user.toObject(), + watch, + reaction, + save, + share, + memeRecommendWatchCount, + }; +} + async function createUser(deviceId: string): Promise { try { const foundUser = await UserModel.findOne( @@ -41,33 +73,9 @@ async function createUser(deviceId: string): Promise { ); if (foundUser) { - const countInteractionType = (type: InteractionType) => - MemeInteractionModel.countDocuments({ - deviceId: foundUser.deviceId, - interactionType: type, - }); - - const [watch, reaction, share, save] = await Promise.all([ - countInteractionType(InteractionType.WATCH), - countInteractionType(InteractionType.REACTION), - countInteractionType(InteractionType.SHARE), - countInteractionType(InteractionType.SAVE), - ]); - - const todayWeekStart = startOfWeek(new Date(), { weekStartsOn: 1 }); - const memeRecommendWatchCount = await MemeRecommendWatchModel.countDocuments({ - startDate: todayWeekStart, - deviceId: foundUser.deviceId, - isDeleted: false, - }); - + const foundUserInfos = await makeUserInfos(deviceId); return { - ...foundUser.toObject(), - watch, - reaction, - save, - share, - memeRecommendWatchCount, + ...foundUserInfos, }; } @@ -240,5 +248,6 @@ export { updateLastSeenMeme, getLastSeenMemes, getSavedMemes, + makeUserInfos, createMemeRecommendWatch, }; From defd984b21fc866a94846b2a2328fbbded6bb320 Mon Sep 17 00:00:00 2001 From: Hyun_Gwang Date: Sat, 13 Jul 2024 15:14:41 +0900 Subject: [PATCH 02/19] fix: change keyword response --- src/model/keyword.ts | 5 +++ src/model/meme.ts | 14 ++++++ src/routes/keyword.ts | 18 +++++--- src/routes/meme.ts | 43 ++++++++++++------- src/routes/user.ts | 30 +++++++++++-- src/service/keyword.service.ts | 56 +++++++++++++++--------- src/service/meme.service.ts | 78 ++++++++++++++++++++++++++++------ src/service/user.service.ts | 27 +++++++++--- 8 files changed, 206 insertions(+), 65 deletions(-) diff --git a/src/model/keyword.ts b/src/model/keyword.ts index f7a9658..b646dc0 100644 --- a/src/model/keyword.ts +++ b/src/model/keyword.ts @@ -14,6 +14,11 @@ export interface IKeywordWithImage extends IKeyword { topReactionImage: string; } +export interface IKeywordGetResponse { + _id: Types.ObjectId; + name: string; +} + export interface IKeyword { name: string; category: string; diff --git a/src/model/meme.ts b/src/model/meme.ts index f2a51b3..7b5d1d6 100644 --- a/src/model/meme.ts +++ b/src/model/meme.ts @@ -1,4 +1,5 @@ import mongoose, { Schema, Document, Types } from 'mongoose'; +import { IKeywordGetResponse } from './keyword'; export interface IMemeCreatePayload { title: string; @@ -23,6 +24,19 @@ export interface IMeme { isTodayMeme: boolean; } +export interface IMemeGetResponse { + _id: Types.ObjectId; + title: string; + keywords: IKeywordGetResponse[]; + image: string; + reaction: number; + source: string; + isTodayMeme: boolean; + createdAt: Date; + updatedAt: Date; + isDeleted: boolean; +} + export interface IMemeDocument extends Document { _id: Types.ObjectId; title: string; diff --git a/src/routes/keyword.ts b/src/routes/keyword.ts index 2e9c2ad..62c26bf 100644 --- a/src/routes/keyword.ts +++ b/src/routes/keyword.ts @@ -446,13 +446,17 @@ router.patch('/count', getKeywordInfoByName, increaseSearchCount); * type: string * example: "상황" * keywords: - * type: array - * items: - * type: string - * example: - * - "키워드1" - * - "키워드2" - * - "키워드3" + * type: object + * properties: + * _id: + * type: string + * example: "667ff3d1239eeaf78630a283" + * name: + * type: string + * example: "웃긴" + * name: + * type: string + * example: "웃긴" * 500: * description: Internal server error * content: diff --git a/src/routes/meme.ts b/src/routes/meme.ts index 383706c..84413b6 100644 --- a/src/routes/meme.ts +++ b/src/routes/meme.ts @@ -94,10 +94,17 @@ const router = express.Router(); * isTodayMeme: * type: boolean * example: false - * keywordIds: + * keywords: * type: array * items: - * example: "667fee7ac58681a42d57dc3b" + * type: object + * properties: + * _id: + * type: string + * example: "667fee6dc58681a42d57dc37" + * name: + * type: string + * example: "무한도전" * title: * type: string * example: "무한상사 정총무" @@ -222,8 +229,14 @@ router.get('/list', getAllMemeList); // meme 목록 전체 조회 (페이지네 * keywords: * type: array * items: - * type: string - * example: "angry" + * type: object + * properties: + * _id: + * type: string + * example: "66805b1372ef94c9c0ba1349" + * name: + * type: string + * example: "무한도전" * 400: * description: Invalid request parameters * content: @@ -464,11 +477,14 @@ router.post('/', createMeme); // meme 생성 * keywords: * type: array * items: - * type: string - * example: - * - "무한상사" - * - "정총무" - * - "전자두뇌" + * type: object + * properties: + * _id: + * type: string + * example: "66805b1372ef94c9c0ba1349" + * name: + * type: string + * example: "무한도전" * 400: * description: Bad Request * content: @@ -1265,22 +1281,17 @@ router.post('/:memeId/reaction', getRequestedUserInfo, getRequestedMemeInfo, cre * isTodayMeme: * type: boolean * example: false - * keywordIds: + * keywords: * type: array * items: * type: object * properties: * _id: * type: string - * example: "667fee7ac58681a42d57dc3b" + * example: "66805b1a72ef94c9c0ba134c" * name: * type: string * example: "행복" - * example: - * - _id: "667fee7ac58681a42d57dc3b" - * name: "행복" - * - _id: "667fee7ac58681a42d57dc3d" - * name: "장원영" * title: * type: string * example: "무한상사 정총무" diff --git a/src/routes/user.ts b/src/routes/user.ts index dc02639..065a332 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -286,10 +286,17 @@ router.get('/', getRequestedUserInfo, UserController.getUser); // user 조회 * isTodayMeme: * type: boolean * example: false - * keywordIds: + * keywords: * type: array * items: - * example: "667fee7ac58681a42d57dc3b" + * type: object + * properties: + * _id: + * type: string + * example: "5f6f6b1d6ab9c8f7d9a4b5c6" + * title: + * type: string + * example: "무한도전" * title: * type: string * example: "무한상사 정총무" @@ -300,6 +307,10 @@ router.get('/', getRequestedUserInfo, UserController.getUser); // user 조회 * type: integer * example: 99 * description: 밈 리액션 수 + * watch: + * type: integer + * example: 999 + * description: 조회 수 * createdAt: * type: string * format: date-time @@ -394,10 +405,17 @@ router.get('/saved-memes', getRequestedUserInfo, UserController.getSavedMemes); * isTodayMeme: * type: boolean * example: false - * keywordIds: + * keywords: * type: array * items: - * example: "667fee7ac58681a42d57dc3b" + * type: object + * properties: + * _id: + * type: string + * example: "667fee7ac58681a42d57dc3b" + * name: + * type: string + * example: "무한도전" * title: * type: string * example: "무한상사 정총무" @@ -408,6 +426,10 @@ router.get('/saved-memes', getRequestedUserInfo, UserController.getSavedMemes); * type: integer * example: 99 * description: 밈 리액션 수 + * watch: + * type: integer + * example: 999 + * description: 밈 조회수 * createdAt: * type: string * format: date-time diff --git a/src/service/keyword.service.ts b/src/service/keyword.service.ts index 97620f9..2246579 100644 --- a/src/service/keyword.service.ts +++ b/src/service/keyword.service.ts @@ -3,7 +3,12 @@ import { Types } from 'mongoose'; import CustomError from '../errors/CustomError'; import { HttpCode } from '../errors/HttpCode'; -import { IKeywordCreatePayload, KeywordModel, IKeywordDocument } from '../model/keyword'; +import { + IKeywordCreatePayload, + KeywordModel, + IKeywordDocument, + IKeywordGetResponse, +} from '../model/keyword'; import { KeywordCategoryModel } from '../model/keywordCategory'; import { logger } from '../util/logger'; @@ -98,7 +103,26 @@ async function getKeywordById(keywordId: Types.ObjectId): Promise { +async function getKeywordInfoByKeywordIds( + keywordIds: Types.ObjectId[], +): Promise { + try { + const keyword = await KeywordModel.find( + { _id: { $in: keywordIds }, isDeleted: false }, + { + _id: 1, + name: 1, + }, + ).lean(); + return keyword; + } catch (err) { + logger.info(`Failed to get a Keyword Info By id (${keywordIds})`); + } +} + +async function getRecommendedKeywords(): Promise< + { title: string; keywords: IKeywordGetResponse[] }[] +> { try { const result = await KeywordCategoryModel.aggregate([ { @@ -119,23 +143,16 @@ async function getRecommendedKeywords(): Promise<{ title: string; keywords: stri $project: { _id: 0, category: '$name', - keywords: '$keywords.name', - }, - }, - { - $unwind: '$keywords', - }, - { - $group: { - _id: '$category', - keywords: { $push: '$keywords' }, - }, - }, - { - $project: { - _id: 0, - category: '$_id', - keywords: 1, + keywords: { + $map: { + input: '$keywords', + as: 'keyword', + in: { + name: '$$keyword.name', + _id: '$$keyword._id', + }, + }, + }, }, }, ]); @@ -157,4 +174,5 @@ export { getKeywordByName, getKeywordById, getRecommendedKeywords, + getKeywordInfoByKeywordIds, }; diff --git a/src/service/meme.service.ts b/src/service/meme.service.ts index 3f4f461..5f3461d 100644 --- a/src/service/meme.service.ts +++ b/src/service/meme.service.ts @@ -3,10 +3,18 @@ import { Types } from 'mongoose'; import CustomError from '../errors/CustomError'; import { HttpCode } from '../errors/HttpCode'; -import { IKeywordDocument } from '../model/keyword'; -import { IMemeCreatePayload, IMemeDocument, MemeModel, IMemeWithKeywords } from '../model/meme'; +import { IKeywordDocument, KeywordModel } from '../model/keyword'; +import { + IMemeCreatePayload, + IMemeDocument, + MemeModel, + IMemeWithKeywords, + IMemeGetResponse, +} from '../model/meme'; import { InteractionType, MemeInteractionModel } from '../model/memeInteraction'; import { IUserDocument } from '../model/user'; + +import * as KeywordService from './keyword.service'; import { logger } from '../util/logger'; async function getMeme(memeId: string): Promise { @@ -30,7 +38,9 @@ async function getMeme(memeId: string): Promise { async function getMemeWithKeywords(memeId: string): Promise { try { const meme = await MemeModel.aggregate([ - { $match: { _id: new Types.ObjectId(memeId), isDeleted: false } }, + { + $match: { _id: new Types.ObjectId(memeId), isDeleted: false }, + }, { $lookup: { from: 'keyword', @@ -41,10 +51,21 @@ async function getMemeWithKeywords(memeId: string): Promise { const todayMemeList = await MemeModel.aggregate([ - { $match: { isTodayMeme: true, isDeleted: false } }, - { $limit: limit }, + { + $match: { isTodayMeme: true, isDeleted: false }, + }, + { + $limit: limit, + }, { $lookup: { from: 'keyword', @@ -73,10 +98,21 @@ async function getTodayMemeList(limit: number = 5): Promise }, { $addFields: { - keywords: '$keywords.name', + keywords: { + $map: { + input: '$keywords', + as: 'keyword', + in: { + name: '$$keyword.name', + _id: '$$keyword._id', + }, + }, + }, }, }, - { $project: { keywordIds: 0, isDeleted: 0 } }, + { + $project: { keywordIds: 0, isDeleted: 0 }, + }, ]); const memeIds = todayMemeList.map((meme) => meme._id); @@ -89,20 +125,28 @@ async function getTodayMemeList(limit: number = 5): Promise async function getAllMemeList( page: number, size: number, -): Promise<{ total: number; page: number; totalPages: number; data: IMemeDocument[] }> { +): Promise<{ total: number; page: number; totalPages: number; data: IMemeGetResponse[] }> { const totalMemes = await MemeModel.countDocuments(); const memeList = await MemeModel.find({ isDeleted: false }, { isDeleted: 0 }) .skip((page - 1) * size) .limit(size) .sort({ createdAt: -1 }); + + const ret: IMemeGetResponse[] = await Promise.all( + memeList.map(async (meme) => { + const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); + return { ..._.omit(meme.toObject(), 'keywordIds'), keywords }; + }), + ); + logger.info(`Get all meme list - page(${page}), size(${size}), total(${totalMemes})`); return { total: totalMemes, page, totalPages: Math.ceil(totalMemes / size), - data: memeList, + data: ret, }; } @@ -158,7 +202,7 @@ async function searchMemeByKeyword( page: number, size: number, keyword: IKeywordDocument, -): Promise<{ total: number; page: number; totalPages: number; data: IMemeDocument[] }> { +): Promise<{ total: number; page: number; totalPages: number; data: IMemeGetResponse[] }> { try { const totalMemes = await MemeModel.countDocuments({ keywordIds: { $in: keyword._id }, @@ -172,9 +216,15 @@ async function searchMemeByKeyword( .skip((page - 1) * size) .limit(size) .sort({ reaction: -1 }) - .populate('keywordIds', 'name') .lean(); + const ret = await Promise.all( + memeList.map(async (meme) => { + const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); + return { ..._.omit(meme, 'keywordIds'), keywords }; + }), + ); + logger.info( `Get all meme list with keyword(${keyword.name}) - page(${page}), size(${size}), total(${totalMemes})`, ); @@ -183,7 +233,7 @@ async function searchMemeByKeyword( total: totalMemes, page, totalPages: Math.ceil(totalMemes / size), - data: memeList, + data: ret, }; } catch (err) { logger.error(`Failed to search meme list with keyword(${keyword})`, err.message); diff --git a/src/service/user.service.ts b/src/service/user.service.ts index f91cec1..7e3a66e 100644 --- a/src/service/user.service.ts +++ b/src/service/user.service.ts @@ -4,7 +4,7 @@ import { Types } from 'mongoose'; import CustomError from '../errors/CustomError'; import { HttpCode } from '../errors/HttpCode'; -import { IMemeDocument, MemeModel } from '../model/meme'; +import { IMemeDocument, IMemeGetResponse, MemeModel } from '../model/meme'; import { InteractionType, MemeInteractionModel } from '../model/memeInteraction'; import { MemeRecommendWatchModel, @@ -12,6 +12,8 @@ import { IMemeRecommendWatchCreatePayload, } from '../model/memeRecommendWatch'; import { IUser, IUserDocument, IUserInfos, UserModel } from '../model/user'; + +import * as KeywordService from './keyword.service'; import { logger } from '../util/logger'; async function getUser(deviceId: string): Promise { @@ -133,7 +135,7 @@ async function updateLastSeenMeme(user: IUserDocument, meme: IMemeDocument): Pro } } -async function getLastSeenMemes(user: IUserDocument): Promise { +async function getLastSeenMemes(user: IUserDocument): Promise { try { const lastSeenMeme = user.lastSeenMeme; const memeList = await MemeModel.find( @@ -144,7 +146,15 @@ async function getLastSeenMemes(user: IUserDocument): Promise { { isDeleted: 0 }, ).lean(); - return memeList; + const ret = await Promise.all( + memeList.map(async (meme) => { + const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); + return { ..._.omit(meme, 'keywordIds'), keywords }; + }), + ); + logger.info(`Get lastSeenMeme - deviceId(${user.deviceId}), memeList(${ret})`); + + return ret; } catch (err) { logger.error(`Failed get lastSeenMeme`, err.message); throw new CustomError( @@ -158,7 +168,7 @@ async function getSavedMemes( page: number, size: number, user: IUserDocument, -): Promise<{ total: number; page: number; totalPages: number; data: IMemeDocument[] }> { +): Promise<{ total: number; page: number; totalPages: number; data: IMemeGetResponse[] }> { try { const totalSavedMemes = await MemeInteractionModel.countDocuments({ deviceId: user.deviceId, @@ -185,11 +195,18 @@ async function getSavedMemes( { isDeleted: 0 }, ).lean(); + const ret = await Promise.all( + memeList.map(async (meme) => { + const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); + return { ..._.omit(meme, 'keywordIds'), keywords }; + }), + ); + return { total: totalSavedMemes, page, totalPages: Math.ceil(totalSavedMemes / size), - data: memeList, + data: ret, }; } catch (error) { throw new CustomError(`Failed to get saved memes`, HttpCode.INTERNAL_SERVER_ERROR, error); From e92543865ae40d18ae89e3518699f3d4f2f7fc1a Mon Sep 17 00:00:00 2001 From: Hyun_Gwang Date: Sat, 13 Jul 2024 15:16:39 +0900 Subject: [PATCH 03/19] fix: swagger image url --- src/routes/keyword.ts | 2 +- src/routes/meme.ts | 16 ++++++++-------- src/routes/user.ts | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/routes/keyword.ts b/src/routes/keyword.ts index 62c26bf..bca66ba 100644 --- a/src/routes/keyword.ts +++ b/src/routes/keyword.ts @@ -267,7 +267,7 @@ router.delete('/:keywordId', getKeywordInfoById, deleteKeyword); * example: 100 * topReactionImage: * type: string - * example: "https://example.com/top-reaction-image.jpg" + * example: "https://ppac-meme.s3.ap-northeast-2.amazonaws.com/17207029441190.png" * createdAt: * type: string * example: "2024-07-05T15:21:34.012Z" diff --git a/src/routes/meme.ts b/src/routes/meme.ts index 84413b6..5db57dc 100644 --- a/src/routes/meme.ts +++ b/src/routes/meme.ts @@ -90,7 +90,7 @@ const router = express.Router(); * example: "66805b1a72ef94c9c0ba134c" * image: * type: string - * example: "https://example.com/meme.jpg" + * example: "https://ppac-meme.s3.ap-northeast-2.amazonaws.com/17207029441190.png" * isTodayMeme: * type: boolean * example: false @@ -208,7 +208,7 @@ router.get('/list', getAllMemeList); // meme 목록 전체 조회 (페이지네 * example: "title1" * image: * type: string - * example: "image1" + * example: "https://ppac-meme.s3.ap-northeast-2.amazonaws.com/17207029441190.png" * reaction: * type: integer * example: 0 @@ -298,7 +298,7 @@ router.get('/recommend-memes', getTodayMemeList); // 오늘의 추천 밈 (5개) * description: 밈 제목 * image: * type: string - * example: "https://example.com/meme.jpg" + * example: "https://ppac-meme.s3.ap-northeast-2.amazonaws.com/17207029441190.png" * description: 밈 이미지 주소 * source: * type: string @@ -340,7 +340,7 @@ router.get('/recommend-memes', getTodayMemeList); // 오늘의 추천 밈 (5개) * description: 밈 제목 * image: * type: string - * example: "https://example.com/meme.jpg" + * example: "https://ppac-meme.s3.ap-northeast-2.amazonaws.com/17207029441190.png" * description: 밈 이미지 주소 * source: * type: string @@ -453,7 +453,7 @@ router.post('/', createMeme); // meme 생성 * example: "무한도전 정총무" * image: * type: string - * example: "https://example.com/meme.jpg" + * example: "https://ppac-meme.s3.ap-northeast-2.amazonaws.com/17207029441190.png" * reaction: * type: integer * example: 0 @@ -572,7 +572,7 @@ router.get('/:memeId', getMemeWithKeywords); // meme 조회 * description: 밈 제목 * image: * type: string - * example: "https://example.com/meme.jpg" + * example: "https://ppac-meme.s3.ap-northeast-2.amazonaws.com/17207029441190.png" * description: 밈 이미지 주소 * source: * type: string @@ -614,7 +614,7 @@ router.get('/:memeId', getMemeWithKeywords); // meme 조회 * description: 밈 제목 * image: * type: string - * example: "https://example.com/meme.jpg" + * example: "https://ppac-meme.s3.ap-northeast-2.amazonaws.com/17207029441190.png" * description: 밈 이미지 주소 * source: * type: string @@ -1277,7 +1277,7 @@ router.post('/:memeId/reaction', getRequestedUserInfo, getRequestedMemeInfo, cre * example: "66805b1a72ef94c9c0ba134c" * image: * type: string - * example: "https://example.com/meme.jpg" + * example: "https://ppac-meme.s3.ap-northeast-2.amazonaws.com/17207029441190.png" * isTodayMeme: * type: boolean * example: false diff --git a/src/routes/user.ts b/src/routes/user.ts index 065a332..a09c4c6 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -282,7 +282,7 @@ router.get('/', getRequestedUserInfo, UserController.getUser); // user 조회 * example: "66805b1a72ef94c9c0ba134c" * image: * type: string - * example: "https://example.com/meme.jpg" + * example: "https://ppac-meme.s3.ap-northeast-2.amazonaws.com/17207029441190.png" * isTodayMeme: * type: boolean * example: false @@ -401,7 +401,7 @@ router.get('/saved-memes', getRequestedUserInfo, UserController.getSavedMemes); * example: "66805b1a72ef94c9c0ba134c" * image: * type: string - * example: "https://example.com/meme.jpg" + * example: "https://ppac-meme.s3.ap-northeast-2.amazonaws.com/17207029441190.png" * isTodayMeme: * type: boolean * example: false From 072c841a26e19460c48922a9a63c20b17a538243 Mon Sep 17 00:00:00 2001 From: Hyun_Gwang Date: Sat, 13 Jul 2024 16:23:06 +0900 Subject: [PATCH 04/19] feat: make deleteMeme --- src/routes/meme.ts | 100 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/src/routes/meme.ts b/src/routes/meme.ts index 5db57dc..2201f34 100644 --- a/src/routes/meme.ts +++ b/src/routes/meme.ts @@ -12,6 +12,7 @@ import { createMemeReaction, createMemeWatch, searchMemeListByKeyword, + deleteMemeSave, } from '../controller/meme.controller'; import { getRequestedMemeInfo, @@ -897,6 +898,105 @@ router.delete('/:memeId', getRequestedMemeInfo, deleteMeme); // meme 삭제 */ router.post('/:memeId/save', getRequestedUserInfo, getRequestedMemeInfo, createMemeSave); // meme 저장하기 +/** + * @swagger + * /api/meme/{memeId}/save: + * delete: + * tags: [Meme] + * summary: 밈 저장 + * description: 밈 저장할 취소할 때 사용되는 api + * parameters: + * - name: x-device-id + * in: header + * description: 유저의 고유한 deviceId + * required: true + * type: string + * - in: path + * name: memeId + * schema: + * type: string + * description: 저장할 밈 id + * responses: + * 200: + * description: Meme successfully saved + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * code: + * type: integer + * example: 200 + * message: + * type: string + * example: Deleted Meme Save + * data: + * type: boolean + * example: true + * 400: + * description: Invalid parameters + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * code: + * type: integer + * example: 400 + * message: + * type: string + * example: 'deviceId should be provided' + * data: + * type: null + * example: null + * 404: + * description: Meme or user not found + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * code: + * type: integer + * example: 404 + * message: + * type: string + * example: Meme(66805b1372ef94c9c0ba1349) does not exist + * data: + * type: null + * example: null + * 500: + * description: Internal server error + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: error + * code: + * type: integer + * example: 500 + * message: + * type: string + * example: Internal server error + * data: + * type: null + * example: null + * + * */ +router.delete('/:memeId/save', getRequestedUserInfo, getRequestedMemeInfo, deleteMemeSave); + /** * @swagger * /api/meme/{memeId}/share: From a6cbd8147fb931074c2dffe9c08660d7f223c780 Mon Sep 17 00:00:00 2001 From: Hyun_Gwang Date: Sat, 13 Jul 2024 16:56:10 +0900 Subject: [PATCH 05/19] =?UTF-8?q?isSaved(=EB=82=B4=ED=8C=8C=EB=B0=88?= =?UTF-8?q?=EB=B3=B4=EA=B4=80=ED=95=A8=20=EC=97=AC=EB=B6=80)=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/meme.controller.ts | 31 +++--- src/model/meme.ts | 6 +- src/routes/meme.ts | 18 ++- src/routes/user.ts | 6 + src/service/meme.service.ts | 175 ++++++++++++++---------------- src/service/user.service.ts | 16 ++- 6 files changed, 128 insertions(+), 124 deletions(-) diff --git a/src/controller/meme.controller.ts b/src/controller/meme.controller.ts index 8d365fc..a97fb79 100644 --- a/src/controller/meme.controller.ts +++ b/src/controller/meme.controller.ts @@ -40,24 +40,17 @@ const getMeme = async (req: Request, res: Response, next: NextFunction) => { } }; -const getMemeWithKeywords = async (req: Request, res: Response, next: NextFunction) => { - const memeId = req.params?.memeId || null; - - if (_.isNull(memeId)) { - return next(new CustomError(`'memeId' field should be provided`, HttpCode.BAD_REQUEST)); - } - - if (!mongoose.Types.ObjectId.isValid(memeId)) { - return next(new CustomError(`'memeId' is not a valid ObjectId`, HttpCode.BAD_REQUEST)); - } +const getMemeWithKeywords = async (req: CustomRequest, res: Response, next: NextFunction) => { + const user = req.requestedUser; + const meme = req.requestedMeme; try { - const meme = await MemeService.getMemeWithKeywords(memeId); - if (_.isNull(meme)) { - return next(new CustomError(`Meme(${memeId}) not found.`, HttpCode.NOT_FOUND)); + const ret = await MemeService.getMemeWithKeywords(user, meme); + if (_.isNull(ret)) { + return next(new CustomError(`Meme(${meme._id}) not found.`, HttpCode.NOT_FOUND)); } - logger.info(`Get meme with keywords - ${memeId})`); + logger.info(`Get meme with keywords - ${meme._id})`); return res.json(createSuccessResponse(HttpCode.OK, 'Get Meme', meme)); } catch (err) { return next(new CustomError(err.message, err.status)); @@ -120,7 +113,8 @@ const deleteMeme = async (req: CustomRequest, res: Response, next: NextFunction) } }; -const getAllMemeList = async (req: Request, res: Response, next: NextFunction) => { +const getAllMemeList = async (req: CustomRequest, res: Response, next: NextFunction) => { + const user = req.requestedUser; const page = parseInt(req.query.page as string) || 1; if (page < 1) { return next(new CustomError(`Invalid 'page' parameter`, HttpCode.BAD_REQUEST)); @@ -132,7 +126,7 @@ const getAllMemeList = async (req: Request, res: Response, next: NextFunction) = } try { - const memeList = await MemeService.getAllMemeList(page, size); + const memeList = await MemeService.getAllMemeList(page, size, user); const data = { pagination: { @@ -151,7 +145,8 @@ const getAllMemeList = async (req: Request, res: Response, next: NextFunction) = } }; -const getTodayMemeList = async (req: Request, res: Response, next: NextFunction) => { +const getTodayMemeList = async (req: CustomRequest, res: Response, next: NextFunction) => { + const user = req.requestedUser; const size = parseInt(req.query.size as string) || 5; if (size > 5) { @@ -164,7 +159,7 @@ const getTodayMemeList = async (req: Request, res: Response, next: NextFunction) } try { - const todayMemeList = await MemeService.getTodayMemeList(size); + const todayMemeList = await MemeService.getTodayMemeList(size, user); return res.json(createSuccessResponse(HttpCode.OK, 'Get today meme list', todayMemeList)); } catch (err) { return next(new CustomError(err.message, err.status)); diff --git a/src/model/meme.ts b/src/model/meme.ts index 7b5d1d6..8a2f47a 100644 --- a/src/model/meme.ts +++ b/src/model/meme.ts @@ -32,6 +32,7 @@ export interface IMemeGetResponse { reaction: number; source: string; isTodayMeme: boolean; + isSaved: boolean; // 나의 파밈함 저장 여부 createdAt: Date; updatedAt: Date; isDeleted: boolean; @@ -50,11 +51,6 @@ export interface IMemeDocument extends Document { isDeleted: boolean; } -// keywordIds로 조회한 keywords로 대체된 Meme 정보 -export interface IMemeWithKeywords extends Omit { - keywords: string[]; -} - const MemeSchema: Schema = new Schema( { title: { type: String, required: true }, diff --git a/src/routes/meme.ts b/src/routes/meme.ts index 2201f34..de1147b 100644 --- a/src/routes/meme.ts +++ b/src/routes/meme.ts @@ -95,6 +95,9 @@ const router = express.Router(); * isTodayMeme: * type: boolean * example: false + * isSaved: + * type: boolean + * example: true * keywords: * type: array * items: @@ -163,7 +166,7 @@ const router = express.Router(); * type: null * example: null */ -router.get('/list', getAllMemeList); // meme 목록 전체 조회 (페이지네이션) +router.get('/list', getRequestedUserInfo, getAllMemeList); // meme 목록 전체 조회 (페이지네이션) /** * @swagger @@ -227,6 +230,9 @@ router.get('/list', getAllMemeList); // meme 목록 전체 조회 (페이지네 * type: string * format: date-time * example: "2024-06-29T19:05:55.638Z" + * isSaved: + * type: boolean + * example: true * keywords: * type: array * items: @@ -277,7 +283,7 @@ router.get('/list', getAllMemeList); // meme 목록 전체 조회 (페이지네 * type: null * example: null */ -router.get('/recommend-memes', getTodayMemeList); // 오늘의 추천 밈 (5개) +router.get('/recommend-memes', getRequestedUserInfo, getTodayMemeList); // 오늘의 추천 밈 (5개) /** * @swagger @@ -475,6 +481,9 @@ router.post('/', createMeme); // meme 생성 * type: string * format: date-time * example: "2024-06-29T19:05:55.638Z" + * isSaved: + * type: boolean + * example: true * keywords: * type: array * items: @@ -544,7 +553,7 @@ router.post('/', createMeme); // meme 생성 * type: null * example: null */ -router.get('/:memeId', getMemeWithKeywords); // meme 조회 +router.get('/:memeId', getRequestedUserInfo, getRequestedMemeInfo, getMemeWithKeywords); // meme 조회 /** * @swagger @@ -1381,6 +1390,9 @@ router.post('/:memeId/reaction', getRequestedUserInfo, getRequestedMemeInfo, cre * isTodayMeme: * type: boolean * example: false + * isSaved: + * type: boolean + * example: true * keywords: * type: array * items: diff --git a/src/routes/user.ts b/src/routes/user.ts index a09c4c6..adc1352 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -286,6 +286,9 @@ router.get('/', getRequestedUserInfo, UserController.getUser); // user 조회 * isTodayMeme: * type: boolean * example: false + * isSaved: + * type: boolean + * example: false * keywords: * type: array * items: @@ -405,6 +408,9 @@ router.get('/saved-memes', getRequestedUserInfo, UserController.getSavedMemes); * isTodayMeme: * type: boolean * example: false + * isSaved: + * type: boolean + * example: false * keywords: * type: array * items: diff --git a/src/service/meme.service.ts b/src/service/meme.service.ts index 5f3461d..49612b6 100644 --- a/src/service/meme.service.ts +++ b/src/service/meme.service.ts @@ -4,13 +4,7 @@ import { Types } from 'mongoose'; import CustomError from '../errors/CustomError'; import { HttpCode } from '../errors/HttpCode'; import { IKeywordDocument, KeywordModel } from '../model/keyword'; -import { - IMemeCreatePayload, - IMemeDocument, - MemeModel, - IMemeWithKeywords, - IMemeGetResponse, -} from '../model/meme'; +import { IMemeCreatePayload, IMemeDocument, MemeModel, IMemeGetResponse } from '../model/meme'; import { InteractionType, MemeInteractionModel } from '../model/memeInteraction'; import { IUserDocument } from '../model/user'; @@ -35,96 +29,64 @@ async function getMeme(memeId: string): Promise { } } -async function getMemeWithKeywords(memeId: string): Promise { +async function getMemeWithKeywords( + user: IUserDocument, + meme: IMemeDocument, +): Promise { try { - const meme = await MemeModel.aggregate([ - { - $match: { _id: new Types.ObjectId(memeId), isDeleted: false }, - }, - { - $lookup: { - from: 'keyword', - localField: 'keywordIds', - foreignField: '_id', - as: 'keywords', - }, - }, - { - $addFields: { - keywords: { - $map: { - input: '$keywords', - as: 'keyword', - in: { - name: '$$keyword.name', - _id: '$$keyword._id', - }, - }, - }, - }, - }, - { - $project: { keywordIds: 0, isDeleted: 0 }, - }, - ]); - - if (!meme) { - logger.info(`Meme(${memeId}) not found.`); - return null; - } + const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); + const isSaved = await MemeInteractionModel.findOne({ + deviceId: user.deviceId, + memeId: meme._id, + type: InteractionType.SAVE, + isDeleted: false, + }); - return meme[0] || null; + return { + ..._.omit(meme, 'keywordIds'), + keywords, + isSaved: !_.isNil(isSaved), + }; } catch (err) { - logger.error(`Failed to get a meme(${memeId}): ${err.message}`); - throw new CustomError(`Failed to get a meme(${memeId})`, HttpCode.INTERNAL_SERVER_ERROR); + logger.error(`Failed to get a meme(${meme._id}): ${err.message}`); + throw new CustomError(`Failed to get a meme(${meme._id})`, HttpCode.INTERNAL_SERVER_ERROR); } } -async function getTodayMemeList(limit: number = 5): Promise { - const todayMemeList = await MemeModel.aggregate([ - { - $match: { isTodayMeme: true, isDeleted: false }, - }, - { - $limit: limit, - }, - { - $lookup: { - from: 'keyword', - localField: 'keywordIds', - foreignField: '_id', - as: 'keywords', - }, - }, - { - $addFields: { - keywords: { - $map: { - input: '$keywords', - as: 'keyword', - in: { - name: '$$keyword.name', - _id: '$$keyword._id', - }, - }, - }, - }, - }, - { - $project: { keywordIds: 0, isDeleted: 0 }, - }, - ]); +async function getTodayMemeList( + limit: number = 5, + user: IUserDocument, +): Promise { + const todayMemeList = await MemeModel.find( + { isDeleted: false, isTodayMeme: true }, + { isDeleted: 0 }, + ); + + const ret = await Promise.all( + todayMemeList.map(async (meme) => { + const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); + const isSaved = await MemeInteractionModel.findOne({ + deviceId: user.deviceId, + memeId: meme._id, + type: InteractionType.SAVE, + isDeleted: false, + }); + return { ..._.omit(meme, 'keywordIds'), keywords, isSaved: !_.isNil(isSaved) }; + }), + ); const memeIds = todayMemeList.map((meme) => meme._id); logger.info( `Get all today meme list(${todayMemeList.length}) - memeIds(${memeIds}), limit(${limit})`, ); - return todayMemeList; + + return ret; } async function getAllMemeList( page: number, size: number, + user: IUserDocument, ): Promise<{ total: number; page: number; totalPages: number; data: IMemeGetResponse[] }> { const totalMemes = await MemeModel.countDocuments(); @@ -136,7 +98,13 @@ async function getAllMemeList( const ret: IMemeGetResponse[] = await Promise.all( memeList.map(async (meme) => { const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); - return { ..._.omit(meme.toObject(), 'keywordIds'), keywords }; + const isSaved = await MemeInteractionModel.findOne({ + deviceId: user.deviceId, + memeId: meme._id, + type: InteractionType.SAVE, + isDeleted: false, + }); + return { ..._.omit(meme.toObject(), 'keywordIds'), keywords, isSaved: !_.isNil(isSaved) }; }), ); @@ -221,7 +189,12 @@ async function searchMemeByKeyword( const ret = await Promise.all( memeList.map(async (meme) => { const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); - return { ..._.omit(meme, 'keywordIds'), keywords }; + const isSaved = await MemeInteractionModel.findOne({ + memeId: meme._id, + type: InteractionType.SAVE, + isDeleted: false, + }); + return { ..._.omit(meme, 'keywordIds'), keywords, isSaved: !_.isNil(isSaved) }; }), ); @@ -254,7 +227,6 @@ async function createMemeInteraction( memeId: meme._id, deviceId: user.deviceId, interactionType, - isDeleted: false, }); // 밈당 interaction은 1회 @@ -262,6 +234,29 @@ async function createMemeInteraction( logger.info( `Already ${interactionType} meme - deviceId(${user.deviceId}), memeId(${meme._id}`, ); + + if (interactionType === InteractionType.SAVE && memeInteraction.isDeleted) { + // 'save'인 경우 isDeleted를 false로 업데이트한다. + await MemeInteractionModel.findOneAndUpdate( + { memeId: meme._id, deviceId: user.deviceId, interactionType }, + { $set: { isDeleted: false } }, + ); + } else if (interactionType === InteractionType.REACTION) { + // 'reaction'인 경우에만 Meme의 reaction 수를 업데이트한다. + await MemeModel.findOneAndUpdate( + { memeId: meme._id, isDeleted: false }, + { $inc: { reaction: 1 } }, + { + projection: { _id: 0, createdAt: 0, updatedAt: 0 }, + returnDocument: 'after', + }, + ).lean(); + } else { + // 'watch', 'share'인 경우 isDeleted 여부 로그를 남긴다. + logger.debug( + `${memeInteraction.interactionType} document exist - isDeleted(${memeInteraction.isDeleted})`, + ); + } } else { const newMemeInteraction = await MemeInteractionModel.create({ memeId: meme._id, @@ -271,18 +266,6 @@ async function createMemeInteraction( await newMemeInteraction.save(); } - // 'reaction'인 경우에만 Meme의 reaction 수를 업데이트한다. - if (interactionType === InteractionType.REACTION) { - await MemeModel.findOneAndUpdate( - { memeId: meme._id, isDeleted: false }, - { $inc: { reaction: 1 } }, - { - projection: { _id: 0, createdAt: 0, updatedAt: 0 }, - returnDocument: 'after', - }, - ).lean(); - } - return true; } catch (err) { logger.error(`Failed to create memeInteraction`, err.message); diff --git a/src/service/user.service.ts b/src/service/user.service.ts index 7e3a66e..c7c2a84 100644 --- a/src/service/user.service.ts +++ b/src/service/user.service.ts @@ -149,7 +149,13 @@ async function getLastSeenMemes(user: IUserDocument): Promise { const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); - return { ..._.omit(meme, 'keywordIds'), keywords }; + const isSaved = await MemeInteractionModel.findOne({ + deviceId: user.deviceId, + memeId: meme._id, + type: InteractionType.SAVE, + isDeleted: false, + }); + return { ..._.omit(meme, 'keywordIds'), keywords, isSaved: !_.isNil(isSaved) }; }), ); logger.info(`Get lastSeenMeme - deviceId(${user.deviceId}), memeList(${ret})`); @@ -198,7 +204,13 @@ async function getSavedMemes( const ret = await Promise.all( memeList.map(async (meme) => { const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); - return { ..._.omit(meme, 'keywordIds'), keywords }; + const isSaved = await MemeInteractionModel.findOne({ + deviceId: user.deviceId, + memeId: meme._id, + type: InteractionType.SAVE, + isDeleted: false, + }); + return { ..._.omit(meme, 'keywordIds'), keywords, isSaved: !_.isNil(isSaved) }; }), ); From 866662bf02e21a04469558031afc7136a101bafa Mon Sep 17 00:00:00 2001 From: seohyun0120 Date: Sun, 14 Jul 2024 11:24:56 +0900 Subject: [PATCH 06/19] =?UTF-8?q?header=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/meme.controller.ts | 1 + src/routes/meme.ts | 22 +++++++++++++++++++++- src/routes/user.ts | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/controller/meme.controller.ts b/src/controller/meme.controller.ts index a97fb79..7df081b 100644 --- a/src/controller/meme.controller.ts +++ b/src/controller/meme.controller.ts @@ -167,6 +167,7 @@ const getTodayMemeList = async (req: CustomRequest, res: Response, next: NextFun }; const searchMemeListByKeyword = async (req: CustomRequest, res: Response, next: NextFunction) => { + const user = req.requestedUser; const keyword = req.requestedKeyword; const page = parseInt(req.query.page as string) || 1; diff --git a/src/routes/meme.ts b/src/routes/meme.ts index de1147b..d9e4b04 100644 --- a/src/routes/meme.ts +++ b/src/routes/meme.ts @@ -31,6 +31,11 @@ const router = express.Router(); * summary: 밈 전체 목록 조회 (페이지네이션 적용) * description: 밈 전체 목록 조회 * parameters: + * - name: x-device-id + * in: header + * description: 유저의 고유한 deviceId + * required: true + * type: string * - in: query * name: page * schema: @@ -176,6 +181,11 @@ router.get('/list', getRequestedUserInfo, getAllMemeList); // meme 목록 전체 * summary: 추천 밈 정보 조회 * description: 추천 밈 목록을 조회한다. (현재는 주 단위, 추후 일 단위로 변경될 수 있음) * parameters: + * - name: x-device-id + * in: header + * description: 유저의 고유한 deviceId + * required: true + * type: string * - in: query * name: size * schema: @@ -426,6 +436,11 @@ router.post('/', createMeme); // meme 생성 * summary: 밈 정보 조회(키워드 포함) * description: 밈 정보를 조회한다. 밈의 키워드 정보도 함께 포함한다. 이때 키워드는 키워드명만 제공된다 (키워드의 개별 정보 X) * parameters: + * - name: x-device-id + * in: header + * description: 유저의 고유한 deviceId + * required: true + * type: string * - in: path * name: memeId * required: true @@ -1319,6 +1334,11 @@ router.post('/:memeId/reaction', getRequestedUserInfo, getRequestedMemeInfo, cre * summary: 키워드가 포함된 밈 검색 (페이지네이션 적용) * description: 키워드 클릭 시 해당 키워드를 포함한 밈을 조회하고 목록을 반환한다. * parameters: + * - name: x-device-id + * in: header + * description: 유저의 고유한 deviceId + * required: true + * type: string * - in: query * name: page * schema: @@ -1462,6 +1482,6 @@ router.post('/:memeId/reaction', getRequestedUserInfo, getRequestedMemeInfo, cre * type: null * example: null */ -router.get('/search/:name', getKeywordInfoByName, searchMemeListByKeyword); // 키워드에 해당하는 밈 검색하기 (페이지네이션) +router.get('/search/:name', getRequestedUserInfo, getKeywordInfoByName, searchMemeListByKeyword); // 키워드에 해당하는 밈 검색하기 (페이지네이션) export default router; diff --git a/src/routes/user.ts b/src/routes/user.ts index adc1352..d444acb 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -288,7 +288,7 @@ router.get('/', getRequestedUserInfo, UserController.getUser); // user 조회 * example: false * isSaved: * type: boolean - * example: false + * example: true * keywords: * type: array * items: From 16fcd1e319cf3702ee9aeb5ac533583041a7b135 Mon Sep 17 00:00:00 2001 From: seohyun0120 Date: Sun, 14 Jul 2024 11:46:05 +0900 Subject: [PATCH 07/19] =?UTF-8?q?swagger=20indent=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/meme.ts | 52 +++++++++++++++++++++++----------------------- src/routes/user.ts | 4 ++-- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/routes/meme.ts b/src/routes/meme.ts index d9e4b04..dc324a9 100644 --- a/src/routes/meme.ts +++ b/src/routes/meme.ts @@ -446,7 +446,7 @@ router.post('/', createMeme); // meme 생성 * required: true * schema: * type: string - * description: 밈 ID + * description: 밈 ID * responses: * 200: * description: The meme @@ -841,7 +841,7 @@ router.delete('/:memeId', getRequestedMemeInfo, deleteMeme); // meme 삭제 * name: memeId * schema: * type: string - * description: 저장할 밈 id + * description: 저장할 밈 id * responses: * 201: * description: Meme successfully saved @@ -939,7 +939,7 @@ router.post('/:memeId/save', getRequestedUserInfo, getRequestedMemeInfo, createM * name: memeId * schema: * type: string - * description: 저장할 밈 id + * description: 저장할 밈 id * responses: * 200: * description: Meme successfully saved @@ -1038,7 +1038,7 @@ router.delete('/:memeId/save', getRequestedUserInfo, getRequestedMemeInfo, delet * name: memeId * schema: * type: string - * description: 공유할 밈 id + * description: 공유할 밈 id * responses: * 201: * description: Meme successfully shared @@ -1137,7 +1137,7 @@ router.post('/:memeId/share', getRequestedUserInfo, getRequestedMemeInfo, create * required: true * schema: * type: string - * description: 밈 id + * description: 밈 id * - in: path * name: type * required: true @@ -1247,8 +1247,8 @@ router.post('/:memeId/watch/:type', getRequestedUserInfo, getRequestedMemeInfo, * name: memeId * schema: * type: string - * required: true - * description: 리액션할 밈 id + * required: true + * description: 리액션할 밈 id * responses: * 201: * description: Created Meme Reaction @@ -1339,25 +1339,25 @@ router.post('/:memeId/reaction', getRequestedUserInfo, getRequestedMemeInfo, cre * description: 유저의 고유한 deviceId * required: true * type: string - * - in: query - * name: page - * schema: - * type: number - * example: 1 - * description: 현재 페이지 번호 (기본값 1) - * - in: query - * name: size - * schema: - * type: number - * example: 10 - * description: 한 번에 조회할 밈 개수 (기본값 10) - * - in: path - * name: name - * schema: - * type: string - * example: "행복" - * required: true - * description: 키워드명 + * - in: query + * name: page + * schema: + * type: number + * example: 1 + * description: 현재 페이지 번호 (기본값 1) + * - in: query + * name: size + * schema: + * type: number + * example: 10 + * description: 한 번에 조회할 밈 개수 (기본값 10) + * - in: path + * name: name + * schema: + * type: string + * example: "행복" + * required: true + * description: 키워드명 * responses: * 200: * description: 키워드를 포함한 밈 목록 diff --git a/src/routes/user.ts b/src/routes/user.ts index d444acb..19247a5 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -362,7 +362,7 @@ router.get('/', getRequestedUserInfo, UserController.getUser); // user 조회 * type: null * example: null */ -router.get('/saved-memes', getRequestedUserInfo, UserController.getSavedMemes); // user가 저장한 meme 조회 (페이지네이션 적용) +router.get('/saved-memes', getRequestedUserInfo, UserController.getSavedMemeList); // user가 저장한 meme 조회 (페이지네이션 적용) /** * @swagger @@ -484,6 +484,6 @@ router.get('/saved-memes', getRequestedUserInfo, UserController.getSavedMemes); * type: null * example: null */ -router.get('/recent-memes', getRequestedUserInfo, UserController.getLastSeenMemes); // user가 최근에 본 밈 정보 조회 (10개 제한) +router.get('/recent-memes', getRequestedUserInfo, UserController.getLastSeenMemeList); // user가 최근에 본 밈 정보 조회 (10개 제한) export default router; From 1362a36d727cf20952ebe98b2a87929777153256 Mon Sep 17 00:00:00 2001 From: seohyun0120 Date: Sun, 14 Jul 2024 11:46:29 +0900 Subject: [PATCH 08/19] =?UTF-8?q?=ED=95=A8=EC=88=98=EB=AA=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(xxxs=20->=20xxxList)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/user.controller.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/controller/user.controller.ts b/src/controller/user.controller.ts index aa207af..33400ed 100644 --- a/src/controller/user.controller.ts +++ b/src/controller/user.controller.ts @@ -33,18 +33,18 @@ const getUser = async (req: CustomRequest, res: Response, next: NextFunction) => } }; -const getLastSeenMemes = async (req: CustomRequest, res: Response, next: NextFunction) => { +const getLastSeenMemeList = async (req: CustomRequest, res: Response, next: NextFunction) => { const user = req.requestedUser; try { - const memeList = await UserService.getLastSeenMemes(user); + const memeList = await UserService.getLastSeenMemeList(user); return res.json(createSuccessResponse(HttpCode.OK, 'Get Last Seen Meme', memeList)); } catch (err) { return next(new CustomError(err.message, err.status)); } }; -const getSavedMemes = async (req: CustomRequest, res: Response, next: NextFunction) => { +const getSavedMemeList = async (req: CustomRequest, res: Response, next: NextFunction) => { const user = req.requestedUser; const page = parseInt(req.query.page as string) || 1; @@ -58,7 +58,7 @@ const getSavedMemes = async (req: CustomRequest, res: Response, next: NextFuncti } try { - const memeList = await UserService.getSavedMemes(page, size, user); + const memeList = await UserService.getSavedMemeList(page, size, user); const data = { pagination: { @@ -76,7 +76,7 @@ const getSavedMemes = async (req: CustomRequest, res: Response, next: NextFuncti } }; -export { getUser, createUser, getLastSeenMemes, getSavedMemes }; +export { getUser, createUser, getLastSeenMemeList, getSavedMemeList }; function getLevel(watch: number, reaction: number, share: number): number { let level = 1; From 75903b3fc27fd50bfac2ad456a454e5b5236ae79 Mon Sep 17 00:00:00 2001 From: seohyun0120 Date: Sun, 14 Jul 2024 11:47:08 +0900 Subject: [PATCH 09/19] =?UTF-8?q?IMemeGetResponse=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=84=A0=EC=96=B8=20=EC=88=98=EC=A0=95,=20IMemeInteraction=20?= =?UTF-8?q?=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=84=A0=EC=96=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/model/meme.ts | 12 ++---------- src/model/memeInteraction.ts | 8 ++++---- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/model/meme.ts b/src/model/meme.ts index 8a2f47a..e350606 100644 --- a/src/model/meme.ts +++ b/src/model/meme.ts @@ -1,4 +1,5 @@ import mongoose, { Schema, Document, Types } from 'mongoose'; + import { IKeywordGetResponse } from './keyword'; export interface IMemeCreatePayload { @@ -24,18 +25,9 @@ export interface IMeme { isTodayMeme: boolean; } -export interface IMemeGetResponse { - _id: Types.ObjectId; - title: string; +export interface IMemeGetResponse extends Omit { keywords: IKeywordGetResponse[]; - image: string; - reaction: number; - source: string; - isTodayMeme: boolean; isSaved: boolean; // 나의 파밈함 저장 여부 - createdAt: Date; - updatedAt: Date; - isDeleted: boolean; } export interface IMemeDocument extends Document { diff --git a/src/model/memeInteraction.ts b/src/model/memeInteraction.ts index c8a312f..d644d64 100644 --- a/src/model/memeInteraction.ts +++ b/src/model/memeInteraction.ts @@ -8,14 +8,14 @@ export enum InteractionType { } export interface IMemeInteraction { - deviceId: String; + deviceId: string; memeId: Types.ObjectId; interactionType: InteractionType; } -export interface IMemeInteraction extends Document { +export interface IMemeInteractionDocument extends Document { _id: Types.ObjectId; - deviceId: String; + deviceId: string; memeId: Types.ObjectId; interactionType: InteractionType; isDeleted: boolean; @@ -37,7 +37,7 @@ const MemeInteractionSchema: Schema = new Schema( }, ); -export const MemeInteractionModel = mongoose.model( +export const MemeInteractionModel = mongoose.model( 'memeInteraction', MemeInteractionSchema, ); From 12b5eb0593b125808f6bec1f9bc7cdd64526aee8 Mon Sep 17 00:00:00 2001 From: seohyun0120 Date: Sun, 14 Jul 2024 11:49:13 +0900 Subject: [PATCH 10/19] =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/meme.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/meme.controller.ts b/src/controller/meme.controller.ts index 7df081b..cd8a6ee 100644 --- a/src/controller/meme.controller.ts +++ b/src/controller/meme.controller.ts @@ -181,7 +181,7 @@ const searchMemeListByKeyword = async (req: CustomRequest, res: Response, next: } try { - const memeList = await MemeService.searchMemeByKeyword(page, size, keyword); + const memeList = await MemeService.searchMemeByKeyword(page, size, keyword, user); const data = { pagination: { total: memeList.total, From bcb3b13e05a4dc265596950031fdd4651a1ccf1b Mon Sep 17 00:00:00 2001 From: seohyun0120 Date: Sun, 14 Jul 2024 11:49:26 +0900 Subject: [PATCH 11/19] =?UTF-8?q?MemeInteraction=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=EC=9D=80=20=ED=95=B4=EB=8B=B9=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EC=97=90=EC=84=9C=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/service/memeInteraction.service.ts | 211 +++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 src/service/memeInteraction.service.ts diff --git a/src/service/memeInteraction.service.ts b/src/service/memeInteraction.service.ts new file mode 100644 index 0000000..433157a --- /dev/null +++ b/src/service/memeInteraction.service.ts @@ -0,0 +1,211 @@ +import CustomError from '../errors/CustomError'; +import { HttpCode } from '../errors/HttpCode'; +import { IMemeDocument, MemeModel } from '../model/meme'; +import { + IMemeInteractionDocument, + InteractionType, + MemeInteractionModel, +} from '../model/memeInteraction'; +import { IUserDocument } from '../model/user'; +import { logger } from '../util/logger'; + +async function getMemeInteractionInfo( + user: IUserDocument, + meme: IMemeDocument, + interactionType: InteractionType, +): Promise { + try { + const condition = { + deviceId: user.deviceId, + memeId: meme._id, + interactionType, + }; + + // 'save' interaction은 isDeleted 조건 검색 필요없음 + const isDeletedCondition = interactionType !== InteractionType.SAVE ? { isDeleted: false } : {}; + + const memeInteraction = await MemeInteractionModel.findOne({ + ...condition, + ...isDeletedCondition, + }); + + return memeInteraction || null; + } catch (error) { + logger.error(`Failed to get a MemeInteraction Info(${meme._id} - ${interactionType})`, { + error, + }); + throw new CustomError( + `Failed to get a MemeInteraction Info(${meme._id} - ${interactionType})`, + HttpCode.INTERNAL_SERVER_ERROR, + ); + } +} + +async function getMemeInteractionInfoWithCondition( + user: IUserDocument, + meme: IMemeDocument, + interactionType: InteractionType, + findCondition: Partial = {}, +): Promise { + try { + const condition: Partial = { + deviceId: user.deviceId, + memeId: meme._id, + interactionType, + ...findCondition, + }; + + const memeInteraction = await MemeInteractionModel.findOne(condition); + return memeInteraction || null; + } catch (err) { + logger.error(`Failed to get a MemeInteraction Info(${meme._id} - ${interactionType})`); + throw new CustomError( + `Failed to get a MemeInteraction Info(${meme._id} - ${interactionType})`, + HttpCode.INTERNAL_SERVER_ERROR, + ); + } +} + +async function getMemeInteractionCount( + user: IUserDocument, + interactionType: InteractionType, +): Promise { + try { + const count = await MemeInteractionModel.countDocuments({ + deviceId: user.deviceId, + interactionType, + isDeleted: false, + }); + return count; + } catch (err) { + logger.error(`Failed to count MemeInteraction(${interactionType})`); + throw new CustomError( + `Failed to count MemeInteraction(${interactionType}) (${err.message})`, + HttpCode.INTERNAL_SERVER_ERROR, + ); + } +} + +async function getMemeInteractionList( + page: number, + size: number, + user: IUserDocument, + interactionType: InteractionType, +): Promise { + try { + const memeInteractionList = await MemeInteractionModel.find( + { + deviceId: user.deviceId, + interactionType: InteractionType.SAVE, + isDeleted: false, + }, + { isDeleted: 0 }, + ) + .skip((page - 1) * size) + .limit(size) + .sort({ createdAt: -1 }); + + return memeInteractionList; + } catch (err) { + logger.error(`Failed to count MemeInteraction(${interactionType})`); + throw new CustomError( + `Failed to count MemeInteraction(${interactionType}) (${err.message})`, + HttpCode.INTERNAL_SERVER_ERROR, + ); + } +} + +async function createMemeInteraction( + user: IUserDocument, + meme: IMemeDocument, + interactionType: InteractionType, +): Promise { + try { + const newMemeInteraction = new MemeInteractionModel({ + deviceId: user.deviceId, + memeId: meme._id, + interactionType, + }); + await newMemeInteraction.save(); + return newMemeInteraction; + } catch (err) { + logger.error(`Failed to create a MemeInteraction(${meme._id} - ${interactionType})`); + throw new CustomError( + `Failed to create a MemeInteraction(${meme._id} - ${interactionType})`, + HttpCode.INTERNAL_SERVER_ERROR, + ); + } +} + +async function updateMemeInteraction( + user: IUserDocument, + meme: IMemeDocument, + interactionType: InteractionType, +): Promise { + switch (interactionType) { + case InteractionType.SAVE: + await MemeInteractionModel.findOneAndUpdate( + { memeId: meme._id, deviceId: user.deviceId, interactionType }, + { $set: { isDeleted: false } }, + ); + logger.debug(`[${interactionType}] interaction - updated isDeleted to 'false'`); + break; + + case InteractionType.REACTION: + await MemeModel.findOneAndUpdate( + { memeId: meme._id, isDeleted: false }, + { $inc: { reaction: 1 } }, + { + projection: { _id: 0, createdAt: 0, updatedAt: 0 }, + returnDocument: 'after', + }, + ).lean(); + logger.debug(`[${interactionType}] interaction - increased Meme reaction count`); + break; + + case InteractionType.SHARE: + case InteractionType.WATCH: + logger.debug(`${interactionType} interaction don't need to be updated. `); + break; + + default: + logger.error(`Unsupported interactionType(${interactionType})`); + throw new CustomError( + `Unsupported interactionType(${interactionType})`, + HttpCode.BAD_REQUEST, + ); + } +} + +async function deleteMemeInteraction( + user: IUserDocument, + meme: IMemeDocument, + interactionType: InteractionType, +): Promise { + try { + const memeInteraction = await MemeInteractionModel.findOneAndUpdate( + { deviceId: user.deviceId, memeId: meme._id, interactionType: InteractionType.SAVE }, + { + isDeleted: true, + }, + ); + + return memeInteraction; + } catch (err) { + logger.error(`Failed to delete a MemeInteraction(${meme._id} - ${interactionType})`); + throw new CustomError( + `Failed to delete a MemeInteraction(${meme._id} - ${interactionType})`, + HttpCode.INTERNAL_SERVER_ERROR, + ); + } +} + +export { + getMemeInteractionInfo, + getMemeInteractionInfoWithCondition, + getMemeInteractionCount, + getMemeInteractionList, + createMemeInteraction, + updateMemeInteraction, + deleteMemeInteraction, +}; From 426d21e37d43103cdf9b6be9d9c90a7cbf007809 Mon Sep 17 00:00:00 2001 From: seohyun0120 Date: Sun, 14 Jul 2024 11:54:27 +0900 Subject: [PATCH 12/19] =?UTF-8?q?=ED=95=A8=EC=88=98=EB=AA=85=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD,?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/service/user.service.ts | 74 ++++++++++++------------------------- 1 file changed, 23 insertions(+), 51 deletions(-) diff --git a/src/service/user.service.ts b/src/service/user.service.ts index c7c2a84..81018cb 100644 --- a/src/service/user.service.ts +++ b/src/service/user.service.ts @@ -2,6 +2,8 @@ import { startOfWeek } from 'date-fns'; import _ from 'lodash'; import { Types } from 'mongoose'; +import * as MemeService from './meme.service'; +import * as MemeInteractionService from './memeInteraction.service'; import CustomError from '../errors/CustomError'; import { HttpCode } from '../errors/HttpCode'; import { IMemeDocument, IMemeGetResponse, MemeModel } from '../model/meme'; @@ -12,8 +14,6 @@ import { IMemeRecommendWatchCreatePayload, } from '../model/memeRecommendWatch'; import { IUser, IUserDocument, IUserInfos, UserModel } from '../model/user'; - -import * as KeywordService from './keyword.service'; import { logger } from '../util/logger'; async function getUser(deviceId: string): Promise { @@ -135,7 +135,7 @@ async function updateLastSeenMeme(user: IUserDocument, meme: IMemeDocument): Pro } } -async function getLastSeenMemes(user: IUserDocument): Promise { +async function getLastSeenMemeList(user: IUserDocument): Promise { try { const lastSeenMeme = user.lastSeenMeme; const memeList = await MemeModel.find( @@ -146,21 +146,10 @@ async function getLastSeenMemes(user: IUserDocument): Promise { - const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); - const isSaved = await MemeInteractionModel.findOne({ - deviceId: user.deviceId, - memeId: meme._id, - type: InteractionType.SAVE, - isDeleted: false, - }); - return { ..._.omit(meme, 'keywordIds'), keywords, isSaved: !_.isNil(isSaved) }; - }), - ); - logger.info(`Get lastSeenMeme - deviceId(${user.deviceId}), memeList(${ret})`); + const getLastSeenMemeList = await MemeService.getMemeListWithKeywordsAndisSaved(user, memeList); + logger.info(`Get lastSeenMeme - deviceId(${user.deviceId}), memeList(${getLastSeenMemeList})`); - return ret; + return getLastSeenMemeList; } catch (err) { logger.error(`Failed get lastSeenMeme`, err.message); throw new CustomError( @@ -170,55 +159,38 @@ async function getLastSeenMemes(user: IUserDocument): Promise { try { - const totalSavedMemes = await MemeInteractionModel.countDocuments({ - deviceId: user.deviceId, - interactionType: InteractionType.SAVE, - isDeleted: false, - }); + const totalSavedMemes = await MemeInteractionService.getMemeInteractionCount( + user, + InteractionType.SAVE, + ); - const savedMemes = await MemeInteractionModel.find( - { - deviceId: user.deviceId, - interactionType: InteractionType.SAVE, - isDeleted: false, - }, - { isDeleted: 0 }, - ) - .skip((page - 1) * size) - .limit(size) - .sort({ createdAt: -1 }) - .lean(); + const savedMemeInteractionList = await MemeInteractionService.getMemeInteractionList( + page, + size, + user, + InteractionType.SAVE, + ); - const memeIds = savedMemes.map(({ memeId }) => new Types.ObjectId(memeId)); + const memeIds = savedMemeInteractionList.map(({ memeId }) => memeId); const memeList = await MemeModel.find( { _id: { $in: memeIds }, isDeleted: false }, { isDeleted: 0 }, ).lean(); - const ret = await Promise.all( - memeList.map(async (meme) => { - const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); - const isSaved = await MemeInteractionModel.findOne({ - deviceId: user.deviceId, - memeId: meme._id, - type: InteractionType.SAVE, - isDeleted: false, - }); - return { ..._.omit(meme, 'keywordIds'), keywords, isSaved: !_.isNil(isSaved) }; - }), - ); + const savedMemeList = await MemeService.getMemeListWithKeywordsAndisSaved(user, memeList); + logger.info(`Get savedMemeList - deviceId(${user.deviceId}), memeList(${savedMemeList})`); return { total: totalSavedMemes, page, totalPages: Math.ceil(totalSavedMemes / size), - data: ret, + data: savedMemeList, }; } catch (error) { throw new CustomError(`Failed to get saved memes`, HttpCode.INTERNAL_SERVER_ERROR, error); @@ -275,8 +247,8 @@ export { getUser, createUser, updateLastSeenMeme, - getLastSeenMemes, - getSavedMemes, + getLastSeenMemeList, + getSavedMemeList, makeUserInfos, createMemeRecommendWatch, }; From 657347b0d83e78b1875aa3f43ebb4de78262a5a9 Mon Sep 17 00:00:00 2001 From: seohyun0120 Date: Sun, 14 Jul 2024 11:57:03 +0900 Subject: [PATCH 13/19] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EB=A0=88=EB=B2=A8?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD,=20error=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/service/keyword.service.ts | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/service/keyword.service.ts b/src/service/keyword.service.ts index 2246579..7eda07b 100644 --- a/src/service/keyword.service.ts +++ b/src/service/keyword.service.ts @@ -85,21 +85,29 @@ async function increaseSearchCount(keywordId: Types.ObjectId): Promise { +async function getKeywordByName(keywordName: string): Promise { try { const keyword = await KeywordModel.findOne({ name: keywordName, isDeleted: false }).lean(); - return keyword; + return keyword || null; } catch (err) { - logger.info(`Failed to get a Keyword Info By Name(${keywordName})`); + logger.error(`Failed to get a Keyword Info By Name(${keywordName})`); + throw new CustomError( + `Failed to get a Keyword Info By Name(${keywordName}) (${err.message})`, + HttpCode.INTERNAL_SERVER_ERROR, + ); } } -async function getKeywordById(keywordId: Types.ObjectId): Promise { +async function getKeywordById(keywordId: Types.ObjectId): Promise { try { const keyword = await KeywordModel.findOne({ _id: keywordId, isDeleted: false }).lean(); - return keyword; + return keyword || null; } catch (err) { logger.info(`Failed to get a Keyword Info By id (${keywordId})`); + throw new CustomError( + `Failed to get a Keyword Info By id(${keywordId}) (${err.message})`, + HttpCode.INTERNAL_SERVER_ERROR, + ); } } @@ -116,7 +124,11 @@ async function getKeywordInfoByKeywordIds( ).lean(); return keyword; } catch (err) { - logger.info(`Failed to get a Keyword Info By id (${keywordIds})`); + logger.error(`Failed to get a Keyword Info By keywordIds(${JSON.stringify(keywordIds)})`); + throw new CustomError( + `Failed to get a Keyword Info By keywordIds(${JSON.stringify(keywordIds)})`, + HttpCode.INTERNAL_SERVER_ERROR, + ); } } From b0289e118f2ee9f014afbd0de4d367dc404ccb0b Mon Sep 17 00:00:00 2001 From: seohyun0120 Date: Sun, 14 Jul 2024 12:02:42 +0900 Subject: [PATCH 14/19] =?UTF-8?q?isNull=EB=A1=9C=20null=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/middleware/requestedInfo.ts | 5 ++--- src/service/keyword.service.ts | 4 ++-- src/service/keywordCategory.service.ts | 12 +++++++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/middleware/requestedInfo.ts b/src/middleware/requestedInfo.ts index 0527baa..ea6841e 100644 --- a/src/middleware/requestedInfo.ts +++ b/src/middleware/requestedInfo.ts @@ -56,8 +56,7 @@ export const getKeywordInfoByName = async ( } const keyword = await getKeywordByName(keywordName); - - if (!keyword) { + if (_.isNull(keyword)) { return next( new CustomError(`Keyword with name ${keywordName} does not exist`, HttpCode.NOT_FOUND), ); @@ -100,7 +99,7 @@ export const getRequestedUserInfo = async ( const user = await getUser(deviceId); - if (!user) { + if (_.isNull(user)) { return next(new CustomError(`user(${deviceId}) does not exist`, HttpCode.NOT_FOUND)); } diff --git a/src/service/keyword.service.ts b/src/service/keyword.service.ts index 7eda07b..6bb7704 100644 --- a/src/service/keyword.service.ts +++ b/src/service/keyword.service.ts @@ -49,7 +49,7 @@ async function updateKeyword( } async function deleteKeyword(keywordId: Types.ObjectId): Promise { const deletedKeyword = await KeywordModel.findOneAndDelete({ _id: keywordId }).lean(); - if (!deletedKeyword) { + if (_.isNull(deletedKeyword)) { throw new CustomError(`Keyword with ID ${keywordId} not found`, HttpCode.NOT_FOUND); } return true; @@ -75,7 +75,7 @@ async function increaseSearchCount(keywordId: Types.ObjectId): Promise { }, { isDeleted: true }, ); - if (!deletedCategory) { + if (_.isNull(deletedCategory)) { throw new CustomError(`Category with Name ${categoryName} not found`, HttpCode.NOT_FOUND); } return true; } -async function getKeywordCategory(categoryName: string): Promise { +async function getKeywordCategory(categoryName: string): Promise { const keywordCategory = await KeywordCategoryModel.findOne({ name: categoryName, isDeleted: false, }); - if (!keywordCategory) { + if (_.isNull(keywordCategory)) { throw new CustomError(`Category with Name ${categoryName} not found`, HttpCode.NOT_FOUND); } From 86cf418abfdb926e39bb3f0ffa6f6952c94df70a Mon Sep 17 00:00:00 2001 From: seohyun0120 Date: Sun, 14 Jul 2024 12:02:49 +0900 Subject: [PATCH 15/19] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/service/user.service.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/service/user.service.ts b/src/service/user.service.ts index 81018cb..0d29bee 100644 --- a/src/service/user.service.ts +++ b/src/service/user.service.ts @@ -147,13 +147,15 @@ async function getLastSeenMemeList(user: IUserDocument): Promise Date: Sun, 14 Jul 2024 12:03:38 +0900 Subject: [PATCH 16/19] =?UTF-8?q?MemeInteractionService=20=EC=93=B0?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/service/meme.service.ts | 216 ++++++++++++++++-------------------- 1 file changed, 97 insertions(+), 119 deletions(-) diff --git a/src/service/meme.service.ts b/src/service/meme.service.ts index 49612b6..7440f55 100644 --- a/src/service/meme.service.ts +++ b/src/service/meme.service.ts @@ -1,14 +1,14 @@ import _ from 'lodash'; import { Types } from 'mongoose'; +import * as KeywordService from './keyword.service'; +import * as MemeInteractionService from './memeInteraction.service'; import CustomError from '../errors/CustomError'; import { HttpCode } from '../errors/HttpCode'; -import { IKeywordDocument, KeywordModel } from '../model/keyword'; +import { IKeywordDocument } from '../model/keyword'; import { IMemeCreatePayload, IMemeDocument, MemeModel, IMemeGetResponse } from '../model/meme'; -import { InteractionType, MemeInteractionModel } from '../model/memeInteraction'; +import { InteractionType } from '../model/memeInteraction'; import { IUserDocument } from '../model/user'; - -import * as KeywordService from './keyword.service'; import { logger } from '../util/logger'; async function getMeme(memeId: string): Promise { @@ -17,12 +17,7 @@ async function getMeme(memeId: string): Promise { .and([{ isDeleted: false }]) .lean(); - if (!meme) { - logger.info(`Meme(${memeId}) not found.`); - return null; - } - - return meme; + return meme || null; } catch (err) { logger.error(`Failed to get a meme(${memeId}): ${err.message}`); throw new CustomError(`Failed to get a meme(${memeId})`, HttpCode.INTERNAL_SERVER_ERROR); @@ -35,12 +30,11 @@ async function getMemeWithKeywords( ): Promise { try { const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); - const isSaved = await MemeInteractionModel.findOne({ - deviceId: user.deviceId, - memeId: meme._id, - type: InteractionType.SAVE, - isDeleted: false, - }); + const isSaved = await MemeInteractionService.getMemeInteractionInfo( + user, + meme, + InteractionType.SAVE, + ); return { ..._.omit(meme, 'keywordIds'), @@ -48,8 +42,11 @@ async function getMemeWithKeywords( isSaved: !_.isNil(isSaved), }; } catch (err) { - logger.error(`Failed to get a meme(${meme._id}): ${err.message}`); - throw new CustomError(`Failed to get a meme(${meme._id})`, HttpCode.INTERNAL_SERVER_ERROR); + logger.error(`Failed to get a meme(${meme._id}) with keywords: ${err.message}`); + throw new CustomError( + `Failed to get a meme(${meme._id}) with keywords`, + HttpCode.INTERNAL_SERVER_ERROR, + ); } } @@ -57,30 +54,27 @@ async function getTodayMemeList( limit: number = 5, user: IUserDocument, ): Promise { - const todayMemeList = await MemeModel.find( - { isDeleted: false, isTodayMeme: true }, - { isDeleted: 0 }, - ); + try { + const todayMemeList = await MemeModel.find( + { isDeleted: false, isTodayMeme: true }, + { isDeleted: 0 }, + ); - const ret = await Promise.all( - todayMemeList.map(async (meme) => { - const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); - const isSaved = await MemeInteractionModel.findOne({ - deviceId: user.deviceId, - memeId: meme._id, - type: InteractionType.SAVE, - isDeleted: false, - }); - return { ..._.omit(meme, 'keywordIds'), keywords, isSaved: !_.isNil(isSaved) }; - }), - ); + const memeList = await getMemeListWithKeywordsAndisSaved(user, todayMemeList); - const memeIds = todayMemeList.map((meme) => meme._id); - logger.info( - `Get all today meme list(${todayMemeList.length}) - memeIds(${memeIds}), limit(${limit})`, - ); + const memeIds = todayMemeList.map((meme) => meme._id); + logger.info( + `Get all today meme list(${todayMemeList.length}) - memeIds(${memeIds}), limit(${limit})`, + ); - return ret; + return memeList; + } catch (err) { + logger.error(`Failed to get today meme list: ${err.message}`); + throw new CustomError( + `Failed to get today meme list ${err.message}`, + HttpCode.INTERNAL_SERVER_ERROR, + ); + } } async function getAllMemeList( @@ -95,18 +89,7 @@ async function getAllMemeList( .limit(size) .sort({ createdAt: -1 }); - const ret: IMemeGetResponse[] = await Promise.all( - memeList.map(async (meme) => { - const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); - const isSaved = await MemeInteractionModel.findOne({ - deviceId: user.deviceId, - memeId: meme._id, - type: InteractionType.SAVE, - isDeleted: false, - }); - return { ..._.omit(meme.toObject(), 'keywordIds'), keywords, isSaved: !_.isNil(isSaved) }; - }), - ); + const allMemeList = await getMemeListWithKeywordsAndisSaved(user, memeList); logger.info(`Get all meme list - page(${page}), size(${size}), total(${totalMemes})`); @@ -114,10 +97,40 @@ async function getAllMemeList( total: totalMemes, page, totalPages: Math.ceil(totalMemes / size), - data: ret, + data: allMemeList, }; } +// MemeList에서 keywords와 isSaved 정보를 확인하여 추가 반환 +async function getMemeListWithKeywordsAndisSaved( + user: IUserDocument, + memeList: IMemeDocument[], +): Promise { + try { + return await Promise.all( + memeList.map(async (meme: IMemeDocument) => { + const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); + const isSaved = await MemeInteractionService.getMemeInteractionInfo( + user, + meme, + InteractionType.SAVE, + ); + return { + ..._.omit(meme, 'keywordIds'), + keywords, + isSaved: !_.isNil(isSaved), + } as IMemeGetResponse; + }), + ); + } catch (err) { + logger.error('Failed to get keywords and isSaved info from meme list', err.message); + throw new CustomError( + `Failed to get keywords and isSaved info from meme list ${err.message}`, + HttpCode.INTERNAL_SERVER_ERROR, + ); + } +} + async function createMeme(info: IMemeCreatePayload): Promise { const meme = await MemeModel.create({ ...info, @@ -170,6 +183,7 @@ async function searchMemeByKeyword( page: number, size: number, keyword: IKeywordDocument, + user: IUserDocument, ): Promise<{ total: number; page: number; totalPages: number; data: IMemeGetResponse[] }> { try { const totalMemes = await MemeModel.countDocuments({ @@ -177,7 +191,7 @@ async function searchMemeByKeyword( isDeleted: false, }); - const memeList = await MemeModel.find( + const searchedMemeList = await MemeModel.find( { isDeleted: false, keywordIds: { $in: keyword._id } }, { isDeleted: 0 }, ) @@ -186,17 +200,7 @@ async function searchMemeByKeyword( .sort({ reaction: -1 }) .lean(); - const ret = await Promise.all( - memeList.map(async (meme) => { - const keywords = await KeywordService.getKeywordInfoByKeywordIds(meme.keywordIds); - const isSaved = await MemeInteractionModel.findOne({ - memeId: meme._id, - type: InteractionType.SAVE, - isDeleted: false, - }); - return { ..._.omit(meme, 'keywordIds'), keywords, isSaved: !_.isNil(isSaved) }; - }), - ); + const memeList = await getMemeListWithKeywordsAndisSaved(user, searchedMemeList); logger.info( `Get all meme list with keyword(${keyword.name}) - page(${page}), size(${size}), total(${totalMemes})`, @@ -206,7 +210,7 @@ async function searchMemeByKeyword( total: totalMemes, page, totalPages: Math.ceil(totalMemes / size), - data: ret, + data: memeList, }; } catch (err) { logger.error(`Failed to search meme list with keyword(${keyword})`, err.message); @@ -223,54 +227,30 @@ async function createMemeInteraction( interactionType: InteractionType, ): Promise { try { - const memeInteraction = await MemeInteractionModel.findOne({ - memeId: meme._id, - deviceId: user.deviceId, + // interaction 조회 + const memeInteraction = await MemeInteractionService.getMemeInteractionInfo( + user, + meme, interactionType, - }); + ); - // 밈당 interaction은 1회 - if (!_.isNull(memeInteraction)) { + if (_.isNull(memeInteraction)) { + // 신규 생성 + await MemeInteractionService.createMemeInteraction(user, meme, interactionType); + } else { logger.info( `Already ${interactionType} meme - deviceId(${user.deviceId}), memeId(${meme._id}`, ); - if (interactionType === InteractionType.SAVE && memeInteraction.isDeleted) { - // 'save'인 경우 isDeleted를 false로 업데이트한다. - await MemeInteractionModel.findOneAndUpdate( - { memeId: meme._id, deviceId: user.deviceId, interactionType }, - { $set: { isDeleted: false } }, - ); - } else if (interactionType === InteractionType.REACTION) { - // 'reaction'인 경우에만 Meme의 reaction 수를 업데이트한다. - await MemeModel.findOneAndUpdate( - { memeId: meme._id, isDeleted: false }, - { $inc: { reaction: 1 } }, - { - projection: { _id: 0, createdAt: 0, updatedAt: 0 }, - returnDocument: 'after', - }, - ).lean(); - } else { - // 'watch', 'share'인 경우 isDeleted 여부 로그를 남긴다. - logger.debug( - `${memeInteraction.interactionType} document exist - isDeleted(${memeInteraction.isDeleted})`, - ); - } - } else { - const newMemeInteraction = await MemeInteractionModel.create({ - memeId: meme._id, - deviceId: user.deviceId, - interactionType, - }); - await newMemeInteraction.save(); + // interactionType에 따른 동작 처리 (MemeInteracionService에서 진행) + await MemeInteractionService.updateMemeInteraction(user, meme, interactionType); } - return true; } catch (err) { - logger.error(`Failed to create memeInteraction`, err.message); + logger.error(`Failed to create memeInteraction(${interactionType})`, err.message); + throw new CustomError( - `Failed to create memeInteraction(${err.message})`, + `Failed to create memeInteraction(${interactionType}) (${err.message})`, HttpCode.INTERNAL_SERVER_ERROR, ); } @@ -278,29 +258,26 @@ async function createMemeInteraction( async function deleteMemeSave(user: IUserDocument, meme: IMemeDocument): Promise { try { - const meemSaveInteraction = await MemeInteractionModel.findOne({ - memeId: meme._id, - deviceId: user.deviceId, - interactionType: InteractionType.SAVE, - isDeleted: false, - }); + const memeSaveInteraction = await MemeInteractionService.getMemeInteractionInfoWithCondition( + user, + meme, + InteractionType.SAVE, + { isDeleted: true }, + ); - if (_.isNull(meemSaveInteraction)) { + if (!_.isNull(memeSaveInteraction)) { logger.info(`Already delete memeSave - deviceId(${user.deviceId}), memeId(${meme._id}`); return false; } - await MemeInteractionModel.findOneAndUpdate( - { deviceId: user.deviceId, memeId: meme._id, interactionType: InteractionType.SAVE }, - { - isDeleted: true, - }, - ).lean(); - + await MemeInteractionService.deleteMemeInteraction(user, meme, InteractionType.SAVE); return true; } catch (err) { - logger.error(`Failed delete memeSave`, err.message); - throw new CustomError(`Failed delete memeSave(${err.message})`, HttpCode.INTERNAL_SERVER_ERROR); + logger.error(`Failed to delete meme save`, err.message); + throw new CustomError( + `Failed to delete meme save(${err.message})`, + HttpCode.INTERNAL_SERVER_ERROR, + ); } } async function getTopReactionImage(keyword: IKeywordDocument): Promise { @@ -332,6 +309,7 @@ export { deleteMemeSave, getTodayMemeList, getAllMemeList, + getMemeListWithKeywordsAndisSaved, deleteKeywordOfMeme, getMemeWithKeywords, searchMemeByKeyword, From 244600c5f120cc23d9058df8678fd37f6c4ee1b7 Mon Sep 17 00:00:00 2001 From: seohyun0120 Date: Sun, 14 Jul 2024 12:29:01 +0900 Subject: [PATCH 17/19] =?UTF-8?q?=EB=B0=88=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=8B=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=EB=90=9C=20?= =?UTF-8?q?=EB=B0=88=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controller/meme.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/meme.controller.ts b/src/controller/meme.controller.ts index cd8a6ee..7f37703 100644 --- a/src/controller/meme.controller.ts +++ b/src/controller/meme.controller.ts @@ -51,7 +51,7 @@ const getMemeWithKeywords = async (req: CustomRequest, res: Response, next: Next } logger.info(`Get meme with keywords - ${meme._id})`); - return res.json(createSuccessResponse(HttpCode.OK, 'Get Meme', meme)); + return res.json(createSuccessResponse(HttpCode.OK, 'Get Meme', ret)); } catch (err) { return next(new CustomError(err.message, err.status)); } From 97d892c082ef2108845fa8a5fe15dc705d41c252 Mon Sep 17 00:00:00 2001 From: seohyun0120 Date: Sun, 14 Jul 2024 12:29:53 +0900 Subject: [PATCH 18/19] =?UTF-8?q?=EC=8A=A4=ED=8E=99=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20-=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=20=EC=B6=94=EA=B0=80,=20user=20mock=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- get-s3.js | 1 + test/meme/delete-meme.test.ts | 7 ++++++- test/meme/get-meme-list.test.ts | 19 +++++++++++++++---- test/meme/get-meme.test.ts | 13 +++++++++++-- test/meme/get-recommend-meme-list.test.ts | 22 +++++++++++++--------- test/meme/patch-meme.test.ts | 9 +++++++-- 6 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 get-s3.js diff --git a/get-s3.js b/get-s3.js new file mode 100644 index 0000000..b3b809c --- /dev/null +++ b/get-s3.js @@ -0,0 +1 @@ +const AWS = require('aws-sdk') \ No newline at end of file diff --git a/test/meme/delete-meme.test.ts b/test/meme/delete-meme.test.ts index 7644d36..dbf29f1 100644 --- a/test/meme/delete-meme.test.ts +++ b/test/meme/delete-meme.test.ts @@ -3,8 +3,10 @@ import request from 'supertest'; import app from '../../src/app'; import { KeywordModel } from '../../src/model/keyword'; import { MemeModel } from '../../src/model/meme'; +import { UserModel } from '../../src/model/user'; import { createMockData as createKeywordMockData } from '../util/keyword.mock'; import { createMockData } from '../util/meme.mock'; +import { mockUser } from '../util/user.mock'; let testMemeId = ''; let keywordIds = []; @@ -20,10 +22,13 @@ describe("[DELETE] '/api/meme/:memeId' ", () => { await MemeModel.insertMany(mockDatas); memeList = await MemeModel.find({}); testMemeId = memeList[0]._id.toString(); + + await UserModel.insertMany(mockUser); }); afterAll(async () => { await MemeModel.deleteMany({}); + await UserModel.deleteMany({}); }); it('should delete a meme', async () => { @@ -31,7 +36,7 @@ describe("[DELETE] '/api/meme/:memeId' ", () => { expect(response.statusCode).toBe(200); expect(response.body.data).toBeTruthy(); - response = await request(app).get(`/api/meme/list`); + response = await request(app).get(`/api/meme/list`).set('x-device-id', 'deviceId'); expect(response.statusCode).toBe(200); expect(response.body.data.memeList.length).toBe(1); }); diff --git a/test/meme/get-meme-list.test.ts b/test/meme/get-meme-list.test.ts index 8947025..d7abada 100644 --- a/test/meme/get-meme-list.test.ts +++ b/test/meme/get-meme-list.test.ts @@ -3,8 +3,10 @@ import request from 'supertest'; import app from '../../src/app'; import { KeywordModel } from '../../src/model/keyword'; import { MemeModel } from '../../src/model/meme'; +import { UserModel } from '../../src/model/user'; import { createMockData as createKeywordMockData } from '../util/keyword.mock'; import { createMockData } from '../util/meme.mock'; +import { mockUser } from '../util/user.mock'; const totalCount = 15; let keywordIds = []; @@ -19,14 +21,17 @@ describe("[GET] '/api/meme/list' ", () => { const memeMockDatas = createMockData(totalCount, 1, keywordIds); await MemeModel.insertMany(memeMockDatas); + + await UserModel.insertMany(mockUser); }); afterAll(async () => { await MemeModel.deleteMany({}); + await UserModel.deleteMany({}); }); it('should return the default paginated list of memes', async () => { - const response = await request(app).get('/api/meme/list'); + const response = await request(app).get('/api/meme/list').set('x-device-id', 'deviceId'); expect(response.statusCode).toBe(200); expect(response.body.data.pagination.total).toBe(totalCount); expect(response.body.data.pagination.page).toBe(1); @@ -42,7 +47,9 @@ describe("[GET] '/api/meme/list' ", () => { it('should return paginated list of memes for specific page and size', async () => { const size = 5; const page = 1; - const response = await request(app).get(`/api/meme/list?page=${page}&size=${size}`); + const response = await request(app) + .get(`/api/meme/list?page=${page}&size=${size}`) + .set('x-device-id', 'deviceId'); expect(response.statusCode).toBe(200); expect(response.body.data.pagination.total).toBe(totalCount); @@ -54,7 +61,9 @@ describe("[GET] '/api/meme/list' ", () => { it('should return an error for invalid page', async () => { const size = 5; const page = -1; - const response = await request(app).get(`/api/meme/list?page=${page}&size=${size}`); + const response = await request(app) + .get(`/api/meme/list?page=${page}&size=${size}`) + .set('x-device-id', 'deviceId'); expect(response.statusCode).toBe(400); }); @@ -62,7 +71,9 @@ describe("[GET] '/api/meme/list' ", () => { it('should return an error for invalid size', async () => { const size = -1; const page = 3; - const response = await request(app).get(`/api/meme/list?page=${page}&size=${size}`); + const response = await request(app) + .get(`/api/meme/list?page=${page}&size=${size}`) + .set('x-device-id', 'deviceId'); expect(response.statusCode).toBe(400); }); diff --git a/test/meme/get-meme.test.ts b/test/meme/get-meme.test.ts index d21b6c0..0205704 100644 --- a/test/meme/get-meme.test.ts +++ b/test/meme/get-meme.test.ts @@ -4,8 +4,10 @@ import request from 'supertest'; import app from '../../src/app'; import { KeywordModel } from '../../src/model/keyword'; import { MemeModel } from '../../src/model/meme'; +import { UserModel } from '../../src/model/user'; import { createMockData as createKeywordMockData } from '../util/keyword.mock'; import { createMockData as createMemeMockData } from '../util/meme.mock'; +import { mockUser } from '../util/user.mock'; let testMemeId = ''; let keywordIds = []; @@ -23,21 +25,28 @@ describe("[GET] '/api/meme/:memeId' ", () => { memeList = await MemeModel.find({}); testMemeId = memeList[0]._id.toString(); + + await UserModel.insertMany(mockUser); }); afterAll(async () => { await MemeModel.deleteMany({}); + await UserModel.deleteMany({}); }); it('should get a meme', async () => { - const response = await request(app).get(`/api/meme/${testMemeId}`); + const response = await request(app) + .get(`/api/meme/${testMemeId}`) + .set('x-device-id', 'deviceId'); expect(response.body.data._id).toBe(testMemeId); expect(response.body.data).toHaveProperty('keywords'); expect(response.body.data.isTodayMeme).toBeFalsy(); }); it('should not get a meme with nonexisting id', async () => { - const response = await request(app).get(`/api/meme/nonexistingId`); + const response = await request(app) + .get(`/api/meme/nonexistingId`) + .set('x-device-id', 'deviceId'); expect(response.statusCode).toBe(400); }); }); diff --git a/test/meme/get-recommend-meme-list.test.ts b/test/meme/get-recommend-meme-list.test.ts index 1c0fc2b..e5f35a9 100644 --- a/test/meme/get-recommend-meme-list.test.ts +++ b/test/meme/get-recommend-meme-list.test.ts @@ -3,31 +3,35 @@ import request from 'supertest'; import app from '../../src/app'; import { KeywordModel } from '../../src/model/keyword'; import { MemeModel } from '../../src/model/meme'; +import { UserModel } from '../../src/model/user'; import { createMockData as createKeywordMockData } from '../util/keyword.mock'; import { createMockData } from '../util/meme.mock'; +import { mockUser } from '../util/user.mock'; const totalCount = 10; let keywordIds = []; -let keywords = []; describe("[GET] '/api/meme/recommend-memes' ", () => { beforeEach(async () => { const keywordMockDatas = createKeywordMockData(5); const createdKeywords = await KeywordModel.insertMany(keywordMockDatas); keywordIds = createdKeywords.map((k) => k._id); - keywords = createdKeywords.map((k) => k.name); + await UserModel.insertMany(mockUser); }); afterEach(async () => { await MemeModel.deleteMany({}); await KeywordModel.deleteMany({}); + await UserModel.deleteMany({}); }); it('should return list of recommend-memes - default size: 5', async () => { const mockDatas = createMockData(totalCount, 5, keywordIds); await MemeModel.insertMany(mockDatas); - const response = await request(app).get('/api/meme/recommend-memes'); + const response = await request(app) + .get('/api/meme/recommend-memes') + .set('x-device-id', 'deviceId'); expect(response.statusCode).toBe(200); expect(response.body.data.length).toBe(5); }); @@ -37,9 +41,9 @@ describe("[GET] '/api/meme/recommend-memes' ", () => { const mockDatas = createMockData(totalCount, customizedTodayMemeCount, keywordIds); await MemeModel.insertMany(mockDatas); - const response = await request(app).get( - `/api/meme/recommend-memes?size=${customizedTodayMemeCount}`, - ); + const response = await request(app) + .get(`/api/meme/recommend-memes?size=${customizedTodayMemeCount}`) + .set('x-device-id', 'deviceId'); expect(response.statusCode).toBe(200); expect(response.body.data.length).toBe(customizedTodayMemeCount); @@ -50,9 +54,9 @@ describe("[GET] '/api/meme/recommend-memes' ", () => { const mockDatas = createMockData(totalCount, customizedTodayMemeCount, keywordIds); await MemeModel.insertMany(mockDatas); - const response = await request(app).get( - `/api/meme/recommend-memes?size=${customizedTodayMemeCount}`, - ); + const response = await request(app) + .get(`/api/meme/recommend-memes?size=${customizedTodayMemeCount}`) + .set('x-device-id', 'deviceId'); expect(response.statusCode).toBe(400); }); diff --git a/test/meme/patch-meme.test.ts b/test/meme/patch-meme.test.ts index 4714195..7434843 100644 --- a/test/meme/patch-meme.test.ts +++ b/test/meme/patch-meme.test.ts @@ -3,8 +3,10 @@ import request from 'supertest'; import app from '../../src/app'; import { KeywordModel } from '../../src/model/keyword'; import { IMemeUpdatePayload, MemeModel } from '../../src/model/meme'; +import { UserModel } from '../../src/model/user'; import { createMockData as createKeywordMockData } from '../util/keyword.mock'; import { createMockData } from '../util/meme.mock'; +import { mockUser } from '../util/user.mock'; let memeList = []; let keywordIds = []; @@ -22,10 +24,13 @@ describe("[PATCH] '/api/meme/:memeId' ", () => { await MemeModel.insertMany(mockDatas); memeList = await MemeModel.find({}); testMemeId = memeList[0]._id.toString(); + + await UserModel.insertMany(mockUser); }); afterAll(async () => { await MemeModel.deleteMany({}); + await UserModel.deleteMany({}); }); it('should patch a meme', async () => { @@ -37,9 +42,9 @@ describe("[PATCH] '/api/meme/:memeId' ", () => { expect(response.statusCode).toBe(200); expect(response.body.data._id).toBe(memeList[0]._id.toString()); - response = await request(app).get(`/api/meme/${testMemeId}`); + response = await request(app).get(`/api/meme/${testMemeId}`).set('x-device-id', 'deviceId'); expect(response.body.data._id).toBe(memeList[0]._id.toString()); - expect(response.body.data.keywords).toEqual([keywords[1]]); + expect(response.body.data.keywords).toHaveProperty({ _id: keywordIds[1], name: keywords[1] }); expect(response.body.data.isTodayMeme).toBeTruthy(); }); }); From 55a097d853fc148df9c1e5e323509db155d07bff Mon Sep 17 00:00:00 2001 From: seohyun0120 Date: Sun, 14 Jul 2024 12:30:52 +0900 Subject: [PATCH 19/19] =?UTF-8?q?keywords=20=ED=95=84=EB=93=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/meme/patch-meme.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/meme/patch-meme.test.ts b/test/meme/patch-meme.test.ts index 7434843..6bee178 100644 --- a/test/meme/patch-meme.test.ts +++ b/test/meme/patch-meme.test.ts @@ -44,7 +44,8 @@ describe("[PATCH] '/api/meme/:memeId' ", () => { response = await request(app).get(`/api/meme/${testMemeId}`).set('x-device-id', 'deviceId'); expect(response.body.data._id).toBe(memeList[0]._id.toString()); - expect(response.body.data.keywords).toHaveProperty({ _id: keywordIds[1], name: keywords[1] }); + expect(response.body.data.keywords[0]).toHaveProperty('_id'); + expect(response.body.data.keywords[0]).toHaveProperty('name'); expect(response.body.data.isTodayMeme).toBeTruthy(); }); });