11package kr .co .pinup .postImages .service ;
22
3- import jakarta .transaction .Transactional ;
3+ import kr .co .pinup .custom .s3 .exception .ImageDeleteFailedException ;
4+ import kr .co .pinup .postImages .model .dto .PostImageUploadRequest ;
5+ import org .springframework .transaction .annotation .Propagation ;
6+ import org .springframework .transaction .annotation .Transactional ;
7+ import org .springframework .transaction .support .TransactionSynchronization ;
8+ import org .springframework .transaction .support .TransactionSynchronizationManager ;
9+
410import kr .co .pinup .custom .logging .AppLogger ;
511import kr .co .pinup .custom .logging .model .dto .ErrorLog ;
612import kr .co .pinup .custom .logging .model .dto .InfoLog ;
713import kr .co .pinup .custom .logging .model .dto .WarnLog ;
814import kr .co .pinup .custom .s3 .S3Service ;
9- import kr . co . pinup . custom . s3 . exception . ImageDeleteFailedException ;
15+
1016import kr .co .pinup .postImages .PostImage ;
1117import kr .co .pinup .postImages .exception .postimage .PostImageDeleteFailedException ;
1218import kr .co .pinup .postImages .exception .postimage .PostImageNotFoundException ;
1319import kr .co .pinup .postImages .exception .postimage .PostImageSaveFailedException ;
20+ import kr .co .pinup .postImages .model .dto .CreatePostImageRequest ;
1421import kr .co .pinup .postImages .model .dto .PostImageResponse ;
15- import kr . co . pinup . postImages . model . dto . PostImageUploadRequest ;
22+
1623import kr .co .pinup .postImages .model .dto .UpdatePostImageRequest ;
1724import kr .co .pinup .postImages .repository .PostImageRepository ;
1825import kr .co .pinup .posts .Post ;
@@ -35,102 +42,138 @@ public class PostImageService {
3542
3643 private static final String PATH_PREFIX = "post" ;
3744
38- @ Transactional
39- public List <PostImage > savePostImages (PostImageUploadRequest postImageUploadRequest , Post post ) {
40- if (postImageUploadRequest .getImages () == null || postImageUploadRequest .getImages ().isEmpty ()) {
45+ public List <String > uploadImagesOnly (CreatePostImageRequest req ) {
46+ if (req .getImages () == null || req .getImages ().isEmpty ()) {
4147 throw new PostImageNotFoundException ("업로드할 이미지가 없습니다." );
4248 }
4349
44- List <String > imageUrls = uploadFiles (postImageUploadRequest .getImages (),PATH_PREFIX );
50+ List <String > imageUrls = uploadFiles (req .getImages (), PATH_PREFIX );
51+
52+ appLogger .info (new InfoLog ("이미지 업로드 완료" )
53+ .setStatus ("201" )
54+ .addDetails ("count" , String .valueOf (imageUrls .size ())));
55+
56+ return imageUrls ;
57+ }
58+
59+ @ Transactional
60+ public List <PostImage > saveImageUrls (Post post , List <String > imageUrls ) {
61+ if (imageUrls == null || imageUrls .isEmpty ()) {
62+ throw new PostImageNotFoundException ("저장할 이미지 URL이 없습니다." );
63+ }
4564
4665 List <PostImage > postImages = imageUrls .stream ()
4766 .map (s3Url -> new PostImage (post , s3Url ))
4867 .collect (Collectors .toList ());
4968
5069 try {
5170 postImageRepository .saveAll (postImages );
52- appLogger .info (new InfoLog ("이미지 저장 완료" )
71+ appLogger .info (new InfoLog ("이미지 DB 저장 완료" )
5372 .setStatus ("201" )
5473 .setTargetId (post .getId ().toString ())
5574 .addDetails ("count" , String .valueOf (postImages .size ())));
75+ return postImages ;
76+
5677 } catch (Exception e ) {
57- appLogger .error (new ErrorLog ("이미지 저장 실패" , e )
78+ appLogger .error (new ErrorLog ("이미지 DB 저장 실패" , e )
5879 .setStatus ("500" )
5980 .setTargetId (post .getId ().toString ())
6081 .addDetails ("reason" , e .getMessage ()));
6182 throw new PostImageSaveFailedException ("이미지 저장 중 문제가 발생했습니다." , e );
6283 }
84+ }
6385
64- return postImages ;
86+ public void cleanupUploadedOnRollback (List <String > uploadedUrls ) {
87+ if (uploadedUrls == null || uploadedUrls .isEmpty ()) return ;
88+ if (!TransactionSynchronizationManager .isSynchronizationActive ()) {
89+ appLogger .warn (new WarnLog ("롤백 보상 등록 불가(트랜잭션 바깥)" )
90+ .addDetails ("count" , String .valueOf (uploadedUrls .size ())));
91+ return ;
92+ }
93+ TransactionSynchronizationManager .registerSynchronization (new TransactionSynchronization () {
94+ @ Override public void afterCompletion (int status ) {
95+ if (status == STATUS_ROLLED_BACK ) deleteS3ByUrlsQuietly (uploadedUrls );
96+ }
97+ });
98+ }
99+
100+ public void deleteS3ByUrlsQuietly (List <String > imageUrls ) {
101+ if (imageUrls == null || imageUrls .isEmpty ()) return ;
102+ for (String url : imageUrls ) {
103+ String key = PATH_PREFIX + "/" + s3Service .extractFileName (url );
104+ try { s3Service .deleteFromS3 (key ); }
105+ catch (Exception ex ) {
106+ appLogger .warn (new WarnLog ("보상 삭제 실패" )
107+ .addDetails ("file" , key ).addDetails ("reason" , ex .getMessage ()));
108+ }
109+ }
65110 }
66111
67112 @ Transactional
68113 public void deleteAllByPost (Long postId ) {
69114 List <PostImage > postImages = postImageRepository .findByPostId (postId );
70- if (postImages .isEmpty ()) {
71- return ;
72- }
73- try {
74- postImages .forEach (postImage -> {
75- String fileUrl = postImage .getS3Url ();
76- String fileName = PATH_PREFIX + "/" + s3Service .extractFileName (fileUrl );
115+ if (postImages .isEmpty ()) return ;
77116
78- s3Service . deleteFromS3 ( fileName );
79- });
117+ List < String > urls = postImages . stream (). map ( PostImage :: getS3Url ). collect ( Collectors . toList () );
118+ try {
80119 postImageRepository .deleteAllByPostId (postId );
81- appLogger .info (new InfoLog ("전체 이미지 삭제 완료" ).setTargetId (postId .toString ()));
120+ appLogger .info (new InfoLog ("전체 이미지 DB 삭제 완료" ).setTargetId (postId .toString ()));
82121 } catch (Exception e ) {
83- appLogger .error (new ErrorLog ("전체 이미지 삭제 실패" , e ).setStatus ("500" ).setTargetId (postId .toString ()).addDetails ("reason" , e .getMessage ()));
122+ appLogger .error (new ErrorLog ("전체 이미지 DB 삭제 실패" , e )
123+ .setStatus ("500" ).setTargetId (postId .toString ())
124+ .addDetails ("reason" , e .getMessage ()));
84125 throw new PostImageDeleteFailedException ("이미지 삭제 중 문제가 발생했습니다." , e );
85126 }
127+ deleteS3QuietlyAfterCommit (urls );
86128 }
87129
130+
131+ @ Transactional
88132 public void deleteSelectedImages (Long postId , UpdatePostImageRequest updatePostImageRequest ) {
89- List <String > imagesToDelete = updatePostImageRequest .getImagesToDelete ();
90-
91- if (imagesToDelete != null && !imagesToDelete .isEmpty ()) {
92- List <PostImage > postImages = postImageRepository .findByPostIdAndS3UrlIn (postId , imagesToDelete );
93-
94- postImages .forEach (postImage -> {
95- String fileUrl = postImage .getS3Url ();
96- String fileName = PATH_PREFIX + "/" + s3Service .extractFileName (fileUrl );
97- try {
98- s3Service .deleteFromS3 (fileName );
99- } catch (ImageDeleteFailedException e ) {
100- appLogger .error (new ErrorLog ("S3 이미지 삭제 실패" , e )
101- .setTargetId (postId .toString ())
102- .setStatus ("500" )
103- .addDetails ("file" , fileName ));
104- throw new ImageDeleteFailedException ("이미지 삭제 중 문제가 발생했습니다." , e );
105- }
106- });
133+ List <String > reqUrls = updatePostImageRequest .getImagesToDelete ();
134+ List <String > actuallyDeleted = deleteSelectedImagesDbOnly (postId , reqUrls );
135+ deleteS3QuietlyAfterCommit (actuallyDeleted );
136+ }
107137
108- try {
109- postImageRepository .deleteAll (postImages );
110- appLogger .info (new InfoLog ("선택 이미지 삭제 완료" )
111- .setTargetId (postId .toString ())
112- .addDetails ("count" , String .valueOf (postImages .size ())));
113- } catch (Exception e ) {
114- appLogger .error (new ErrorLog ("DB 이미지 삭제 실패" , e )
115- .setTargetId (postId .toString ())
116- .setStatus ("500" )
117- .addDetails ("reason" , e .getMessage ()));
118- throw new PostImageDeleteFailedException ("이미지 삭제 중 문제가 발생했습니다." , e );
119- }
120- } else {
138+ @ Transactional (propagation = Propagation .REQUIRES_NEW )
139+ public List <String > deleteSelectedImagesDbOnly (Long postId , List <String > imagesToDelete ) {
140+ if (imagesToDelete == null || imagesToDelete .isEmpty ()) {
121141 appLogger .warn (new WarnLog ("삭제 요청 이미지 없음" )
122- .setTargetId (postId .toString ())
123- .setStatus ("400" ));
142+ .setTargetId (postId .toString ()).setStatus ("400" ));
124143 throw new PostImageNotFoundException ("삭제할 이미지 URL이 없습니다." );
125144 }
126- }
127-
145+ List <PostImage > targets = postImageRepository .findByPostIdAndS3UrlIn (postId , imagesToDelete );
146+ try {
147+ if (!targets .isEmpty ()) {
148+ postImageRepository .deleteAll (targets );
149+ appLogger .info (new InfoLog ("선택 이미지 DB 삭제 완료" )
150+ .setTargetId (postId .toString ())
151+ .addDetails ("count" , String .valueOf (targets .size ())));
152+ }
128153
129- public PostImage findFirstImageByPostId (Long postId ) {
130- return postImageRepository .findTopByPostIdOrderByIdAsc (postId );
154+ return targets .stream ().map (PostImage ::getS3Url ).collect (Collectors .toList ());
155+ } catch (Exception e ) {
156+ appLogger .error (new ErrorLog ("DB 이미지 삭제 실패" , e )
157+ .setTargetId (postId .toString ()).setStatus ("500" )
158+ .addDetails ("reason" , e .getMessage ()));
159+ throw new PostImageDeleteFailedException ("이미지 삭제 중 문제가 발생했습니다." , e );
160+ }
131161 }
132162
163+ public void deleteS3QuietlyAfterCommit (List <String > imageUrls ) {
164+ if (imageUrls == null || imageUrls .isEmpty ()) return ;
165+ if (TransactionSynchronizationManager .isSynchronizationActive ()) {
166+ TransactionSynchronizationManager .registerSynchronization (new TransactionSynchronization () {
167+ @ Override public void afterCommit () {
168+ deleteS3ByUrlsQuietly (imageUrls );
169+ }
170+ });
171+ } else {
172+ deleteS3ByUrlsQuietly (imageUrls );
173+ }
174+ }
133175
176+ @ Transactional (readOnly = true )
134177 public List <PostImageResponse > findImagesByPostId (Long postId ) {
135178 log .debug ("이미지 목록 조회: postId={}" , postId );
136179 List <PostImage > postImages = postImageRepository .findByPostId (postId );
0 commit comments