diff --git a/.github/workflows/test-ci.yaml b/.github/workflows/test-ci.yaml index 08c7bfc..df5e24b 100644 --- a/.github/workflows/test-ci.yaml +++ b/.github/workflows/test-ci.yaml @@ -13,7 +13,7 @@ jobs: strategy: matrix: node-version: [20, 21, 22, 23] - fail-fast: false # 한 버전 실패 시 전체 중단 방지 + fail-fast: false # 한 버전 실패 시 전체 중단 방지 steps: - name: Checkout repository @@ -50,6 +50,14 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Create .env file + run: | + echo "DATABASE_NAME=${{ secrets.DATABASE_NAME }}" >> .env + echo "POSTGRES_USER=${{ secrets.POSTGRES_USER }}" >> .env + echo "POSTGRES_HOST=${{ secrets.POSTGRES_HOST }}" >> .env + echo "POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}" >> .env + echo "POSTGRES_PORT=${{ secrets.POSTGRES_PORT }}" >> .env + - name: Run lint run: pnpm run lint @@ -57,4 +65,4 @@ jobs: run: pnpm run test - name: Run build - run: pnpm run build \ No newline at end of file + run: pnpm run build diff --git a/README.md b/README.md index 5e2df06..92af825 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Velog Dashboard Project - Velog dashboard V2 백엔드, API 서버 -- ***`node 20+`*** +- **_`node 20+`_** ## Project Setup Guide @@ -30,6 +30,12 @@ cp .env.sample .env pnpm dev ``` +4. 로컬 테스팅을 위해서 `post.repo.integration.test` 를 필수 참조해주세요. + +- 해당 테스트는 mocking 없이 DBMS connection 을 맺고 repo 계층의 실제 수행을 테스트 합니다. +- 이에 따라, local DBMS 와 connection 을 맺는다면 **_테스트로 제공해야 할 TEST CASE 의 값들이 달라져야 합니다._** +- 이 때문에 전체 테스트에 이슈가 있을 수 있으니 해당 값 꼭 체크 해주세요. + ## 실행 가능한 명령어 ```bash @@ -46,17 +52,17 @@ pnpm start # 빌드된 프로젝트 시작 ```bash ├── src/ -├── __test__/ # 테스트 파일 -├── configs/ # 설정 파일 (DB 등) -├── constants/ # 상수 데이터 파일 -├── controllers/ # API 컨트롤러 -├── exception/ # 커스텀 에러 파일 -├── middlewares/ # 각종 미들웨어 (인증, 에러, 데이터 검증 등) -├── modules/ # 모듈 파일 (슬랙 등) -├── repositories/ # 데이터 액세스 레이어 -├── routers/ # API 라우트 정의 -├── services/ # 비즈니스 로직 -├┬── types/ # Enum, DTO 등 데이터 타입 정의 -│└── models/ # 데이터 모델 -└── utils/ # 편의성 함수 정의 +│   ├── __test__/ # 테스트 파일 +│   ├── configs/ # 설정 파일 (DB 등) +│   ├── constants/ # 상수 데이터 파일 +│   ├── controllers/ # API 컨트롤러 +│   ├── exception/ # 커스텀 에러 파일 +│   ├── middlewares/ # 각종 미들웨어 (인증, 에러, 데이터 검증 등) +│   ├── modules/ # 모듈 파일 (슬랙 등) +│   ├── repositories/ # 데이터 액세스 레이어 +│   ├── routers/ # API 라우트 정의 +│   ├── services/ # 비즈니스 로직 +│   ├── types/ # Enum, DTO 등 데이터 타입 정의 +│   │   ├── models/ # 데이터 모델 +│   └── utils/ # 편의성 함수 정의 ``` diff --git a/src/controllers/post.controller.ts b/src/controllers/post.controller.ts index 36e879f..8c8a20f 100644 --- a/src/controllers/post.controller.ts +++ b/src/controllers/post.controller.ts @@ -13,7 +13,7 @@ import { export class PostController { constructor(private postService: PostService) {} - getAllPost: RequestHandler = async ( + getAllPosts: RequestHandler = async ( req: Request, res: Response, next: NextFunction, @@ -38,7 +38,7 @@ export class PostController { } }; - getAllPostStatistics: RequestHandler = async ( + getAllPostsStatistics: RequestHandler = async ( req: Request, res: Response, next: NextFunction, @@ -46,7 +46,7 @@ export class PostController { try { const { id } = req.user; - const stats = await this.postService.getAllPostStatistics(id); + const stats = await this.postService.getAllPostsStatistics(id); const totalPostCount = await this.postService.getTotalPostCounts(id); const response = new PostStatisticsResponseDto( diff --git a/src/modules/__test__/test.aes.encryption.test.ts b/src/modules/__test__/aes.encryption.test.ts similarity index 100% rename from src/modules/__test__/test.aes.encryption.test.ts rename to src/modules/__test__/aes.encryption.test.ts diff --git a/src/modules/__test__/test.slack.notifier.test.ts b/src/modules/__test__/slack.notifier.test.ts similarity index 100% rename from src/modules/__test__/test.slack.notifier.test.ts rename to src/modules/__test__/slack.notifier.test.ts diff --git a/src/repositories/__test__/post.repo.integration.test.ts b/src/repositories/__test__/post.repo.integration.test.ts new file mode 100644 index 0000000..f0d3cd8 --- /dev/null +++ b/src/repositories/__test__/post.repo.integration.test.ts @@ -0,0 +1,449 @@ +import dotenv from 'dotenv'; +import { Pool } from 'pg'; +import pg from 'pg'; +import { PostRepository } from '../post.repository'; +import logger from '@/configs/logger.config'; + + +dotenv.config(); +jest.setTimeout(30000); + +/** + * PostRepository 통합 테스트 + * + * 이 테스트 파일은 실제 데이터베이스와 연결하여 PostRepository의 모든 메서드를 + * 실제 환경과 동일한 조건에서 테스트합니다. + */ +describe('PostRepository 통합 테스트', () => { + let testPool: Pool; + let repo: PostRepository; + + // 테스트에 사용할 기본 데이터 ID + // eslint-disable-next-line @typescript-eslint/naming-convention + const TEST_DATA = { + USER_ID: 1, + POST_ID: 2445, + POST_UUID: 'e5053714-513f-422a-8e8f-99dbb9d4f2a4', + }; + + beforeAll(async () => { + try { + + const testPoolConfig: pg.PoolConfig = { + database: process.env.DATABASE_NAME, + user: process.env.POSTGRES_USER, + host: process.env.POSTGRES_HOST, + password: process.env.POSTGRES_PASSWORD, + port: Number(process.env.POSTGRES_PORT), + max: 1, // 최대 연결 수 + idleTimeoutMillis: 30000, // 연결 유휴 시간 (30초) + connectionTimeoutMillis: 5000, // 연결 시간 초과 (5초) + allowExitOnIdle: false, // 유휴 상태에서 종료 허용 + statement_timeout: 30000, + }; + + // localhost 가 아니면 ssl 필수 + if (process.env.POSTGRES_HOST != 'localhost') { + testPoolConfig.ssl = { + rejectUnauthorized: false, + }; + } + + testPool = new Pool(testPoolConfig); + + // 연결 확인 + await testPool.query('SELECT 1'); + logger.info('테스트 DB 연결 성공'); + + // 리포지토리 인스턴스 생성 + repo = new PostRepository(testPool); + + // 테스트 데이터 존재 여부 확인 + const postCheck = await testPool.query( + 'SELECT COUNT(*) FROM posts_post WHERE id = $1', + [TEST_DATA.POST_ID] + ); + + const statsCheck = await testPool.query( + 'SELECT COUNT(*) FROM posts_postdailystatistics WHERE post_id = $1', + [TEST_DATA.POST_ID] + ); + + const hasPostData = parseInt(postCheck.rows[0].count) > 0; + const hasPostDailyStats = parseInt(statsCheck.rows[0].count) > 0; + + if (!hasPostData) { + logger.warn(`주의: post_id=${TEST_DATA.POST_ID}에 해당하는 posts_post 데이터가 없습니다.`); + } + + if (!hasPostDailyStats) { + logger.warn(`주의: post_id=${TEST_DATA.POST_ID}에 해당하는 통계 데이터가 없습니다.`); + } + } catch (error) { + logger.error('테스트 설정 중 오류 발생:', error); + throw error; + } + }); + + afterAll(async () => { + try { + // 모든 쿼리 완료 대기 + await new Promise(resolve => setTimeout(resolve, 1000)); + + // 풀 완전 종료 + if (testPool) { + // 강제 종료: 모든 활성 쿼리와 연결 중지 + await testPool.end(); + } + + // 추가 정리 시간 + await new Promise(resolve => setTimeout(resolve, 1000)); + + logger.info('테스트 DB 연결 종료'); + } catch (error) { + logger.error('테스트 종료 중 오류:', error); + } + }); + + /** + * findPostsByUserId 테스트 + */ + describe('findPostsByUserId', () => { + it('사용자 ID로 게시물 목록을 조회할 수 있어야 한다', async () => { + // 실행 + const result = await repo.findPostsByUserId(TEST_DATA.USER_ID); + + // 검증 + expect(result).toBeDefined(); + expect(result).toHaveProperty('posts'); + expect(result).toHaveProperty('nextCursor'); + expect(Array.isArray(result.posts)).toBe(true); + }); + + it('페이지네이션을 위한 nextCursor를 제공해야 한다', async () => { + // 먼저 제한된 수의 결과를 가져옴 + const limitedResult = await repo.findPostsByUserId(TEST_DATA.USER_ID, undefined, undefined, false, 1); + + // 최소 2개 이상의 게시물이 있으면 nextCursor가 있어야 함 + const totalCount = await repo.getTotalPostCounts(TEST_DATA.USER_ID); + + if (totalCount <= 1 || limitedResult.posts.length !== 1) { + logger.info('페이지네이션 테스트를 위한 충분한 데이터가 없습니다.'); + return; + } + + expect(limitedResult.nextCursor).toBeTruthy(); + + // nextCursor를 사용한 두 번째 쿼리 + const secondPage = await repo.findPostsByUserId( + TEST_DATA.USER_ID, + limitedResult.nextCursor || undefined + ); + + expect(secondPage.posts).toBeDefined(); + expect(Array.isArray(secondPage.posts)).toBe(true); + + }); + + it('정렬 옵션을 적용할 수 있어야 한다', async () => { + // 조회수 기준 내림차순 정렬 + const result = await repo.findPostsByUserId(TEST_DATA.USER_ID, undefined, 'dailyViewCount', false); + + // 결과가 2개 이상인 경우만 의미 있는 검증 가능 + if (result.posts.length < 2) { + logger.info('페이지네이션 테스트를 위한 충분한 데이터가 없습니다.'); + return; + } + + // 내림차순 정렬 확인 + const isSortedByViews = result.posts.every((post, index) => { + if (index === 0) return true; + return post.daily_view_count <= result.posts[index - 1].daily_view_count; + }); + expect(isSortedByViews).toBe(true); + + }); + + it('오름차순 정렬이 제대로 동작해야 한다', async () => { + // 오름차순 정렬 (released_at 기준) + const result = await repo.findPostsByUserId(TEST_DATA.USER_ID, undefined, undefined, true); + + expect(result).toBeDefined(); + expect(Array.isArray(result.posts)).toBe(true); + + // 결과가 2개 이상인 경우에만 검증 + if (result.posts.length >= 2) { + const isSortedAsc = result.posts.every((post, index) => { + if (index === 0) return true; + // released_at 날짜를 비교 + const prevDate = new Date(result.posts[index - 1].post_released_at).getTime(); + const currDate = new Date(post.post_released_at).getTime(); + return prevDate <= currDate; // 오름차순 + }); + + // eslint-disable-next-line jest/no-conditional-expect + expect(isSortedAsc).toBe(true); + } + }); + + it('다양한 정렬 기준으로 결과를 반환해야 한다', async () => { + // 좋아요 수 기준 내림차순 정렬 + const resultByLikes = await repo.findPostsByUserId(TEST_DATA.USER_ID, undefined, 'dailyLikeCount', false); + + expect(resultByLikes).toBeDefined(); + expect(Array.isArray(resultByLikes.posts)).toBe(true); + + // released_at 기준 내림차순 정렬 (기본값) + const resultByDate = await repo.findPostsByUserId(TEST_DATA.USER_ID); + + expect(resultByDate).toBeDefined(); + expect(Array.isArray(resultByDate.posts)).toBe(true); + + // 정렬 기준이 다르면 결과 순서도 달라야 함 + if (resultByLikes.posts.length >= 2 && resultByDate.posts.length >= 2) { + // 두 결과의 첫 번째 항목이 다른지 확인 (정렬 기준에 따라 다른 결과가 나와야 함) + const areDifferent = + resultByLikes.posts[0].id !== resultByDate.posts[0].id || + resultByLikes.posts[1].id !== resultByDate.posts[1].id; + + // 데이터 상태에 따라 결과가 같을 수도 있어 조건부 검증 + if (areDifferent) { + // eslint-disable-next-line jest/no-conditional-expect + expect(areDifferent).toBe(true); + } + } + }); + + it('limit 매개변수가 결과 개수를 제한해야 한다', async () => { + const totalCount = await repo.getTotalPostCounts(TEST_DATA.USER_ID); + + if (totalCount < 3) { + logger.info('limit 테스트를 위한 충분한 데이터가 없습니다.'); + return; + } + + // 서로 다른 limit 값으로 조회 + const result1 = await repo.findPostsByUserId(TEST_DATA.USER_ID, undefined, undefined, false, 1); + const result2 = await repo.findPostsByUserId(TEST_DATA.USER_ID, undefined, undefined, false, 2); + + expect(result1.posts.length).toBe(1); + expect(result2.posts.length).toBe(2); + }); + + it('cursor를 사용한 페이지네이션이 일관성 있게 동작해야 한다', async () => { + const totalCount = await repo.getTotalPostCounts(TEST_DATA.USER_ID); + + if (totalCount < 3) { + logger.info('페이지네이션 연속성 테스트를 위한 충분한 데이터가 없습니다.'); + return; + } + + // 1페이지: 첫 2개 항목 + const page1 = await repo.findPostsByUserId(TEST_DATA.USER_ID, undefined, undefined, false, 2); + expect(page1.posts.length).toBe(2); + expect(page1.nextCursor).toBeTruthy(); + + // 2페이지: 다음 2개 항목 + const page2 = await repo.findPostsByUserId( + TEST_DATA.USER_ID, + page1.nextCursor || undefined, + undefined, + false, + 2 + ); + expect(page2.posts.length).toBeGreaterThan(0); + + // 첫 페이지와 두 번째 페이지의 항목은 중복되지 않아야 함 + const page1Ids = page1.posts.map(post => post.id); + const page2Ids = page2.posts.map(post => post.id); + + const hasDuplicates = page1Ids.some(id => page2Ids.includes(id)); + expect(hasDuplicates).toBe(false); + }); + }); + + + /** + * getTotalPostCounts 테스트 + */ + describe('getTotalPostCounts', () => { + it('사용자의 총 게시물 수를 반환해야 한다', async () => { + const count = await repo.getTotalPostCounts(TEST_DATA.USER_ID); + + expect(typeof count).toBe('number'); + expect(count).toBeGreaterThanOrEqual(0); + }); + }); + + /** + * getYesterdayAndTodayViewLikeStats 테스트 + */ + describe('getYesterdayAndTodayViewLikeStats', () => { + it('어제와 오늘의 통계 데이터를 반환해야 한다', async () => { + const stats = await repo.getYesterdayAndTodayViewLikeStats(TEST_DATA.USER_ID); + + expect(stats).toBeDefined(); + expect(stats).toHaveProperty('daily_view_count'); + expect(stats).toHaveProperty('daily_like_count'); + expect(stats).toHaveProperty('yesterday_views'); + expect(stats).toHaveProperty('yesterday_likes'); + expect(stats).toHaveProperty('last_updated_date'); + }); + + it('통계 값이 음수가 아니어야 한다', async () => { + const stats = await repo.getYesterdayAndTodayViewLikeStats(TEST_DATA.USER_ID); + + expect(Number(stats.daily_view_count)).toBeGreaterThanOrEqual(0); + expect(Number(stats.daily_like_count)).toBeGreaterThanOrEqual(0); + expect(Number(stats.yesterday_views)).toBeGreaterThanOrEqual(0); + expect(Number(stats.yesterday_likes)).toBeGreaterThanOrEqual(0); + }); + + it('반환된 통계가 숫자로 변환 가능해야 한다', async () => { + const stats = await repo.getYesterdayAndTodayViewLikeStats(TEST_DATA.USER_ID); + + expect(Number.isNaN(Number(stats.daily_view_count))).toBe(false); + expect(Number.isNaN(Number(stats.daily_like_count))).toBe(false); + expect(Number.isNaN(Number(stats.yesterday_views))).toBe(false); + expect(Number.isNaN(Number(stats.yesterday_likes))).toBe(false); + }); + + it('존재하지 않는 사용자 ID에 대해 기본값을 반환해야 한다', async () => { + const nonExistentUserId = 9999999; // 존재하지 않을 가능성이 높은 ID + const stats = await repo.getYesterdayAndTodayViewLikeStats(nonExistentUserId); + + expect(stats).toBeDefined(); + expect(Number(stats.daily_view_count)).toBe(0); + expect(Number(stats.daily_like_count)).toBe(0); + expect(Number(stats.yesterday_views)).toBe(0); + expect(Number(stats.yesterday_likes)).toBe(0); + }); + }); + + + /** + * findPostByPostId 테스트 + */ + describe('findPostByPostId', () => { + it('게시물 ID로 통계 데이터를 조회할 수 있어야 한다', async () => { + const result = await repo.findPostByPostId(TEST_DATA.POST_ID); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + result.forEach(item => { + expect(item).toHaveProperty('date'); + expect(item).toHaveProperty('daily_view_count'); + expect(item).toHaveProperty('daily_like_count'); + }); + }); + + it('날짜 범위를 지정하여 조회할 수 있어야 한다', async () => { + // 현재 날짜 기준 한 달 + const endDate = new Date().toISOString().split('T')[0]; + const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + + const result = await repo.findPostByPostId(TEST_DATA.POST_ID, startDate, endDate); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + // 모든 결과가 지정된 날짜 범위 내에 있어야 함 + result.forEach(entry => { + const entryDate = new Date(entry.date).toISOString().split('T')[0]; + expect(entryDate >= startDate && entryDate <= endDate).toBe(true); + }); + }); + + it('날짜 오름차순으로 정렬된 결과를 반환해야 한다', async () => { + const result = await repo.findPostByPostId(TEST_DATA.POST_ID); + + // 2개 이상의 결과가 있는 경우에만 정렬 검증 + if (result.length >= 2) { + let isSorted = true; + + for (let i = 1; i < result.length; i++) { + const prevDate = new Date(result[i - 1].date).getTime(); + const currDate = new Date(result[i].date).getTime(); + + if (prevDate > currDate) { + isSorted = false; + break; + } + } + + // eslint-disable-next-line jest/no-conditional-expect + expect(isSorted).toBe(true); + } + }); + + it('존재하지 않는 게시물 ID에 대해 빈 배열을 반환해야 한다', async () => { + const nonExistentPostId = 9999999; + const result = await repo.findPostByPostId(nonExistentPostId); + + 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); + + if (result.length <= 0) { + logger.info('존재하지 않는 게시물 ID에 대해 빈 배열을 테스트를 위한 충분한 데이터가 없습니다.'); + return + } + + const dateItem = result[0]; + expect(dateItem.date).toBeDefined(); + + // date가 유효한 Date 객체로 변환될 수 있는지 확인 + const dateObj = new Date(dateItem.date); + expect(dateObj.toString()).not.toBe('Invalid Date'); + + // 날짜 범위가 합리적인지 확인 + const year = dateObj.getFullYear(); + expect(year).toBeGreaterThanOrEqual(2020); + expect(year).toBeLessThanOrEqual(2026); // 현재 + 미래 대비 + + }); + + it('일일 조회수와 좋아요 수가 숫자 타입이어야 한다', async () => { + const result = await repo.findPostByPostId(TEST_DATA.POST_ID); + + if (result.length <= 0) { + logger.info('일일 조회수와 좋아요 수가 숫자 타입인지 테스트를 위한 충분한 데이터가 없습니다.'); + return + } + + result.forEach(item => { + expect(typeof item.daily_view_count).toBe('number'); + expect(typeof item.daily_like_count).toBe('number'); + }); + + }); + }); + + + /** + * findPostByPostUUID 테스트 + */ + describe('findPostByPostUUID', () => { + it('게시물 UUID로 통계 데이터를 조회할 수 있어야 한다', async () => { + // 현재 날짜 기준 일주일 + const endDate = new Date().toISOString().split('T')[0]; + const startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + + const result = await repo.findPostByPostUUID(TEST_DATA.POST_UUID, startDate, endDate); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + result.forEach(item => { + expect(item).toHaveProperty('date'); + expect(item).toHaveProperty('daily_view_count'); + expect(item).toHaveProperty('daily_like_count'); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/repositories/__test__/post.repository.test.ts b/src/repositories/__test__/post.repo.test.ts similarity index 100% rename from src/repositories/__test__/post.repository.test.ts rename to src/repositories/__test__/post.repo.test.ts diff --git a/src/repositories/post.repository.ts b/src/repositories/post.repository.ts index 78ac5bd..da33460 100644 --- a/src/repositories/post.repository.ts +++ b/src/repositories/post.repository.ts @@ -3,7 +3,7 @@ import logger from '@/configs/logger.config'; import { DBError } from '@/exception'; export class PostRepository { - constructor(private pool: Pool) {} + constructor(private pool: Pool) { } async findPostsByUserId(userId: number, cursor?: string, sort?: string, isAsc?: boolean, limit: number = 15) { try { @@ -162,33 +162,44 @@ export class PostRepository { async findPostByPostId(postId: number, start?: string, end?: string) { try { - let query = ` - SELECT - (pds.date AT TIME ZONE 'Asia/Seoul') AT TIME ZONE 'UTC' AS date, - pds.daily_view_count, - pds.daily_like_count - FROM posts_postdailystatistics pds - WHERE pds.post_id = $1 - ORDER BY pds.date ASC + // 기본 쿼리 부분 + const baseQuery = ` + SELECT + (pds.date AT TIME ZONE 'Asia/Seoul') AT TIME ZONE 'UTC' AS date, + pds.daily_view_count, + pds.daily_like_count + FROM posts_postdailystatistics pds + WHERE pds.post_id = $1 `; - const values: (number | string)[] = [postId]; + // 날짜 필터링 조건 구성 + const dateFilterQuery = (start && end) + ? ` + AND (pds.date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date >= ($2 AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date + AND (pds.date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date <= ($3 AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date + ` + : ''; - if (start && end) { - query += ` AND (pds.date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date >= ($2 AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date - AND (pds.date AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date <= ($3 AT TIME ZONE 'Asia/Seoul' AT TIME ZONE 'UTC')::date`; - values.push(start, end); - } + // 정렬 조건 추가 + const orderByQuery = `ORDER BY pds.date ASC`; - const result = await this.pool.query(query, values); + // 최종 쿼리 조합 + const fullQuery = [baseQuery, dateFilterQuery, orderByQuery].join(' '); + + // 파라미터 배열 구성 + const queryParams: Array = [postId]; + if (start && end) queryParams.push(start, end); + + // 쿼리 실행 + const result = await this.pool.query(fullQuery, queryParams); return result.rows; } catch (error) { - logger.error('Post Repo findPostByPostId error : ', error); + logger.error('Post Repo findPostByPostId error:', error); throw new DBError('단건 post 조회 중 문제가 발생했습니다.'); } } - async findPostByPostUUID(postId: string, start: string, end: string) { + async findPostByPostUUID(postUUUID: string, start: string, end: string) { try { const query = ` SELECT @@ -203,7 +214,7 @@ export class PostRepository { ORDER BY pds.date ASC `; - const values = [postId, start, end]; + const values = [postUUUID, start, end]; const result = await this.pool.query(query, values); return result.rows; diff --git a/src/routes/post.router.ts b/src/routes/post.router.ts index b24e273..e37ed14 100644 --- a/src/routes/post.router.ts +++ b/src/routes/post.router.ts @@ -49,7 +49,7 @@ router.get( '/posts', authMiddleware.verify, validateRequestDto(GetAllPostsQueryDto, 'query'), - postController.getAllPost, + postController.getAllPosts, ); /** @@ -69,7 +69,7 @@ router.get( * '500': * description: 서버 오류 / 데이터 베이스 조회 오류 */ -router.get('/posts-stats', authMiddleware.verify, postController.getAllPostStatistics); +router.get('/posts-stats', authMiddleware.verify, postController.getAllPostsStatistics); /** * @swagger diff --git a/src/services/__test__/post.service.test.ts b/src/services/__test__/post.service.test.ts new file mode 100644 index 0000000..3aa4fed --- /dev/null +++ b/src/services/__test__/post.service.test.ts @@ -0,0 +1,528 @@ +import { PostService } from '@/services/post.service'; +import { PostRepository } from '@/repositories/post.repository'; +import { DBError } from '@/exception'; +import { Pool } from 'pg'; + +jest.mock('@/repositories/post.repository'); + +describe('PostService', () => { + let postService: PostService; + let postRepo: jest.Mocked; + let mockPool: jest.Mocked; + + beforeEach(() => { + // DB Pool 목 설정 + const mockPoolObj = {}; + mockPool = mockPoolObj as jest.Mocked; + + // PostRepository 목 설정 + const postRepoInstance = new PostRepository(mockPool); + postRepo = postRepoInstance as jest.Mocked; + + // 테스트 대상 서비스 인스턴스 생성 + postService = new PostService(postRepo); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getAllposts', () => { + const mockPostsData = { + nextCursor: '2023-11-19T09:19:36.811Z,519212', + posts: [ + { + id: '519211', + title: 'velog dashboard test post (2)', + slug: 'velog-dashboard-test-post-2', + daily_view_count: 147, + daily_like_count: 2, + yesterday_daily_view_count: 147, + yesterday_daily_like_count: 2, + post_created_at: '2025-02-08T02:58:24.347Z', + post_released_at: '2023-11-20T02:15:14.209Z', + }, + { + id: '519212', + title: 'velog dashboard test post (1)', + slug: 'velog-dashboard-test-post-1', + daily_view_count: 208, + daily_like_count: 1, + yesterday_daily_view_count: 208, + yesterday_daily_like_count: 1, + post_created_at: '2025-02-08T02:58:24.347Z', + post_released_at: '2023-11-19T09:19:36.811Z', + }, + ], + }; + + const expectedTransformedPosts = [ + { + id: '519211', + title: 'velog dashboard test post (2)', + slug: 'velog-dashboard-test-post-2', + views: 147, + likes: 2, + yesterdayViews: 147, + yesterdayLikes: 2, + createdAt: '2025-02-08T02:58:24.347Z', + releasedAt: '2023-11-20T02:15:14.209Z', + }, + { + id: '519212', + title: 'velog dashboard test post (1)', + slug: 'velog-dashboard-test-post-1', + views: 208, + likes: 1, + yesterdayViews: 208, + yesterdayLikes: 1, + createdAt: '2025-02-08T02:58:24.347Z', + releasedAt: '2023-11-19T09:19:36.811Z', + }, + ]; + + it('기본 게시물 목록 조회 - 데이터 형식 변환 테스트', async () => { + // Arrange + postRepo.findPostsByUserId.mockResolvedValue(mockPostsData); + + // Act + const result = await postService.getAllposts(1); + + // Assert + expect(result.posts).toEqual(expectedTransformedPosts); + expect(result.nextCursor).toBe('2023-11-19T09:19:36.811Z,519212'); + expect(postRepo.findPostsByUserId).toHaveBeenCalledWith(1, undefined, '', undefined, 15); + }); + + it('커서 및 정렬 옵션을 포함한 게시물 목록 조회', async () => { + // Arrange + const cursor = 'some-cursor-value'; + const sort = 'dailyViewCount'; + const isAsc = true; + const limit = 10; + + postRepo.findPostsByUserId.mockResolvedValue(mockPostsData); + + // Act + const result = await postService.getAllposts(1, cursor, sort, isAsc, limit); + + // Assert + expect(result.posts).toEqual(expectedTransformedPosts); + expect(postRepo.findPostsByUserId).toHaveBeenCalledWith(1, cursor, sort, isAsc, limit); + }); + + it('빈 게시물 목록 처리', async () => { + // Arrange + const emptyResponse = { posts: [], nextCursor: null }; + postRepo.findPostsByUserId.mockResolvedValue(emptyResponse); + + // Act + const result = await postService.getAllposts(1); + + // Assert + expect(result.posts).toEqual([]); + expect(result.nextCursor).toBeNull(); + }); + + it('쿼리 오류 발생 시 예외를 그대로 전파', async () => { + // Arrange + const errorMessage = '전체 post 조회 중 문제가 발생했습니다.'; + const dbError = new DBError(errorMessage); + postRepo.findPostsByUserId.mockRejectedValue(dbError); + + // Act & Assert + await expect(postService.getAllposts(1)).rejects.toThrow(errorMessage); + expect(postRepo.findPostsByUserId).toHaveBeenCalledTimes(1); + }); + + it('일부 데이터가 누락된 경우 정상 처리', async () => { + // Arrange + const incompleteData = { + nextCursor: 'cursor-value', + posts: [ + { + id: '123', + title: 'Incomplete Post', + slug: 'incomplete-post', + daily_view_count: 100, + daily_like_count: null, // 누락된 필드 (null) + yesterday_daily_view_count: undefined, // 누락된 필드 (undefined) + yesterday_daily_like_count: 0, + post_created_at: '2025-01-01T00:00:00.000Z', + post_released_at: '2025-01-01T00:00:00.000Z', + }, + ], + }; + + postRepo.findPostsByUserId.mockResolvedValue(incompleteData); + + // Act + const result = await postService.getAllposts(1); + + // Assert + expect(result.posts[0]).toEqual({ + id: '123', + title: 'Incomplete Post', + slug: 'incomplete-post', + views: 100, + likes: null, // null 값 그대로 전달 + yesterdayViews: undefined, // undefined 값 그대로 전달 + yesterdayLikes: 0, + createdAt: '2025-01-01T00:00:00.000Z', + releasedAt: '2025-01-01T00:00:00.000Z', + }); + }); + }); + + describe('getAllPostStatistics', () => { + it('게시물 전체 통계 조회 - 문자열을 숫자로 변환', async () => { + // Arrange + const mockStatistics = { + daily_view_count: '355', + daily_like_count: '3', + yesterday_views: '355', + yesterday_likes: '3', + last_updated_date: '2025-03-14T15:52:40.767Z', + }; + + postRepo.getYesterdayAndTodayViewLikeStats.mockResolvedValue(mockStatistics); + + // Act + const result = await postService.getAllPostsStatistics(1); + + // Assert + expect(result).toEqual({ + totalViews: 355, + totalLikes: 3, + yesterdayViews: 355, + yesterdayLikes: 3, + lastUpdatedDate: '2025-03-14T15:52:40.767Z', + }); + expect(postRepo.getYesterdayAndTodayViewLikeStats).toHaveBeenCalledWith(1); + }); + + it('숫자 형태의 문자열이 아닌 경우 처리', async () => { + // Arrange + const mockStatistics = { + daily_view_count: 'invalid', + daily_like_count: '3.5', // 소수점 있는 문자열 + yesterday_views: '355', + yesterday_likes: '', // 빈 문자열 + last_updated_date: '2025-03-14T15:52:40.767Z', + }; + + postRepo.getYesterdayAndTodayViewLikeStats.mockResolvedValue(mockStatistics); + + // Act + const result = await postService.getAllPostsStatistics(1); + + // Assert + expect(result.totalViews).toBe(0); // 'invalid'는 0이 됨 + expect(result.totalLikes).toBe(3); // '3.5'는 parseInt 사용시 3으로 변환 + expect(result.yesterdayViews).toBe(355); + expect(result.yesterdayLikes).toBe(0); // 빈 문자열은 0으로 변환 + }); + + it('누락된 통계 필드에 대한 처리', async () => { + // Arrange + const mockStatistics = { + daily_view_count: '100', + // daily_like_count missing + yesterday_views: '90', + // yesterday_likes missing + last_updated_date: '2025-03-14T15:52:40.767Z', + }; + + postRepo.getYesterdayAndTodayViewLikeStats.mockResolvedValue(mockStatistics); + + // Act + const result = await postService.getAllPostsStatistics(1); + + // Assert + expect(result.totalViews).toBe(100); + expect(result.totalLikes).toBe(0); // 누락 필드는 0이 됨 + expect(result.yesterdayViews).toBe(90); + expect(result.yesterdayLikes).toBe(0); // 누락 필드는 0이 됨 + }); + + it('쿼리 오류 발생 시 예외를 그대로 전파', async () => { + // Arrange + const errorMessage = '통계 조회 중 문제가 발생했습니다.'; + postRepo.getYesterdayAndTodayViewLikeStats.mockRejectedValue(new DBError(errorMessage)); + + // Act & Assert + await expect(postService.getAllPostsStatistics(1)).rejects.toThrow(errorMessage); + }); + }); + + describe('getTotalPostCounts', () => { + it('게시물 개수 조회', async () => { + // Arrange + const mockCount = 42; + postRepo.getTotalPostCounts.mockResolvedValue(mockCount); + + // Act + const result = await postService.getTotalPostCounts(1); + + // Assert + expect(result).toBe(42); + expect(postRepo.getTotalPostCounts).toHaveBeenCalledWith(1); + }); + + it('0개의 게시물 처리', async () => { + // Arrange + postRepo.getTotalPostCounts.mockResolvedValue(0); + + // Act + const result = await postService.getTotalPostCounts(1); + + // Assert + expect(result).toBe(0); + }); + + it('쿼리 오류 발생 시 예외를 그대로 전파', async () => { + // Arrange + const errorMessage = '총 게시물 수 조회 중 문제가 발생했습니다.'; + postRepo.getTotalPostCounts.mockRejectedValue(new DBError(errorMessage)); + + // Act & Assert + await expect(postService.getTotalPostCounts(1)).rejects.toThrow(errorMessage); + }); + }); + + describe('getPostByPostId', () => { + const mockPostStats = [ + { + date: '2025-03-08T00:00:00.000Z', + daily_view_count: 145, + daily_like_count: 2, + }, + { + date: '2025-03-09T00:00:00.000Z', + daily_view_count: 145, + daily_like_count: 2, + }, + ]; + + const expectedTransformedStats = [ + { + date: '2025-03-08T00:00:00.000Z', + dailyViewCount: 145, + dailyLikeCount: 2, + }, + { + date: '2025-03-09T00:00:00.000Z', + dailyViewCount: 145, + dailyLikeCount: 2, + }, + ]; + + it('게시물 ID로 상세 통계 조회', async () => { + // Arrange + postRepo.findPostByPostId.mockResolvedValue(mockPostStats); + + // Act + const result = await postService.getPostByPostId(1); + + // Assert + expect(result).toEqual(expectedTransformedStats); + expect(postRepo.findPostByPostId).toHaveBeenCalledWith(1, undefined, undefined); + }); + + it('시작일과 종료일을 지정하여 상세 통계 조회', async () => { + // Arrange + const start = '2025-03-01'; + const end = '2025-03-10'; + + postRepo.findPostByPostId.mockResolvedValue(mockPostStats); + + // Act + const result = await postService.getPostByPostId(1, start, end); + + // Assert + expect(result).toEqual(expectedTransformedStats); + expect(postRepo.findPostByPostId).toHaveBeenCalledWith(1, start, end); + }); + + it('빈 통계 목록 처리', async () => { + // Arrange + postRepo.findPostByPostId.mockResolvedValue([]); + + // Act + const result = await postService.getPostByPostId(1); + + // Assert + expect(result).toEqual([]); + }); + + it('쿼리 오류 발생 시 예외를 그대로 전파', async () => { + // Arrange + const errorMessage = '게시물 조회 중 문제가 발생했습니다.'; + postRepo.findPostByPostId.mockRejectedValue(new DBError(errorMessage)); + + // Act & Assert + await expect(postService.getPostByPostId(1)).rejects.toThrow(errorMessage); + }); + + it('숫자가 아닌 ID를 전달해도 처리되어야 함', async () => { + // Arrange + postRepo.findPostByPostId.mockResolvedValue(mockPostStats); + + // Act + const result = await postService.getPostByPostId('abc' as unknown as number); + + // Assert + expect(result).toEqual(expectedTransformedStats); + // Repository에 ID가 'abc'로 전달됨 (내부적으로 변환하지 않음) + expect(postRepo.findPostByPostId).toHaveBeenCalledWith('abc', undefined, undefined); + }); + }); + + describe('getPostByPostUUID', () => { + const mockPostStats = [ + { + date: '2025-03-08T00:00:00.000Z', + daily_view_count: 145, + daily_like_count: 2, + }, + { + date: '2025-03-09T00:00:00.000Z', + daily_view_count: 145, + daily_like_count: 2, + }, + ]; + + const expectedTransformedStats = [ + { + date: '2025-03-08T00:00:00.000Z', + dailyViewCount: 145, + dailyLikeCount: 2, + }, + { + date: '2025-03-09T00:00:00.000Z', + dailyViewCount: 145, + dailyLikeCount: 2, + }, + ]; + + beforeEach(() => { + // 테스트용 Date 고정 + jest.useFakeTimers(); + jest.setSystemTime(new Date('2025-03-15T12:00:00Z')); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('게시물 UUID로 상세 통계 조회 (기본 7일 범위)', async () => { + // Arrange + postRepo.findPostByPostUUID.mockResolvedValue(mockPostStats); + + // Act + const result = await postService.getPostByPostUUID('uuid-1234'); + + // Assert + expect(result).toEqual(expectedTransformedStats); + // 7일 범위 설정 확인 (현재 날짜 2025-03-15 기준) + expect(postRepo.findPostByPostUUID).toHaveBeenCalledWith( + 'uuid-1234', + '2025-03-09', // 6일 전 + '2025-03-15' // 오늘 + ); + }); + + it('빈 통계 목록 처리', async () => { + // Arrange + postRepo.findPostByPostUUID.mockResolvedValue([]); + + // Act + const result = await postService.getPostByPostUUID('uuid-1234'); + + // Assert + expect(result).toEqual([]); + }); + + it('쿼리 오류 발생 시 예외를 그대로 전파', async () => { + // Arrange + const errorMessage = 'UUID로 게시물 조회 중 문제가 발생했습니다.'; + postRepo.findPostByPostUUID.mockRejectedValue(new DBError(errorMessage)); + + // Act & Assert + await expect(postService.getPostByPostUUID('uuid-1234')).rejects.toThrow(errorMessage); + }); + + it('빈 UUID 처리', async () => { + // Arrange + postRepo.findPostByPostUUID.mockResolvedValue([]); + + // Act + const result = await postService.getPostByPostUUID(''); + + // Assert + expect(result).toEqual([]); + expect(postRepo.findPostByPostUUID).toHaveBeenCalledWith( + '', // 빈 문자열 그대로 전달됨 + expect.any(String), + expect.any(String) + ); + }); + }); + + describe('transformPosts utility method', () => { + // private 메소드를 테스트하기 위해 접근법 변경 + it('필드 이름 변환 확인', async () => { + // Arrange + const mockPosts = [ + { + date: '2025-03-08T00:00:00.000Z', + daily_view_count: 100, + daily_like_count: 10, + extra_field: 'should be ignored' // 추가 필드는 무시되어야 함 + } + ]; + + postRepo.findPostByPostId.mockResolvedValue(mockPosts); + + // Act - private 메소드를 직접 호출하지 않고, 공개 메소드를 통해 간접 테스트 + const result = await postService.getPostByPostId(1); + + // Assert + expect(result).toEqual([ + { + date: '2025-03-08T00:00:00.000Z', + dailyViewCount: 100, + dailyLikeCount: 10 + // extra_field는 변환 후 존재하지 않아야 함 + } + ]); + // result에 extra_field 속성이 없는지 확인 + expect(result[0]).not.toHaveProperty('extra_field'); + }); + + it('null 또는 undefined 값이 있는 경우 처리', async () => { + // Arrange + const mockPosts = [ + { + date: '2025-03-08T00:00:00.000Z', + daily_view_count: null, + daily_like_count: undefined + } + ]; + + postRepo.findPostByPostId.mockResolvedValue(mockPosts); + + // Act + const result = await postService.getPostByPostId(1); + + // Assert + expect(result).toEqual([ + { + date: '2025-03-08T00:00:00.000Z', + dailyViewCount: null, + dailyLikeCount: undefined + } + ]); + }); + }); +}); \ No newline at end of file diff --git a/src/services/__test__/test.post.service.test.ts b/src/services/__test__/test.post.service.test.ts deleted file mode 100644 index 1d84624..0000000 --- a/src/services/__test__/test.post.service.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { PostService } from '@/services/post.service'; -import { PostRepository } from '@/repositories/post.repository'; -import { DBError } from '@/exception'; -import { Pool } from 'pg'; - -jest.mock('@/repositories/post.repository'); - -// 모든 파라미터는 Route 단에서 검증하기 때문에 파라미터를 제대로 받았는지는 확인하지 않음 -describe('PostService', () => { - let postService: PostService; - let postRepo: jest.Mocked; - let mockPool: jest.Mocked; - - beforeEach(() => { - mockPool = {} as jest.Mocked; - postRepo = new PostRepository(mockPool) as jest.Mocked; - postService = new PostService(postRepo); - }); - - describe('getAllposts', () => { - it('게시물 목록 조회', async () => { - const mockPosts = { - nextCursor: '2023-11-19T09:19:36.811Z,519212', - posts: [ - { - id: '519211', - title: 'velog dashboard test post (2)', - slug: 'velog-dashboard-test-post-2', - daily_view_count: 147, - daily_like_count: 2, - yesterday_daily_view_count: 147, - yesterday_daily_like_count: 2, - post_created_at: '2025-02-08T02:58:24.347Z', - post_released_at: '2023-11-20T02:15:14.209Z', - }, - { - id: '519212', - title: 'velog dashboard test post (1)', - slug: 'velog-dashboard-test-post-1', - daily_view_count: 208, - daily_like_count: 1, - yesterday_daily_view_count: 208, - yesterday_daily_like_count: 1, - post_created_at: '2025-02-08T02:58:24.347Z', - post_released_at: '2023-11-19T09:19:36.811Z', - }, - ], - }; - - postRepo.findPostsByUserId.mockResolvedValue(mockPosts); - - const result = await postService.getAllposts(1); - - expect(result.posts).toEqual([ - { - id: '519211', - title: 'velog dashboard test post (2)', - slug: 'velog-dashboard-test-post-2', - views: 147, - likes: 2, - yesterdayViews: 147, - yesterdayLikes: 2, - createdAt: '2025-02-08T02:58:24.347Z', - releasedAt: '2023-11-20T02:15:14.209Z', - }, - { - id: '519212', - title: 'velog dashboard test post (1)', - slug: 'velog-dashboard-test-post-1', - views: 208, - likes: 1, - yesterdayViews: 208, - yesterdayLikes: 1, - createdAt: '2025-02-08T02:58:24.347Z', - releasedAt: '2023-11-19T09:19:36.811Z', - }, - ]); - expect(result.nextCursor).toBe('2023-11-19T09:19:36.811Z,519212'); - }); - - it('쿼리 중 오류 발생 시 DBError Throw', async () => { - const errorMessage = '전체 post 조회 중 문제가 발생했습니다.'; - postRepo.findPostsByUserId.mockRejectedValue(new DBError(errorMessage)); - - await expect(postService.getAllposts(1)).rejects.toThrow(errorMessage); - }); - }); - - describe('getAllPostStatistics', () => { - it('게시물 전체 통계 조회', async () => { - const mockStatistics = { - daily_view_count: '355', - daily_like_count: '3', - yesterday_views: '355', - yesterday_likes: '3', - last_updated_date: '2025-03-14T15:52:40.767Z', - }; - - postRepo.getYesterdayAndTodayViewLikeStats.mockResolvedValue(mockStatistics); - - const result = await postService.getAllPostStatistics(1); - - expect(result).toEqual({ - totalViews: 355, - totalLikes: 3, - yesterdayViews: 355, - yesterdayLikes: 3, - lastUpdatedDate: '2025-03-14T15:52:40.767Z', - }); - }); - - it('쿼리 중 오류 발생 시 DBError Throw', async () => { - const errorMessage = '통계 조회 중 문제가 발생했습니다.'; - postRepo.getYesterdayAndTodayViewLikeStats.mockRejectedValue(new DBError(errorMessage)); - - await expect(postService.getAllPostStatistics(1)).rejects.toThrow(errorMessage); - }); - }); - - describe('getTotalPostCounts', () => { - it('게시물 개수 조회', async () => { - const mockCount = 2; - postRepo.getTotalPostCounts.mockResolvedValue(mockCount); - - const result = await postService.getTotalPostCounts(1); - - expect(result).toBe(mockCount); - }); - - it('쿼리 중 오류 발생 시 DBError Throw', async () => { - const errorMessage = '총 게시물 수 조회 중 문제가 발생했습니다.'; - postRepo.getTotalPostCounts.mockRejectedValue(new DBError(errorMessage)); - - await expect(postService.getTotalPostCounts(1)).rejects.toThrow(errorMessage); - }); - }); - - describe('getPostByPostId', () => { - it('게시물 상세 통계 조회', async () => { - const mockPosts = [ - { - date: '2025-03-08T00:00:00.000Z', - daily_view_count: 145, - daily_like_count: 2, - }, - { - date: '2025-03-09T00:00:00.000Z', - daily_view_count: 145, - daily_like_count: 2, - }, - { - date: '2025-03-10T00:00:00.000Z', - daily_view_count: 147, - daily_like_count: 2, - }, - { - date: '2025-03-11T00:00:00.000Z', - daily_view_count: 147, - daily_like_count: 2, - }, - ]; - - postRepo.findPostByPostId.mockResolvedValue(mockPosts); - - const result = await postService.getPostByPostId(1); - - expect(result).toEqual([ - { - date: '2025-03-08T00:00:00.000Z', - dailyViewCount: 145, - dailyLikeCount: 2, - }, - { - date: '2025-03-09T00:00:00.000Z', - dailyViewCount: 145, - dailyLikeCount: 2, - }, - { - date: '2025-03-10T00:00:00.000Z', - dailyViewCount: 147, - dailyLikeCount: 2, - }, - { - date: '2025-03-11T00:00:00.000Z', - dailyViewCount: 147, - dailyLikeCount: 2, - }, - ]); - }); - - it('쿼리 중 오류 발생 시 DBError Throw', async () => { - const errorMessage = '게시물 조회 중 문제가 발생했습니다.'; - postRepo.findPostByPostId.mockRejectedValue(new DBError(errorMessage)); - - await expect(postService.getPostByPostId(1)).rejects.toThrow(errorMessage); - }); - }); - - describe('getPostByPostUUID', () => { - it('게시물 상세 통계 조회', async () => { - const mockPosts = [ - { - date: '2025-03-08T00:00:00.000Z', - daily_view_count: 145, - daily_like_count: 2, - }, - { - date: '2025-03-09T00:00:00.000Z', - daily_view_count: 145, - daily_like_count: 2, - }, - { - date: '2025-03-10T00:00:00.000Z', - daily_view_count: 147, - daily_like_count: 2, - }, - { - date: '2025-03-11T00:00:00.000Z', - daily_view_count: 147, - daily_like_count: 2, - }, - ]; - - postRepo.findPostByPostUUID.mockResolvedValue(mockPosts); - - const result = await postService.getPostByPostUUID('uuid-1234'); - - expect(result).toEqual([ - { - date: '2025-03-08T00:00:00.000Z', - dailyViewCount: 145, - dailyLikeCount: 2, - }, - { - date: '2025-03-09T00:00:00.000Z', - dailyViewCount: 145, - dailyLikeCount: 2, - }, - { - date: '2025-03-10T00:00:00.000Z', - dailyViewCount: 147, - dailyLikeCount: 2, - }, - { - date: '2025-03-11T00:00:00.000Z', - dailyViewCount: 147, - dailyLikeCount: 2, - }, - ]); - }); - - it('쿼리 중 오류 발생 시 DBError Throw', async () => { - const errorMessage = 'UUID로 게시물 조회 중 문제가 발생했습니다.'; - postRepo.findPostByPostUUID.mockRejectedValue(new DBError(errorMessage)); - - await expect(postService.getPostByPostUUID('uuid-1234')).rejects.toThrow(errorMessage); - }); - }); -}); diff --git a/src/services/post.service.ts b/src/services/post.service.ts index dcc97af..099b428 100644 --- a/src/services/post.service.ts +++ b/src/services/post.service.ts @@ -3,7 +3,7 @@ import { PostRepository } from '@/repositories/post.repository'; import { RawPostType } from '@/types'; export class PostService { - constructor(private postRepo: PostRepository) {} + constructor(private postRepo: PostRepository) { } async getAllposts(userId: number, cursor?: string, sort: string = '', isAsc?: boolean, limit: number = 15) { try { @@ -26,26 +26,26 @@ export class PostService { nextCursor: result.nextCursor, }; } catch (error) { - logger.error('PostService getAllpost error : ', error); + logger.error('PostService getAllposts error : ', error); throw error; } } - async getAllPostStatistics(userId: number) { + async getAllPostsStatistics(userId: number) { try { const postsStatistics = await this.postRepo.getYesterdayAndTodayViewLikeStats(userId); const transformedStatistics = { - totalViews: parseInt(postsStatistics.daily_view_count), - totalLikes: parseInt(postsStatistics.daily_like_count), - yesterdayViews: parseInt(postsStatistics.yesterday_views), - yesterdayLikes: parseInt(postsStatistics.yesterday_likes), + totalViews: parseInt(postsStatistics.daily_view_count) || 0, + totalLikes: parseInt(postsStatistics.daily_like_count) || 0, + yesterdayViews: parseInt(postsStatistics.yesterday_views) || 0, + yesterdayLikes: parseInt(postsStatistics.yesterday_likes) || 0, lastUpdatedDate: postsStatistics.last_updated_date, }; return transformedStatistics; } catch (error) { - logger.error('PostService getAllPostStatistics error : ', error); + logger.error('PostService getAllPostsStatistics error : ', error); throw error; } } @@ -67,7 +67,7 @@ export class PostService { } } - async getPostByPostUUID(postId: string) { + async getPostByPostUUID(postUUUID: string) { try { const seoulNow = new Date(new Date().getTime() + 9 * 60 * 60 * 1000); const sevenDaysAgo = new Date(seoulNow); @@ -76,7 +76,7 @@ export class PostService { sevenDaysAgo.setDate(seoulNow.getDate() - 6); const start = sevenDaysAgo.toISOString().split('T')[0]; - const posts = await this.postRepo.findPostByPostUUID(postId, start, end); + const posts = await this.postRepo.findPostByPostUUID(postUUUID, start, end); const transformedPosts = this.transformPosts(posts);