Skip to content

Commit

Permalink
[Server] 피드화면 api 구현 (#196)
Browse files Browse the repository at this point in the history
* 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: 에러메시지 수정
  • Loading branch information
pminsung12 authored Dec 6, 2023
1 parent 55ade94 commit 0014655
Show file tree
Hide file tree
Showing 10 changed files with 271 additions and 24 deletions.
4 changes: 4 additions & 0 deletions server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -48,6 +50,7 @@ import { APP_INTERCEPTOR } from '@nestjs/core';
PrivateChecklistModel,
SharedChecklistModel,
SharedChecklistItemModel,
FeedModel,
],
synchronize: true, // DO NOT USE IN PRODUCTION
}),
Expand All @@ -60,6 +63,7 @@ import { APP_INTERCEPTOR } from '@nestjs/core';
SharedChecklistsModule,
ChecklistAiModule,
CategoriesModule,
FeedsModule,
],
controllers: [AppController],
providers: [
Expand Down
2 changes: 1 addition & 1 deletion server/src/categories/categories.controller.ts
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
48 changes: 48 additions & 0 deletions server/src/categories/const/categories.const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '비누' },
],
},
],
},
],
};
20 changes: 0 additions & 20 deletions server/src/checklist-ai/checklist-ai.controller.spec.ts

This file was deleted.

11 changes: 8 additions & 3 deletions server/src/common/entity/checklist.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
14 changes: 14 additions & 0 deletions server/src/feeds/entity/feed.entity.ts
Original file line number Diff line number Diff line change
@@ -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;
}
22 changes: 22 additions & 0 deletions server/src/feeds/feeds.controller.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
12 changes: 12 additions & 0 deletions server/src/feeds/feeds.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
118 changes: 118 additions & 0 deletions server/src/feeds/feeds.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;

describe('FeedsService', () => {
let service: FeedsService;
let mockFeedsRepository: MockRepository<FeedModel>;

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>(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);
});
});
44 changes: 44 additions & 0 deletions server/src/feeds/feeds.service.ts
Original file line number Diff line number Diff line change
@@ -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<FeedModel>,
) {}

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);
}
}

0 comments on commit 0014655

Please sign in to comment.