Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] πŸ”§ fix : query λ³„λ‘œ 응닡이 μ•„λ‹Œ ν•œλ²ˆμ˜ μš”μ²­μ— λ‘κ°œμ˜ λž­ν‚Ή λͺ¨λ‘ μ‘λ‹΅ν•˜κ²Œ λ³€κ²½(#124) #169

Merged
merged 4 commits into from
Nov 21, 2024
23 changes: 23 additions & 0 deletions BE/src/ranking/dto/ranking-data.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
9 changes: 5 additions & 4 deletions BE/src/ranking/dto/ranking-response.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
17 changes: 17 additions & 0 deletions BE/src/ranking/dto/ranking-result.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
20 changes: 5 additions & 15 deletions BE/src/ranking/ranking.controller.ts
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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<RankingResponseDto> {
async getRanking(@Req() req: Request): Promise<RankingResponseDto> {
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);
}
}
122 changes: 70 additions & 52 deletions BE/src/ranking/ranking.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<RankingResponseDto> {
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<RankingResponseDto> {
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<RankingResultDto> {
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,
};
}

Expand All @@ -100,7 +112,10 @@ export class RankingService {
this.redisDomainService.zadd(
profitRateKey,
rank.profitRate,
rank.nickname,
JSON.stringify({
nickname: rank.nickname,
profitRate: rank.profitRate,
}),
),
),
),
Expand All @@ -109,7 +124,10 @@ export class RankingService {
this.redisDomainService.zadd(
assetKey,
rank.totalAsset,
rank.nickname,
JSON.stringify({
nickname: rank.nickname,
totalAsset: rank.totalAsset,
}),
),
),
),
Expand Down
Loading