11import path from 'path' ;
2- import fs from 'fs' ;
32import multer from 'multer' ;
3+ import { uploadToS3 , deleteFromS3 } from '../../s3.upload.js' ;
44
55// 관련 에러 클래스 import
66import {
@@ -22,30 +22,14 @@ import { UserNotFoundError } from '../../common/errors/user.errors.js';
2222// Repository import
2323import reviewRepository from '../repository/review.repository.js' ;
2424
25- // TODO: 추후 프로젝트 완성 시 AWS S3 연동으로 변경 예정
2625class ReviewService {
2726
2827 /**
29- * 파일 업로드를 위한 multer 설정
28+ * 파일 업로드를 위한 multer 설정 초기화
3029 */
3130 constructor ( ) {
32- // 업로드 디렉토리 설정 (프로젝트 루트/uploads/reviews)
33- this . uploadDir = path . join ( process . cwd ( ) , 'uploads' , 'reviews' ) ;
34- this . ensureUploadDir ( ) ;
35-
36- // multer 저장소 설정
37- this . storage = multer . diskStorage ( {
38- destination : ( req , file , cb ) => {
39- cb ( null , this . uploadDir ) ;
40- } ,
41- filename : ( req , file , cb ) => {
42- // 파일명 생성: review_현재시간_랜덤값.확장자
43- const timestamp = Date . now ( ) ;
44- const random = Math . round ( Math . random ( ) * 1E9 ) ;
45- const ext = path . extname ( file . originalname ) ;
46- cb ( null , `review_${ timestamp } _${ random } ${ ext } ` ) ;
47- }
48- } ) ;
31+ // 업로드 파일 메모리에 적재
32+ this . storage = multer . memoryStorage ( ) ;
4933
5034 // 파일 필터 (이미지만 허용)
5135 this . fileFilter = ( req , file , cb ) => {
@@ -68,17 +52,6 @@ class ReviewService {
6852 } ) ;
6953 }
7054
71- /**
72- * 업로드 디렉토리가 없으면 생성
73- * TODO: S3 연동 시 이 메서드는 불필요하므로 삭제 예정
74- */
75- ensureUploadDir ( ) {
76- if ( ! fs . existsSync ( this . uploadDir ) ) {
77- fs . mkdirSync ( this . uploadDir , { recursive : true } ) ;
78- console . log ( `업로드 디렉토리 생성: ${ this . uploadDir } ` ) ;
79- }
80- }
81-
8255 /**
8356 * 이미지 업로드 처리
8457 *
@@ -87,7 +60,6 @@ class ReviewService {
8760 *
8861 * @example
8962 * const result = await reviewService.uploadImage(req.file);
90- * // result: { image_url: "http://localhost:3000/uploads/reviews/review_123_456.jpg", file_size: 1024, file_type: "image/jpeg" }
9163 */
9264 async uploadImage ( file ) {
9365 try {
@@ -98,52 +70,54 @@ class ReviewService {
9870
9971 // 2. 파일 크기 추가 검증
10072 if ( file . size > 5 * 1024 * 1024 ) {
101- // 업로드된 파일 삭제
102- this . deleteFile ( file . path ) ;
10373 throw new FileSizeExceededError ( { fileSize : file . size } ) ;
10474 }
10575
106- // 3. 파일 URL 생성 (현재: 로컬 환경용)
107- // TODO: S3 연동 시 S3 URL 생성 로직으로 변경 필요
108- const baseUrl = process . env . BASE_URL || 'http://localhost:3000' ;
109- const imageUrl = `${ baseUrl } /uploads/reviews/${ file . filename } ` ;
76+ // 3. 파일 확장자 기준 추가 검증
77+ const ext = path . extname ( file . originalname ) . toLowerCase ( ) . replace ( '.' , '' ) ;
78+ if ( ! [ 'jpeg' , 'jpg' , 'png' ] . includes ( ext ) ) {
79+ throw new UnsupportedImageFormatError ( { fileType : file . mimetype } ) ;
80+ }
81+
82+ // 4. S3 업로드 (리뷰 이미지 전용으로 reviews/ 폴더에 저장)
83+ const imageUrl = await uploadToS3 (
84+ file . buffer ,
85+ 'reviews' ,
86+ ext
87+ ) ;
11088
111- // 4. 성공 응답 반환
11289 return {
11390 image_url : imageUrl ,
11491 file_size : file . size ,
11592 file_type : file . mimetype
11693 } ;
11794
11895 } catch ( error ) {
119- // 오류 발생 시 업로드된 파일 삭제
120- if ( file && file . path ) {
121- this . deleteFile ( file . path ) ;
122- }
12396 throw error ;
12497 }
12598 }
12699
127100 /**
128- * 파일 삭제 헬퍼 메서드
129- * TODO: S3 연동 시 S3 객체 삭제 로직으로 변경 필요
130- * @param {string } filePath - 삭제할 파일 경로
101+ * S3 객체 삭제
102+ *
103+ * @param {string } imageUrl - 삭제할 이미지의 퍼블릭 URL
104+ * @returns {Promise<void> }
131105 */
132- deleteFile ( filePath ) {
106+ async deleteS3File ( imageUrl ) {
133107 try {
134- if ( fs . existsSync ( filePath ) ) {
135- fs . unlinkSync ( filePath ) ;
136- console . log ( `파일 삭제: ${ filePath } ` ) ;
108+ if ( imageUrl && imageUrl . includes ( 's3.amazonaws.com' ) ) {
109+ await deleteFromS3 ( imageUrl ) ;
110+ console . log ( `S3 파일 삭제 완료 : ${ imageUrl } ` ) ;
137111 }
138112 } catch ( error ) {
139- console . error ( '파일 삭제 실패:' , error ) ;
113+ console . error ( 'S3 파일 삭제 실패:' , error ) ;
140114 }
141115 }
142116
143117
144118 /**
145119 * 리뷰 작성
146- *
120+ *
147121 * @param {BigInt } requestId - 커미션 신청 ID
148122 * @param {BigInt } userId - 사용자 ID
149123 * @param {ReviewCreateDto } reviewDto - 검증된 DTO 객체
@@ -265,18 +239,24 @@ class ReviewService {
265239 // 5. 이미지 업데이트 (프론트에서 보낸 최종 이미지 목록으로 교체)
266240 // 프론트 로직: 기존 이미지 로드 > 사용자가 추가/삭제 > 최종 결과만 백으로 전송
267241 // 백엔드 로직: 기존 이미지 목록 전체 삭제 > 새로 받은 이미지들로 교체
268- if ( image_urls && image_urls . length > 0 ) {
269- // 기존 이미지들 전체 삭제
270- await reviewRepository . deleteAllReviewImages ( reviewId ) ;
242+
243+ // 5-1. 기존 이미지들 조회 후 S3에서 삭제
244+ const existingImages = await reviewRepository . getImagesByTarget ( 'review' , reviewId ) ;
245+ for ( const image of existingImages ) {
246+ if ( image . imageUrl ) {
247+ await this . deleteS3File ( image . imageUrl ) ;
248+ }
249+ }
250+
251+ // 5-2. DB에서 기존 이미지 정보 삭제
252+ await reviewRepository . deleteAllReviewImages ( reviewId ) ;
271253
272- // 새로운 이미지들 추가 (최대 5개)
254+ // 5-3. 새로운 이미지들 추가 (최대 5개)
255+ if ( image_urls && image_urls . length > 0 ) {
273256 const imagesToSave = image_urls . slice ( 0 , 5 ) ;
274257 for ( const imageUrl of imagesToSave ) {
275258 await reviewRepository . createImage ( 'review' , reviewId , imageUrl ) ;
276259 }
277- } else {
278- // 이미지 배열이 비어있으면 모든 이미지 삭제 (사용자가 모든 이미지 제거)
279- await reviewRepository . deleteAllReviewImages ( reviewId ) ;
280260 }
281261
282262 // 6. Controller로 반환할 데이터 구성
@@ -311,13 +291,21 @@ class ReviewService {
311291 throw new ReviewPermissionDeniedError ( { userId, reviewId } ) ;
312292 }
313293
314- // 3. 관련 이미지들 먼저 삭제
294+ // 3. 관련 이미지들 조회 후 S3에서 삭제
295+ const reviewImages = await reviewRepository . getImagesByTarget ( 'review' , reviewId ) ;
296+ for ( const image of reviewImages ) {
297+ if ( image . imageUrl ) {
298+ await this . deleteS3File ( image . imageUrl ) ;
299+ }
300+ }
301+
302+ // 4. DB에서 이미지 정보 삭제
315303 await reviewRepository . deleteAllReviewImages ( reviewId ) ;
316304
317- // 4 . 리뷰 삭제
305+ // 5 . 리뷰 삭제
318306 await reviewRepository . deleteReview ( reviewId ) ;
319307
320- // 5 . 성공 메시지 반환
308+ // 6 . 성공 메시지 반환
321309 return {
322310 message : "리뷰가 성공적으로 삭제되었습니다."
323311 } ;
0 commit comments