// 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;
}
추가된 로직은 다음과 같다.
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에서 파일을 삭제하는 로직도 추가함.
별글 등록
GET /post/:id로 조회. 복호화해보니 content도 잘 들어감
image filename으로 Object Storage에도 잘 들어간 것 확인
image 테이블에도 정보 잘 들어간 것 확인
PATCH /post/:id로 별글 수정. 선별적으로 content, file만 수정해보자.
이후 기존의 filename으로 조회해보면 정상적으로 이전 파일이 삭제된 것을 확인할 수 있음.
데이터베이스에서도 기존의 image 레코드(id 67)가 삭제되고 68번이 생성된 것을 확인할 수 있음.
GET /post/224로 조회해보면 content도 잘 바뀐 것을 확인할 수 있고, image url도 바뀐 것으로 잘 변경되어 있음. 성공!
-- 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 });
}
like 레코드 자동 삭제 확인을 위해 좋아요를 하나 누르고 테스트해보았다.
255번 게시글 생성
좋아요
잘 반영되었고
조인 테이블에도 추가됨 (255번 게시글, 5번 유저)
DELETE /post/225
당연히 게시글 삭제 됐고
이미지도 찾아보니 없음. 잘 삭제됨
Image 테이블에서도 정상적으로 레코드가 삭제됨
Like 조인테이블에서도 정상적으로 삭제됨
마지막으로 Star 컬렉션에서도 정상적으로 삭제됨. 끝!
// 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() 함수로 변경된 객체를 가져와서 리턴한다.
POST로 별글 등록. 이제 star는 필수 입력값으로 바뀌었다.
PATCH /star/226으로 해당 별글의 별 스타일을 변경.
마음대로 속성을 추가할 수 있다.
기존에 있던 속성은 변경되고, 새로운 속성은 추가되는 등
정상적으로 업데이트가 이루어진 것을 확인할 수 있다. 성공!