diff --git a/src/backend/package.json b/src/backend/package.json index f1231e9a..e575b43b 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -39,9 +39,9 @@ "dataloader": "2.2.3", "dayjs": "1.11.13", "discord-webhook-node": "1.1.8", + "es-toolkit": "1.42.0", "express-session": "1.18.1", "graphql": "16.9.0", - "lodash": "4.17.21", "nestjs-cls": "6.1.0", "passport": "0.7.0", "passport-discord": "0.1.4", @@ -61,7 +61,6 @@ "@types/express": "5.0.0", "@types/express-session": "1.18.1", "@types/jest": "29.5.2", - "@types/lodash": "4.17.13", "@types/node": "20.3.1", "@types/passport": "1.0.17", "@types/passport-discord": "0.1.14", diff --git a/src/backend/pnpm-lock.yaml b/src/backend/pnpm-lock.yaml index eb5244a5..b447c3f6 100644 --- a/src/backend/pnpm-lock.yaml +++ b/src/backend/pnpm-lock.yaml @@ -58,15 +58,15 @@ importers: discord-webhook-node: specifier: 1.1.8 version: 1.1.8 + es-toolkit: + specifier: 1.42.0 + version: 1.42.0 express-session: specifier: 1.18.1 version: 1.18.1 graphql: specifier: 16.9.0 version: 16.9.0 - lodash: - specifier: 4.17.21 - version: 4.17.21 nestjs-cls: specifier: 6.1.0 version: 6.1.0(@nestjs/common@10.4.16(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.0)(rxjs@7.8.1))(@nestjs/core@10.0.0)(reflect-metadata@0.2.0)(rxjs@7.8.1) @@ -119,9 +119,6 @@ importers: "@types/jest": specifier: 29.5.2 version: 29.5.2 - "@types/lodash": - specifier: 4.17.13 - version: 4.17.13 "@types/node": specifier: 20.3.1 version: 20.3.1 @@ -1889,12 +1886,6 @@ packages: integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, } - "@types/lodash@4.17.13": - resolution: - { - integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==, - } - "@types/long@4.0.2": resolution: { @@ -3339,6 +3330,12 @@ packages: } engines: { node: ">= 0.4" } + es-toolkit@1.42.0: + resolution: + { + integrity: sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==, + } + esbuild-register@3.6.0: resolution: { @@ -7702,8 +7699,6 @@ snapshots: "@types/json-schema@7.0.15": {} - "@types/lodash@4.17.13": {} - "@types/long@4.0.2": {} "@types/methods@1.1.4": {} @@ -8679,6 +8674,8 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 + es-toolkit@1.42.0: {} + esbuild-register@3.6.0(esbuild@0.27.0): dependencies: debug: 4.4.3 diff --git a/src/backend/src/content/group/group.service.spec.ts b/src/backend/src/content/group/group.service.spec.ts index 115a9107..95a91938 100644 --- a/src/backend/src/content/group/group.service.spec.ts +++ b/src/backend/src/content/group/group.service.spec.ts @@ -185,9 +185,9 @@ describe("GroupService", () => { const result = service.groupContentsByNameAndCategory(contents); - expect(result.size).toBe(2); - expect(result.get("아브렐슈드_1")).toHaveLength(2); - expect(result.get("쿠크세이튼_1")).toHaveLength(1); + expect(Object.keys(result)).toHaveLength(2); + expect(result["아브렐슈드_1"]).toHaveLength(2); + expect(result["쿠크세이튼_1"]).toHaveLength(1); }); it("should separate same name but different categories", () => { @@ -198,9 +198,9 @@ describe("GroupService", () => { const result = service.groupContentsByNameAndCategory(contents); - expect(result.size).toBe(2); - expect(result.get("컨텐츠_1")).toHaveLength(1); - expect(result.get("컨텐츠_2")).toHaveLength(1); + expect(Object.keys(result)).toHaveLength(2); + expect(result["컨텐츠_1"]).toHaveLength(1); + expect(result["컨텐츠_2"]).toHaveLength(1); }); }); diff --git a/src/backend/src/content/group/group.service.ts b/src/backend/src/content/group/group.service.ts index cbe404a3..3337b99b 100644 --- a/src/backend/src/content/group/group.service.ts +++ b/src/backend/src/content/group/group.service.ts @@ -1,6 +1,7 @@ import { Injectable } from "@nestjs/common"; import { Prisma } from "@prisma/client"; -import _ from "lodash"; +import { groupBy, sum } from "es-toolkit"; +import { orderBy } from "es-toolkit/compat"; import { ValidationException } from "src/common/exception"; import { OrderByArg } from "src/common/object/order-by-arg.object"; import { PrismaService } from "src/prisma"; @@ -10,6 +11,13 @@ import { ContentWageService } from "../wage/wage.service"; import { ContentGroupFilter, ContentGroupWageListFilter } from "./group.dto"; import { ContentGroup, ContentGroupWage } from "./group.object"; +type ContentGroupable = { + contentCategoryId: number; + id: number; + level: number; + name: string; +}; + @Injectable() export class GroupService { constructor( @@ -64,14 +72,11 @@ export class GroupService { } async calculateGroupDuration(contentIds: number[]): Promise { - let duration = 0; - - for (const contentId of contentIds) { - const contentDuration = await this.userContentService.getContentDuration(contentId); - duration += contentDuration; - } + const durations = await Promise.all( + contentIds.map((contentId) => this.userContentService.getContentDuration(contentId)) + ); - return duration; + return sum(durations); } async findContentGroup(filter?: ContentGroupFilter): Promise { @@ -92,7 +97,7 @@ export class GroupService { async findContentGroupWageList( filter?: ContentGroupWageListFilter, - orderBy?: OrderByArg[], + orderByArgs?: OrderByArg[], userId?: number ): Promise { const contents = await this.prisma.content.findMany({ @@ -110,7 +115,7 @@ export class GroupService { const contentGroups = this.groupContentsByNameAndCategory(contents); - const promises = Array.from(contentGroups.entries()).map(async ([_, groupContents]) => { + const promises = Object.values(contentGroups).map(async (groupContents) => { const contentIds = groupContents.map((content) => content.id); const representative = groupContents[0]; @@ -133,7 +138,7 @@ export class GroupService { const result = await Promise.all(promises); - return orderBy ? this.sortResults(result, orderBy) : result; + return orderByArgs ? this.sortResults(result, orderByArgs) : result; } async findContentsByIds(contentIds: number[]) { @@ -144,16 +149,11 @@ export class GroupService { }); } - groupContentsByNameAndCategory(contents: any[]): Map { - const grouped = _.groupBy( - contents, - (content) => `${content.name}_${content.contentCategoryId}` - ); - - return new Map(Object.entries(grouped)); + groupContentsByNameAndCategory(contents: T[]): Record { + return groupBy(contents, (content) => `${content.name}_${content.contentCategoryId}`); } - validateContentGroup(contents: any[]): void { + validateContentGroup(contents: ContentGroupable[]): void { if (contents.length === 0) { return; } @@ -175,11 +175,11 @@ export class GroupService { return DEFAULT_CONTENT_ORDER_BY; } - private sortResults(results: ContentGroupWage[], orderBy: OrderByArg[]): ContentGroupWage[] { - return _.orderBy( + private sortResults(results: ContentGroupWage[], orderByArgs: OrderByArg[]): ContentGroupWage[] { + return orderBy( results, - orderBy.map((order) => order.field), - orderBy.map((order) => order.order) + orderByArgs.map((o) => o.field), + orderByArgs.map((o) => o.order) ); } } diff --git a/src/backend/src/content/wage/wage.resolver.ts b/src/backend/src/content/wage/wage.resolver.ts index 27668b1c..24906057 100644 --- a/src/backend/src/content/wage/wage.resolver.ts +++ b/src/backend/src/content/wage/wage.resolver.ts @@ -1,7 +1,6 @@ import { Args, Mutation, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql"; -import { User } from "@prisma/client"; -import { Prisma } from "@prisma/client"; -import _ from "lodash"; +import { Prisma, User } from "@prisma/client"; +import { orderBy } from "es-toolkit/compat"; import { CurrentUser } from "src/common/decorator/current-user.decorator"; import { OrderByArg } from "src/common/object/order-by-arg.object"; import { PrismaService } from "src/prisma"; @@ -30,7 +29,7 @@ export class WageResolver { nullable: true, type: () => [OrderByArg], }) - orderBy?: OrderByArg[], + orderByArgs?: OrderByArg[], @CurrentUser() user?: User ) { const contents = await this.prisma.content.findMany({ @@ -65,15 +64,15 @@ export class WageResolver { }); }); - const result = orderBy - ? _.orderBy( - await Promise.all(promises), - orderBy.map((order) => order.field), - orderBy.map((order) => order.order) - ) - : await Promise.all(promises); + const results = await Promise.all(promises); - return result; + return orderByArgs + ? orderBy( + results, + orderByArgs.map((o) => o.field), + orderByArgs.map((o) => o.order) + ) + : results; } @Mutation(() => CalculateCustomContentWageResult) diff --git a/src/backend/src/content/wage/wage.service.ts b/src/backend/src/content/wage/wage.service.ts index e7662426..b35a87d0 100644 --- a/src/backend/src/content/wage/wage.service.ts +++ b/src/backend/src/content/wage/wage.service.ts @@ -1,7 +1,8 @@ import { Injectable } from "@nestjs/common"; -import { UserContentService } from "../../user/service/user-content.service"; -import { UserGoldExchangeRateService } from "src/user/service/user-gold-exchange-rate.service"; +import { sum, sumBy } from "es-toolkit"; import { PrismaService } from "src/prisma"; +import { UserGoldExchangeRateService } from "src/user/service/user-gold-exchange-rate.service"; +import { UserContentService } from "../../user/service/user-content.service"; type Reward = { averageQuantity: number; @@ -30,16 +31,14 @@ export class ContentWageService { ) {} async calculateGold(rewards: Reward[], userId?: number) { - let gold = 0; - - for (const reward of rewards) { - const price = await this.userContentService.getItemPrice(reward.itemId, userId); - - const averageQuantity = reward.averageQuantity; - gold += price * averageQuantity; - } + const goldValues = await Promise.all( + rewards.map(async (reward) => { + const price = await this.userContentService.getItemPrice(reward.itemId, userId); + return price * reward.averageQuantity; + }) + ); - return gold; + return sum(goldValues); } async calculateSeeMoreRewardsGold( @@ -51,12 +50,7 @@ export class ContentWageService { includeItemIds?: number[] ) { const seeMoreRewards = contentSeeMoreRewards - .filter((reward) => { - if (includeItemIds && !includeItemIds.includes(reward.itemId)) { - return false; - } - return true; - }) + .filter((reward) => !includeItemIds || includeItemIds.includes(reward.itemId)) .map((reward) => ({ averageQuantity: reward.quantity, itemId: reward.itemId, @@ -85,8 +79,8 @@ export class ContentWageService { contentIds.map((id) => this.calculateContentWageData(id, userId, filter)) ); - const totalGold = dataList.reduce((sum, data) => sum + data.gold, 0); - const totalDuration = dataList.reduce((sum, data) => sum + data.duration, 0); + const totalGold = sumBy(dataList, (data) => data.gold); + const totalDuration = sumBy(dataList, (data) => data.duration); const { goldAmountPerHour, krwAmountPerHour } = await this.calculateWage( { duration: totalDuration, gold: totalGold }, diff --git a/src/backend/src/dataloader/data-loader.utils.ts b/src/backend/src/dataloader/data-loader.utils.ts index 46045301..411d3243 100644 --- a/src/backend/src/dataloader/data-loader.utils.ts +++ b/src/backend/src/dataloader/data-loader.utils.ts @@ -1,15 +1,17 @@ -import _ from "lodash"; import DataLoader from "dataloader"; +import { groupBy, keyBy } from "es-toolkit"; import { ItemSortOrder } from "src/content/shared/constants"; import { ManyLoader, UniqueLoader } from "./data-loader.types"; +const UNORDERED_ITEM_SORT_PRIORITY = 999; + export const createUniqueLoader = ( batchFn: (ids: readonly number[]) => Promise, entityName: string ): UniqueLoader => { const loader = new DataLoader(async (ids) => { const items = await batchFn(ids); - const itemsMap = _.keyBy(items, "id"); + const itemsMap = keyBy(items, (item) => item.id); return ids.map((id) => itemsMap[id]); }); @@ -30,14 +32,14 @@ export const createManyLoader = (async (contentIds) => { const items = await batchFn(contentIds); - const sortedItems = _.cloneDeep(items).sort((a, b) => { - const aOrder = ItemSortOrder[a.item.name] || 999; - const bOrder = ItemSortOrder[b.item.name] || 999; + const sortedItems = [...items].sort((a, b) => { + const aOrder = ItemSortOrder[a.item.name] ?? UNORDERED_ITEM_SORT_PRIORITY; + const bOrder = ItemSortOrder[b.item.name] ?? UNORDERED_ITEM_SORT_PRIORITY; return aOrder - bOrder; }); - const grouped = _.groupBy(sortedItems, "contentId"); - return contentIds.map((id) => grouped[id] || []); + const grouped = groupBy(sortedItems, (item) => item.contentId); + return contentIds.map((id) => grouped[id] ?? []); }); return { diff --git a/src/backend/src/user/service/user-content.service.ts b/src/backend/src/user/service/user-content.service.ts index b5506712..6aeaead1 100644 --- a/src/backend/src/user/service/user-content.service.ts +++ b/src/backend/src/user/service/user-content.service.ts @@ -1,4 +1,5 @@ import { Injectable } from "@nestjs/common"; +import { keyBy } from "es-toolkit"; import { PrismaService } from "../../prisma"; @Injectable() @@ -102,24 +103,15 @@ export class UserContentService { }, }); - const userRewardMap = new Map(userRewards.map((reward) => [reward.itemId, reward])); + const userRewardByItemId = keyBy(userRewards, (r) => r.itemId); result = defaultRewards.map(({ averageQuantity, isSellable, itemId }) => { - const userReward = userRewardMap.get(itemId); - - if (userReward) { - return { - averageQuantity: userReward.averageQuantity.toNumber(), - isSellable: userReward.isSellable, - itemId, - }; - } else { - return { - averageQuantity: averageQuantity.toNumber(), - isSellable, - itemId, - }; - } + const userReward = userRewardByItemId[itemId]; + return { + averageQuantity: (userReward?.averageQuantity ?? averageQuantity).toNumber(), + isSellable: userReward?.isSellable ?? isSellable, + itemId, + }; }); } else { result = defaultRewards.map(({ averageQuantity, isSellable, itemId }) => ({ @@ -188,15 +180,12 @@ export class UserContentService { }, }); - const userRewardMap = new Map(userRewards.map((reward) => [reward.itemId, reward])); + const userRewardByItemId = keyBy(userRewards, (r) => r.itemId); - return defaultRewards.map(({ itemId, quantity }) => { - const userReward = userRewardMap.get(itemId); - return { - itemId, - quantity: userReward ? userReward.quantity.toNumber() : quantity.toNumber(), - }; - }); + return defaultRewards.map(({ itemId, quantity }) => ({ + itemId, + quantity: (userRewardByItemId[itemId]?.quantity ?? quantity).toNumber(), + })); } return defaultRewards.map(({ itemId, quantity }) => ({