From 530e97eea708ff98ea993ab0a9317b9c93247bbb Mon Sep 17 00:00:00 2001 From: kubrickcode Date: Tue, 25 Nov 2025 13:29:52 +0000 Subject: [PATCH] =?UTF-8?q?refactor(backend):=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EA=B3=84=EC=B8=B5=20=EC=BD=94=EB=93=9C=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UserContentService: getUserOverride 제네릭 헬퍼로 4개 메서드의 반복 패턴 통합 - UserContentService: getItemPrice 중복 DB 쿼리 제거 (2회→1회) - ContentWageService: calculateContentWageData로 80% 중복 로직 통합 - ContentWageService: Promise.all로 병렬 처리 개선 - 공통 상수 DEFAULT_CONTENT_ORDER_BY 추출 (ContentService, GroupService) - 매직 넘버 SECONDS_PER_HOUR 상수화 fix #235 --- .../src/content/content/content.service.ts | 29 +-- .../src/content/group/group.service.ts | 15 +- src/backend/src/content/shared/constants.ts | 14 ++ src/backend/src/content/wage/wage.service.ts | 119 +++++------- .../src/user/service/user-content.service.ts | 177 +++++++++--------- 5 files changed, 151 insertions(+), 203 deletions(-) diff --git a/src/backend/src/content/content/content.service.ts b/src/backend/src/content/content/content.service.ts index 95ffdec5..d55b0a04 100644 --- a/src/backend/src/content/content/content.service.ts +++ b/src/backend/src/content/content/content.service.ts @@ -3,6 +3,7 @@ import { Content, Prisma } from "@prisma/client"; import { OrderByArg } from "src/common/object/order-by-arg.object"; import { NotFoundException } from "src/common/exception/not-found.exception"; import { PrismaService } from "src/prisma"; +import { DEFAULT_CONTENT_ORDER_BY } from "../shared/constants"; import { ContentListFilter, ContentsFilter, @@ -66,19 +67,7 @@ export class ContentService { return orderBy.map((order) => ({ [order.field]: order.order })); } - return [ - { - contentCategory: { - id: "asc", - }, - }, - { - level: "asc", - }, - { - id: "asc", - }, - ]; + return DEFAULT_CONTENT_ORDER_BY; } async createContent(input: CreateContentInput): Promise { @@ -153,19 +142,7 @@ export class ContentService { async findContents(filter?: ContentsFilter): Promise { return await this.prisma.content.findMany({ - orderBy: [ - { - contentCategory: { - id: "asc", - }, - }, - { - level: "asc", - }, - { - id: "asc", - }, - ], + orderBy: DEFAULT_CONTENT_ORDER_BY, where: this.buildContentsWhere(filter), }); } diff --git a/src/backend/src/content/group/group.service.ts b/src/backend/src/content/group/group.service.ts index 0bd3a3ab..cbe404a3 100644 --- a/src/backend/src/content/group/group.service.ts +++ b/src/backend/src/content/group/group.service.ts @@ -5,6 +5,7 @@ import { ValidationException } from "src/common/exception"; import { OrderByArg } from "src/common/object/order-by-arg.object"; import { PrismaService } from "src/prisma"; import { UserContentService } from "src/user/service/user-content.service"; +import { DEFAULT_CONTENT_ORDER_BY } from "../shared/constants"; import { ContentWageService } from "../wage/wage.service"; import { ContentGroupFilter, ContentGroupWageListFilter } from "./group.dto"; import { ContentGroup, ContentGroupWage } from "./group.object"; @@ -171,19 +172,7 @@ export class GroupService { } private getContentOrderBy(): Prisma.ContentOrderByWithRelationInput[] { - return [ - { - contentCategory: { - id: "asc", - }, - }, - { - level: "asc", - }, - { - id: "asc", - }, - ]; + return DEFAULT_CONTENT_ORDER_BY; } private sortResults(results: ContentGroupWage[], orderBy: OrderByArg[]): ContentGroupWage[] { diff --git a/src/backend/src/content/shared/constants.ts b/src/backend/src/content/shared/constants.ts index ec1e33fb..3ea6123b 100644 --- a/src/backend/src/content/shared/constants.ts +++ b/src/backend/src/content/shared/constants.ts @@ -1,3 +1,17 @@ +import { Prisma } from "@prisma/client"; + +/** + * Content 기본 정렬 순서 + * - 카테고리 ID 오름차순 + * - 레벨 오름차순 + * - ID 오름차순 + */ +export const DEFAULT_CONTENT_ORDER_BY: Prisma.ContentOrderByWithRelationInput[] = [ + { contentCategory: { id: "asc" } }, + { level: "asc" }, + { id: "asc" }, +]; + // 추후 유지보수성이 확실치 않은 데이터 구조 및 순서라 db에서 관리하지 않고 임시로 상수로 보상 아이템 순서를 관리함. export const ItemSortOrder = { "1레벨 보석": 7, diff --git a/src/backend/src/content/wage/wage.service.ts b/src/backend/src/content/wage/wage.service.ts index 1cf2ab5e..e7662426 100644 --- a/src/backend/src/content/wage/wage.service.ts +++ b/src/backend/src/content/wage/wage.service.ts @@ -8,8 +8,21 @@ type Reward = { itemId: number; }; +type WageFilter = { + includeBound?: boolean; + includeItemIds?: number[]; + includeSeeMore?: boolean; +}; + +type ContentWageData = { + duration: number; + gold: number; +}; + @Injectable() export class ContentWageService { + private static readonly SECONDS_PER_HOUR = 3600; + constructor( private userContentService: UserContentService, private userGoldExchangeRateService: UserGoldExchangeRateService, @@ -57,7 +70,7 @@ export class ContentWageService { const totalKRW = (gold * goldExchangeRate.krwAmount) / goldExchangeRate.goldAmount; - const hours = duration / 3600; + const hours = duration / ContentWageService.SECONDS_PER_HOUR; const hourlyWage = totalKRW / hours; const hourlyGold = gold / hours; @@ -67,77 +80,47 @@ export class ContentWageService { }; } - async getContentGroupWage( - contentIds: number[], - userId: number | undefined, - filter: { - includeBound?: boolean; - includeItemIds?: number[]; - includeSeeMore?: boolean; - } - ) { - let totalGold = 0; - let totalDuration = 0; - - for (const contentId of contentIds) { - const content = await this.prisma.content.findUniqueOrThrow({ - where: { id: contentId }, - }); - - const rewards = await this.userContentService.getContentRewards(content.id, userId, { - includeBound: filter?.includeBound, - includeItemIds: filter?.includeItemIds, - }); - - const seeMoreRewards = await this.userContentService.getContentSeeMoreRewards( - content.id, - userId, - { - includeItemIds: filter?.includeItemIds, - } - ); - - const rewardsGold = await this.calculateGold(rewards, userId); + async getContentGroupWage(contentIds: number[], userId: number | undefined, filter: WageFilter) { + const dataList = await Promise.all( + contentIds.map((id) => this.calculateContentWageData(id, userId, filter)) + ); - const shouldIncludeSeeMoreRewards = - filter?.includeSeeMore && filter?.includeBound !== false && seeMoreRewards.length > 0; + const totalGold = dataList.reduce((sum, data) => sum + data.gold, 0); + const totalDuration = dataList.reduce((sum, data) => sum + data.duration, 0); - const seeMoreGold = shouldIncludeSeeMoreRewards - ? await this.calculateSeeMoreRewardsGold(seeMoreRewards, userId, filter.includeItemIds) - : 0; + const { goldAmountPerHour, krwAmountPerHour } = await this.calculateWage( + { duration: totalDuration, gold: totalGold }, + userId + ); - const gold = rewardsGold + seeMoreGold; - totalGold += gold; + return { + goldAmountPerClear: Math.round(totalGold), + goldAmountPerHour, + krwAmountPerHour, + }; + } - const duration = await this.userContentService.getContentDuration(content.id, userId); - totalDuration += duration; - } + async getContentWage(contentId: number, userId: number | undefined, filter: WageFilter) { + const { duration, gold } = await this.calculateContentWageData(contentId, userId, filter); const { goldAmountPerHour, krwAmountPerHour } = await this.calculateWage( - { - duration: totalDuration, - gold: totalGold, - }, + { duration, gold }, userId ); return { - goldAmountPerClear: Math.round(totalGold), + contentId, + goldAmountPerClear: Math.round(gold), goldAmountPerHour, krwAmountPerHour, }; } - // TODO: test - async getContentWage( + private async calculateContentWageData( contentId: number, userId: number | undefined, - filter: { - includeBound?: boolean; - includeItemIds?: number[]; - includeSeeMore?: boolean; - } - ) { + filter: WageFilter + ): Promise { const content = await this.prisma.content.findUniqueOrThrow({ where: { id: contentId }, }); @@ -157,30 +140,24 @@ export class ContentWageService { const rewardsGold = await this.calculateGold(rewards, userId); - const shouldIncludeSeeMoreRewards = - filter?.includeSeeMore && filter?.includeBound !== false && seeMoreRewards.length > 0; + const shouldIncludeSeeMoreRewards = this.shouldIncludeSeeMore(filter, seeMoreRewards); const seeMoreGold = shouldIncludeSeeMoreRewards ? await this.calculateSeeMoreRewardsGold(seeMoreRewards, userId, filter.includeItemIds) : 0; const gold = rewardsGold + seeMoreGold; - const duration = await this.userContentService.getContentDuration(content.id, userId); - const { goldAmountPerHour, krwAmountPerHour } = await this.calculateWage( - { - duration, - gold, - }, - userId - ); + return { duration, gold }; + } - return { - contentId: content.id, - goldAmountPerClear: Math.round(gold), - goldAmountPerHour, - krwAmountPerHour, - }; + private shouldIncludeSeeMore( + filter: WageFilter, + seeMoreRewards: { itemId: number; quantity: number }[] + ): boolean { + return ( + filter?.includeSeeMore === true && filter?.includeBound !== false && seeMoreRewards.length > 0 + ); } } diff --git a/src/backend/src/user/service/user-content.service.ts b/src/backend/src/user/service/user-content.service.ts index 9c9d0b80..b5506712 100644 --- a/src/backend/src/user/service/user-content.service.ts +++ b/src/backend/src/user/service/user-content.service.ts @@ -6,50 +6,40 @@ export class UserContentService { constructor(private readonly prisma: PrismaService) {} async getContentDuration(contentId: number, userId?: number) { - const contentDuration = await this.prisma.contentDuration.findUniqueOrThrow({ - where: { - contentId, - }, - }); - - if (userId) { - const userContentDuration = await this.prisma.userContentDuration.findUnique({ - where: { - contentId_userId: { - contentId, - userId, - }, - }, - }); - - return userContentDuration ? userContentDuration.value : contentDuration.value; - } - - return contentDuration.value; + return this.getUserOverride( + userId, + () => + this.prisma.contentDuration.findUniqueOrThrow({ + where: { contentId }, + }), + (userId) => + this.prisma.userContentDuration.findUnique({ + where: { contentId_userId: { contentId, userId } }, + }), + (entity) => entity.value + ); } async getContentRewardAverageQuantity(contentRewardId: number, userId?: number) { const contentReward = await this.prisma.contentReward.findUniqueOrThrow({ - where: { - id: contentRewardId, - }, + where: { id: contentRewardId }, }); - if (userId) { - const userContentReward = await this.prisma.userContentReward.findUnique({ - where: { - userId_contentId_itemId: { - contentId: contentReward.contentId, - itemId: contentReward.itemId, - userId, + return this.getUserOverride( + userId, + () => Promise.resolve(contentReward), + (userId) => + this.prisma.userContentReward.findUnique({ + where: { + userId_contentId_itemId: { + contentId: contentReward.contentId, + itemId: contentReward.itemId, + userId, + }, }, - }, - }); - - return userContentReward ? userContentReward.averageQuantity : contentReward.averageQuantity; - } - - return contentReward.averageQuantity; + }), + (entity) => entity.averageQuantity + ); } async getContentRewardIsSellable(contentRewardId: number, userId?: number) { @@ -57,21 +47,21 @@ export class UserContentService { where: { id: contentRewardId }, }); - if (userId) { - const userContentReward = await this.prisma.userContentReward.findUnique({ - where: { - userId_contentId_itemId: { - contentId: contentReward.contentId, - itemId: contentReward.itemId, - userId, + return this.getUserOverride( + userId, + () => Promise.resolve(contentReward), + (userId) => + this.prisma.userContentReward.findUnique({ + where: { + userId_contentId_itemId: { + contentId: contentReward.contentId, + itemId: contentReward.itemId, + userId, + }, }, - }, - }); - - return userContentReward ? userContentReward.isSellable : contentReward.isSellable; - } - - return contentReward.isSellable; + }), + (entity) => entity.isSellable + ); } async getContentRewards( @@ -93,11 +83,13 @@ export class UserContentService { where, }); - let result: { + type RewardResult = { averageQuantity: number; isSellable: boolean; itemId: number; - }[]; + }; + + let result: RewardResult[]; if (userId) { const userRewards = await this.prisma.userContentReward.findMany({ @@ -150,23 +142,21 @@ export class UserContentService { where: { id: contentSeeMoreRewardId }, }); - if (userId) { - const userContentSeeMoreReward = await this.prisma.userContentSeeMoreReward.findUnique({ - where: { - userId_contentId_itemId: { - contentId: contentSeeMoreReward.contentId, - itemId: contentSeeMoreReward.itemId, - userId, + return this.getUserOverride( + userId, + () => Promise.resolve(contentSeeMoreReward), + (userId) => + this.prisma.userContentSeeMoreReward.findUnique({ + where: { + userId_contentId_itemId: { + contentId: contentSeeMoreReward.contentId, + itemId: contentSeeMoreReward.itemId, + userId, + }, }, - }, - }); - - return userContentSeeMoreReward - ? userContentSeeMoreReward.quantity - : contentSeeMoreReward.quantity; - } - - return contentSeeMoreReward.quantity; + }), + (entity) => entity.quantity + ); } async getContentSeeMoreRewards( @@ -215,35 +205,20 @@ export class UserContentService { })); } - // Test 작성 async getItemPrice(itemId: number, userId?: number) { - const { price: defaultPrice } = await this.prisma.item.findUniqueOrThrow({ - where: { - id: itemId, - }, + const item = await this.prisma.item.findUniqueOrThrow({ + where: { id: itemId }, }); - const { isEditable } = await this.prisma.item.findUniqueOrThrow({ - where: { - id: itemId, - }, + if (!userId || !item.isEditable) { + return item.price.toNumber(); + } + + const userItem = await this.prisma.userItem.findUniqueOrThrow({ + where: { userId_itemId: { itemId, userId } }, }); - const price = - userId && isEditable - ? ( - await this.prisma.userItem.findUniqueOrThrow({ - where: { - userId_itemId: { - itemId, - userId, - }, - }, - }) - ).price - : defaultPrice; - - return price.toNumber(); + return userItem.price.toNumber(); } async validateUserItem(itemId: number, userId: number) { @@ -257,4 +232,20 @@ export class UserContentService { return true; } + + private async getUserOverride( + userId: number | undefined, + fetchDefault: () => Promise, + fetchUserOverride: (userId: number) => Promise, + extractValue: (source: TDefault | TUser) => TValue + ): Promise { + const defaultEntity = await fetchDefault(); + + if (!userId) { + return extractValue(defaultEntity); + } + + const userEntity = await fetchUserOverride(userId); + return userEntity ? extractValue(userEntity) : extractValue(defaultEntity); + } }