diff --git a/src/repositories/__test__/integration/leaderboard.repo.integration.test.ts b/src/repositories/__test__/integration/leaderboard.repo.integration.test.ts index e9cfeb8..6e177fb 100644 --- a/src/repositories/__test__/integration/leaderboard.repo.integration.test.ts +++ b/src/repositories/__test__/integration/leaderboard.repo.integration.test.ts @@ -12,7 +12,7 @@ import { PostLeaderboardSortType, UserLeaderboardSortType } from '@/types'; dotenv.config(); -jest.setTimeout(20000); // 각 케이스당 20초 타임아웃 설정 +jest.setTimeout(60000); // 각 케이스당 60초 타임아웃 설정 /** * LeaderboardRepository 통합 테스트 @@ -44,7 +44,7 @@ describe('LeaderboardRepository 통합 테스트', () => { idleTimeoutMillis: 30000, // 연결 유휴 시간 (30초) connectionTimeoutMillis: 5000, // 연결 시간 초과 (5초) allowExitOnIdle: false, // 유휴 상태에서 종료 허용 - statement_timeout: 30000, + statement_timeout: 60000, // 쿼리 타임아웃 증가 (60초) }; // localhost 가 아니면 ssl 필수 @@ -105,6 +105,7 @@ describe('LeaderboardRepository 통합 테스트', () => { result.forEach((leaderboardUser) => { expect(leaderboardUser).toHaveProperty('id'); expect(leaderboardUser).toHaveProperty('email'); + expect(leaderboardUser).toHaveProperty('username'); expect(leaderboardUser).toHaveProperty('total_views'); expect(leaderboardUser).toHaveProperty('total_likes'); expect(leaderboardUser).toHaveProperty('total_posts'); @@ -214,13 +215,13 @@ describe('LeaderboardRepository 통합 테스트', () => { } }); - it('email이 null인 사용자는 제외되어야 한다', async () => { + it('username이 null인 사용자는 제외되어야 한다', async () => { const result = await repo.getUserLeaderboard(DEFAULT_PARAMS.USER_SORT, DEFAULT_PARAMS.DATE_RANGE, 30); - if (!isEnoughData(result, 1, '사용자 리더보드 email null 제외')) return; + if (!isEnoughData(result, 1, '사용자 리더보드 username null 제외')) return; result.forEach((user) => { - expect(user.email).not.toBeNull(); + expect(user.username).not.toBeNull(); }); }); }); @@ -241,6 +242,7 @@ describe('LeaderboardRepository 통합 테스트', () => { expect(leaderboardPost).toHaveProperty('id'); expect(leaderboardPost).toHaveProperty('title'); expect(leaderboardPost).toHaveProperty('slug'); + expect(leaderboardPost).toHaveProperty('username'); expect(leaderboardPost).toHaveProperty('total_views'); expect(leaderboardPost).toHaveProperty('total_likes'); expect(leaderboardPost).toHaveProperty('view_diff'); diff --git a/src/repositories/__test__/leaderboard.repo.test.ts b/src/repositories/__test__/leaderboard.repo.test.ts index be3f111..2daf91f 100644 --- a/src/repositories/__test__/leaderboard.repo.test.ts +++ b/src/repositories/__test__/leaderboard.repo.test.ts @@ -6,7 +6,6 @@ import { mockPool, createMockQueryResult } from '@/utils/fixtures'; jest.mock('pg'); - describe('LeaderboardRepository', () => { let repo: LeaderboardRepository; @@ -20,6 +19,7 @@ describe('LeaderboardRepository', () => { { id: '1', email: 'test@test.com', + username: 'test', total_views: 100, total_likes: 50, total_posts: 1, @@ -30,6 +30,7 @@ describe('LeaderboardRepository', () => { { id: '2', email: 'test2@test.com', + username: 'test2', total_views: 200, total_likes: 100, total_posts: 2, @@ -79,7 +80,7 @@ describe('LeaderboardRepository', () => { expect(mockPool.query).toHaveBeenCalledWith( expect.stringContaining('WHERE date >='), // pastDateKST를 사용하는 부분 확인 - [expect.any(Number)] // limit + [expect.any(Number)], // limit ); }); @@ -96,6 +97,7 @@ describe('LeaderboardRepository', () => { id: '2', title: 'test2', slug: 'test2', + username: 'test2', total_views: 200, total_likes: 100, view_diff: 20, @@ -106,6 +108,7 @@ describe('LeaderboardRepository', () => { id: '1', title: 'test', slug: 'test', + username: 'test', total_views: 100, total_likes: 50, view_diff: 10, @@ -154,7 +157,7 @@ describe('LeaderboardRepository', () => { expect(mockPool.query).toHaveBeenCalledWith( expect.stringContaining('WHERE date >='), // pastDateKST를 사용하는 부분 확인 - [expect.any(Number)] // limit + [expect.any(Number)], // limit ); }); diff --git a/src/repositories/leaderboard.repository.ts b/src/repositories/leaderboard.repository.ts index 6786aa9..025a341 100644 --- a/src/repositories/leaderboard.repository.ts +++ b/src/repositories/leaderboard.repository.ts @@ -17,6 +17,7 @@ export class LeaderboardRepository { SELECT u.id AS id, u.email AS email, + u.username AS username, COALESCE(SUM(ts.today_view), 0) AS total_views, COALESCE(SUM(ts.today_like), 0) AS total_likes, COUNT(DISTINCT CASE WHEN p.is_active = true THEN p.id END) AS total_posts, @@ -27,8 +28,8 @@ export class LeaderboardRepository { LEFT JOIN posts_post p ON p.user_id = u.id LEFT JOIN today_stats ts ON ts.post_id = p.id LEFT JOIN start_stats ss ON ss.post_id = p.id - WHERE u.email IS NOT NULL - GROUP BY u.id, u.email + WHERE u.username IS NOT NULL + GROUP BY u.id, u.email, u.username ORDER BY ${this.SORT_COL_MAPPING[sort]} DESC, u.id LIMIT $1; `; @@ -52,11 +53,13 @@ export class LeaderboardRepository { p.title, p.slug, p.released_at, + u.username AS username, COALESCE(ts.today_view, 0) AS total_views, COALESCE(ts.today_like, 0) AS total_likes, COALESCE(ts.today_view, 0) - COALESCE(ss.start_view, COALESCE(ts.today_view, 0)) AS view_diff, COALESCE(ts.today_like, 0) - COALESCE(ss.start_like, COALESCE(ts.today_like, 0)) AS like_diff FROM posts_post p + LEFT JOIN users_user u ON u.id = p.user_id LEFT JOIN today_stats ts ON ts.post_id = p.id LEFT JOIN start_stats ss ON ss.post_id = p.id WHERE p.is_active = true diff --git a/src/services/__test__/leaderboard.service.test.ts b/src/services/__test__/leaderboard.service.test.ts index 8fde1a2..5428c2a 100644 --- a/src/services/__test__/leaderboard.service.test.ts +++ b/src/services/__test__/leaderboard.service.test.ts @@ -30,6 +30,7 @@ describe('LeaderboardService', () => { { id: '1', email: 'test@test.com', + username: 'test', total_views: '100', total_likes: '50', total_posts: '1', @@ -40,6 +41,7 @@ describe('LeaderboardService', () => { { id: '2', email: 'test2@test.com', + username: 'test2', total_views: '200', total_likes: '100', total_posts: '2', @@ -54,6 +56,7 @@ describe('LeaderboardService', () => { { id: '1', email: 'test@test.com', + username: 'test', totalViews: 100, totalLikes: 50, totalPosts: 1, @@ -64,6 +67,7 @@ describe('LeaderboardService', () => { { id: '2', email: 'test2@test.com', + username: 'test2', totalViews: 200, totalLikes: 100, totalPosts: 2, @@ -121,6 +125,7 @@ describe('LeaderboardService', () => { id: '1', title: 'test', slug: 'test-slug', + username: 'test', total_views: '100', total_likes: '50', view_diff: '20', @@ -131,6 +136,7 @@ describe('LeaderboardService', () => { id: '2', title: 'test2', slug: 'test2-slug', + username: 'test2', total_views: '200', total_likes: '100', view_diff: '10', @@ -145,6 +151,7 @@ describe('LeaderboardService', () => { id: '1', title: 'test', slug: 'test-slug', + username: 'test', totalViews: 100, totalLikes: 50, viewDiff: 20, @@ -155,6 +162,7 @@ describe('LeaderboardService', () => { id: '2', title: 'test2', slug: 'test2-slug', + username: 'test2', totalViews: 200, totalLikes: 100, viewDiff: 10, diff --git a/src/services/leaderboard.service.ts b/src/services/leaderboard.service.ts index a44b44a..475b9fc 100644 --- a/src/services/leaderboard.service.ts +++ b/src/services/leaderboard.service.ts @@ -41,7 +41,8 @@ export class LeaderboardService { private mapRawUserResult(rawResult: RawUserResult[]): UserLeaderboardData { const users = rawResult.map((user) => ({ id: user.id, - email: user.email, + email: user.email || null, + username: user.username, totalViews: Number(user.total_views), totalLikes: Number(user.total_likes), totalPosts: Number(user.total_posts), @@ -58,6 +59,7 @@ export class LeaderboardService { id: post.id, title: post.title, slug: post.slug, + username: post.username || null, totalViews: Number(post.total_views), totalLikes: Number(post.total_likes), viewDiff: Number(post.view_diff), @@ -69,24 +71,26 @@ export class LeaderboardService { } } -interface RawPostResult { +interface RawUserResult { id: string; - title: string; - slug: string; + email: string | null; + username: string; total_views: string; total_likes: string; + total_posts: string; view_diff: string; like_diff: string; - released_at: string; + post_diff: string; } -interface RawUserResult { +interface RawPostResult { id: string; - email: string; + title: string; + slug: string; + username: string | null; total_views: string; total_likes: string; - total_posts: string; view_diff: string; like_diff: string; - post_diff: string; + released_at: string; } diff --git a/src/types/dto/responses/leaderboardResponse.type.ts b/src/types/dto/responses/leaderboardResponse.type.ts index 0497be1..e3184ed 100644 --- a/src/types/dto/responses/leaderboardResponse.type.ts +++ b/src/types/dto/responses/leaderboardResponse.type.ts @@ -13,6 +13,10 @@ import { BaseResponseDto } from '@/types/dto/responses/baseResponse.type'; * email: * type: string * description: 사용자 이메일 + * nullable: true + * username: + * type: string + * description: 사용자 이름 * totalViews: * type: integer * description: 누적 조회수 @@ -34,7 +38,8 @@ import { BaseResponseDto } from '@/types/dto/responses/baseResponse.type'; */ interface LeaderboardUser { id: string; - email: string; + email: string | null; + username: string; totalViews: number; totalLikes: number; totalPosts: number; @@ -89,6 +94,10 @@ export class UserLeaderboardResponseDto extends BaseResponseDto