Skip to content

Commit f766630

Browse files
committed
Fix: 게시글 및 프로필 파일 관련 로직 보완
1 parent dde13f6 commit f766630

File tree

5 files changed

+71
-118
lines changed

5 files changed

+71
-118
lines changed

src/main/java/com/back/domain/board/post/service/PostService.java

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import com.back.domain.file.entity.FileAttachment;
1818
import com.back.domain.file.repository.AttachmentMappingRepository;
1919
import com.back.domain.file.repository.FileAttachmentRepository;
20-
import com.back.domain.file.service.FileService;
20+
import com.back.domain.file.service.AttachmentMappingService;
2121
import com.back.domain.user.common.entity.User;
2222
import com.back.domain.user.common.repository.UserRepository;
2323
import com.back.global.exception.CustomException;
@@ -41,7 +41,7 @@ public class PostService {
4141
private final PostCategoryRepository postCategoryRepository;
4242
private final FileAttachmentRepository fileAttachmentRepository;
4343
private final AttachmentMappingRepository attachmentMappingRepository;
44-
private final FileService fileService;
44+
private final AttachmentMappingService attachmentMappingService;
4545

4646
/**
4747
* 게시글 생성 서비스
@@ -169,6 +169,7 @@ public PostResponse updatePost(Long postId, PostRequest request, Long userId) {
169169
private List<FileAttachment> updatePostAttachments(Post post, List<Long> newImageIds, Long userId) {
170170
List<Long> newIds = (newImageIds != null) ? newImageIds : List.of();
171171

172+
// 기존 매핑 조회
172173
List<AttachmentMapping> existingMappings =
173174
attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId());
174175
List<Long> existingIds = existingMappings.stream()
@@ -182,32 +183,26 @@ private List<FileAttachment> updatePostAttachments(Post post, List<Long> newImag
182183
.toList();
183184
}
184185

185-
// 기존 첨부 삭제
186-
deletePostAttachments(post, userId);
186+
// 기존 중 newIds에 없는 첨부만 삭제
187+
attachmentMappingService.deleteRemovedAttachments(EntityType.POST, post.getId(), userId, newIds);
187188

188-
// 새 첨부 매핑 등록
189-
if (newIds.isEmpty()) return List.of();
190-
191-
List<FileAttachment> attachments = validateAndFindAttachments(newIds);
192-
attachments.forEach(attachment ->
193-
attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.POST, post.getId()))
194-
);
195-
return attachments;
196-
}
189+
// 새로 추가된 첨부만 매핑 생성
190+
List<Long> addedIds = newIds.stream()
191+
.filter(id -> !existingIds.contains(id))
192+
.toList();
197193

198-
/**
199-
* 게시글 첨부파일 삭제 (S3 + 매핑)
200-
*/
201-
private void deletePostAttachments(Post post, Long userId) {
202-
List<AttachmentMapping> mappings =
203-
attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId());
204-
for (AttachmentMapping mapping : mappings) {
205-
FileAttachment file = mapping.getFileAttachment();
206-
if (file != null) {
207-
fileService.deleteFile(file.getId(), userId);
208-
}
194+
if (!addedIds.isEmpty()) {
195+
List<FileAttachment> newAttachments = validateAndFindAttachments(addedIds);
196+
newAttachments.forEach(attachment ->
197+
attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.POST, post.getId()))
198+
);
209199
}
210-
attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(EntityType.POST, post.getId());
200+
201+
// 최신 매핑 다시 조회
202+
return attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId())
203+
.stream()
204+
.map(AttachmentMapping::getFileAttachment)
205+
.toList();
211206
}
212207

213208
/**
@@ -231,15 +226,7 @@ public void deletePost(Long postId, Long userId) {
231226
}
232227

233228
// 첨부 파일 삭제
234-
List<AttachmentMapping> mappings =
235-
attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId());
236-
for (AttachmentMapping mapping : mappings) {
237-
FileAttachment fileAttachment = mapping.getFileAttachment();
238-
if (fileAttachment != null) {
239-
fileService.deleteFile(fileAttachment.getId(), userId);
240-
}
241-
}
242-
attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(EntityType.POST, post.getId());
229+
attachmentMappingService.deleteAttachments(EntityType.POST, post.getId(), userId);
243230

244231
// Post 삭제
245232
post.remove();

src/main/java/com/back/domain/file/service/AttachmentMappingService.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,31 @@ public void deleteAttachments(EntityType entityType, Long entityId, Long userId)
109109
attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(entityType, entityId);
110110
}
111111

112+
/**
113+
* 기존 매핑 중 새 요청(newIds)에 없는 첨부만 삭제
114+
* - S3 객체 삭제
115+
* - 매핑 테이블 + 파일 정보 삭제
116+
*/
117+
@Transactional
118+
public void deleteRemovedAttachments(EntityType entityType, Long entityId, Long userId, List<Long> newIds) {
119+
List<AttachmentMapping> mappings =
120+
attachmentMappingRepository.findAllByEntityTypeAndEntityId(entityType, entityId);
121+
122+
for (AttachmentMapping mapping : mappings) {
123+
FileAttachment attachment = mapping.getFileAttachment();
124+
125+
if (attachment == null) continue;
126+
127+
Long attachmentId = attachment.getId();
128+
129+
// 새 요청에 포함되지 않은 첨부만 삭제
130+
if (!newIds.contains(attachmentId)) {
131+
s3Delete(attachment.getStoredName());
132+
attachmentMappingRepository.delete(mapping);
133+
}
134+
}
135+
}
136+
112137
private void s3Delete(String fileName) {
113138
amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
114139
}

src/main/java/com/back/domain/user/account/service/AccountService.java

Lines changed: 6 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,8 @@
55
import com.back.domain.board.common.dto.PageResponse;
66
import com.back.domain.board.post.dto.PostListResponse;
77
import com.back.domain.board.post.repository.PostRepository;
8-
import com.back.domain.file.entity.AttachmentMapping;
98
import com.back.domain.file.entity.EntityType;
10-
import com.back.domain.file.entity.FileAttachment;
11-
import com.back.domain.file.repository.AttachmentMappingRepository;
12-
import com.back.domain.file.repository.FileAttachmentRepository;
13-
import com.back.domain.file.service.FileService;
9+
import com.back.domain.file.service.AttachmentMappingService;
1410
import com.back.domain.user.account.dto.ChangePasswordRequest;
1511
import com.back.domain.user.account.dto.UserProfileRequest;
1612
import com.back.domain.user.account.dto.UserDetailResponse;
@@ -40,9 +36,7 @@ public class AccountService {
4036
private final UserProfileRepository userProfileRepository;
4137
private final CommentRepository commentRepository;
4238
private final PostRepository postRepository;
43-
private final FileAttachmentRepository fileAttachmentRepository;
44-
private final AttachmentMappingRepository attachmentMappingRepository;
45-
private final FileService fileService;
39+
private final AttachmentMappingService attachmentMappingService;
4640
private final PasswordEncoder passwordEncoder;
4741

4842
/**
@@ -76,87 +70,24 @@ public UserDetailResponse updateUserProfile(Long userId, UserProfileRequest requ
7670
throw new CustomException(ErrorCode.NICKNAME_DUPLICATED);
7771
}
7872

79-
8073
// UserProfile 업데이트
8174
UserProfile profile = user.getUserProfile();
8275
profile.setNickname(request.nickname());
8376
profile.setBio(request.bio());
8477
profile.setBirthDate(request.birthDate());
8578

86-
// TODO: 프로필 이미지 및 매핑 업데이트 리팩토링 필요
8779
// 프로필 이미지 변경이 있는 경우만 수행
8880
String newUrl = request.profileImageUrl();
8981
String oldUrl = profile.getProfileImageUrl();
9082
if (!Objects.equals(newUrl, oldUrl)) {
91-
// 외부 이미지(S3 외부 URL)는 매핑 로직 제외
92-
if (isExternalImageUrl(newUrl)) {
93-
// 기존 매핑만 제거 (소셜 이미지로 바뀌면 내부 매핑 필요 없음)
94-
removeExistingMapping(userId);
95-
} else {
96-
updateProfileImage(userId, newUrl);
97-
}
83+
attachmentMappingService.replaceAttachmentByUrl(EntityType.PROFILE, profile.getId(), userId, newUrl);
9884
profile.setProfileImageUrl(newUrl);
9985
}
10086

10187
// UserDetailResponse로 변환하여 반환
10288
return UserDetailResponse.from(user);
10389
}
10490

105-
/**
106-
* 내부 저장소(S3) 이미지 교체 로직
107-
* - 기존 매핑 및 파일 삭제 후 새 매핑 생성
108-
*/
109-
private void updateProfileImage(Long userId, String newImageUrl) {
110-
111-
// 기존 매핑 제거
112-
removeExistingMapping(userId);
113-
114-
// 새 이미지가 없는 경우
115-
if (newImageUrl == null || newImageUrl.isBlank()) {
116-
return;
117-
}
118-
119-
// 새 파일 조회 및 검증
120-
FileAttachment newAttachment = fileAttachmentRepository
121-
.findByPublicURL(newImageUrl)
122-
.orElseThrow(() -> new CustomException(ErrorCode.FILE_NOT_FOUND));
123-
124-
if (!newAttachment.getUser().getId().equals(userId)) {
125-
throw new CustomException(ErrorCode.FILE_ACCESS_DENIED);
126-
}
127-
128-
// 새 매핑 생성 및 저장
129-
AttachmentMapping newMapping = new AttachmentMapping(newAttachment, EntityType.PROFILE, userId);
130-
attachmentMappingRepository.save(newMapping);
131-
}
132-
133-
/**
134-
* 기존 프로필 이미지 매핑 및 파일 삭제
135-
*/
136-
private void removeExistingMapping(Long userId) {
137-
attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.PROFILE, userId)
138-
.ifPresent(mapping -> {
139-
FileAttachment oldAttachment = mapping.getFileAttachment();
140-
if (oldAttachment != null) {
141-
fileService.deleteFile(oldAttachment.getId(), userId);
142-
}
143-
attachmentMappingRepository.delete(mapping);
144-
});
145-
}
146-
147-
/**
148-
* 외부 이미지 URL 판별
149-
* - 우리 S3 또는 CDN이 아니면 true
150-
* - 필요 시 application.yml에서 환경변수로 관리
151-
*/
152-
private boolean isExternalImageUrl(String url) {
153-
if (url == null || url.isBlank()) return true;
154-
155-
// TODO: 하드 코딩 제거
156-
return !(url.startsWith("https://team5-s3-1.s3.ap-northeast-2.amazonaws.com")
157-
|| url.contains("cdn.example.com"));
158-
}
159-
16091
/**
16192
* 비밀번호 변경 서비스
16293
* 1. 사용자 조회 및 상태 검증
@@ -196,9 +127,6 @@ public void deleteUser(Long userId) {
196127
// 사용자 조회 및 상태 검증
197128
User user = getValidUser(userId);
198129

199-
// 프로필 이미지 및 매핑 삭제
200-
removeExistingMapping(userId);
201-
202130
// 상태 변경 (soft delete)
203131
user.setUserStatus(UserStatus.DELETED);
204132

@@ -211,6 +139,9 @@ public void deleteUser(Long userId) {
211139
// 개인정보 마스킹
212140
UserProfile profile = user.getUserProfile();
213141
if (profile != null) {
142+
// 프로필 이미지 및 매핑 삭제
143+
attachmentMappingService.deleteAttachments(EntityType.PROFILE, profile.getId(), userId);
144+
214145
profile.setNickname("탈퇴한 회원");
215146
profile.setProfileImageUrl(null);
216147
profile.setBio(null);

src/test/java/com/back/domain/board/post/service/PostServiceTest.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.back.domain.board.post.service;
22

3+
import com.amazonaws.services.s3.AmazonS3;
34
import com.back.domain.board.common.dto.PageResponse;
45
import com.back.domain.board.post.entity.Post;
56
import com.back.domain.board.post.entity.PostCategory;
@@ -15,7 +16,7 @@
1516
import com.back.domain.file.entity.FileAttachment;
1617
import com.back.domain.file.repository.AttachmentMappingRepository;
1718
import com.back.domain.file.repository.FileAttachmentRepository;
18-
import com.back.domain.file.service.FileService;
19+
import com.back.domain.file.service.AttachmentMappingService;
1920
import com.back.domain.user.common.entity.User;
2021
import com.back.domain.user.common.entity.UserProfile;
2122
import com.back.domain.user.common.enums.UserStatus;
@@ -26,6 +27,7 @@
2627
import org.junit.jupiter.api.Test;
2728
import org.springframework.beans.factory.annotation.Autowired;
2829
import org.springframework.boot.test.context.SpringBootTest;
30+
import org.springframework.boot.test.mock.mockito.MockBean;
2931
import org.springframework.data.domain.PageRequest;
3032
import org.springframework.data.domain.Pageable;
3133
import org.springframework.data.domain.Sort;
@@ -61,8 +63,11 @@ class PostServiceTest {
6163
@Autowired
6264
private AttachmentMappingRepository attachmentMappingRepository;
6365

64-
@MockitoBean
65-
private FileService fileService;
66+
@Autowired
67+
private AttachmentMappingService attachmentMappingService;
68+
69+
@MockBean
70+
private AmazonS3 amazonS3; // S3 호출 차단용 mock
6671

6772
// ====================== 게시글 생성 테스트 ======================
6873

src/test/java/com/back/domain/user/account/service/AccountServiceTest.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.back.domain.user.account.service;
22

3+
import com.amazonaws.services.s3.AmazonS3;
34
import com.back.domain.board.comment.dto.MyCommentResponse;
45
import com.back.domain.board.comment.entity.Comment;
56
import com.back.domain.board.comment.repository.CommentRepository;
@@ -14,7 +15,7 @@
1415
import com.back.domain.file.entity.FileAttachment;
1516
import com.back.domain.file.repository.AttachmentMappingRepository;
1617
import com.back.domain.file.repository.FileAttachmentRepository;
17-
import com.back.domain.file.service.FileService;
18+
import com.back.domain.file.service.AttachmentMappingService;
1819
import com.back.domain.user.account.dto.ChangePasswordRequest;
1920
import com.back.domain.user.account.dto.UserProfileRequest;
2021
import com.back.domain.user.account.dto.UserDetailResponse;
@@ -28,6 +29,7 @@
2829
import org.junit.jupiter.api.Test;
2930
import org.springframework.beans.factory.annotation.Autowired;
3031
import org.springframework.boot.test.context.SpringBootTest;
32+
import org.springframework.boot.test.mock.mockito.MockBean;
3133
import org.springframework.data.domain.PageRequest;
3234
import org.springframework.data.domain.Pageable;
3335
import org.springframework.data.domain.Sort;
@@ -73,8 +75,11 @@ class AccountServiceTest {
7375
@Autowired
7476
private PasswordEncoder passwordEncoder;
7577

76-
@MockitoBean
77-
private FileService fileService;
78+
@Autowired
79+
private AttachmentMappingService attachmentMappingService;
80+
81+
@MockBean
82+
private AmazonS3 amazonS3; // S3 호출 차단용 mock
7883

7984
private MultipartFile mockMultipartFile(String filename) {
8085
return new MockMultipartFile(filename, filename, "image/png", new byte[]{1, 2, 3});
@@ -169,7 +174,7 @@ void updateUserProfile_success() {
169174
assertThat(response.profile().nickname()).isEqualTo("새닉네임");
170175

171176
// 새 매핑이 존재하고 기존 매핑은 삭제되었는지 검증
172-
List<AttachmentMapping> mappings = attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.PROFILE, user.getId());
177+
List<AttachmentMapping> mappings = attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.PROFILE, user.getUserProfile().getId());
173178
assertThat(mappings).hasSize(1);
174179
assertThat(mappings.get(0).getFileAttachment().getPublicURL()).isEqualTo(newAttachment.getPublicURL());
175180

@@ -364,7 +369,7 @@ void deleteUser_success() {
364369
// 프로필 이미지 매핑 설정
365370
FileAttachment attachment = new FileAttachment("profile_uuid_img.png", mockMultipartFile("profile.png"), user, "https://cdn.example.com/profile.png");
366371
fileAttachmentRepository.save(attachment);
367-
attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.PROFILE, user.getId()));
372+
attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.PROFILE, user.getUserProfile().getId()));
368373

369374
// when: 탈퇴 처리
370375
accountService.deleteUser(user.getId());
@@ -385,7 +390,7 @@ void deleteUser_success() {
385390
assertThat(profile.getBirthDate()).isNull();
386391

387392
// 프로필 이미지 및 매핑 삭제 검증
388-
assertThat(attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.PROFILE, user.getId())).isEmpty();
393+
assertThat(attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.PROFILE, user.getUserProfile().getId())).isEmpty();
389394
assertThat(fileAttachmentRepository.findByPublicURL("https://cdn.example.com/profile.png")).isEmpty();
390395
}
391396

0 commit comments

Comments
 (0)