NestJS 폼데이터 처리, 서비스 개선(연관 레코드/리소스 함께 수정/삭제), 에러 처리, API 인터페이스 변경 등

박재하·2023년 12월 1일
0

목차

  • PATCH /post/:id 개선
    • 계획
    • 구현
    • 동작 화면
  • DELETE /post/:id 개선
    • 계획
    • 구현
    • 동작 화면
  • PATCH /star/:id 구현
    • 계획
    • 구현
    • 동작 화면

PATCH /post/:id 개선

계획

  • 게시글에 대한 수정 요청은 생성 요청과 마찬가지로 사진을 받을 수 있어야 한다.
    • 따라서 폼 데이터로 요청받도록 변경하고, 사진 항목이 있을 경우 기존 사진을 삭제하고 덮어씌운다.
  • 별 스타일에 대한 수정 요청은 별도의 API로 두자는 요청이 있었음.
    • 따라서 별 수정이 컬럼으로 들어오면 400 에러를 리턴하고, 별도로 PATCH /star/:id를 만듬

구현

// board.controller.ts
@Patch(':id')
@UseGuards(CookieAuthGuard)
@UseInterceptors(FilesInterceptor('file', 3))
@UsePipes(ValidationPipe)
@UpdateBoardSwaggerDecorator()
updateBoard(
  @Param('id', ParseIntPipe) id: number,
  @Body() updateBoardDto: UpdateBoardDto,
  @GetUser() userData: UserDataDto,
  @UploadedFiles() files: Express.Multer.File[],
) {
  return this.boardService.updateBoard(id, updateBoardDto, userData, files);
}

컨트롤러 단에서 폼 데이터로 받도록 UseInterceptors(multer 인터셉터)를 추가해줌.
파일은 3개까지 받아 별도 파라미터로 처리해서 서비스단에 넘겨줌.

async updateBoard(
  id: number,
  updateBoardDto: UpdateBoardDto,
  userData: UserDataDto,
  files: Express.Multer.File[],
) {
  const board: Board = await this.findBoardById(id);

  // 게시글 작성자와 수정 요청자가 다른 경우
  if (board.user.id !== userData.userId) {
    throw new BadRequestException('You are not the author of this post');
  }

  // star에 대한 수정은 별도 API(PATCH /star/:id)로 처리하므로 400 에러 리턴
  if (updateBoardDto.star) {
    throw new BadRequestException(
      'You cannot update star with this API. use PATCH /star/:id',
    );
  }

  if (files.length > 0) {
    const images: Image[] = [];
    for (const file of files) {
      const image = await this.uploadFile(file);
      images.push(image);
    }
    // 기존 이미지 삭제
    for (const image of board.images) {
      // 이미지 리포지토리에서 삭제
      await this.imageRepository.delete({ id: image.id });
      // AWS S3에서 삭제
      await this.deleteFile(image.filename);
    }
    // 새로운 이미지로 교체
    board.images = images;
  }

  // updateBoardDto.content가 존재하면 AES 암호화하여 저장
  if (updateBoardDto.content) {
    updateBoardDto.content = encryptAes(updateBoardDto.content);
  }

  const updatedBoard: Board = await this.boardRepository.save({
    ...board,
    ...updateBoardDto,
  });
  return updatedBoard;
}

추가된 로직은 다음과 같다.

  1. star에 대한 수정 요청이 있을 경우 400 에러 리턴
  2. file에 대한 수정 요청이 있을 경우 다음을 수행
  3. 요청받은 파일 업로드 후 Image 객체 DB에 저장
  4. 이미지 리포지토리에서 기존 이미지 삭제
  5. S3(NCP Object Storage)에서 기존 이미지 삭제
  6. Board 객체 갱신 시 새로운 이미지 배열로 대체
async deleteFile(filename: string): Promise<void> {
  // NCP Object Storage 삭제
  AWS.config.update(awsConfig);
  const result = await new AWS.S3()
    .deleteObject({
      Bucket: bucketName,
      Key: filename,
    })
    .promise();
  Logger.log('deleteFile result:', result);
}

이에 따라 서비스 하단에 NCP Object Storage에서 파일을 삭제하는 로직도 추가함.

동작 화면

1. POST /post 통해 별글 등록

스크린샷 2023-11-29 오후 2 17 35

별글 등록

스크린샷 2023-11-29 오후 2 23 53

GET /post/:id로 조회. 복호화해보니 content도 잘 들어감

스크린샷 2023-11-29 오후 2 25 08

image filename으로 Object Storage에도 잘 들어간 것 확인

스크린샷 2023-11-29 오후 2 25 31

image 테이블에도 정보 잘 들어간 것 확인

2. PATCH /post/:id 통해 별글 수정

스크린샷 2023-11-29 오후 2 37 27

PATCH /post/:id로 별글 수정. 선별적으로 content, file만 수정해보자.

스크린샷 2023-11-29 오후 2 38 20

이후 기존의 filename으로 조회해보면 정상적으로 이전 파일이 삭제된 것을 확인할 수 있음.

스크린샷 2023-11-29 오후 2 44 29

데이터베이스에서도 기존의 image 레코드(id 67)가 삭제되고 68번이 생성된 것을 확인할 수 있음.

스크린샷 2023-11-29 오후 2 39 14

GET /post/224로 조회해보면 content도 잘 바뀐 것을 확인할 수 있고, image url도 바뀐 것으로 잘 변경되어 있음. 성공!

DELETE /post/:id 개선

계획

  • 삭제 시에도 Update와 유사하게 연관된 다음 내용을 모두 삭제해줘야 한다.
    • star 레코드
    • image 레코드
    • Object Storage의 image 파일
    • like 조인 테이블의 해당 게시물과 연관된 모든 레코드
-- Active: 1693885143266@@192.168.64.2@3306@b1g1
CREATE TABLE `board_likes_user` (
  `boardId` int NOT NULL,
  `userId` int NOT NULL,
  PRIMARY KEY (`boardId`,`userId`),
  KEY `IDX_cc61d27acb747ad30ab37c7399` (`boardId`),
  KEY `IDX_e14a2e3175cb17290e3e23488c` (`userId`),
  CONSTRAINT `FK_cc61d27acb747ad30ab37c73995` FOREIGN KEY (`boardId`) REFERENCES `board` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
  CONSTRAINT `FK_e14a2e3175cb17290e3e23488cb` FOREIGN KEY (`userId`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

위는 board와 user의 like를 위한 Join Table DDL인데, 외래키 제약조건에서 확인할 수 있듯
ON DELETE CASCADE를 이미 설정해 놓아서 게시글 삭제 시 관련 레코드 자동 삭제됨. 따라서 image, like만 고려하면 된다.

구현

board.service.ts의 deleteBoard()만 수정해주면 된다.

async deleteBoard(id: number, userData: UserDataDto): Promise<void> {
  const board: Board = await this.boardRepository.findOneBy({ id });
  if (!board) {
    throw new NotFoundException(`Not found board with id: ${id}`);
  }

  // 게시글 작성자와 삭제 요청자가 다른 경우
  if (board.user.id !== userData.userId) {
    throw new BadRequestException('You are not the author of this post');
  }

  // 연관된 이미지 삭제
  for (const image of board.images) {
    // 이미지 리포지토리에서 삭제
    await this.imageRepository.delete({ id: image.id });
    // NCP Object Storage에서 삭제
    await this.deleteFile(image.filename);
  }

  // 연관된 별 스타일 삭제
  if (board.star) {
    await this.starModel.deleteOne({ _id: board.star });
  }

  // like 조인테이블 레코드들은 자동으로 삭제됨 (외래키 제약조건 ON DELETE CASCADE)

  // 게시글 삭제
  const result = await this.boardRepository.delete({ id });
}

동작 화면

POST /post로 게시글 생성, PATCH /post/:id/like로 좋아요

like 레코드 자동 삭제 확인을 위해 좋아요를 하나 누르고 테스트해보았다.

스크린샷 2023-11-29 오후 3 15 56

255번 게시글 생성

스크린샷 2023-11-29 오후 3 16 37

좋아요

스크린샷 2023-11-29 오후 3 16 42

잘 반영되었고

스크린샷 2023-11-29 오후 3 16 54

조인 테이블에도 추가됨 (255번 게시글, 5번 유저)

DELETE /post/:id로 게시글 삭제

스크린샷 2023-11-29 오후 3 17 03

DELETE /post/225

스크린샷 2023-11-29 오후 3 17 09

당연히 게시글 삭제 됐고

스크린샷 2023-11-29 오후 3 18 15

이미지도 찾아보니 없음. 잘 삭제됨

스크린샷 2023-11-29 오후 3 17 28

Image 테이블에서도 정상적으로 레코드가 삭제됨

스크린샷 2023-11-29 오후 3 17 15

Like 조인테이블에서도 정상적으로 삭제됨

스크린샷 2023-11-29 오후 3 25 19

마지막으로 Star 컬렉션에서도 정상적으로 삭제됨. 끝!

PATCH /star/:id 구현

계획

  • star 변경은 post_id 기준으로 해달라는 요청이 있어, 여기서의 :id는 board 테이블의 id 값이다.
    • 이렇게 해석하도록 하고, 넘어오는 Object는 조건없이 document에 덮어씌우는 걸로 구현

구현

// star.controller.ts
// 게시글 id를 이용해 별 정보를 수정함
@Patch(':id')
@UseGuards(CookieAuthGuard)
@UpdateStarByPostIdSwaggerDecorator()
updateStarByPostId(
  @Param('id', ParseIntPipe) post_id: number,
  @Body() updateStarDto: UpdateStarDto,
  @GetUser() userData: UserDataDto,
): Promise<Star> {
  return this.starService.updateStarByPostId(
    post_id,
    updateStarDto,
    userData,
  );
}
// star.service.ts
async updateStarByPostId(
  post_id: number,
  updateStarDto: UpdateStarDto,
  userData: UserDataDto,
): Promise<Star> {
  const board: Board = await this.boardRepository.findOneBy({ id: post_id });
  if (!board) {
    throw new BadRequestException(`Not found board with id: ${post_id}`);
  }

  // 게시글 작성자와 수정 요청자가 다른 경우
  if (board.user.id !== userData.userId) {
    throw new BadRequestException('You are not the author of this star');
  }

  // 별 id를 조회하여 없으면 에러 리턴
  const star_id = board.star;
  if (!star_id) {
    throw new BadRequestException(
      `Not found star_id with this post_id: ${post_id}`,
    );
  }

  // 별 스타일이 존재하면 MongoDB에 저장
  const result = await this.starModel.updateOne(
    { _id: star_id },
    { ...updateStarDto },
  );
  if (!result) {
    throw new InternalServerErrorException(
      `Failed to update star with this post_id: ${post_id}, star_id: ${star_id}`,
    );
  } else if (result.matchedCount === 0) {
    throw new BadRequestException(
      `Not found star with this post_id: ${post_id}, star_id: ${star_id}`,
    );
  } else if (result.modifiedCount === 0) {
    throw new BadRequestException(`Nothing to update`);
  }

  const updatedStar = await this.starModel.findOne({
    _id: star_id,
  });

  return updatedStar;
}

에러처리를 최대한 꼼꼼하게 하도록 노력했다.

typeorm과 달리 update 후에 해당 객체가 아닌 결과 object를 리턴해주기 때문에,
다시 findOne() 함수로 변경된 객체를 가져와서 리턴한다.

동작 화면

스크린샷 2023-11-29 오후 4 16 52

POST로 별글 등록. 이제 star는 필수 입력값으로 바뀌었다.

스크린샷 2023-11-29 오후 4 31 53

PATCH /star/226으로 해당 별글의 별 스타일을 변경.
마음대로 속성을 추가할 수 있다.

스크린샷 2023-11-29 오후 4 32 37

기존에 있던 속성은 변경되고, 새로운 속성은 추가되는 등
정상적으로 업데이트가 이루어진 것을 확인할 수 있다. 성공!

profile
해커 출신 개발자

0개의 댓글