Skip to content

Commit 059a4c0

Browse files
authored
[25.05.12 / TASK-179] Refactor - 타임존 이슈 모두 해결!! (#31)
* feature: node runtime utc time 을 kst 의 문자열로 바꾸는 유틸 함수 추가 및 테스트 코드 추가 * refactor: 시간 관련 연산 모두 변경 * refactor: 리더보드의 시간 관련 연산 모두 변경 * refactor: 리더보드의 시간 관련 테스트 코드 업데이트 * feature: post 의 상세 통계에서 start, end 에 대한 벨리데이션, 그리고 KST 기준 연산과 이를 위한 모든 부분 대응 개발 * modify: console log 제거 * feature: findPostByPostId 의 날짜를 필수 형태로, findPostByPostUUID 를 고도화, 테스트 업데이트
1 parent 587514d commit 059a4c0

File tree

14 files changed

+369
-161
lines changed

14 files changed

+369
-161
lines changed

src/controllers/post.controller.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '@/types';
1212

1313
export class PostController {
14-
constructor(private postService: PostService) {}
14+
constructor(private postService: PostService) { }
1515

1616
getAllPosts: RequestHandler = async (
1717
req: Request<object, object, object, GetAllPostsQuery>,
@@ -70,10 +70,9 @@ export class PostController {
7070
) => {
7171
try {
7272
const postId = Number(req.params.postId);
73-
const { start, end } = req.query;
7473

74+
const { start, end } = req.query;
7575
const post = await this.postService.getPostByPostId(postId, start, end);
76-
7776
const response = new PostResponseDto(true, '단건 post 조회에 성공하였습니다.', { post }, null);
7877

7978
res.status(200).json(response);

src/modules/slack/slack.notifier.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ dotenv.config();
88
if (!process.env.SLACK_WEBHOOK_URL) {
99
throw new Error('SLACK_WEBHOOK_URL is not defined in environment variables.');
1010
}
11+
12+
// eslint-disable-next-line @typescript-eslint/naming-convention
1113
const SLACK_WEBHOOK_URL: string = process.env.SLACK_WEBHOOK_URL;
1214

1315
interface SlackPayload {
@@ -20,8 +22,7 @@ interface SlackPayload {
2022
*/
2123
export async function sendSlackMessage(message: string): Promise<void> {
2224
const payload: SlackPayload = { text: message };
23-
const response = await axios.post(SLACK_WEBHOOK_URL, payload, {
25+
await axios.post(SLACK_WEBHOOK_URL, payload, {
2426
headers: { 'Content-Type': 'application/json' },
2527
});
26-
console.log(response);
2728
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/**
2+
* 주의: 이 통합 테스트는 현재 시간에 의존적입니다.
3+
* getCurrentKSTDateString과 getKSTDateStringWithOffset 함수는 실제 시간을 기준으로
4+
* 날짜 문자열을 생성하므로, 테스트 실행 시간에 따라 결과가 달라질 수 있습니다.
5+
*/
6+
17
import logger from '@/configs/logger.config';
28
import dotenv from 'dotenv';
39
import pg from 'pg';

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ describe('LeaderboardRepository', () => {
8282
await repo.getUserLeaderboard('viewCount', 30, mockLimit);
8383

8484
expect(mockPool.query).toHaveBeenCalledWith(
85-
expect.stringContaining('LIMIT $2'),
86-
expect.arrayContaining([30, mockLimit]),
85+
expect.stringContaining('LIMIT $1'),
86+
expect.arrayContaining([mockLimit]),
8787
);
8888
});
8989

@@ -93,8 +93,8 @@ describe('LeaderboardRepository', () => {
9393
await repo.getUserLeaderboard('viewCount', mockDateRange, 10);
9494

9595
expect(mockPool.query).toHaveBeenCalledWith(
96-
expect.stringContaining('make_interval(days := $1::int)'),
97-
expect.arrayContaining([mockDateRange, expect.anything()]),
96+
expect.stringContaining('WHERE date >='), // pastDateKST를 사용하는 부분 확인
97+
[expect.any(Number)] // limit
9898
);
9999
});
100100

@@ -157,8 +157,8 @@ describe('LeaderboardRepository', () => {
157157
await repo.getPostLeaderboard('viewCount', 30, mockLimit);
158158

159159
expect(mockPool.query).toHaveBeenCalledWith(
160-
expect.stringContaining('LIMIT $2'),
161-
expect.arrayContaining([30, mockLimit]),
160+
expect.stringContaining('LIMIT $1'),
161+
expect.arrayContaining([mockLimit]),
162162
);
163163
});
164164

@@ -168,8 +168,8 @@ describe('LeaderboardRepository', () => {
168168
await repo.getPostLeaderboard('viewCount', mockDateRange, 10);
169169

170170
expect(mockPool.query).toHaveBeenCalledWith(
171-
expect.stringContaining('make_interval(days := $1::int)'),
172-
expect.arrayContaining([mockDateRange, expect.anything()]),
171+
expect.stringContaining('WHERE date >='), // pastDateKST를 사용하는 부분 확인
172+
[expect.any(Number)] // limit
173173
);
174174
});
175175

src/repositories/__test__/post.repo.integration.test.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Pool } from 'pg';
33
import pg from 'pg';
44
import { PostRepository } from '../post.repository';
55
import logger from '@/configs/logger.config';
6+
import { getCurrentKSTDateString, getKSTDateStringWithOffset } from '@/utils/date.util';
67

78

89
dotenv.config();
@@ -407,7 +408,9 @@ describe('PostRepository 통합 테스트', () => {
407408
*/
408409
describe('findPostByPostId', () => {
409410
it('게시물 ID로 통계 데이터를 조회할 수 있어야 한다', async () => {
410-
const result = await repo.findPostByPostId(TEST_DATA.POST_ID);
411+
const sevenDayAgoKST = getKSTDateStringWithOffset(-24 * 60 * 7);
412+
const endKST = getCurrentKSTDateString();
413+
const result = await repo.findPostByPostId(TEST_DATA.POST_ID, sevenDayAgoKST, endKST);
411414

412415
expect(result).toBeDefined();
413416
expect(Array.isArray(result)).toBe(true);
@@ -437,7 +440,9 @@ describe('PostRepository 통합 테스트', () => {
437440
});
438441

439442
it('날짜 오름차순으로 정렬된 결과를 반환해야 한다', async () => {
440-
const result = await repo.findPostByPostId(TEST_DATA.POST_ID);
443+
const sevenDayAgoKST = getKSTDateStringWithOffset(-24 * 60 * 7);
444+
const endKST = getCurrentKSTDateString();
445+
const result = await repo.findPostByPostId(TEST_DATA.POST_ID, sevenDayAgoKST, endKST);
441446

442447
// 2개 이상의 결과가 있는 경우에만 정렬 검증
443448
if (result.length >= 2) {
@@ -459,16 +464,20 @@ describe('PostRepository 통합 테스트', () => {
459464
});
460465

461466
it('존재하지 않는 게시물 ID에 대해 빈 배열을 반환해야 한다', async () => {
467+
const sevenDayAgoKST = getKSTDateStringWithOffset(-24 * 60 * 7);
468+
const endKST = getCurrentKSTDateString();
462469
const nonExistentPostId = 9999999;
463-
const result = await repo.findPostByPostId(nonExistentPostId);
470+
const result = await repo.findPostByPostId(nonExistentPostId, sevenDayAgoKST, endKST);
464471

465472
expect(result).toBeDefined();
466473
expect(Array.isArray(result)).toBe(true);
467474
expect(result.length).toBe(0);
468475
});
469476

470477
it('날짜 형식이 올바르게 변환되어야 한다', async () => {
471-
const result = await repo.findPostByPostId(TEST_DATA.POST_ID);
478+
const sevenDayAgoKST = getKSTDateStringWithOffset(-24 * 60 * 7);
479+
const endKST = getCurrentKSTDateString();
480+
const result = await repo.findPostByPostId(TEST_DATA.POST_ID, sevenDayAgoKST, endKST);
472481

473482
if (result.length <= 0) {
474483
logger.info('존재하지 않는 게시물 ID에 대해 빈 배열을 테스트를 위한 충분한 데이터가 없습니다.');
@@ -490,7 +499,9 @@ describe('PostRepository 통합 테스트', () => {
490499
});
491500

492501
it('일일 조회수와 좋아요 수가 숫자 타입이어야 한다', async () => {
493-
const result = await repo.findPostByPostId(TEST_DATA.POST_ID);
502+
const sevenDayAgoKST = getKSTDateStringWithOffset(-24 * 60 * 7);
503+
const endKST = getCurrentKSTDateString();
504+
const result = await repo.findPostByPostId(TEST_DATA.POST_ID, sevenDayAgoKST, endKST);
494505

495506
if (result.length <= 0) {
496507
logger.info('일일 조회수와 좋아요 수가 숫자 타입인지 테스트를 위한 충분한 데이터가 없습니다.');

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ describe('PostRepository', () => {
240240

241241
mockPool.query.mockResolvedValue(createMockQueryResult(mockStats));
242242

243-
const result = await repo.findPostByPostId(1);
243+
const result = await repo.findPostByPostId(1, '2025-05-01', '2025-05-08');
244244
expect(result).toEqual(mockStats);
245245
});
246246
});

src/repositories/leaderboard.repository.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import logger from '@/configs/logger.config';
22
import { Pool } from 'pg';
33
import { DBError } from '@/exception';
44
import { UserLeaderboardSortType, PostLeaderboardSortType } from '@/types/index';
5+
import { getCurrentKSTDateString, getKSTDateStringWithOffset } from '@/utils/date.util';
56

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

910
async getUserLeaderboard(sort: UserLeaderboardSortType, dateRange: number, limit: number) {
1011
try {
11-
const cteQuery = this.buildLeaderboardCteQuery();
12+
const pastDateKST = getKSTDateStringWithOffset(-dateRange * 24 * 60);
13+
const cteQuery = this.buildLeaderboardCteQuery(dateRange);
1214

1315
const query = `
1416
${cteQuery}
@@ -20,17 +22,17 @@ export class LeaderboardRepository {
2022
COUNT(DISTINCT CASE WHEN p.is_active = true THEN p.id END) AS total_posts,
2123
SUM(COALESCE(ts.today_view, 0) - COALESCE(ss.start_view, COALESCE(ts.today_view, 0))) AS view_diff,
2224
SUM(COALESCE(ts.today_like, 0) - COALESCE(ss.start_like, COALESCE(ts.today_like, 0))) AS like_diff,
23-
COUNT(DISTINCT CASE WHEN p.released_at >= CURRENT_DATE - make_interval(days := $1::int) AND p.is_active = true THEN p.id END) AS post_diff
25+
COUNT(DISTINCT CASE WHEN p.released_at >= '${pastDateKST}' AND p.is_active = true THEN p.id END) AS post_diff
2426
FROM users_user u
2527
LEFT JOIN posts_post p ON p.user_id = u.id
2628
LEFT JOIN today_stats ts ON ts.post_id = p.id
2729
LEFT JOIN start_stats ss ON ss.post_id = p.id
2830
WHERE u.email IS NOT NULL
2931
GROUP BY u.id, u.email
3032
ORDER BY ${this.SORT_COL_MAPPING[sort]} DESC, u.id
31-
LIMIT $2;
33+
LIMIT $1;
3234
`;
33-
const result = await this.pool.query(query, [dateRange, limit]);
35+
const result = await this.pool.query(query, [limit]);
3436

3537
return result.rows;
3638
} catch (error) {
@@ -41,7 +43,7 @@ export class LeaderboardRepository {
4143

4244
async getPostLeaderboard(sort: PostLeaderboardSortType, dateRange: number, limit: number) {
4345
try {
44-
const cteQuery = this.buildLeaderboardCteQuery();
46+
const cteQuery = this.buildLeaderboardCteQuery(dateRange);
4547

4648
const query = `
4749
${cteQuery}
@@ -59,9 +61,9 @@ export class LeaderboardRepository {
5961
LEFT JOIN start_stats ss ON ss.post_id = p.id
6062
WHERE p.is_active = true
6163
ORDER BY ${this.SORT_COL_MAPPING[sort]} DESC, p.id
62-
LIMIT $2;
64+
LIMIT $1;
6365
`;
64-
const result = await this.pool.query(query, [dateRange, limit]);
66+
const result = await this.pool.query(query, [limit]);
6567

6668
return result.rows;
6769
} catch (error) {
@@ -71,7 +73,11 @@ export class LeaderboardRepository {
7173
}
7274

7375
// 오늘 날짜와 기준 날짜의 통계를 가져오는 CTE(임시 결과 집합) 쿼리 빌드
74-
private buildLeaderboardCteQuery() {
76+
private buildLeaderboardCteQuery(dateRange: number) {
77+
const nowDateKST = getCurrentKSTDateString();
78+
// 과거 날짜 계산 (dateRange일 전)
79+
const pastDateKST = getKSTDateStringWithOffset(-dateRange * 24 * 60);
80+
7581
return `
7682
WITH
7783
today_stats AS (
@@ -80,7 +86,7 @@ export class LeaderboardRepository {
8086
daily_view_count AS today_view,
8187
daily_like_count AS today_like
8288
FROM posts_postdailystatistics
83-
WHERE (date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date <= (NOW() AT TIME ZONE 'UTC')::date
89+
WHERE date <= '${nowDateKST}'
8490
ORDER BY post_id, date DESC
8591
),
8692
start_stats AS (
@@ -89,7 +95,7 @@ export class LeaderboardRepository {
8995
daily_view_count AS start_view,
9096
daily_like_count AS start_like
9197
FROM posts_postdailystatistics
92-
WHERE (date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date >= ((NOW() AT TIME ZONE 'UTC')::date - make_interval(days := $1::int))
98+
WHERE date >= '${pastDateKST}'
9399
ORDER BY post_id, date ASC
94100
)
95101
`;

0 commit comments

Comments
 (0)