본문 바로가기
Backend/NestJS

이미지 파일 업로드 심화 - 선업로드

by 찬찬2 2024. 6. 25.

파일을 업로드할때 파일의 크기에 따라 처리시간이 오래걸릴 수 있다. 요즘 시대가 어떤 시대인가? 8282 대한민국 사람이라면 불편할 수 있다.

 

그래서 고안된 방법이, 파일을 선업로드하는 것이다. 사용자가 업로드 버튼을 누르기 전 단계, 즉 사용자가 파일을 선택한 순간 업로드를 시키는 것이다. 물론 장단점은 있다.

 

사용자가 선택했지만 업로드하지 않았을때와 같은 경우... 리소스 낭비가 될 수 있다.

 

[1] 선업로드: temp 폴더에 임시로 이미지 파일을 저장

위 코드는 이전 게시글에 있는 코드를 그대로 가져왔다. 이 부분이 바로 선업로드로 사용될 부분. 

MulterModule 에 register 메서드의 인자중 storage 가 바로 사용자가 이미지를 최종 업로드하기 전 바로 선택한 단계이고, 이 단계에서 임시로 temp 폴더에 이미지를 저장시켜준다.

 

import { BadRequestException, Module } from '@nestjs/common';
import { CommonService } from './common.service';
import { CommonController } from './common.controller';
import { MulterModule } from '@nestjs/platform-express';
import { extname } from 'path';
import * as multer from 'multer'
import { TEMP_FOLDER_PATH } from 'src/common/const/path.const';
import { v4 as uuid } from 'uuid';

@Module({
  imports: [
    MulterModule.register({
      limits: {
        fileSize: 10000000 // bytes
      },
      fileFilter: (req, file, callBack) => {
        const ext = extname(file.originalname)
        if(ext !== '.jpg' && ext !== '.jpeg' && ext !== '.png') {
          return callBack(new BadRequestException('jpg/jpeg/png 파일만 업로드 가능합니다.'), false);
        };
        return callBack(null, true)
      },
      storage: multer.diskStorage({
        destination: (req, file, callBack) => {
          callBack(null, TEMP_FOLDER_PATH)
        },
        filename: (req, file, callBack) => {
          callBack(null, `${uuid()}${extname(file.originalname)}`); // 123123-123123-123123-123123.jpg
        }
      })
    })
  ],
  controllers: [CommonController],
  providers: [CommonService],
  exports: [CommonService]
})
export class CommonModule {}

 

import { Controller, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common';
import { CommonService } from './common.service';
import { FileInterceptor } from '@nestjs/platform-express';
import { AccessTokenGuard } from 'src/auth/guard/bearer-token.guard';

@Controller('common')
export class CommonController {
  constructor(private readonly commonService: CommonService) {}

  @Post('image')
  @UseInterceptors(FileInterceptor('image'))
  @UseGuards(AccessTokenGuard)
  postImage(
    @UploadedFile() file: Express.Multer.File
  ){
    return {
      fileName: file.filename
    };
  }

}

 

[2] 최종 업로드

사용자가 최종적으로 업로드 버튼을 눌렀을때 선택한 이미지, temp 폴더에 있는 이미지가 posts 폴더로 이동되면서 image_model 리파지토리에 저장되고 동시에 posts_model 리파지토리에도 저장된다.

 

import { Controller, Post, Body, UploadedFile,  } from '@nestjs/common';
import { User } from 'src/users/decorator/user.decorator';
import { PostsService } from './posts.service';

@Controller('posts')
export class CommonController {
  constructor(private readonly postsService: PostsService) {}

  @Post()
  @UseGuards(AccessTokenGuard)
  postImage(
    @User('id') userId: number, // 커스텀 데코레이터, ExecutionContext 에서 Request 에 있는 유저객체 가져옴
    @Body() body: CreatePostDto
  ){
   // posts_model 리포지토리에 저장
   const post = this.postsService.createPost(userId, body); 
   
   // image_model 리포지토리에 저장
   // temp 폴더에서 posts 폴더로 이동
   for(let i = 0; i < body.images.length; i++){
      await this.postsService.createPostImage({
         post,
         order: i,
         path: body.images[i],
         type: ImageModelType.POST_IMAGE // enum 이고 0 이다.
      });
   };
   
   return this.postsService.getPostById(post.id);
  }
}


// service.ts
import { basename, join } from 'path';
import { promises } from 'fs'; // File System
import { PostsModel } from "src/posts/entity/posts.entity";
import { ImageModel } from "src/image/entity/image.entity";

constructor(){
   @InjectRepository(PostsModel)
   private readonly postRepository: Repository<PostsModel>,
   @InjectRepository(ImageModel)
   private readonly imageRepository: Repository<ImageModel>
}

async createPostImage(dto: CreatePostDto){
  // dto의 이미지 이름을 기반으로
  // 파일의 경로를 생성한다.
  const tempFilePath = join(
    TEMP_FOLDER_PATH,
    dto.image,
  );

  try{
    await promises.access(tempFilePath); // 파일이 존재하는지 확인, 만약에 존재하지 않는다면 에
  }catch(e){
    throw new BadRequestException('존재하지 않는 파일입니다.');
  };

  const fileName = basename(tempFilePath); // 파일의 이름만 가져오기, 예를들어, /a/b/c.jpg 라면 c.jpg 만 가져온다.

   // image 리파지토리에 저장 
  const result = await this.imageRepository.save({
    ...dto,
  });

  // {프로젝트 경로}/public/posts/c.jpg
  const newPath = join(
    POST_IMAGE_PATH,
    fileName
  );
  
  await promises.rename(tempFilePath, newPath); // temp 에서 posts 로 파일 이동
}

  createPost(authorId: number, postDto: CreatePostDto){    
    const post = this.postRepository.create({
      author: {
        id: authorId
      },
      ...postDto,
      images: [],
      likeCount: 0,
      commentCount: 0
    });
    
    return this.postRepository.save(post);
  }

 

 

 

posts_model 은 image_model 과 relation 이 맺어져있다. 외래키(foreign key)는 image_model 쪽에 있고, posts_model 을 SELECT 할때 relation 옵션을 추가하면  두 리파지토리에 있는 정보를 연결지어 같이 볼 수 있다.

 

ManyToOne 데코레이터가 있는 쪽의 리파지토리에 외래 키가 생성된다.

 

 

{
    "id": 114,
    "updatedAt": "2024-06-24T22:42:04.134Z",
    "createdAt": "2024-06-24T22:42:04.134Z",
    "title": "제목",
    "content": "내용",
    "likeCount": 0,
    "commentCount": 0,
    "author": {
        "id": 2,
        "updatedAt": "2024-06-24T16:27:32.564Z",
        "createdAt": "2024-06-24T16:27:32.564Z",
        "nickname": "chanchan1",
        "email": "chanchan1@test.com",
        "password": "$2b$10$GOBnuLWuF2exQmoJtFYLie6Hc8BkHL7wYOZKyMeQAcmsVbiNhSQge",
        "role": "user"
    },
    "images": [
        {
            "id": 9,
            "updatedAt": "2024-06-24T22:42:04.134Z",
            "createdAt": "2024-06-24T22:42:04.134Z",
            "order": 0,
            "type": 0,
            "path": "C:\\Users\\farmc\\OneDrive\\바탕 화면\\workspace\\NestJS\\cf_sns\\public\\posts\\a1c216c3-3ba6-4a2c-970e-9e6deb533113.jpg"
        }
    ]
}

댓글