Skip to content

Commit

Permalink
Merge pull request #76 from boostcampwm-2024/be/feature/rank
Browse files Browse the repository at this point in the history
[BE/feature] 상위 5명 조회 api 작성
  • Loading branch information
HBLEEEEE authored Nov 21, 2024
2 parents f1bdfe6 + d590852 commit d4b23e1
Show file tree
Hide file tree
Showing 13 changed files with 260 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@ jobs:
echo "Running new container..."
docker run -d --name myapp -p 3000:3000 -p 8080:8080 --env-file /root/src/config/.env ${{ secrets.DOCKER_HUB_USERNAME }}/myapp:latest
echo "Removing unused Docker images..."
docker image prune
1 change: 1 addition & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/schedule": "^4.1.1",
"@nestjs/swagger": "^8.0.5",
"@types/bcrypt": "^5.0.2",
"backend": "file:",
Expand Down
6 changes: 5 additions & 1 deletion apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { AuthModule } from './auth/auth.module';
import { MailModule } from './mail/mail.module';
import { ConfigModule } from '@nestjs/config';
import { LottoModule } from './lotto/lotto.module';
import { RankModule } from './rank/rank.module';
import { ScheduleModule } from '@nestjs/schedule';

@Module({
imports: [
Expand All @@ -15,7 +17,9 @@ import { LottoModule } from './lotto/lotto.module';
OrderModule,
MailModule,
LottoModule,
ConfigModule.forRoot()
ConfigModule.forRoot(),
ScheduleModule.forRoot(),
RankModule
],
controllers: [],
providers: []
Expand Down
7 changes: 5 additions & 2 deletions apps/backend/src/global/successhandler.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Nullable } from './utils/dataCustomType';

interface SuccessMessage {
code: number;
message: string;
Expand All @@ -13,10 +15,11 @@ export const successMessage = {
GET_MAIL_SUCCESS: { code: 200, message: '메일 조회를 완료했습니다.' },
DELETE_MAIL_SUCCESS: { code: 200, message: '메일 삭제를 완료했습니다.' },
GET_MAIL_ALARM_SUCCESS: { code: 200, message: 'Catch alarm!.' },
BUY_LOTTO_SUCCESS: { code: 200, message: '로또 구매를 완료했습니다.' }
BUY_LOTTO_SUCCESS: { code: 200, message: '로또 구매를 완료했습니다.' },
TOP5_RANK_GET_SUCCESS: { code: 200, message: '상위 5명을 조회했습니다.' }
};

export function successhandler<T>(success: SuccessMessage, data: T | null = null) {
export function successhandler<T>(success: SuccessMessage, data: Nullable<T> = null) {
return {
code: success.code,
message: success.message,
Expand Down
15 changes: 15 additions & 0 deletions apps/backend/src/rank/decorator/top5rank.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { applyDecorators } from '@nestjs/common';
import { ApiResponse } from '@nestjs/swagger';
import { TokenDecorator } from 'src/global/utils/tokenSwagger';
import { rank5SuccessResponseDto } from '../dto/top5rank.dto';

export function top5rankResponseDecorator() {
return applyDecorators(
TokenDecorator(),
ApiResponse({
status: 200,
description: '5명 조회 성공',
type: rank5SuccessResponseDto
})
);
}
42 changes: 42 additions & 0 deletions apps/backend/src/rank/dto/top5rank.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ApiProperty } from '@nestjs/swagger';

export class rank5DataDto {
@ApiProperty({
description: '닉네임',
example: '홍길동'
})
nickname: string;

@ApiProperty({
description: '점수',
example: '20000'
})
score: number;
}

export class rank5SuccessResponseDto {
@ApiProperty({
description: '응답 코드',
example: 200
})
code: number;

@ApiProperty({
description: '응답 메세지',
example: '상위 5명을 조회했습니다.'
})
message: string;

@ApiProperty({
description: '응답 데이터',
type: [rank5DataDto],
example: [
{ nickname: '파이썬', score: 50000 },
{ nickname: '자바', score: 40000 },
{ nickname: '자바스크립트', score: 30000 },
{ nickname: '타입스크립트', score: 20000 },
{ nickname: 'C 언어', score: 10000 }
]
})
data: rank5DataDto;
}
18 changes: 18 additions & 0 deletions apps/backend/src/rank/rank.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RankController } from './rank.controller';

describe('RankController', () => {
let controller: RankController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [RankController]
}).compile();

controller = module.get<RankController>(RankController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
20 changes: 20 additions & 0 deletions apps/backend/src/rank/rank.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { RankService } from './rank.service';
import { successhandler, successMessage } from 'src/global/successhandler';
import { JwtAuthGuard } from 'src/global/utils/jwtAuthGuard';
import { ApiOperation } from '@nestjs/swagger';
import { top5rankResponseDecorator } from './decorator/top5rank.decorator';

@UseGuards(JwtAuthGuard)
@Controller('api/rank')
export class RankController {
constructor(private readonly rankService: RankService) {}

@ApiOperation({ summary: '상위 랭킹 5명 반환 api' })
@top5rankResponseDecorator()
@Get('top5')
async top5rank() {
const data = await this.rankService.getTopRankings();
return successhandler(successMessage.TOP5_RANK_GET_SUCCESS, data);
}
}
23 changes: 23 additions & 0 deletions apps/backend/src/rank/rank.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Module } from '@nestjs/common';
import { RankController } from './rank.controller';
import { RankService } from './rank.service';
import { DatabaseModule } from 'src/database/database.module';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
imports: [
DatabaseModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '1h' }
})
})
],
controllers: [RankController],
providers: [RankService]
})
export class RankModule {}
16 changes: 16 additions & 0 deletions apps/backend/src/rank/rank.queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const rankQueries = {
moneyDataQuery: `SELECT
m.nickname,
m.total_cash + COALESCE(SUM(mc.quantity * cp.price), 0) AS total_asset
FROM
members m
LEFT JOIN
member_crops mc ON m.member_id = mc.member_id
LEFT JOIN
crop_prices cp ON mc.crop_id = cp.crop_id
AND cp.time = (SELECT MAX(time) FROM crop_prices WHERE crop_id = mc.crop_id)
GROUP BY
m.member_id, m.total_cash
ORDER BY
total_asset DESC;`
};
18 changes: 18 additions & 0 deletions apps/backend/src/rank/rank.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { RankService } from './rank.service';

describe('RankService', () => {
let service: RankService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [RankService]
}).compile();

service = module.get<RankService>(RankService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
38 changes: 38 additions & 0 deletions apps/backend/src/rank/rank.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Inject, Injectable } from '@nestjs/common';
import { RedisClientType } from 'redis';
import { DatabaseService } from 'src/database/database.service';
import { rankQueries } from './rank.queries';
import { Cron, CronExpression } from '@nestjs/schedule';

@Injectable()
export class RankService {
constructor(
private readonly databaseService: DatabaseService,
@Inject('REDIS_CLIENT') private readonly redisClient: RedisClientType
) {}

async onApplicationBootstrap() {
await this.storeMoneyRanking();
}

@Cron(CronExpression.EVERY_2ND_HOUR)
async handleCron() {
await this.storeMoneyRanking();
}

async storeMoneyRanking() {
await this.redisClient.del('ranking');
const membersMoney = await this.databaseService.query(rankQueries.moneyDataQuery);
for (const memberMoney of membersMoney.rows) {
await this.redisClient.zAdd('ranking', {
score: memberMoney.total_asset,
value: memberMoney.nickname
});
}
}

async getTopRankings() {
const members = await this.redisClient.zRangeWithScores('ranking', -5, -1);
return members.reverse();
}
}
56 changes: 56 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d4b23e1

Please sign in to comment.