Skip to content

Commit d99696e

Browse files
committed
modify: 코드래빗 리뷰 반영
유저 리더보드 조회시 GROUP BY절에 user.email 추가 타입 안정성 강화: DTO에 유효성 검증 추가 및 관련 코드 수정에 따른 테스트 케이스 삭제
1 parent 4765e25 commit d99696e

File tree

4 files changed

+20
-41
lines changed

4 files changed

+20
-41
lines changed

src/repositories/__test__/leaderboard.repo.test.ts

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ describe('LeaderboardRepository', () => {
194194

195195
await repo.getLeaderboard('user', 'viewCount', 30, 10);
196196

197-
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('GROUP BY u.id'), expect.anything());
197+
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('GROUP BY u.id, u.email'), expect.anything());
198198
});
199199

200200
it('post 타입에는 GROUP BY 절이 포함되지 않아야 한다', async () => {
@@ -225,33 +225,6 @@ describe('LeaderboardRepository', () => {
225225
);
226226
});
227227

228-
it('유효하지 않은 sort 값이 전달되면 기본값(view_diff)을 사용해야 한다', async () => {
229-
const mockResult = [{ view_diff: 10 }];
230-
231-
mockPool.query.mockResolvedValue({
232-
rows: mockResult,
233-
rowCount: mockResult.length,
234-
} as unknown as QueryResult);
235-
236-
await repo.getLeaderboard('user', 'invalidSort', 30, 10);
237-
238-
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('view_diff DESC'), expect.anything());
239-
});
240-
241-
it('유효하지 않은 type 값이 전달되면 기본값(user)을 사용해야 한다', async () => {
242-
const mockResult = [{ view_diff: 10 }];
243-
244-
mockPool.query.mockResolvedValue({
245-
rows: mockResult,
246-
rowCount: mockResult.length,
247-
} as unknown as QueryResult);
248-
249-
const result = await repo.getLeaderboard('invalidType', 'viewCount', 30, 10);
250-
251-
expect(result).toEqual(mockResult);
252-
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('FROM users_user u'), expect.anything());
253-
});
254-
255228
it('데이터가 없는 경우 빈 배열을 반환해야 한다', async () => {
256229
mockPool.query.mockResolvedValue({
257230
rows: [],

src/repositories/leaderboard.repository.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { Pool } from 'pg';
22
import logger from '@/configs/logger.config';
33
import { DBError } from '@/exception';
4+
import { LeaderboardSortType, LeaderboardType } from '@/types/dto/requests/getLeaderboardQuery.type';
45

56
export class LeaderboardRepository {
67
constructor(private pool: Pool) {}
78

8-
async getLeaderboard(type: string, sort: string, dateRange: number, limit: number) {
9+
async getLeaderboard(type: LeaderboardType, sort: LeaderboardSortType, dateRange: number, limit: number) {
910
try {
1011
const cteQuery = this.buildLeaderboardCteQuery();
1112
const selectQuery = this.buildLeaderboardSelectQuery(type);
@@ -49,7 +50,7 @@ export class LeaderboardRepository {
4950
}
5051

5152
// 메인 연산을 포함하는 SELECT 절 빌드
52-
private buildLeaderboardSelectQuery(type: string) {
53+
private buildLeaderboardSelectQuery(type: LeaderboardType) {
5354
if (type === 'post') {
5455
return `
5556
SELECT
@@ -98,7 +99,7 @@ export class LeaderboardRepository {
9899
}
99100

100101
// CTE 테이블 조인 및 WHERE 절을 포함하는 FROM 절 빌드
101-
private buildLeaderboardFromClause(type: string) {
102+
private buildLeaderboardFromClause(type: LeaderboardType) {
102103
if (type === 'post') {
103104
return `
104105
FROM posts_post p
@@ -118,7 +119,7 @@ export class LeaderboardRepository {
118119
}
119120

120121
// sort 매개변수를 정렬 컬럼으로 매핑
121-
private mapSortColByType(sort: string, type: string) {
122+
private mapSortColByType(sort: LeaderboardSortType, type: LeaderboardType) {
122123
let sortCol = '';
123124

124125
switch (sort) {
@@ -138,15 +139,15 @@ export class LeaderboardRepository {
138139
}
139140

140141
// 매핑된 정렬 컬럼으로 ORDER BY 절 및 LIMIT 절 빌드
141-
private buildLeaderboardGroupOrderClause(sortCol: string, type: string) {
142+
private buildLeaderboardGroupOrderClause(sortCol: string, type: LeaderboardType) {
142143
if (type === 'post') {
143144
return `
144145
ORDER BY ${sortCol} DESC
145146
LIMIT $2;
146147
`;
147148
} else {
148149
return `
149-
GROUP BY u.id
150+
GROUP BY u.id, u.email
150151
ORDER BY ${sortCol} DESC
151152
LIMIT $2;
152153
`;

src/services/leaderboard.service.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logger from '@/configs/logger.config';
22
import { LeaderboardRepository } from '@/repositories/leaderboard.repository';
3+
import { LeaderboardSortType, LeaderboardType } from '@/types/dto/requests/getLeaderboardQuery.type';
34
import {
45
LeaderboardPostType,
56
LeaderboardResponseData,
@@ -10,8 +11,8 @@ export class LeaderboardService {
1011
constructor(private leaderboardRepo: LeaderboardRepository) {}
1112

1213
async getLeaderboard(
13-
type: string = 'user',
14-
sort: string = 'viewCount',
14+
type: LeaderboardType = 'user',
15+
sort: LeaderboardSortType = 'viewCount',
1516
dateRange: number = 30,
1617
limit: number = 10,
1718
): Promise<LeaderboardResponseData> {
@@ -26,7 +27,10 @@ export class LeaderboardService {
2627
}
2728
}
2829

29-
private mapRawResultToLeaderboardResponseData(rawResult: unknown[], type: string): LeaderboardResponseData {
30+
private mapRawResultToLeaderboardResponseData(
31+
rawResult: RawPostResult[] | RawUserResult[],
32+
type: LeaderboardType,
33+
): LeaderboardResponseData {
3034
const result: LeaderboardResponseData = { posts: null, users: null };
3135

3236
if (type === 'post') {
@@ -60,7 +64,6 @@ export class LeaderboardService {
6064
return result;
6165
}
6266
}
63-
6467
interface RawPostResult {
6568
id: number;
6669
title: string;

src/types/dto/requests/getLeaderboardQuery.type.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Transform } from 'class-transformer';
2-
import { IsNumber, IsOptional, IsString, Max, Min } from 'class-validator';
2+
import { IsEnum, IsNumber, IsOptional, Max, Min } from 'class-validator';
33

44
/**
55
* @swagger
@@ -68,11 +68,13 @@ export interface GetLeaderboardQuery {
6868
*/
6969
export class GetLeaderboardQueryDto {
7070
@IsOptional()
71-
@IsString()
71+
@IsEnum(['user', 'post'])
72+
@Transform(({ value }) => (value === '' ? 'user' : value))
7273
type?: LeaderboardType;
7374

7475
@IsOptional()
75-
@IsString()
76+
@IsEnum(['viewCount', 'likeCount', 'postCount'])
77+
@Transform(({ value }) => (value === '' ? 'viewCount' : value))
7678
sort?: LeaderboardSortType;
7779

7880
@IsOptional()

0 commit comments

Comments
 (0)