Skip to content
5 changes: 2 additions & 3 deletions src/controllers/post.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '@/types';

export class PostController {
constructor(private postService: PostService) {}
constructor(private postService: PostService) { }

getAllPosts: RequestHandler = async (
req: Request<object, object, object, GetAllPostsQuery>,
Expand Down Expand Up @@ -70,10 +70,9 @@ export class PostController {
) => {
try {
const postId = Number(req.params.postId);
const { start, end } = req.query;

const { start, end } = req.query;
const post = await this.postService.getPostByPostId(postId, start, end);

const response = new PostResponseDto(true, '단건 post 조회에 성공하였습니다.', { post }, null);

res.status(200).json(response);
Expand Down
5 changes: 3 additions & 2 deletions src/modules/slack/slack.notifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ dotenv.config();
if (!process.env.SLACK_WEBHOOK_URL) {
throw new Error('SLACK_WEBHOOK_URL is not defined in environment variables.');
}

// eslint-disable-next-line @typescript-eslint/naming-convention
const SLACK_WEBHOOK_URL: string = process.env.SLACK_WEBHOOK_URL;

interface SlackPayload {
Expand All @@ -20,8 +22,7 @@ interface SlackPayload {
*/
export async function sendSlackMessage(message: string): Promise<void> {
const payload: SlackPayload = { text: message };
const response = await axios.post(SLACK_WEBHOOK_URL, payload, {
await axios.post(SLACK_WEBHOOK_URL, payload, {
headers: { 'Content-Type': 'application/json' },
});
console.log(response);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* 주의: 이 통합 테스트는 현재 시간에 의존적입니다.
* getCurrentKSTDateString과 getKSTDateStringWithOffset 함수는 실제 시간을 기준으로
* 날짜 문자열을 생성하므로, 테스트 실행 시간에 따라 결과가 달라질 수 있습니다.
*/

import logger from '@/configs/logger.config';
import dotenv from 'dotenv';
import pg from 'pg';
Expand Down
16 changes: 8 additions & 8 deletions src/repositories/__test__/leaderboard.repo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ describe('LeaderboardRepository', () => {
await repo.getUserLeaderboard('viewCount', 30, mockLimit);

expect(mockPool.query).toHaveBeenCalledWith(
expect.stringContaining('LIMIT $2'),
expect.arrayContaining([30, mockLimit]),
expect.stringContaining('LIMIT $1'),
expect.arrayContaining([mockLimit]),
);
});

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

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

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

expect(mockPool.query).toHaveBeenCalledWith(
expect.stringContaining('LIMIT $2'),
expect.arrayContaining([30, mockLimit]),
expect.stringContaining('LIMIT $1'),
expect.arrayContaining([mockLimit]),
);
});

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

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

Expand Down
21 changes: 16 additions & 5 deletions src/repositories/__test__/post.repo.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Pool } from 'pg';
import pg from 'pg';
import { PostRepository } from '../post.repository';
import logger from '@/configs/logger.config';
import { getCurrentKSTDateString, getKSTDateStringWithOffset } from '@/utils/date.util';


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

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

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

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

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

expect(result).toBeDefined();
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(0);
});

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

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

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

if (result.length <= 0) {
logger.info('일일 조회수와 좋아요 수가 숫자 타입인지 테스트를 위한 충분한 데이터가 없습니다.');
Expand Down
2 changes: 1 addition & 1 deletion src/repositories/__test__/post.repo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ describe('PostRepository', () => {

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

const result = await repo.findPostByPostId(1);
const result = await repo.findPostByPostId(1, '2025-05-01', '2025-05-08');
expect(result).toEqual(mockStats);
});
});
Expand Down
26 changes: 16 additions & 10 deletions src/repositories/leaderboard.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import logger from '@/configs/logger.config';
import { Pool } from 'pg';
import { DBError } from '@/exception';
import { UserLeaderboardSortType, PostLeaderboardSortType } from '@/types/index';
import { getCurrentKSTDateString, getKSTDateStringWithOffset } from '@/utils/date.util';

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

async getUserLeaderboard(sort: UserLeaderboardSortType, dateRange: number, limit: number) {
try {
const cteQuery = this.buildLeaderboardCteQuery();
const pastDateKST = getKSTDateStringWithOffset(-dateRange * 24 * 60);
const cteQuery = this.buildLeaderboardCteQuery(dateRange);

const query = `
${cteQuery}
Expand All @@ -20,17 +22,17 @@ export class LeaderboardRepository {
COUNT(DISTINCT CASE WHEN p.is_active = true THEN p.id END) AS total_posts,
SUM(COALESCE(ts.today_view, 0) - COALESCE(ss.start_view, COALESCE(ts.today_view, 0))) AS view_diff,
SUM(COALESCE(ts.today_like, 0) - COALESCE(ss.start_like, COALESCE(ts.today_like, 0))) AS like_diff,
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
COUNT(DISTINCT CASE WHEN p.released_at >= '${pastDateKST}' AND p.is_active = true THEN p.id END) AS post_diff
FROM users_user u
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
ORDER BY ${this.SORT_COL_MAPPING[sort]} DESC, u.id
LIMIT $2;
LIMIT $1;
`;
const result = await this.pool.query(query, [dateRange, limit]);
const result = await this.pool.query(query, [limit]);

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

async getPostLeaderboard(sort: PostLeaderboardSortType, dateRange: number, limit: number) {
try {
const cteQuery = this.buildLeaderboardCteQuery();
const cteQuery = this.buildLeaderboardCteQuery(dateRange);

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

return result.rows;
} catch (error) {
Expand All @@ -71,7 +73,11 @@ export class LeaderboardRepository {
}

// 오늘 날짜와 기준 날짜의 통계를 가져오는 CTE(임시 결과 집합) 쿼리 빌드
private buildLeaderboardCteQuery() {
private buildLeaderboardCteQuery(dateRange: number) {
const nowDateKST = getCurrentKSTDateString();
// 과거 날짜 계산 (dateRange일 전)
const pastDateKST = getKSTDateStringWithOffset(-dateRange * 24 * 60);

return `
WITH
today_stats AS (
Expand All @@ -80,7 +86,7 @@ export class LeaderboardRepository {
daily_view_count AS today_view,
daily_like_count AS today_like
FROM posts_postdailystatistics
WHERE (date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date <= (NOW() AT TIME ZONE 'UTC')::date
WHERE date <= '${nowDateKST}'
ORDER BY post_id, date DESC
),
start_stats AS (
Expand All @@ -89,7 +95,7 @@ export class LeaderboardRepository {
daily_view_count AS start_view,
daily_like_count AS start_like
FROM posts_postdailystatistics
WHERE (date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date >= ((NOW() AT TIME ZONE 'UTC')::date - make_interval(days := $1::int))
WHERE date >= '${pastDateKST}'
ORDER BY post_id, date ASC
)
`;
Expand Down
Loading
Loading