Skip to content

Commit abc3a3a

Browse files
committed
modify: 사용자, 게시물 별로 Leaderboard 조회 API 분리
라우터, 타입, 함수 모두 분리 repo 쿼리 주석 삭제 및 파라미터에 따른 날짜 변환을 make_interval로 통일 불필요해진 테스트 코드 삭제
1 parent e15be82 commit abc3a3a

File tree

9 files changed

+431
-327
lines changed

9 files changed

+431
-327
lines changed

src/controllers/leaderboard.controller.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,48 @@
11
import logger from '@/configs/logger.config';
22
import { NextFunction, RequestHandler, Request, Response } from 'express';
33
import { LeaderboardService } from '@/services/leaderboard.service';
4-
import { GetLeaderboardQuery, LeaderboardResponseDto } from '@/types/index';
4+
import {
5+
GetUserLeaderboardQuery,
6+
GetPostLeaderboardQuery,
7+
UserLeaderboardResponseDto,
8+
PostLeaderboardResponseDto,
9+
} from '@/types/index';
510

611
export class LeaderboardController {
712
constructor(private leaderboardService: LeaderboardService) {}
813

9-
getLeaderboard: RequestHandler = async (
10-
req: Request<object, object, object, GetLeaderboardQuery>,
11-
res: Response<LeaderboardResponseDto>,
14+
getUserLeaderboard: RequestHandler = async (
15+
req: Request<object, object, object, GetUserLeaderboardQuery>,
16+
res: Response<UserLeaderboardResponseDto>,
1217
next: NextFunction,
1318
) => {
1419
try {
15-
const { type, sort, dateRange, limit } = req.query;
20+
const { sort, dateRange, limit } = req.query;
1621

17-
const result = await this.leaderboardService.getLeaderboard(type, sort, dateRange, limit);
18-
const response = new LeaderboardResponseDto(true, '리더보드 조회에 성공하였습니다.', result, null);
22+
const users = await this.leaderboardService.getUserLeaderboard(sort, dateRange, limit);
23+
const response = new UserLeaderboardResponseDto(true, '사용자 리더보드 조회에 성공하였습니다.', users, null);
1924

2025
res.status(200).json(response);
2126
} catch (error) {
22-
logger.error('리더보드 조회 실패:', error);
27+
logger.error('사용자 리더보드 조회 실패:', error);
28+
next(error);
29+
}
30+
};
31+
32+
getPostLeaderboard: RequestHandler = async (
33+
req: Request<object, object, object, GetPostLeaderboardQuery>,
34+
res: Response<PostLeaderboardResponseDto>,
35+
next: NextFunction,
36+
) => {
37+
try {
38+
const { sort, dateRange, limit } = req.query;
39+
40+
const posts = await this.leaderboardService.getPostLeaderboard(sort, dateRange, limit);
41+
const response = new PostLeaderboardResponseDto(true, '게시물 리더보드 조회에 성공하였습니다.', posts, null);
42+
43+
res.status(200).json(response);
44+
} catch (error) {
45+
logger.error('게시물 리더보드 조회 실패:', error);
2346
next(error);
2447
}
2548
};

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

Lines changed: 74 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -17,43 +17,8 @@ describe('LeaderboardRepository', () => {
1717
repo = new LeaderboardRepository(mockPool as unknown as Pool);
1818
});
1919

20-
describe('getLeaderboard', () => {
21-
it('type이 post인 경우 post 데이터를 반환해야 한다.', async () => {
22-
const mockResult = [
23-
{
24-
id: 2,
25-
title: 'test2',
26-
slug: 'test2',
27-
total_views: 200,
28-
total_likes: 100,
29-
view_diff: 20,
30-
like_diff: 10,
31-
released_at: '2025-01-02',
32-
},
33-
{
34-
id: 1,
35-
title: 'test',
36-
slug: 'test',
37-
total_views: 100,
38-
total_likes: 50,
39-
view_diff: 10,
40-
like_diff: 5,
41-
released_at: '2025-01-01',
42-
},
43-
];
44-
45-
mockPool.query.mockResolvedValue({
46-
rows: mockResult,
47-
rowCount: mockResult.length,
48-
} as unknown as QueryResult);
49-
50-
const result = await repo.getLeaderboard('post', 'viewCount', 30, 10);
51-
52-
expect(result).toEqual(mockResult);
53-
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('FROM posts_post p'), expect.anything());
54-
});
55-
56-
it('type이 user인 경우 user 데이터를 반환해야 한다.', async () => {
20+
describe('getUserLeaderboard', () => {
21+
it('사용자 리더보드를 조회할 수 있어야 한다', async () => {
5722
const mockResult = [
5823
{
5924
id: 1,
@@ -82,7 +47,7 @@ describe('LeaderboardRepository', () => {
8247
rowCount: mockResult.length,
8348
} as unknown as QueryResult);
8449

85-
const result = await repo.getLeaderboard('user', 'viewCount', 30, 10);
50+
const result = await repo.getUserLeaderboard('viewCount', 30, 10);
8651

8752
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('FROM users_user u'), expect.anything());
8853
expect(result).toEqual(mockResult);
@@ -99,7 +64,7 @@ describe('LeaderboardRepository', () => {
9964
rowCount: mockResult.length,
10065
} as unknown as QueryResult);
10166

102-
const result = await repo.getLeaderboard('user', 'viewCount', 30, 10);
67+
const result = await repo.getUserLeaderboard('viewCount', 30, 10);
10368

10469
expect(result).toEqual(mockResult);
10570
expect(result[0].view_diff).toBeGreaterThan(result[1].view_diff);
@@ -116,7 +81,7 @@ describe('LeaderboardRepository', () => {
11681
rowCount: mockResult.length,
11782
} as unknown as QueryResult);
11883

119-
const result = await repo.getLeaderboard('user', 'likeCount', 30, 10);
84+
const result = await repo.getUserLeaderboard('likeCount', 30, 10);
12085

12186
expect(result).toEqual(mockResult);
12287
expect(result[0].like_diff).toBeGreaterThan(result[1].like_diff);
@@ -133,7 +98,7 @@ describe('LeaderboardRepository', () => {
13398
rowCount: mockResult.length,
13499
} as unknown as QueryResult);
135100

136-
const result = await repo.getLeaderboard('user', 'postCount', 30, 10);
101+
const result = await repo.getUserLeaderboard('postCount', 30, 10);
137102

138103
expect(result).toEqual(mockResult);
139104
expect(result[0].post_diff).toBeGreaterThan(result[1].post_diff);
@@ -154,7 +119,7 @@ describe('LeaderboardRepository', () => {
154119
rowCount: mockData.length,
155120
} as unknown as QueryResult);
156121

157-
const result = await repo.getLeaderboard('post', 'viewCount', 30, mockLimit);
122+
const result = await repo.getUserLeaderboard('viewCount', 30, mockLimit);
158123

159124
expect(result).toEqual(mockData);
160125
expect(result.length).toEqual(mockLimit);
@@ -165,45 +130,94 @@ describe('LeaderboardRepository', () => {
165130
);
166131
});
167132

168-
it('type이 post이고 sort가 게시물 수인 경우 조회수를 기준으로 정렬해야 한다.', async () => {
169-
const mockResult = [
170-
{ total_views: 200, total_likes: 5, view_diff: 20, like_diff: 0 },
171-
{ total_views: 100, total_likes: 50, view_diff: 10, like_diff: 5 },
172-
];
133+
it('GROUP BY 절이 포함되어야 한다', async () => {
134+
mockPool.query.mockResolvedValue({
135+
rows: [],
136+
rowCount: 0,
137+
} as unknown as QueryResult);
138+
139+
await repo.getUserLeaderboard('viewCount', 30, 10);
140+
141+
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('GROUP BY u.id, u.email'), expect.anything());
142+
});
143+
144+
it('dateRange 파라미터가 쿼리에 올바르게 적용되어야 한다', async () => {
145+
const mockResult = [{ id: 1 }];
146+
const testDateRange = 30;
173147

174148
mockPool.query.mockResolvedValue({
175149
rows: mockResult,
176150
rowCount: mockResult.length,
177151
} as unknown as QueryResult);
178152

179-
const result = await repo.getLeaderboard('post', 'postCount', 30, 10);
153+
await repo.getUserLeaderboard('viewCount', testDateRange, 10);
180154

181-
expect(result).toEqual(mockResult);
182155
expect(mockPool.query).toHaveBeenCalledWith(
183-
expect.stringContaining('ORDER BY view_diff DESC'),
184-
expect.anything(),
156+
expect.stringContaining('$1::int'),
157+
expect.arrayContaining([testDateRange, expect.anything()]),
185158
);
186-
expect(result[0].view_diff).toBeGreaterThan(result[1].view_diff);
187159
});
188160

189-
it('user 타입에는 GROUP BY 절이 포함되어야 한다', async () => {
161+
it('데이터가 없는 경우 빈 배열을 반환해야 한다', async () => {
190162
mockPool.query.mockResolvedValue({
191163
rows: [],
192164
rowCount: 0,
193165
} as unknown as QueryResult);
194166

195-
await repo.getLeaderboard('user', 'viewCount', 30, 10);
167+
const result = await repo.getUserLeaderboard('viewCount', 30, 10);
196168

197-
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('GROUP BY u.id, u.email'), expect.anything());
169+
expect(result).toEqual([]);
170+
});
171+
172+
it('에러 발생 시 DBError를 던져야 한다', async () => {
173+
mockPool.query.mockRejectedValue(new Error('DB connection failed'));
174+
await expect(repo.getUserLeaderboard('viewCount', 30, 10)).rejects.toThrow(DBError);
175+
});
176+
});
177+
178+
describe('getPostLeaderboard', () => {
179+
it('게시물 리더보드를 조회할 수 있어야 한다', async () => {
180+
const mockResult = [
181+
{
182+
id: 2,
183+
title: 'test2',
184+
slug: 'test2',
185+
total_views: 200,
186+
total_likes: 100,
187+
view_diff: 20,
188+
like_diff: 10,
189+
released_at: '2025-01-02',
190+
},
191+
{
192+
id: 1,
193+
title: 'test',
194+
slug: 'test',
195+
total_views: 100,
196+
total_likes: 50,
197+
view_diff: 10,
198+
like_diff: 5,
199+
released_at: '2025-01-01',
200+
},
201+
];
202+
203+
mockPool.query.mockResolvedValue({
204+
rows: mockResult,
205+
rowCount: mockResult.length,
206+
} as unknown as QueryResult);
207+
208+
const result = await repo.getPostLeaderboard('viewCount', 30, 10);
209+
210+
expect(result).toEqual(mockResult);
211+
expect(mockPool.query).toHaveBeenCalledWith(expect.stringContaining('FROM posts_post p'), expect.anything());
198212
});
199213

200-
it('post 타입에는 GROUP BY 절이 포함되지 않아야 한다', async () => {
214+
it('GROUP BY 절이 포함되지 않아야 한다', async () => {
201215
mockPool.query.mockResolvedValue({
202216
rows: [],
203217
rowCount: 0,
204218
} as unknown as QueryResult);
205219

206-
await repo.getLeaderboard('post', 'viewCount', 30, 10);
220+
await repo.getPostLeaderboard('viewCount', 30, 10);
207221

208222
expect(mockPool.query).toHaveBeenCalledWith(expect.not.stringContaining('GROUP BY'), expect.anything());
209223
});
@@ -217,7 +231,7 @@ describe('LeaderboardRepository', () => {
217231
rowCount: mockResult.length,
218232
} as unknown as QueryResult);
219233

220-
await repo.getLeaderboard('user', 'viewCount', testDateRange, 10);
234+
await repo.getPostLeaderboard('viewCount', testDateRange, 10);
221235

222236
expect(mockPool.query).toHaveBeenCalledWith(
223237
expect.stringContaining('$1::int'),
@@ -231,14 +245,14 @@ describe('LeaderboardRepository', () => {
231245
rowCount: 0,
232246
} as unknown as QueryResult);
233247

234-
const result = await repo.getLeaderboard('user', 'viewCount', 30, 10);
248+
const result = await repo.getPostLeaderboard('viewCount', 30, 10);
235249

236250
expect(result).toEqual([]);
237251
});
238252

239253
it('에러 발생 시 DBError를 던져야 한다', async () => {
240254
mockPool.query.mockRejectedValue(new Error('DB connection failed'));
241-
await expect(repo.getLeaderboard('post', 'postCount', 30, 10)).rejects.toThrow(DBError);
255+
await expect(repo.getPostLeaderboard('viewCount', 30, 10)).rejects.toThrow(DBError);
242256
});
243257
});
244258
});

0 commit comments

Comments
 (0)