From d3cfc8474e66ca05663c11e85e9819abd4d6b2e1 Mon Sep 17 00:00:00 2001 From: jinddings Date: Thu, 21 Nov 2024 14:00:12 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20fix=20:=20query=20=EB=B3=84?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=91=EB=8B=B5=EC=9D=B4=20=EC=95=84=EB=8B=8C=20?= =?UTF-8?q?=ED=95=9C=EB=B2=88=EC=9D=98=20=EC=9A=94=EC=B2=AD=EC=97=90=20?= =?UTF-8?q?=EB=91=90=EA=B0=9C=EC=9D=98=20=EB=9E=AD=ED=82=B9=20=EB=AA=A8?= =?UTF-8?q?=EB=91=90=20=EC=9D=91=EB=8B=B5=ED=95=98=EA=B2=8C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD(#124)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BE/src/ranking/dto/ranking-data.dto.ts | 23 ++++ BE/src/ranking/dto/ranking-response.dto.ts | 9 +- BE/src/ranking/dto/ranking-result.dto.ts | 17 +++ .../interface/ranking-data.interface.ts | 5 + BE/src/ranking/ranking.controller.ts | 14 +- BE/src/ranking/ranking.service.ts | 120 ++++++++++-------- 6 files changed, 121 insertions(+), 67 deletions(-) create mode 100644 BE/src/ranking/dto/ranking-data.dto.ts create mode 100644 BE/src/ranking/dto/ranking-result.dto.ts create mode 100644 BE/src/ranking/interface/ranking-data.interface.ts diff --git a/BE/src/ranking/dto/ranking-data.dto.ts b/BE/src/ranking/dto/ranking-data.dto.ts new file mode 100644 index 00000000..97094da9 --- /dev/null +++ b/BE/src/ranking/dto/ranking-data.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class RankingDataDto { + @ApiProperty({ + description: '사용자 닉네임', + example: 'trader123', + }) + nickname: string; + + @ApiProperty({ + description: '수익률 (%)', + example: 15.7, + required: false, + }) + profitRate?: number; + + @ApiProperty({ + description: '총 자산', + example: 1000000, + required: false, + }) + totalAsset?: number; +} diff --git a/BE/src/ranking/dto/ranking-response.dto.ts b/BE/src/ranking/dto/ranking-response.dto.ts index b1a6ced7..e89f53ce 100644 --- a/BE/src/ranking/dto/ranking-response.dto.ts +++ b/BE/src/ranking/dto/ranking-response.dto.ts @@ -1,13 +1,14 @@ import { ApiProperty } from '@nestjs/swagger'; +import { RankingResultDto } from './ranking-result.dto'; export class RankingResponseDto { @ApiProperty({ - description: 'top 10 유저 랭킹', + description: '수익률 랭킹', }) - topRank: string[]; + profitRateRanking: RankingResultDto; @ApiProperty({ - description: '로그인 한 유저의 랭킹', + description: '자산 랭킹', }) - userRank?: number | null; + assetRanking: RankingResultDto; } diff --git a/BE/src/ranking/dto/ranking-result.dto.ts b/BE/src/ranking/dto/ranking-result.dto.ts new file mode 100644 index 00000000..61fa5888 --- /dev/null +++ b/BE/src/ranking/dto/ranking-result.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { RankingDataDto } from './ranking-data.dto'; + +export class RankingResultDto { + @ApiProperty({ + description: '상위 10명의 랭킹 데이터', + type: [RankingDataDto], + }) + topRank: RankingDataDto[]; + + @ApiProperty({ + description: '현재 사용자의 순위 (없을 경우 null)', + example: 42, + nullable: true, + }) + userRank: number | null; +} diff --git a/BE/src/ranking/interface/ranking-data.interface.ts b/BE/src/ranking/interface/ranking-data.interface.ts new file mode 100644 index 00000000..098604b3 --- /dev/null +++ b/BE/src/ranking/interface/ranking-data.interface.ts @@ -0,0 +1,5 @@ +export interface RankingData { + nickname: string; + profitRate?: number; + totalAsset?: number; +} diff --git a/BE/src/ranking/ranking.controller.ts b/BE/src/ranking/ranking.controller.ts index 3681e957..420c9ebb 100644 --- a/BE/src/ranking/ranking.controller.ts +++ b/BE/src/ranking/ranking.controller.ts @@ -1,10 +1,9 @@ -import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common'; +import { Controller, Get, Req, UseGuards } from '@nestjs/common'; import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Request } from 'express'; import { OptionalAuthGuard } from 'src/auth/optional-auth-guard'; import { RankingService } from './ranking.service'; import { RankingResponseDto } from './dto/ranking-response.dto'; -import { SortType } from './enum/sort-type.enum'; @Controller('/api/ranking') @ApiTags('랭킹 API') @@ -22,18 +21,13 @@ export class RankingController { @ApiQuery({ name: 'sortBy', required: false, - description: 'profitRate: 수익률순, totalAsset: 자산순', - enum: ['profitRate', 'totalAsset'], }) - async getRanking( - @Req() req: Request, - @Query('sortBy') sortBy: SortType = SortType.PROFIT_RATE, - ): Promise { + async getRanking(@Req() req: Request): Promise { if (!req.user) { - return this.rankingService.getRanking(sortBy); + return this.rankingService.getRanking(); } const { nickname } = req.user; - return this.rankingService.getRankingAuthUser(nickname, sortBy); + return this.rankingService.getRankingAuthUser(nickname); } } diff --git a/BE/src/ranking/ranking.service.ts b/BE/src/ranking/ranking.service.ts index ed4e5577..3eb58252 100644 --- a/BE/src/ranking/ranking.service.ts +++ b/BE/src/ranking/ranking.service.ts @@ -4,6 +4,9 @@ import { AssetRepository } from 'src/asset/asset.repository'; import { Cron } from '@nestjs/schedule'; import { SortType } from './enum/sort-type.enum'; import { Ranking } from './interface/ranking.interface'; +import { RankingResponseDto } from './dto/ranking-response.dto'; +import { RankingResultDto } from './dto/ranking-result.dto'; +import { RankingDataDto } from './dto/ranking-data.dto'; @Injectable() export class RankingService { @@ -12,69 +15,74 @@ export class RankingService { private readonly redisDomainService: RedisDomainService, ) {} - async getRanking(sortBy: SortType = SortType.PROFIT_RATE) { - const date = new Date().toISOString().slice(0, 10); - const key = `ranking:${date}:${sortBy}`; - - if (await this.redisDomainService.exists(key)) { - return { - topRank: await this.redisDomainService.zrevrange(key, 0, 9), - userRank: null, - }; - } - - const ranking = await this.calculateRanking(sortBy); + async getRanking(): Promise { + const profitRateRanking = await this.getRankingData(SortType.PROFIT_RATE); + const assetRanking = await this.getRankingData(SortType.ASSET); - await Promise.all( - ranking.map((rank: Ranking) => - this.redisDomainService.zadd( - key, - this.getSortScore(rank, sortBy), - rank.nickname, - ), - ), - ); + return { profitRateRanking, assetRanking }; + } - return { - topRank: await this.redisDomainService.zrevrange(key, 0, 9), - userRank: null, - }; + async getRankingAuthUser(nickname: string): Promise { + const profitRateRanking = await this.getRankingData(SortType.PROFIT_RATE, { + nickname, + }); + const assetRanking = await this.getRankingData(SortType.ASSET, { + nickname, + }); + return { profitRateRanking, assetRanking }; } - async getRankingAuthUser( - nickname: string, - sortBy: SortType = SortType.PROFIT_RATE, - ) { + private async getRankingData( + sortBy: SortType, + options: { nickname?: string } = { nickname: null }, + ): Promise { const date = new Date().toISOString().slice(0, 10); const key = `ranking:${date}:${sortBy}`; - let userRank = null; - - if (await this.redisDomainService.exists(key)) { - userRank = await this.redisDomainService.zrevrank(key, nickname); - - return { - topRank: await this.redisDomainService.zrevrange(key, 0, 9), - userRank: userRank !== null ? userRank + 1 : null, - }; + if (!(await this.redisDomainService.exists(key))) { + const ranking = await this.calculateRanking(sortBy); + await Promise.all( + ranking.map((rank: Ranking) => + this.redisDomainService.zadd( + key, + this.getSortScore(rank, sortBy), + sortBy === SortType.PROFIT_RATE + ? JSON.stringify({ + nickname: rank.nickname, + profitRate: rank.profitRate, + }) + : JSON.stringify({ + nickname: rank.nickname, + totalAsset: rank.totalAsset, + }), + ), + ), + ); } - const ranking = await this.calculateRanking(sortBy); - await Promise.all( - ranking.map((rank: Ranking) => - this.redisDomainService.zadd( - key, - this.getSortScore(rank, sortBy), - rank.nickname, - ), - ), - ); + const [topRank, userRank] = await Promise.all([ + this.redisDomainService.zrevrange(key, 0, 9), + options.nickname !== null + ? this.redisDomainService.zrevrank( + key, + JSON.stringify({ + nickname: options.nickname, + ...(sortBy === SortType.PROFIT_RATE + ? { profitRate: 0 } + : { totalAsset: 0 }), + }), + ) + : null, + ]); - userRank = await this.redisDomainService.zrevrank(key, nickname); + const parsedTopRank: RankingDataDto[] = topRank.map((rank) => + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + JSON.parse(rank), + ); return { - topRank: await this.redisDomainService.zrevrange(key, 0, 9), - userRank: userRank ? userRank + 1 : null, + topRank: parsedTopRank, + userRank: userRank !== null ? userRank + 1 : null, }; } @@ -100,7 +108,10 @@ export class RankingService { this.redisDomainService.zadd( profitRateKey, rank.profitRate, - rank.nickname, + JSON.stringify({ + nickname: rank.nickname, + profitRate: rank.profitRate, + }), ), ), ), @@ -109,7 +120,10 @@ export class RankingService { this.redisDomainService.zadd( assetKey, rank.totalAsset, - rank.nickname, + JSON.stringify({ + nickname: rank.nickname, + totalAsset: rank.totalAsset, + }), ), ), ),