๐ŸŽ‰ berenickt ๋ธ”๋กœ๊ทธ์— ์˜จ ๊ฑธ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค. ๐ŸŽ‰
Back
NestJs
24-file-upload-classic

1. Multer ์„ธํŒ…

Multer๋ฅผ ์‚ฌ์šฉํ•ด ํŒŒ์ผ ์—…๋กœ๋“œ๋ฅผ ๊ตฌํ˜„ํ•  ๊ฒƒ์ด๋‹ค.

1
yarn add multer @types/multer uuid @types/uuid

posts ์—”ํ‹ฐํ‹ฐ์— ์ด๋ฏธ์ง€ ํ”„๋กœํผํ‹ฐ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

posts.entity.ts
1
// ์ƒ๋žต
2
3
@Entity()
4
export class PostsModel extends BaseModel {
5
// ์ƒ๋žต
6
7
@Column({
8
nullable: true,
9
})
10
image?: string
11
12
// ์ƒ๋žต
13
}

common/const/path.const.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

common/const/path.const.ts
1
import { join } from 'path'
2
import * as process from 'process'
3
4
// ์„œ๋ฒ„ ํ”„๋กœ์ ํŠธ์˜ ๋ฃจํŠธ ํด๋”
5
export const PROJECT_ROOT_PATH = process.cwd()
6
// ์™ธ๋ถ€์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ํŒŒ์ผ๋“ค์„ ๋ชจ์•„๋‘” ํด๋” ์ด๋ฆ„
7
export const PUBLIC_FOLDER_NAME = 'public'
8
// ํฌ์ŠคํŠธ ์ด๋ฏธ์ง€๋“ค์„ ์ €์žฅํ•  ํด๋” ์ด๋ฆ„
9
export const POSTS_FOLDER_NAME = 'posts'
10
11
// ์‹ค์ œ ๊ณต๊ฐœํด๋”์˜ ์ ˆ๋Œ€ ๊ฒฝ๋กœ
12
// /{ํ”„๋กœ์ ํŠธ์˜ ์œ„์น˜}/public
13
export const PUBLIC_FOLDER_PATH = join(PROJECT_ROOT_PATH, PUBLIC_FOLDER_NAME)
14
15
// ํฌ์ŠคํŠธ ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•  ํด๋”
16
// /{ํ”„๋กœ์ ํŠธ์˜ ์œ„์น˜}/public/posts
17
export const POST_IMAGE_PATH = join(PUBLIC_FOLDER_PATH, POSTS_FOLDER_NAME)
18
19
// ์ ˆ๋Œ€๊ฒฝ๋กœ x
20
// public/posts/xxx.jpg
21
export const POST_PUBLIC_IMAGE_PATH = join(PUBLIC_FOLDER_NAME, POSTS_FOLDER_NAME)
  • ์ถ”๊ฐ€์ ์œผ๋กœ ํ”„๋กœ์ ํŠธ์˜ ๋ฃจํŠธ ๊ฒฝ๋กœ์— public/posts ํด๋”๋ฅผ ๋งŒ๋“ ๋‹ค.

posts ๋ชจ๋“ˆ์—์„œ Multer ๋ชจ๋“ˆ์„ ์ „๋ถ€ importํ•œ๋‹ค.

posts.module.ts
1
import * as multer from 'multer'
2
import { v4 as uuid } from 'uuid'
3
// ์ƒ๋žต
4
5
@Module({
6
imports: [
7
/*** ๋ชจ๋ธ์— ํ•ด๋‹นํ•˜๋Š” repostory๋ฅผ ์ฃผ์ž… ==> forFeature
8
* repository : ํ•ด๋‹น ๋ชจ๋ธ์„ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ํด๋ž˜์Šค
9
*/
10
TypeOrmModule.forFeature([
11
PostsModel, //
12
]),
13
AuthModule,
14
UsersModule,
15
CommonModule,
16
MulterModule.register({
17
limits: {
18
// byte ๋‹จ์œ„๋กœ ์ž…๋ ฅ (10000000byte -> 10MB๊ฐ€ ๋„˜๋Š” ํŒŒ์ผ์€ ์—๋Ÿฌ)
19
fieldSize: 10000000,
20
},
21
/*** cb(์—๋Ÿฌ, boolean)
22
* ์ฒซ๋ฒˆ์จฐ ํŒŒ๋ผ๋ฏธํ„ฐ์—๋Š” ์—๋Ÿฌ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ์—๋Ÿฌ ์ •๋ณด๋ฅผ ๋„ฃ์–ด์ค€๋‹ค.
23
* ๋‘๋ฒˆ์จฐ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ํŒŒ์ผ์„ ๋ฐ›์„์ง€ ๋ง์ง€ boolean์„ ๋„ฃ์–ด์ค€๋‹ค.
24
*/
25
fileFilter: (req, file, cb) => {
26
// xxx.jpg -> .jpg๊ฐ™์ด ํ™•์žฅ์ž๋งŒ ๊ฐ€์ ธ์™€์คŒ
27
const ext = extname(file.originalname)
28
29
if (ext !== '.jpg' && ext !== '.jpeg' && ext !== '.png') {
30
return cb(
31
new BadRequestException('jpg/jpeg/png ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค!'), //
32
false,
33
)
34
}
35
return cb(null, true)
36
},
37
storage: multer.diskStorage({
38
// ํŒŒ์ผ์„ ์–ด๋””์— ๋ณด๋‚ผ์ง€
39
destination: function (req, res, cb) {
40
cb(null, POST_IMAGE_PATH)
41
},
42
filename: function (req, file, cb) {
43
cb(null, `${uuid()}${extname(file.originalname)}`)
44
},
45
}),
46
}),
47
],
48
// ์ƒ๋žต
49
})
50
export class PostsModule {}

2. FileInterceptor ์ ์šฉ

posts ์ปจํŠธ๋กค๋Ÿฌ์— ์ด๋ฏธ์ง€๋ฅผ ๋ฐ›์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›์•„์„œ ๋„˜๊ฒจ์ค€๋‹ค.

posts.controller.ts
1
// posts.controller.ts ์ƒ๋žต
2
@Post()
3
@UseGuards(AccessTokenGuard)
4
@UseInterceptors(FileInterceptor('image'))
5
postPosts(
6
@User('id') userId: number,
7
@Body() body: CreatePostDto,
8
@UploadedFile() file?: Express.Multer.File,
9
// @Body('title') title: string,
10
// @Body('content') content: string,
11
// ๊ธฐ๋ณธ๊ฐ’์„ true๋กœ ์„ค์ •ํ•˜๋Š” ํŒŒ์ดํ”„
12
// @Body('isPublic', new DefaultValuePipe(true)) isPublic: boolean,
13
) {
14
return this.postsService.createPost(userId, body, file?.filename)
15
}

posts ์„œ๋น„์Šค์—์„œ ๋ฐ›์€ ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•œ๋‹ค.

posts.service.ts
1
// posts.service.ts ์ƒ๋žต
2
async createPost(authorId: number, postDto: CreatePostDto, image?: string) {
3
const post = this.postsRepository.create({
4
author: {
5
id: authorId,
6
},
7
...postDto,
8
image,
9
likeCount: 0,
10
commentCount: 0,
11
})
12
const newPost = await this.postsRepository.save(post)
13
return newPost
14
}