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

[이수현] 스토리 생성 AP 과제 제출 #2

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 0 additions & 11 deletions .env.example

This file was deleted.

111 changes: 76 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
# 미션 - 인스타 스토리 API

## 구현할 기능

- 제목, 이미지, 해시태그, 작성자 이름, 유효시간(12, 24시간)을 포함한 스토리를 생성한다.
- 스토리 조회 시 12시간, 24시간 이내에 생성된 스토리만 조회할 수 있고 해시태그 정보를 함께 조회

## 예외처리

- API에 요청받은 Body 값의 타입을 검증하여 올바르지 않은 타입일 경우 `400 BadRequest` 에러를 리턴해야한다.
- API에 요청받은 Body 값의 필수 값이 누락되거나/빈 값인 경우 `400 BadRequest` 에러를 리턴해야한다.
- 스토리 조회 시, 페이지지와 리미트 값이 유효하지 않으면 `400 BadRequest` 에러를 리턴해야한다.
- 스토리 조회 시, 페이지의 번호가 총 페이지의 번호를 초과하면 `400 BadRequest` 에러를 리턴해야한다.

## 🔍 진행 방식

- 미션은 **기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항** 세 가지로 구성되어 있다.
Expand All @@ -20,23 +33,26 @@ Ran all test suites.
---

## 🚀 기능 요구 사항

인스타 스토리와 유사한 형태의 게시글을 위한 API를 구현한다.

예시)

1. 스토리를 생성한다.
2. 12 or 24시간이내 스토리를 조회한다.

총 2개의 API 엔드포인트로 구성한다.
- 스토리를 생성하는 API
- 프론트엔드 관점에서 스토리에는 `제목`, `이미지`, `해시태그`, `작성자 이름`이 포함되어 구성된어야한다.

- 스토리를 생성하는 API
- 프론트엔드 관점에서 스토리에는 `제목`, `이미지`, `해시태그`, `작성자 이름`이 포함되어 구성된어야한다.
- 스토리를 생성할 때, 스토리의 유효기간 `12시간 | 24시간` 을 설정할 수 있다.
- `작성자 이름`의 경우, 현재 로그인한 유저의 이름을 가져온다고 가정한다.
- `해시태그`의 경우, `#`을 포함한 `문자열`로 구성되어야하고, 중복시 하나의 `해시태그`로 인식한다.
```
예시) #어쩌다 #Nest와 #어쩌다 #스터디에서 #어쩌다는 같은 식별자를 가진 하나의 해시태그이다.
```
```
예시) #어쩌다 #Nest와 #어쩌다 #스터디에서 #어쩌다는 같은 식별자를 가진 하나의 해시태그이다.
```

- 스토리를 조회하는 API
- 스토리를 조회하는 API
- 스토리를 조회하는 시점으로부터 `12시간 | 24시간 이전` 에 생성된 스토리만 조회할 수 있다.
- 스토리 조회 시, `해시태그`의 정보를 함께 조회할 수 있다.

Expand All @@ -45,83 +61,106 @@ Ran all test suites.
- API에 요청받은 Body 값의 타입을 검증하여 올바르지 않은 타입일 경우 `400 BadRequest` 에러를 리턴해야한다.
- API에 요청받은 Body 값의 필수 값이 누락되거나/빈 값인 경우 `400 BadRequest` 에러를 리턴해야한다.


### API 요청/응답 요구 사항

1. 모든 API의 요청/응답은 DTO를 통해 TypeSafe하게 이루어져야한다.
2. DTO의 타입은 `class-validator`를 이용하여 검증한다.
3. DTO 내부 요소의 명칭은 `camelCase`로 작성한다.

#### 요청

- 유효기간은 `12시간 | 24시간`의 `number` 형식이다.

```
validTime : 12 | 24
```

- 제목은 `string` 형식이다.

```
title : '어쩌다 Nest'
```

- 작성자 이름은 `string` 형식이다.

```
author : '어쩌다'
```

- 이미지는 `url 형태의 문자열` 형식이다.

```
image : https://images.unsplash.com/photo-1533738363-b7f9aef128ce?q=80&w=1635&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D
```

- 해시태그는 `#`을 포함한 문자열 리스트 형식이다.

```
hashtags : [ '#어쩌다', '#Nest', '#당근' ]
```

- 스토리 조회시 `Pagination`을 지원한다.

```
page : 1
limit : 10
```

#### 응답

- 정상적으로 스토리가 생성될 시 생성된 데이터를 리턴한다.

```json
{
"id" : 1,
"createdAt" : "2023-11-21T12:00:00.000Z",
"validTime" : 24,
"title" : "어쩌다 Nest",
"author" : "어쩌다",
"image" : "https://images.unsplash.com/photo-1533738363-b7f9aef128ce?q=80&w=1635&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"hashtags" : [ "#어쩌다", "#Nest", "#당근" ]
"id": 1,
"createdAt": "2023-11-21T12:00:00.000Z",
"validTime": 24,
"title": "어쩌다 Nest",
"author": "어쩌다",
"image": "https://images.unsplash.com/photo-1533738363-b7f9aef128ce?q=80&w=1635&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"hashtags": [
"#어쩌다",
"#Nest",
"#당근"
]
}
```

- 정상적으로 스토리가 목록 조회될 시 조회된 데이터를 리턴한다.

```json
{
"data" : [
"data": [
{
"id": 1,
"createdAt": "2023-11-21T12:00:00.000Z",
"validTime": 24,
"title": "어쩌다 Nest",
"author": "어쩌다",
"image": "https://images.unsplash.com/photo-1533738363-b7f9aef128ce?q=80&w=1635&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"hashtags": [
"#어쩌다",
"#Nest",
"#당근"
]
},
{
"id" : 1,
"createdAt" : "2023-11-21T12:00:00.000Z",
"validTime" : 24,
"title" : "어쩌다 Nest",
"author" : "어쩌다",
"image" : "https://images.unsplash.com/photo-1533738363-b7f9aef128ce?q=80&w=1635&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"hashtags" : [ "#어쩌다", "#Nest", "#당근" ]
},{
"id" : 2,
"createdAt" : "2023-11-23T12:00:00.000Z",
"validTime" : 12,
"title" : "NestJs",
"author" : "어쩌다가 팀장",
"image" : "https://images.unsplash.com/photo-1533738363-b7f9aef128ce?q=80&w=1635&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"hashtags" : [ "#어쩌다", "#Nest", "#당근" ]
"id": 2,
"createdAt": "2023-11-23T12:00:00.000Z",
"validTime": 12,
"title": "NestJs",
"author": "어쩌다가 팀장",
"image": "https://images.unsplash.com/photo-1533738363-b7f9aef128ce?q=80&w=1635&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"hashtags": [
"#어쩌다",
"#Nest",
"#당근"
]
}
],
"page" : 1,
"totalPage" : 1,
"limit" : 10
"page": 1,
"totalPage": 1,
"limit": 10
}
```

Expand All @@ -133,13 +172,15 @@ limit : 10
- **Swagger**를 이용하여 API 명세를 작성한다.
- **package.json**에 명시된 라이브러리만을 이용하여 구현한다.
- **eslint**, **prettier** 등의 코드 포맷팅 라이브러리를 이용하여 제공된 코드 컨벤션에 맞추어 코드를 작성한다.
- `node`, `npm` 버전은 `package.json`에 명시된 버전을 사용한다. [Volta를 이용하여 node 버전을 관리한다.](https://docs.volta.sh/guide/getting-started)
- `node`, `npm` 버전은 `package.json`에 명시된 버전을
사용한다. [Volta를 이용하여 node 버전을 관리한다.](https://docs.volta.sh/guide/getting-started)


- **(선택 사항)** API 구현이 완료되고, 유닛 테스트, E2E 테스트등 모든 테스트 코드를 작성하여 테스트를 통과하면 굿!

---

## ✏️ 과제 진행 요구 사항

- 미션은 [nest-insta-story-2](https://github.com/eojjeoda-nest/nest-insta-story-2) 저장소를 Fork & Clone 하고 시작한다.
- **기능을 구현하기 전 `README.md`에 구현할 기능/예외처리를 목록으로 정리**해 추가한다.
- **기능을 구현하기 전 `README.md`에 구현할 기능/예외처리를 목록으로 정리**해 추가한다.
5 changes: 5 additions & 0 deletions 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/entity/story.entity';
import { Hashtag } from './hashtag/entity/hashtag.entity';

@Module({
imports: [
Expand All @@ -16,6 +19,7 @@ import { DataSource } from 'typeorm';
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
entities: [Story, Hashtag],
synchronize: process.env.DB_SYNC === 'true',
timezone: 'Z',
};
Expand All @@ -28,6 +32,7 @@ import { DataSource } from 'typeorm';
return addTransactionalDataSource(new DataSource(options));
},
}),
StoryModule,
],
controllers: [],
providers: [],
Expand Down
4 changes: 4 additions & 0 deletions src/global/common/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum Expiration {
TWELVE_HOURS = 12,
TWENTY_FOUR_HOURS = 24,
}
22 changes: 22 additions & 0 deletions src/global/common/isHashtagArray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { registerDecorator, ValidationOptions } from 'class-validator';

export function IsHashtagArray(validationOptions?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isHashtagArray',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value) {
return (
Array.isArray(value) &&
value.every(
(item) => typeof item === 'string' && item.startsWith('#'),
)
);
},
},
});
};
}
9 changes: 9 additions & 0 deletions src/global/common/timeStamp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { BaseEntity, CreateDateColumn, UpdateDateColumn } from 'typeorm';

export abstract class Timestamp extends BaseEntity {
@CreateDateColumn({ type: 'timestamp' })
createdAt: Date;

@UpdateDateColumn({ type: 'timestamp' })
updatedAt: Date;
}
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/entity/story.entity';

@Entity()
export class Hashtag {
@PrimaryGeneratedColumn('increment', { name: 'hashtag_id' })
id: number;

@Column()
hashtag: string;

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

async function bootstrap() {
initializeTransactionalContext();

const app = await NestFactory.create(AppModule);
// TODO: 프로그램 구현
const options = new DocumentBuilder()
.setTitle('insta story API')
.setDescription('insta story API description')
.setVersion('1.0')
.addTag('API')
.build();

const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api', app, document);
app.useGlobalPipes(new ValidationPipe({ transform: true }));

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

console.log(`Application is running on: ${await app.getUrl()}`);
Expand Down
23 changes: 23 additions & 0 deletions src/story/dto/create-story-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';

export class CreateStoryResponseDto {
@ApiProperty({ description: '스토리의 id' })
id: number;

@ApiProperty({ description: '스토리의 생성 시간' })
createdAt: Date;

@ApiProperty({ description: '스토리의 유효 시간' })
validTime: number;

@ApiProperty({ description: '스토리의 제목' })
title: string;

@ApiProperty({ description: '스토리의 작성자' })
author: string;
@ApiProperty({ description: '스토리의 이미지' })
image: string;

@ApiProperty({ description: '스토리의 해시태그' })
hashtags: string[];
}
36 changes: 36 additions & 0 deletions src/story/dto/create-story.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { IsEnum, IsNotEmpty, IsString, IsUrl } from 'class-validator';
import { Expiration } from '../../global/common/constants';
import { IsHashtagArray } from '../../global/common/isHashtagArray';
import { ApiProperty } from '@nestjs/swagger';

export class CreateStoryDto {
@ApiProperty({ description: 'title', example: 'title' })
@IsString()
@IsNotEmpty()
title: string;

@ApiProperty({
description: 'imageUrl',
example:
'https://images.unsplash.com/photo-1533738363-b7f9aef128ce?q=80&w=1635&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
})
@IsUrl()
@IsNotEmpty()
imageUrl: string;

@ApiProperty({ description: 'author', example: '작성자' })
@IsString()
@IsNotEmpty()
author: string;

@ApiProperty({ description: 'hashtags', example: '["#어쩌다", "#Nest"]' })
@IsHashtagArray({
message: "모든 해시태그는 '#'으로 시작해야 합니다.",
})
@IsNotEmpty()
hashtags: string[];

@ApiProperty({ description: 'validTime', example: 12 })
@IsEnum(Expiration)
validTime: Expiration;
}
16 changes: 16 additions & 0 deletions src/story/dto/read-story-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { CreateStoryResponseDto } from './create-story-response.dto';
import { ApiProperty } from '@nestjs/swagger';

export class ReadStoryResponseDto {
@ApiProperty({ description: '스토리 내용', type: [CreateStoryResponseDto] })
data: CreateStoryResponseDto[];

@ApiProperty({ description: '현재 페이지' })
page: number;

@ApiProperty({ description: '총 페이지 수' })
totalPage: number;

@ApiProperty({ description: '한 페이지에 보여줄 스토리 수' })
limit: number;
}
Loading