Skip to content

Commit 31b6ea5

Browse files
committed
refactor: 시간 관련 연산 모두 변경
1 parent 6b71fd9 commit 31b6ea5

File tree

3 files changed

+121
-91
lines changed

3 files changed

+121
-91
lines changed

src/repositories/post.repository.ts

Lines changed: 51 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Pool } from 'pg';
22
import logger from '@/configs/logger.config';
33
import { DBError } from '@/exception';
4+
import { getCurrentKSTDateString, getKSTDateStringWithOffset } from '@/utils/date.util';
45

56
export class PostRepository {
67
constructor(private pool: Pool) { }
@@ -12,6 +13,10 @@ export class PostRepository {
1213
isAsc: boolean = false,
1314
limit: number = 15
1415
) {
16+
const nowDateKST = getCurrentKSTDateString();
17+
const tomorrowDateKST = getKSTDateStringWithOffset(24 * 60);
18+
const yesterDateKST = getKSTDateStringWithOffset(-24 * 60);
19+
1520
try {
1621
// 1) 정렬 컬럼 매핑
1722
let sortCol = 'p.released_at';
@@ -70,12 +75,12 @@ export class PostRepository {
7075
LEFT JOIN (
7176
SELECT post_id, daily_view_count, daily_like_count, date
7277
FROM posts_postdailystatistics
73-
WHERE (date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date = (NOW() AT TIME ZONE 'UTC')::date
78+
WHERE date >= '${nowDateKST}' AND date < '${tomorrowDateKST}'
7479
) pds ON p.id = pds.post_id
7580
LEFT JOIN (
7681
SELECT post_id, daily_view_count, daily_like_count, date
7782
FROM posts_postdailystatistics
78-
WHERE (date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date = (NOW() AT TIME ZONE 'UTC' - INTERVAL '1 day')::date
83+
WHERE date >= '${yesterDateKST}' AND date < '${nowDateKST}'
7984
) yesterday_stats ON p.id = yesterday_stats.post_id
8085
WHERE p.user_id = $1
8186
AND p.is_active = TRUE
@@ -128,6 +133,10 @@ export class PostRepository {
128133
isAsc: boolean = false,
129134
limit: number = 15
130135
) {
136+
const nowDateKST = getCurrentKSTDateString();
137+
const tomorrowDateKST = getKSTDateStringWithOffset(24 * 60);
138+
const yesterDateKST = getKSTDateStringWithOffset(-24 * 60);
139+
131140
try {
132141
const selectFields = `
133142
p.id,
@@ -170,25 +179,25 @@ export class PostRepository {
170179
}
171180

172181
const query = `
173-
SELECT ${selectFields}
174-
FROM posts_post p
175-
LEFT JOIN (
176-
SELECT post_id, daily_view_count, daily_like_count, date
177-
FROM posts_postdailystatistics
178-
WHERE (date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date = (NOW() AT TIME ZONE 'UTC')::date
179-
) pds ON p.id = pds.post_id
180-
LEFT JOIN (
181-
SELECT post_id, daily_view_count, daily_like_count, date
182-
FROM posts_postdailystatistics
183-
WHERE (date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date = (NOW() AT TIME ZONE 'UTC' - INTERVAL '1 day')::date
184-
) yesterday_stats ON p.id = yesterday_stats.post_id
185-
WHERE p.user_id = $1
186-
AND p.is_active = TRUE
187-
AND (pds.post_id IS NOT NULL OR yesterday_stats.post_id IS NOT NULL)
188-
${cursorCondition}
189-
ORDER BY ${orderByExpression}
190-
LIMIT ${cursor ? '$4' : '$2'}
191-
`;
182+
SELECT ${selectFields}
183+
FROM posts_post p
184+
LEFT JOIN (
185+
SELECT post_id, daily_view_count, daily_like_count, date
186+
FROM posts_postdailystatistics
187+
WHERE date >= '${nowDateKST}' AND date < '${tomorrowDateKST}'
188+
) pds ON p.id = pds.post_id
189+
LEFT JOIN (
190+
SELECT post_id, daily_view_count, daily_like_count, date
191+
FROM posts_postdailystatistics
192+
WHERE date >= '${yesterDateKST}' AND date < '${nowDateKST}'
193+
) yesterday_stats ON p.id = yesterday_stats.post_id
194+
WHERE p.user_id = $1
195+
AND p.is_active = TRUE
196+
AND (pds.post_id IS NOT NULL OR yesterday_stats.post_id IS NOT NULL)
197+
${cursorCondition}
198+
ORDER BY ${orderByExpression}
199+
LIMIT ${cursor ? '$4' : '$2'}
200+
`;
192201

193202
const posts = await this.pool.query(query, params);
194203

@@ -225,25 +234,28 @@ export class PostRepository {
225234
}
226235

227236
async getYesterdayAndTodayViewLikeStats(userId: number) {
228-
// ! pds.updated_at 은 FE 화면을 위해 억지로 9h 시간 더한 값임 주의
237+
const nowDateKST = getCurrentKSTDateString();
238+
const tomorrowDateKST = getKSTDateStringWithOffset(24 * 60);
239+
const yesterDateKST = getKSTDateStringWithOffset(-24 * 60);
240+
229241
try {
230242
const query = `
231243
SELECT
232244
COALESCE(SUM(pds.daily_view_count), 0) AS daily_view_count,
233245
COALESCE(SUM(pds.daily_like_count), 0) AS daily_like_count,
234246
COALESCE(SUM(yesterday_stats.daily_view_count), 0) AS yesterday_views,
235247
COALESCE(SUM(yesterday_stats.daily_like_count), 0) AS yesterday_likes,
236-
(MAX(pds.updated_at AT TIME ZONE 'Asia/Seoul') AT TIME ZONE 'UTC') AS last_updated_date
248+
MAX(pds.updated_at) AS last_updated_date
237249
FROM posts_post p
238250
LEFT JOIN (
239251
SELECT post_id, daily_view_count, daily_like_count, updated_at
240252
FROM posts_postdailystatistics
241-
WHERE (date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date = (NOW() AT TIME ZONE 'UTC')::date
253+
WHERE date >= '${nowDateKST}' AND date < '${tomorrowDateKST}'
242254
) pds ON p.id = pds.post_id
243255
LEFT JOIN (
244256
SELECT post_id, daily_view_count, daily_like_count
245257
FROM posts_postdailystatistics
246-
WHERE (date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date = (NOW() AT TIME ZONE 'UTC' - INTERVAL '1 day')::date
258+
WHERE date >= '${yesterDateKST}' AND date < '${nowDateKST}'
247259
) yesterday_stats ON p.id = yesterday_stats.post_id
248260
WHERE p.user_id = $1
249261
AND p.is_active = TRUE
@@ -263,31 +275,31 @@ export class PostRepository {
263275
// 기본 쿼리 부분
264276
const baseQuery = `
265277
SELECT
266-
(pds.date AT TIME ZONE 'Asia/Seoul') AT TIME ZONE 'UTC' AS date,
278+
pds.date,
267279
pds.daily_view_count,
268280
pds.daily_like_count
269281
FROM posts_postdailystatistics pds
270282
WHERE pds.post_id = $1
271283
`;
272284

273285
// 날짜 필터링 조건 구성
274-
const dateFilterQuery = (start && end)
275-
? `
276-
AND (pds.date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date >= ($2 AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date
277-
AND (pds.date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date <= ($3 AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date
278-
`
279-
: '';
286+
let dateFilterQuery = '';
287+
const queryParams: Array<number | string> = [postId];
288+
289+
if (start && end) {
290+
dateFilterQuery = `
291+
AND pds.date >= $2
292+
AND pds.date <= $3
293+
`;
294+
queryParams.push(start, end);
295+
}
280296

281297
// 정렬 조건 추가
282298
const orderByQuery = `ORDER BY pds.date ASC`;
283299

284300
// 최종 쿼리 조합
285301
const fullQuery = [baseQuery, dateFilterQuery, orderByQuery].join(' ');
286302

287-
// 파라미터 배열 구성
288-
const queryParams: Array<number | string> = [postId];
289-
if (start && end) queryParams.push(start, end);
290-
291303
// 쿼리 실행
292304
const result = await this.pool.query(fullQuery, queryParams);
293305
return result.rows;
@@ -301,15 +313,15 @@ export class PostRepository {
301313
try {
302314
const query = `
303315
SELECT
304-
(pds.date AT TIME ZONE 'Asia/Seoul') AT TIME ZONE 'UTC' AS date,
316+
pds.date,
305317
pds.daily_view_count,
306318
pds.daily_like_count
307319
FROM posts_post p
308320
JOIN posts_postdailystatistics pds ON p.id = pds.post_id
309321
WHERE p.post_uuid = $1
310322
AND p.is_active = TRUE
311-
AND (pds.date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date >= ($2 AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date
312-
AND (pds.date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date <= ($3 AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date
323+
AND pds.date >= $2
324+
AND pds.date <= $3
313325
ORDER BY pds.date ASC
314326
`;
315327

src/utils/__test__/date.util.test.ts

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -33,34 +33,49 @@ describe('Date Utilities', () => {
3333
const year = kst.getUTCFullYear();
3434
const month = String(kst.getUTCMonth() + 1).padStart(2, '0');
3535
const day = String(kst.getUTCDate()).padStart(2, '0');
36-
const hour = String(kst.getUTCHours()).padStart(2, '0');
37-
const minute = String(kst.getUTCMinutes()).padStart(2, '0');
38-
const second = String(kst.getUTCSeconds()).padStart(2, '0');
39-
return `${year}-${month}-${day} ${hour}:${minute}:${second}+09`;
36+
// 시간을 00:00:00으로 고정
37+
return `${year}-${month}-${day} 00:00:00+09`;
4038
};
4139

40+
4241
it('getCurrentKSTDateString이 KST 포맷의 문자열을 반환해야 한다', () => {
43-
// 형식 검증
42+
// 형식 검증 - HH:MM:SS가 항상 00:00:00
4443
const result = getCurrentKSTDateString();
45-
expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\+09$/);
44+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2} 00:00:00\+09$/);
4645

47-
// 현재 시간 기준 내용 검증
46+
// 현재 날짜 기준 내용 검증
4847
const now = new Date();
4948
const expected = formatKST(now);
5049

51-
// 날짜 부분만 검증 (시간은 테스트 실행 중 변할 수 있음)
52-
expect(result.slice(0, 10)).toBe(expected.slice(0, 10));
50+
// 전체 문자열을 비교 (시간은 항상 00:00:00)
51+
expect(result).toBe(expected);
5352
});
5453

5554
it('getKSTDateStringWithOffset이 KST 포맷의 문자열을 반환해야 한다', () => {
5655
const result = getKSTDateStringWithOffset(30);
5756
expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\+09$/);
5857
});
5958

60-
it('getCurrentKSTDateString은 5분 후와 다른 값을 반환해야 한다', () => {
59+
it('getKSTDateStringWithOffset(0)은 getCurrentKSTDateString과 동일한 값을 반환해야 한다', () => {
60+
// 시간을 고정하여 두 함수 호출 사이에 실제 시간이 변경되지 않도록 함
61+
const fixed = Date.now();
62+
Date.now = jest.fn(() => fixed);
63+
64+
const current = getCurrentKSTDateString();
65+
const offsetZero = getKSTDateStringWithOffset(0);
66+
67+
expect(current).toBe(offsetZero);
68+
});
69+
70+
it('getCurrentKSTDateString은 날짜가 변경될 때만 다른 값을 반환해야 한다', () => {
6171
// 고정된 시간 설정
6272
const fixedTime = new originalDate(Date.UTC(2025, 4, 10, 6, 30, 0)); // 2025-05-10 06:30:00 UTC
63-
const fiveMinutesLater = new originalDate(fixedTime.getTime() + 5 * 60 * 1000);
73+
74+
// 같은 날 5분 후
75+
const sameDay = new originalDate(fixedTime.getTime() + 5 * 60 * 1000);
76+
77+
// 다음 날 (날짜가 변경됨)
78+
const nextDay = new originalDate(fixedTime.getTime() + 24 * 60 * 60 * 1000);
6479

6580
let callCount = 0;
6681

@@ -70,46 +85,44 @@ describe('Date Utilities', () => {
7085
if (args.length > 0) {
7186
super(...args);
7287
} else {
73-
// new Date()로 호출될 때 다른 시간 반환
74-
super(callCount++ === 0 ? fixedTime.getTime() : fiveMinutesLater.getTime());
88+
// 호출 순서에 따라 다른 시간 반환
89+
if (callCount === 0) {
90+
super(fixedTime.getTime());
91+
} else if (callCount === 1) {
92+
super(sameDay.getTime());
93+
} else {
94+
super(nextDay.getTime());
95+
}
96+
callCount++;
7597
}
7698
}
7799
}
78100

79101
global.Date = MockDate as unknown as DateConstructor;
80102

81-
const before = getCurrentKSTDateString();
82-
const after = getCurrentKSTDateString();
83-
84-
expect(before).not.toBe(after);
85-
});
103+
const first = getCurrentKSTDateString();
104+
const second = getCurrentKSTDateString(); // 같은 날
105+
const third = getCurrentKSTDateString(); // 다음 날
86106

87-
it('getKSTDateStringWithOffset(0)은 getCurrentKSTDateString과 동일한 값을 반환해야 한다', () => {
88-
// 시간을 고정하여 두 함수 호출 사이에 실제 시간이 변경되지 않도록 함
89-
const fixed = Date.now();
90-
Date.now = jest.fn(() => fixed);
91-
92-
const current = getCurrentKSTDateString();
93-
const offsetZero = getKSTDateStringWithOffset(0);
94-
95-
expect(current).toBe(offsetZero);
107+
expect(first).toBe(second); // 같은 날이므로 동일해야 함
108+
expect(first).not.toBe(third); // 다른 날이므로 달라야 함
96109
});
97110

98-
it('getKSTDateStringWithOffset(60)은 정확히 1시간 후 KST 시간을 반환해야 한다', () => {
99-
// 기준 시간과 1시간 후 시간 설정
111+
it('getKSTDateStringWithOffset(1440)은 정확히 하루 후의 날짜를 반환해야 한다', () => {
112+
// 기준 시간과 하루 후 시간 설정
100113
const baseTime = new Date();
101-
const oneHourLater = new Date(baseTime.getTime() + 60 * 60 * 1000);
114+
const nextDay = new Date(baseTime.getTime() + 24 * 60 * 60 * 1000);
102115

103116
// Date 생성자 모킹
104117
let callCount = 0;
105118
jest.spyOn(global, 'Date').mockImplementation(function (this: Date, time?: number | string | Date): Date {
106119
if (time !== undefined) return new originalDate(time);
107120
// 첫 호출과 두 번째 호출에서 다른 시간 반환
108-
return callCount++ === 0 ? baseTime : oneHourLater;
121+
return callCount++ === 0 ? baseTime : nextDay;
109122
} as unknown as (time?: number | string | Date) => Date);
110123

111-
const result = getKSTDateStringWithOffset(60);
112-
const expected = formatKST(oneHourLater);
124+
const result = getKSTDateStringWithOffset(1440); // 1440분 = 24시간 = 1일
125+
const expected = formatKST(nextDay);
113126

114127
expect(result).toBe(expected);
115128
});

src/utils/date.util.ts

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/**
2-
* 현재 시간을 한국 표준시(KST, UTC+9)의 포맷팅된 문자열로 반환합니다.
2+
* 현재 날짜의 시작 시간(00:00:00)을 한국 표준시(KST, UTC+9)의 포맷팅된 문자열로 반환합니다.
33
*
4-
* @returns {string} 'YYYY-MM-DD HH:MM:SS+09' 형식의 한국 시간 문자열
4+
* @returns {string} 'YYYY-MM-DD 00:00:00+09' 형식의 한국 시간 문자열
55
* @example
6-
* // 반환 예시: '2025-05-10 15:30:25+09'
7-
* const nowKST = getCurrentKSTDateString();
6+
* // 현재 시간이 2025-05-10 15:30:25 KST일 경우
7+
* // 반환 예시: '2025-05-10 00:00:00+09'
8+
* const todayStartKST = getCurrentKSTDateString();
89
*/
910
export function getCurrentKSTDateString(): string {
1011
const now = new Date();
@@ -14,25 +15,31 @@ export function getCurrentKSTDateString(): string {
1415
const year = kstDate.getUTCFullYear();
1516
const month = String(kstDate.getUTCMonth() + 1).padStart(2, '0');
1617
const day = String(kstDate.getUTCDate()).padStart(2, '0');
17-
const hours = String(kstDate.getUTCHours()).padStart(2, '0');
18-
const minutes = String(kstDate.getUTCMinutes()).padStart(2, '0');
19-
const seconds = String(kstDate.getUTCSeconds()).padStart(2, '0');
2018

21-
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}+09`;
19+
// 시간은 항상 00:00:00으로 고정
20+
return `${year}-${month}-${day} 00:00:00+09`;
2221
}
2322

2423
/**
25-
* 현재 시간으로부터 지정된 분(minutes) 후의 시간을 한국 표준시(KST, UTC+9)로 반환합니다.
24+
* 현재 시간으로부터 지정된 분(minutes) 후의 날짜에 대한 시작 시간(00:00:00)을
25+
* 한국 표준시(KST, UTC+9)로 반환합니다.
2626
*
2727
* @param {number} minutes - 현재 시간에 더할 분(minutes)
28-
* @returns {string} 'YYYY-MM-DD HH:MM:SS+09' 형식의 지정된 시간 후의 한국 시간 문자열
28+
* @returns {string} 'YYYY-MM-DD 00:00:00+09' 형식의 지정된 날짜의 시작 시간 문자열
2929
* @example
30-
* // 5분 후의 시간을 얻기
31-
* // 반환 예시: '2025-05-10 15:35:25+09'
32-
* const fiveMinutesLater = getKSTDateStringWithOffset(5);
30+
* // 현재 시간이 2025-05-10 15:30:25 KST일 경우
3331
*
34-
* // 1시간(60분) 후의 시간을 얻기
35-
* const oneHourLater = getKSTDateStringWithOffset(60);
32+
* // 5분 후 날짜의 시작 시간 (같은 날이므로 동일)
33+
* // 반환 예시: '2025-05-10 00:00:00+09'
34+
* const sameDay = getKSTDateStringWithOffset(5);
35+
*
36+
* // 하루 후(1440분)의 날짜 시작 시간
37+
* // 반환 예시: '2025-05-11 00:00:00+09'
38+
* const nextDay = getKSTDateStringWithOffset(1440);
39+
*
40+
* // 하루 전(-1440분)의 날짜 시작 시간
41+
* // 반환 예시: '2025-05-09 00:00:00+09'
42+
* const previousDay = getKSTDateStringWithOffset(-1440);
3643
*/
3744
export function getKSTDateStringWithOffset(minutes: number): string {
3845
const now = new Date();
@@ -44,9 +51,7 @@ export function getKSTDateStringWithOffset(minutes: number): string {
4451
const year = kstDate.getUTCFullYear();
4552
const month = String(kstDate.getUTCMonth() + 1).padStart(2, '0');
4653
const day = String(kstDate.getUTCDate()).padStart(2, '0');
47-
const hours = String(kstDate.getUTCHours()).padStart(2, '0');
48-
const min = String(kstDate.getUTCMinutes()).padStart(2, '0');
49-
const sec = String(kstDate.getUTCSeconds()).padStart(2, '0');
5054

51-
return `${year}-${month}-${day} ${hours}:${min}:${sec}+09`;
55+
// 시간은 항상 00:00:00으로 고정
56+
return `${year}-${month}-${day} 00:00:00+09`;
5257
}

0 commit comments

Comments
 (0)