Skip to content

Commit 92bb4f6

Browse files
authored
Merge pull request #135 from pinup-team/feat-posts
refactor/feat: Post·PostImage B 구조 적용(트랜잭션 분리) — create · update 동시 반영 + 테스트 픽스처 안정화
2 parents ac4f32f + e09f82d commit 92bb4f6

16 files changed

Lines changed: 742 additions & 387 deletions

src/main/java/kr/co/pinup/postImages/service/PostImageService.java

Lines changed: 101 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
package 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+
410
import kr.co.pinup.custom.logging.AppLogger;
511
import kr.co.pinup.custom.logging.model.dto.ErrorLog;
612
import kr.co.pinup.custom.logging.model.dto.InfoLog;
713
import kr.co.pinup.custom.logging.model.dto.WarnLog;
814
import kr.co.pinup.custom.s3.S3Service;
9-
import kr.co.pinup.custom.s3.exception.ImageDeleteFailedException;
15+
1016
import kr.co.pinup.postImages.PostImage;
1117
import kr.co.pinup.postImages.exception.postimage.PostImageDeleteFailedException;
1218
import kr.co.pinup.postImages.exception.postimage.PostImageNotFoundException;
1319
import kr.co.pinup.postImages.exception.postimage.PostImageSaveFailedException;
20+
import kr.co.pinup.postImages.model.dto.CreatePostImageRequest;
1421
import kr.co.pinup.postImages.model.dto.PostImageResponse;
15-
import kr.co.pinup.postImages.model.dto.PostImageUploadRequest;
22+
1623
import kr.co.pinup.postImages.model.dto.UpdatePostImageRequest;
1724
import kr.co.pinup.postImages.repository.PostImageRepository;
1825
import 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);

src/main/java/kr/co/pinup/posts/Post.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import java.util.ArrayList;
1313
import java.util.List;
14+
import java.util.Objects;
1415

1516
@Getter
1617
@Builder(toBuilder = true)
@@ -75,5 +76,19 @@ public void increaseLikeCount() {
7576
}
7677

7778
public void decreaseLikeCount() {if (this.likeCount > 0) {this.likeCount --;}}
79+
80+
public boolean applyTextIfChanged(String newTitle, String newContent) {
81+
boolean changed = false;
82+
83+
if (newTitle != null && !Objects.equals(this.title, newTitle)) {
84+
this.title = newTitle;
85+
changed = true;
86+
}
87+
if (newContent != null && !Objects.equals(this.content, newContent)) {
88+
this.content = newContent;
89+
changed = true;
90+
}
91+
return changed;
92+
}
7893
}
7994

0 commit comments

Comments
 (0)