Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[백한결] 인스타 스토리 API #4

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c9b8be9
feat: 초기세팅
baekhangyeol Mar 17, 2024
7262ddf
feat: stroy 엔티티 생성
baekhangyeol Mar 17, 2024
10eeaaa
fix: hashtags 타입 변경
baekhangyeol Mar 18, 2024
eeceafc
feat: GlobalPipes 설정
baekhangyeol Mar 18, 2024
5112067
feat: app.module에 엔티티 추가
baekhangyeol Mar 18, 2024
25131a0
feat: 모듈 설정
baekhangyeol Mar 18, 2024
be62bb5
feat: 스토리 생성 기능 구현
baekhangyeol Mar 18, 2024
4209b9f
feat: Pagination 설정
baekhangyeol Mar 18, 2024
e5fb294
feat: 스토리 조회 기능 구현
baekhangyeol Mar 18, 2024
facf27b
fix: 쓰지않는 데코레이터 삭제
baekhangyeol Mar 18, 2024
2d3a735
feat: HttpCode 추가
baekhangyeol Mar 18, 2024
45a83bf
fix: Query 타입 Number로 변경해주도록 변경
baekhangyeol Mar 18, 2024
263acbc
feat: Swagger 설정
baekhangyeol Mar 18, 2024
5bb90e6
feat: validTime을 12 or 24만 가능하게 @IsIn() 사용
baekhangyeol Mar 20, 2024
0d1d467
[#9]fix: 해시태그 앞에 #가 필수로 붙도록 설정
baekhangyeol Mar 20, 2024
f949ace
fix: 해시태그 앞에 #가 필수로 붙도록 설정
baekhangyeol Mar 20, 2024
5d400ab
Merge branch 'main' of https://github.com/baekhangyeol/nest-insta-sto…
baekhangyeol Mar 20, 2024
13929fc
fix: createQueryBuilder 사용해서 validTime에 따른 조회 구현
baekhangyeol Mar 20, 2024
39d39b5
feat: 테스트 코드
baekhangyeol Mar 20, 2024
aea3184
fix: getStories 테스트코드 수정
baekhangyeol Mar 21, 2024
cac43fc
fix: 테스트 코드 수정
baekhangyeol Mar 22, 2024
5f29372
fix: hashtags 따로 분리해서 조건에 맞게 수정
baekhangyeol Mar 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
File renamed without changes.
Empty file modified setup.sh
100644 → 100755
Empty file.
5 changes: 5 additions & 0 deletions template/source/typeorm/app.module.ts → src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { addTransactionalDataSource } from 'typeorm-transactional';
import { DataSource } from 'typeorm';
import { StoryModule } from './story/story.module';
import { Story } from './story/entities/story.entity';
import { Hashtag } from './hashtag/entity/hashtag.entity';

@Module({
imports: [
Expand All @@ -17,6 +20,7 @@ import { DataSource } from 'typeorm';
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
synchronize: process.env.DB_SYNC === 'true',
entities: [Story, Hashtag],
timezone: 'Z',
};
},
Expand All @@ -28,6 +32,7 @@ import { DataSource } from 'typeorm';
return addTransactionalDataSource(new DataSource(options));
},
}),
StoryModule,
],
controllers: [],
providers: [],
Expand Down
19 changes: 19 additions & 0 deletions src/common/dto/pagination.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IsInt } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';

export class PaginationDto {
@Type(() => Number)
@IsInt()
@ApiProperty({
example: 1
})
page: number = 1;

@Type(() => Number)
@IsInt()
@ApiProperty({
example: 10
})
limit: number = 10;
}
23 changes: 23 additions & 0 deletions src/common/utils/pagination.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export class PaginationResult<T> {
data: T[];
page: number;
limit: number;
totalCount: number;
totalPages: number;

constructor(data: T[], page: number, limit: number, totalCount: number) {
this.data = data;
this.page = page;
this.limit = limit;
this.totalPages = Math.ceil(totalCount / limit);
}
}

export function createPaginationResult<T>(
data: T[],
page: number,
limit: number,
totalCount: number,
): PaginationResult<T> {
return new PaginationResult(data, page, limit, totalCount);
}
14 changes: 14 additions & 0 deletions src/hashtag/entity/hashtag.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Story } from '../../story/entities/story.entity';

@Entity()
export class Hashtag {
@PrimaryGeneratedColumn()
id: number;

@Column()
hashtag: string;

@ManyToMany(() => Story, (story) => story.hashtags)
stories: Story[];
}
38 changes: 38 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { initializeTransactionalContext } from 'typeorm-transactional';
import { ValidationPipe } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

async function bootstrap() {
initializeTransactionalContext();

const app = await NestFactory.create(AppModule);

app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
)
// TODO: 프로그램 구현

// Swagger
const config = new DocumentBuilder()
.setTitle('어쩌다Nest 과제')
.setDescription('어쩌다Nest 과제 API')
.setVersion('1.0')
.build();
const swaggerOptions = {
operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
};
const document = SwaggerModule.createDocument(app, config, swaggerOptions);
SwaggerModule.setup('api', app, document);

await app.listen(process.env.PORT || 8000);

console.log(`Application is running on: ${await app.getUrl()}`);
}

bootstrap();
40 changes: 40 additions & 0 deletions src/story/dto/request/create-story-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { IsIn, IsInt, IsString, IsUrl, Matches } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class CreateStoryRequestDto {

@IsInt()
@IsIn([12, 24], {
message: 'validTime은 12시간 또는 24시간으로 설정 가능합니다.',
})
@ApiProperty({
description: '유효시간은 12시간 또는 24시간으로 설정 가능합니다.',
example: 12
})
validTime: number;

@IsString()
@ApiProperty({
example: '어쩌다 Nest'
})
title: string;

@IsString()
@ApiProperty({
example: '어쩌다'
})
author: string;

@IsUrl()
@ApiProperty({
example: 'https://example.com/image.jpg'
})
image: string;

@IsString({ each: true })
@Matches(/#[^\s#]+/g, { each: true, message: '모든 해시태그는 #으로 시작해야 합니다.' })
@ApiProperty({
example: ['#어쩌다', '#Nest', '#당근']
})
hashtags: string[];
}
23 changes: 23 additions & 0 deletions src/story/dto/response/create-story-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Story } from '../../entities/story.entity';

export class CreateStoryResponseDto {
id: number;
createdAt: Date;
validTime: number;
title: string;
author: string;
image: string;
hashtags: string[];

public static fromEntity(entity: Story): CreateStoryResponseDto {
const dto = new CreateStoryResponseDto();
dto.id = entity.id;
dto.createdAt = entity.createdAt;
dto.validTime = entity.validTime;
dto.title = entity.title;
dto.author = entity.author;
dto.image = entity.image;
dto.hashtags = entity.hashtags.map(hashtag => hashtag.hashtag);
return dto;
}
}
27 changes: 27 additions & 0 deletions src/story/entities/story.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Column, CreateDateColumn, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Hashtag } from '../../hashtag/entity/hashtag.entity';

@Entity()
export class Story {
@PrimaryGeneratedColumn()
id: number;

@CreateDateColumn()
createdAt: Date;

@Column()
validTime: number;

@Column()
title: string;

@Column()
author: string;

@Column()
image: string;

@ManyToMany(() => Hashtag)
@JoinTable()
hashtags: Hashtag[];
}
74 changes: 74 additions & 0 deletions src/story/story.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Test, TestingModule } from '@nestjs/testing';
import { StoryController } from './story.controller';
import { StoryService } from './story.service';
import { CreateStoryRequestDto } from './dto/request/create-story-request.dto';
import { PaginationDto } from '../common/dto/pagination.dto';

describe('StoryController', () => {
let controller: StoryController;
let storyService: StoryService;

beforeEach(async () => {
const mockStoryService = {
createStory: jest.fn(dto => {
return {
id: Date.now(),
...dto
};
}),
getStories: jest.fn(dto => {
return {
data: [],
page: dto.page,
limit: dto.limit,
totalCount: 0,
totalPages: 0,
};
}),
};

const module: TestingModule = await Test.createTestingModule({
controllers: [StoryController],
providers: [{ provide: StoryService, useValue: mockStoryService }],
}).compile();

controller = module.get<StoryController>(StoryController);
storyService = module.get<StoryService>(StoryService);
});

describe('스토리 생성', () => {
it('스토리를 생성하고 반환해야 함', async () => {
const requestDto: CreateStoryRequestDto = {
validTime: 24,
title: 'test',
author: 'Author',
image: 'http://example.com/image.jpg',
hashtags: ['#test', '#nestjs'],
};

expect(await controller.createStory(requestDto)).toEqual({
id: expect.any(Number),
...requestDto,
});
expect(storyService.createStory).toHaveBeenCalledWith(requestDto);
});
});

describe('스토리 목록 조회', () => {
it('스토리 목록과 페이지네이션 결과를 반환해야 함', async () => {
const requestDto: PaginationDto = {
page: 1,
limit: 10,
};

expect(await controller.getStories(requestDto)).toEqual({
data: [],
page: requestDto.page,
limit: requestDto.limit,
totalCount: 0,
totalPages: 0,
});
expect(storyService.getStories).toHaveBeenCalledWith(requestDto);
});
});
});
33 changes: 33 additions & 0 deletions src/story/story.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, HttpCode, HttpStatus, Query } from '@nestjs/common';
import { StoryService } from './story.service';
import { CreateStoryRequestDto } from './dto/request/create-story-request.dto';
import { PaginationDto } from '../common/dto/pagination.dto';
import { ApiBody, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';

@Controller('stories')
@ApiTags('stories')
export class StoryController {
constructor(private readonly storyService: StoryService) {}

@Post()
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: '스토리 생성 API', description: '스토리를 생성합니다.' })
@ApiBody({ type: CreateStoryRequestDto })
@ApiResponse({ status: 201, description: '스토리 생성 성공' })
@ApiResponse({ status: 400, description: '잘못된 요청' })
async createStory(@Body() request: CreateStoryRequestDto) {
return await this.storyService.createStory(request);
}

@Get()
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '스토리 목록 조회 API', description: '스토리 목록을 조회합니다.' })
@ApiQuery({ name: 'page', type: Number, required: false, description: '페이지 번호' })
@ApiQuery({ name: 'limit', type: Number, required: false, description: '페이지 크기' })
@ApiResponse({ status: 200, description: '스토리 목록 조회 성공' })
@ApiResponse({ status: 400, description: '잘못된 요청' })
async getStories(@Query() request: PaginationDto) {
return await this.storyService.getStories(request);
}

}
13 changes: 13 additions & 0 deletions src/story/story.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { StoryService } from './story.service';
import { StoryController } from './story.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Story } from './entities/story.entity';
import { Hashtag } from '../hashtag/entity/hashtag.entity';

@Module({
imports: [TypeOrmModule.forFeature([Story, Hashtag])],
providers: [StoryService],
controllers: [StoryController],
})
export class StoryModule {}
Loading