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/ranking.controller.ts b/BE/src/ranking/ranking.controller.ts index 3681e957..e3f48d11 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 { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { Controller, Get, Req, UseGuards } from '@nestjs/common'; +import { ApiOperation, 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') @@ -19,21 +18,12 @@ export class RankingController { }) @Get() @UseGuards(OptionalAuthGuard) - @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..dc8e6b96 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,78 @@ 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))) { + 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 findUserRank = async () => { + if (!options.nickname) return null; - if (await this.redisDomainService.exists(key)) { - userRank = await this.redisDomainService.zrevrank(key, nickname); + const members = await this.redisDomainService.zrange(key, 0, -1); + const userMember = members.find((member) => { + const parsed = JSON.parse(member); + return parsed.nickname === options.nickname; + }); - return { - topRank: await this.redisDomainService.zrevrange(key, 0, 9), - userRank: userRank !== null ? userRank + 1 : null, - }; - } + return userMember + ? this.redisDomainService.zrevrank(key, userMember) + : null; + }; - 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), + findUserRank(), + ]); - 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 +112,10 @@ export class RankingService { this.redisDomainService.zadd( profitRateKey, rank.profitRate, - rank.nickname, + JSON.stringify({ + nickname: rank.nickname, + profitRate: rank.profitRate, + }), ), ), ), @@ -109,7 +124,10 @@ export class RankingService { this.redisDomainService.zadd( assetKey, rank.totalAsset, - rank.nickname, + JSON.stringify({ + nickname: rank.nickname, + totalAsset: rank.totalAsset, + }), ), ), ),