From 0014655d8f21b1ff603100102822f2892ba9c36d Mon Sep 17 00:00:00 2001 From: Minseong Park <52368015+pminsung12@users.noreply.github.com> Date: Thu, 7 Dec 2023 03:32:42 +0900 Subject: [PATCH] =?UTF-8?q?[Server]=20=ED=94=BC=EB=93=9C=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20api=20=EA=B5=AC=ED=98=84=20(#196)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: import 문 최적화 * feat: 피드 resource 생성 * feat: checklist entity에 카테고리 컬럼 추가 * feat: feedmodel 정의 private checklist model에서 likeCount와 downloadCount 컬럼 추가 * chore: 사용하지 않는 dto 파일 삭제 * chore: 카테고리 데이터 추가 * chore: 안쓰는 테스트 파일 삭제 * feat: 피드 화면 api 구현 * feat: feeds.service.spec.ts 테스트 코드 작성 * feat: api 에러 핸들링 로직 추가 * test: feeds.service.spec.ts 예외 케이스에 대한 테스트 코드 추가 * feat: 에러메시지 수정 --- server/src/app.module.ts | 4 + .../src/categories/categories.controller.ts | 2 +- .../src/categories/const/categories.const.ts | 48 +++++++ .../checklist-ai.controller.spec.ts | 20 --- server/src/common/entity/checklist.entity.ts | 11 +- server/src/feeds/entity/feed.entity.ts | 14 +++ server/src/feeds/feeds.controller.ts | 22 ++++ server/src/feeds/feeds.module.ts | 12 ++ server/src/feeds/feeds.service.spec.ts | 118 ++++++++++++++++++ server/src/feeds/feeds.service.ts | 44 +++++++ 10 files changed, 271 insertions(+), 24 deletions(-) delete mode 100644 server/src/checklist-ai/checklist-ai.controller.spec.ts create mode 100644 server/src/feeds/entity/feed.entity.ts create mode 100644 server/src/feeds/feeds.controller.ts create mode 100644 server/src/feeds/feeds.module.ts create mode 100644 server/src/feeds/feeds.service.spec.ts create mode 100644 server/src/feeds/feeds.service.ts diff --git a/server/src/app.module.ts b/server/src/app.module.ts index feecadb3..690deb0a 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -27,6 +27,8 @@ import { UsersModule } from './users/users.module'; import { winstonConfig } from './utils/winston.config'; import { LoggingInterceptor } from './common/interceptor/log.interceptor'; import { APP_INTERCEPTOR } from '@nestjs/core'; +import { FeedsModule } from './feeds/feeds.module'; +import { FeedModel } from './feeds/entity/feed.entity'; @Module({ imports: [ @@ -48,6 +50,7 @@ import { APP_INTERCEPTOR } from '@nestjs/core'; PrivateChecklistModel, SharedChecklistModel, SharedChecklistItemModel, + FeedModel, ], synchronize: true, // DO NOT USE IN PRODUCTION }), @@ -60,6 +63,7 @@ import { APP_INTERCEPTOR } from '@nestjs/core'; SharedChecklistsModule, ChecklistAiModule, CategoriesModule, + FeedsModule, ], controllers: [AppController], providers: [ diff --git a/server/src/categories/categories.controller.ts b/server/src/categories/categories.controller.ts index 6a48a905..93e7d626 100644 --- a/server/src/categories/categories.controller.ts +++ b/server/src/categories/categories.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common'; +import { Controller, Get, Param } from '@nestjs/common'; import { CategoriesService } from './categories.service'; @Controller('categories') diff --git a/server/src/categories/const/categories.const.ts b/server/src/categories/const/categories.const.ts index f848da26..7cb88c2f 100644 --- a/server/src/categories/const/categories.const.ts +++ b/server/src/categories/const/categories.const.ts @@ -180,5 +180,53 @@ export const CATEGORIES = { }, ], }, + { + id: 7, + name: '취미', + subcategories: [ + { + id: 701, + name: '음악', + minorCategories: [ + { id: 70101, name: '기타' }, + { id: 70102, name: '피아노' }, + { id: 70103, name: '드럼' }, + { id: 70104, name: '베이스' }, + { id: 70105, name: '보컬' }, + { id: 70106, name: '작곡' }, + { id: 70107, name: '음악 이론' }, + ], + }, + { + id: 702, + name: '미술', + minorCategories: [ + { id: 70201, name: '드로잉' }, + { id: 70202, name: '수채화' }, + { id: 70203, name: '아크릴화' }, + { id: 70204, name: '유화' }, + { id: 70205, name: '파스텔' }, + { id: 70206, name: '캘리그라피' }, + { id: 70207, name: '조소' }, + { id: 70208, name: '판화' }, + { id: 70209, name: '캐리커쳐' }, + ], + }, + { + id: 703, + name: '공예', + minorCategories: [ + { id: 70301, name: '가죽' }, + { id: 70302, name: '목공' }, + { id: 70303, name: '도자기' }, + { id: 70304, name: '비즈' }, + { id: 70305, name: '플라워' }, + { id: 70306, name: '캔들' }, + { id: 70307, name: '향수' }, + { id: 70308, name: '비누' }, + ], + }, + ], + }, ], }; diff --git a/server/src/checklist-ai/checklist-ai.controller.spec.ts b/server/src/checklist-ai/checklist-ai.controller.spec.ts deleted file mode 100644 index e81da6a3..00000000 --- a/server/src/checklist-ai/checklist-ai.controller.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ChecklistAiController } from './checklist-ai.controller'; -import { ChecklistAiService } from './checklist-ai.service'; - -describe('ChecklistAiController', () => { - let controller: ChecklistAiController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [ChecklistAiController], - providers: [ChecklistAiService], - }).compile(); - - controller = module.get(ChecklistAiController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/server/src/common/entity/checklist.entity.ts b/server/src/common/entity/checklist.entity.ts index 42436ae6..ae986f30 100644 --- a/server/src/common/entity/checklist.entity.ts +++ b/server/src/common/entity/checklist.entity.ts @@ -6,7 +6,12 @@ export abstract class ChecklistModel extends BaseModel { @Column() title: string; - // @Column({ default: 0 }) - // @IsNumber() - // progress: number; + @Column({ nullable: true }) + mainCategory: string; + + @Column({ nullable: true }) + subCategory: string; + + @Column({ nullable: true }) + minorCategory: string; } diff --git a/server/src/feeds/entity/feed.entity.ts b/server/src/feeds/entity/feed.entity.ts new file mode 100644 index 00000000..bbf90c59 --- /dev/null +++ b/server/src/feeds/entity/feed.entity.ts @@ -0,0 +1,14 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; +import { PrivateChecklistModel } from '../../folders/private-checklists/entities/private-checklist.entity'; + +@Entity() +export class FeedModel extends PrivateChecklistModel { + @PrimaryGeneratedColumn() + feedId: number; + + @Column({ default: 0 }) + likeCount: number; + + @Column({ default: 0 }) + downloadCount: number; +} diff --git a/server/src/feeds/feeds.controller.ts b/server/src/feeds/feeds.controller.ts new file mode 100644 index 00000000..a8c8b40d --- /dev/null +++ b/server/src/feeds/feeds.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get, Param, Post, Query } from '@nestjs/common'; +import { FeedsService } from './feeds.service'; + +@Controller('feeds') +export class FeedsController { + constructor(private readonly feedsService: FeedsService) {} + + @Get('category') + getAllFeedsByCategory(@Query('category') category: string) { + return this.feedsService.findAllFeedsByCategory(category); + } + + @Post('like/:checklistId') + postLike(@Param('checklistId') id: number) { + return this.feedsService.updateLikeCount(id); + } + + @Post('download/:checklistId') + postDownload(@Param('checklistId') id: number) { + return this.feedsService.updateDownloadCount(id); + } +} diff --git a/server/src/feeds/feeds.module.ts b/server/src/feeds/feeds.module.ts new file mode 100644 index 00000000..dc1c0108 --- /dev/null +++ b/server/src/feeds/feeds.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { FeedsService } from './feeds.service'; +import { FeedsController } from './feeds.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { FeedModel } from './entity/feed.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([FeedModel])], + controllers: [FeedsController], + providers: [FeedsService], +}) +export class FeedsModule {} diff --git a/server/src/feeds/feeds.service.spec.ts b/server/src/feeds/feeds.service.spec.ts new file mode 100644 index 00000000..9c99b8c3 --- /dev/null +++ b/server/src/feeds/feeds.service.spec.ts @@ -0,0 +1,118 @@ +import { BadRequestException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { FeedModel } from './entity/feed.entity'; +import { FeedsService } from './feeds.service'; + +type MockRepository = Partial, jest.Mock>>; + +describe('FeedsService', () => { + let service: FeedsService; + let mockFeedsRepository: MockRepository; + + beforeEach(async () => { + mockFeedsRepository = { + findOne: jest.fn(), + find: jest.fn(), + save: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + FeedsService, + { + provide: getRepositoryToken(FeedModel), + useValue: mockFeedsRepository, + }, + ], + }).compile(); + + service = module.get(FeedsService); + }); + + it('findFeedById(feedId): 피드 ID로 피드를 찾는다', async () => { + const feedId = 1; + const mockFeed = { feedId, likeCount: 10, downloadCount: 5 }; + mockFeedsRepository.findOne.mockResolvedValue(mockFeed); + + const result = await service.findFeedById(feedId); + + expect(mockFeedsRepository.findOne).toHaveBeenCalledWith({ + where: { feedId }, + }); + expect(result).toEqual(mockFeed); + }); + + it('findFeedById(feedId): 존재하지 않는 피드 ID에 대한 예외 처리', async () => { + mockFeedsRepository.findOne.mockResolvedValue(undefined); + + await expect(service.findFeedById(9999)).rejects.toThrow( + BadRequestException, + ); + }); + + it('findAllFeedsByCategory(mainCategory): 주어진 카테고리의 모든 피드를 찾는다', async () => { + const mainCategory = 'Sports'; + const mockFeeds = [ + { feedId: 1, mainCategory }, + { feedId: 2, mainCategory }, + ]; + mockFeedsRepository.find.mockResolvedValue(mockFeeds); + + const result = await service.findAllFeedsByCategory(mainCategory); + + expect(mockFeedsRepository.find).toHaveBeenCalledWith({ + where: { mainCategory }, + }); + expect(result).toEqual(mockFeeds); + }); + + it('findAllFeedsByCategory(mainCategory): 주어진 카테고리에 해당하는 피드가 없을 경우 예외를 던진다', async () => { + const mainCategory = 'NonExistingCategory'; + mockFeedsRepository.find.mockResolvedValue([]); + + await expect(service.findAllFeedsByCategory(mainCategory)).rejects.toThrow( + BadRequestException, + ); + }); + + it('updateLikeCount(feedId): 피드의 좋아요 수를 업데이트한다', async () => { + const feedId = 1; + const mockFeed = { feedId, likeCount: 10, downloadCount: 5 }; + mockFeedsRepository.findOne.mockResolvedValue(mockFeed); + mockFeedsRepository.save.mockResolvedValue({ ...mockFeed, likeCount: 11 }); + + const result = await service.updateLikeCount(feedId); + + expect(mockFeedsRepository.findOne).toHaveBeenCalledWith({ + where: { feedId }, + }); + expect(mockFeedsRepository.save).toHaveBeenCalledWith({ + ...mockFeed, + likeCount: 11, + }); + expect(result.likeCount).toEqual(11); + }); + + it('updateDownloadCount(feedId): 피드의 다운로드 수를 업데이트한다', async () => { + const feedId = 1; + const mockFeed = { feedId, likeCount: 10, downloadCount: 5 }; + mockFeedsRepository.findOne.mockResolvedValue(mockFeed); + mockFeedsRepository.save.mockResolvedValue({ + ...mockFeed, + downloadCount: 6, + }); + + const result = await service.updateDownloadCount(feedId); + + expect(mockFeedsRepository.findOne).toHaveBeenCalledWith({ + where: { feedId }, + }); + expect(mockFeedsRepository.save).toHaveBeenCalledWith({ + ...mockFeed, + downloadCount: 6, + }); + expect(result.downloadCount).toEqual(6); + }); +}); diff --git a/server/src/feeds/feeds.service.ts b/server/src/feeds/feeds.service.ts new file mode 100644 index 00000000..a8fd6024 --- /dev/null +++ b/server/src/feeds/feeds.service.ts @@ -0,0 +1,44 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { FeedModel } from './entity/feed.entity'; +import { Repository } from 'typeorm'; + +@Injectable() +export class FeedsService { + constructor( + @InjectRepository(FeedModel) + private readonly repository: Repository, + ) {} + + async findFeedById(feedId: number) { + const feed = await this.repository.findOne({ where: { feedId } }); + if (!feed) { + throw new BadRequestException( + `${feedId}는 존재하지 않는 피드 id 입니다.`, + ); + } + return feed; + } + + async findAllFeedsByCategory(mainCategory: string) { + const feed = await this.repository.find({ where: { mainCategory } }); + if (feed.length === 0) { + throw new BadRequestException( + `${mainCategory}에 대한 피드가 존재하지 않습니다.`, + ); + } + return feed; + } + + async updateLikeCount(feedId: number) { + const feed = await this.findFeedById(feedId); + feed.likeCount += 1; + return this.repository.save(feed); + } + + async updateDownloadCount(feedId: number) { + const feed = await this.findFeedById(feedId); + feed.downloadCount += 1; + return this.repository.save(feed); + } +}