Skip to content

Commit 0ad4b9d

Browse files
authored
Merge pull request #449 from boostcampwm-2024/docs/file-upload
📝 docs: 파일 업로드 API Swagger 작성
2 parents 5429119 + 6778001 commit 0ad4b9d

File tree

7 files changed

+205
-10
lines changed

7 files changed

+205
-10
lines changed

server/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { MetricsModule } from './common/metrics/metrics.module';
1818
import { APP_INTERCEPTOR } from '@nestjs/core';
1919
import { MetricsInterceptor } from './common/metrics/metrics.interceptor';
2020
import { LikeModule } from './like/module/like.module';
21+
import { FileModule } from './file/module/file.module';
2122

2223
@Module({
2324
imports: [
@@ -50,6 +51,7 @@ import { LikeModule } from './like/module/like.module';
5051
StatisticModule,
5152
CommentModule,
5253
LikeModule,
54+
FileModule,
5355
],
5456
controllers: [],
5557
providers: [

server/src/common/swagger/swagger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export function setupSwagger(app: INestApplication) {
1616
.addTag('Statistic', '통계 정보 조회 API')
1717
.addTag('User', '사용자 관리와 인증 관련 API')
1818
.addTag('OAuth', 'OAuth 관련 API')
19+
.addTag('File', '파일 업로드 및 관리 API')
1920
.setLicense('MIT License', 'https://opensource.org/licenses/MIT')
2021
.build();
2122

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { applyDecorators } from '@nestjs/common';
2+
import {
3+
ApiBadRequestResponse,
4+
ApiNotFoundResponse,
5+
ApiOkResponse,
6+
ApiOperation,
7+
ApiParam,
8+
ApiUnauthorizedResponse,
9+
} from '@nestjs/swagger';
10+
11+
export function ApiDeleteFile() {
12+
return applyDecorators(
13+
ApiOperation({
14+
summary: '파일 삭제 API',
15+
description: '업로드된 파일을 삭제합니다.',
16+
}),
17+
ApiParam({
18+
name: 'id',
19+
description: '삭제할 파일의 ID',
20+
type: 'string',
21+
example: 'uuid-string',
22+
}),
23+
ApiOkResponse({
24+
description: '파일 삭제 성공',
25+
schema: {
26+
properties: {
27+
message: {
28+
type: 'string',
29+
example: '파일이 성공적으로 삭제되었습니다.',
30+
},
31+
},
32+
},
33+
}),
34+
ApiBadRequestResponse({
35+
description: '잘못된 요청',
36+
schema: {
37+
properties: {
38+
message: {
39+
type: 'string',
40+
example: '유효하지 않은 파일 ID입니다.',
41+
},
42+
},
43+
},
44+
}),
45+
ApiNotFoundResponse({
46+
description: '파일을 찾을 수 없음',
47+
schema: {
48+
properties: {
49+
message: {
50+
type: 'string',
51+
example: '파일을 찾을 수 없습니다.',
52+
},
53+
},
54+
},
55+
}),
56+
ApiUnauthorizedResponse({
57+
description: '인증되지 않은 사용자',
58+
schema: {
59+
properties: {
60+
message: {
61+
type: 'string',
62+
example: '인증이 필요합니다.',
63+
},
64+
},
65+
},
66+
}),
67+
);
68+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { applyDecorators } from '@nestjs/common';
2+
import {
3+
ApiBadRequestResponse,
4+
ApiBody,
5+
ApiConsumes,
6+
ApiOkResponse,
7+
ApiOperation,
8+
ApiUnauthorizedResponse,
9+
} from '@nestjs/swagger';
10+
11+
export function ApiUploadProfileFile() {
12+
return applyDecorators(
13+
ApiOperation({
14+
summary: '프로필 이미지 업로드 API',
15+
description: '사용자의 프로필 이미지를 업로드합니다.',
16+
}),
17+
ApiConsumes('multipart/form-data'),
18+
ApiBody({
19+
description: '업로드할 파일',
20+
schema: {
21+
type: 'object',
22+
properties: {
23+
file: {
24+
type: 'string',
25+
format: 'binary',
26+
description: '업로드할 이미지 파일 (JPG, PNG, GIF 등)',
27+
},
28+
},
29+
required: ['file'],
30+
},
31+
}),
32+
ApiOkResponse({
33+
description: '파일 업로드 성공',
34+
schema: {
35+
properties: {
36+
message: {
37+
type: 'string',
38+
example: '파일 업로드에 성공했습니다.',
39+
},
40+
data: {
41+
type: 'object',
42+
properties: {
43+
id: {
44+
type: 'number',
45+
example: 1,
46+
description: '파일 ID',
47+
},
48+
originalName: {
49+
type: 'string',
50+
example: 'my-photo.jpg',
51+
description: '원본 파일명',
52+
},
53+
mimetype: {
54+
type: 'string',
55+
example: 'image/jpeg',
56+
description: '파일 MIME Type',
57+
},
58+
size: {
59+
type: 'number',
60+
example: 1024000,
61+
description: '파일 크기 (bytes)',
62+
},
63+
url: {
64+
type: 'string',
65+
example: '/objects/profile/2024/01/profile-image.jpg',
66+
description: '파일 접근 URL',
67+
},
68+
userId: {
69+
type: 'number',
70+
example: 123,
71+
description: '업로드한 사용자 ID',
72+
},
73+
createdAt: {
74+
type: 'string',
75+
format: 'date-time',
76+
example: '2024-01-01T12:00:00.000Z',
77+
description: '업로드 날짜',
78+
},
79+
},
80+
},
81+
},
82+
},
83+
}),
84+
ApiBadRequestResponse({
85+
description: '잘못된 요청',
86+
schema: {
87+
properties: {
88+
message: {
89+
type: 'string',
90+
example: '파일이 선택되지 않았습니다.',
91+
},
92+
},
93+
},
94+
}),
95+
ApiUnauthorizedResponse({
96+
description: '인증되지 않은 사용자',
97+
schema: {
98+
properties: {
99+
message: {
100+
type: 'string',
101+
example: '인증이 필요합니다.',
102+
},
103+
},
104+
},
105+
}),
106+
);
107+
}

server/src/file/controller/file.controller.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import { ApiTags } from '@nestjs/swagger';
1616
import { JwtGuard } from '../../common/guard/jwt.guard';
1717
import { createDynamicStorage } from '../../common/disk/diskStorage';
1818
import { ApiResponse } from '../../common/response/common.response';
19+
import { ApiUploadProfileFile } from '../api-docs/uploadProfileFile.api-docs';
20+
import { ApiDeleteFile } from '../api-docs/deleteFile.api-docs';
21+
import { FileUploadResponseDto } from '../dto/createFile.dto';
1922

2023
@ApiTags('File')
2124
@Controller('file')
@@ -24,24 +27,27 @@ export class FileController {
2427
constructor(private readonly fileService: FileService) {}
2528

2629
@Post('upload/profile')
30+
@ApiUploadProfileFile()
2731
@UseInterceptors(FileInterceptor('file', createDynamicStorage()))
2832
async upload(@UploadedFile() file: any, @Req() req) {
2933
if (!file) {
3034
throw new BadRequestException('파일이 선택되지 않았습니다.');
3135
}
3236

33-
const resultFile = await this.fileService.create(
37+
const responseDto = await this.fileService.create(
3438
file,
3539
Number.parseInt(req.user.id),
3640
);
3741

38-
return ApiResponse.responseWithData('파일 업로드에 성공했습니다.', {
39-
resultFile,
40-
});
42+
return ApiResponse.responseWithData(
43+
'파일 업로드에 성공했습니다.',
44+
responseDto,
45+
);
4146
}
4247

4348
// TODO: 권한검사 추가
4449
@Delete(':id')
50+
@ApiDeleteFile()
4551
async deleteFile(@Param('id') id: string, @Req() req) {
4652
const fileId = parseInt(id, 10);
4753
if (isNaN(fileId)) {
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
import { ApiProperty } from '@nestjs/swagger';
2-
import { IsOptional, IsString, MaxLength } from 'class-validator';
32

43
export class FileUploadResponseDto {
54
@ApiProperty({ description: '파일 ID' })
65
id: number;
76

8-
@ApiProperty({ description: '저장된 파일명' })
9-
filename: string;
7+
@ApiProperty({ description: '원본 파일명' })
8+
originalName: string;
109

1110
@ApiProperty({ description: '파일 MIME Type' })
12-
mimeType: string;
11+
mimetype: string;
1312

1413
@ApiProperty({ description: '파일 크기 (bytes)' })
1514
size: number;
1615

16+
@ApiProperty({ description: '파일 접근 URL' })
17+
url: string;
18+
19+
@ApiProperty({ description: '업로드한 사용자 ID' })
20+
userId: number;
21+
1722
@ApiProperty({ description: '업로드 날짜' })
1823
createdAt: Date;
1924
}

server/src/file/service/file.service.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import { File } from '../entity/file.entity';
33
import { unlinkSync, existsSync } from 'fs';
44
import { FileRepository } from '../repository/file.repository';
55
import { User } from '../../user/entity/user.entity';
6+
import { FileUploadResponseDto } from '../dto/createFile.dto';
67

78
@Injectable()
89
export class FileService {
910
constructor(private readonly fileRepository: FileRepository) {}
1011

11-
async create(file: any, userId: number) {
12+
async create(file: any, userId: number): Promise<FileUploadResponseDto> {
1213
const { originalName, mimetype, size, path } = file;
1314

1415
const savedFile = await this.fileRepository.save({
@@ -21,8 +22,13 @@ export class FileService {
2122
const accessUrl = this.generateAccessUrl(path);
2223

2324
return {
24-
...savedFile,
25+
id: savedFile.id,
26+
originalName: savedFile.originalName,
27+
mimetype: savedFile.mimetype,
28+
size: savedFile.size,
2529
url: accessUrl,
30+
userId: userId,
31+
createdAt: savedFile.createdAt,
2632
};
2733
}
2834

0 commit comments

Comments
 (0)