Skip to content

Commit d1a3007

Browse files
authored
Fix: 게시글 및 프로필 파일 관련 로직 보완 (#313)
1 parent dde13f6 commit d1a3007

File tree

6 files changed

+460
-119
lines changed

6 files changed

+460
-119
lines changed

commits.txt

Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
commit f76663028be113cb49f058a173729621d0744a78
2+
Author: joyewon0705 <[email protected]>
3+
Date: Thu Oct 16 01:16:42 2025 +0900
4+
5+
Fix: 게시글 및 프로필 파일 관련 로직 보완
6+
7+
diff --git a/src/main/java/com/back/domain/board/post/service/PostService.java b/src/main/java/com/back/domain/board/post/service/PostService.java
8+
index becb576..4869b37 100644
9+
--- a/src/main/java/com/back/domain/board/post/service/PostService.java
10+
+++ b/src/main/java/com/back/domain/board/post/service/PostService.java
11+
@@ -17,7 +17,7 @@ import com.back.domain.file.entity.EntityType;
12+
import com.back.domain.file.entity.FileAttachment;
13+
import com.back.domain.file.repository.AttachmentMappingRepository;
14+
import com.back.domain.file.repository.FileAttachmentRepository;
15+
-import com.back.domain.file.service.FileService;
16+
+import com.back.domain.file.service.AttachmentMappingService;
17+
import com.back.domain.user.common.entity.User;
18+
import com.back.domain.user.common.repository.UserRepository;
19+
import com.back.global.exception.CustomException;
20+
@@ -41,7 +41,7 @@ public class PostService {
21+
private final PostCategoryRepository postCategoryRepository;
22+
private final FileAttachmentRepository fileAttachmentRepository;
23+
private final AttachmentMappingRepository attachmentMappingRepository;
24+
- private final FileService fileService;
25+
+ private final AttachmentMappingService attachmentMappingService;
26+
27+
/**
28+
* 게시글 생성 서비스
29+
@@ -169,6 +169,7 @@ public class PostService {
30+
private List<FileAttachment> updatePostAttachments(Post post, List<Long> newImageIds, Long userId) {
31+
List<Long> newIds = (newImageIds != null) ? newImageIds : List.of();
32+
33+
+ // 기존 매핑 조회
34+
List<AttachmentMapping> existingMappings =
35+
attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId());
36+
List<Long> existingIds = existingMappings.stream()
37+
@@ -182,32 +183,26 @@ public class PostService {
38+
.toList();
39+
}
40+
41+
- // 기존 첨부 삭제
42+
- deletePostAttachments(post, userId);
43+
+ // 기존 중 newIds에 없는 첨부만 삭제
44+
+ attachmentMappingService.deleteRemovedAttachments(EntityType.POST, post.getId(), userId, newIds);
45+
46+
- // 새 첨부 매핑 등록
47+
- if (newIds.isEmpty()) return List.of();
48+
-
49+
- List<FileAttachment> attachments = validateAndFindAttachments(newIds);
50+
- attachments.forEach(attachment ->
51+
- attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.POST, post.getId()))
52+
- );
53+
- return attachments;
54+
- }
55+
+ // 새로 추가된 첨부만 매핑 생성
56+
+ List<Long> addedIds = newIds.stream()
57+
+ .filter(id -> !existingIds.contains(id))
58+
+ .toList();
59+
60+
- /**
61+
- * 게시글 첨부파일 삭제 (S3 + 매핑)
62+
- */
63+
- private void deletePostAttachments(Post post, Long userId) {
64+
- List<AttachmentMapping> mappings =
65+
- attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId());
66+
- for (AttachmentMapping mapping : mappings) {
67+
- FileAttachment file = mapping.getFileAttachment();
68+
- if (file != null) {
69+
- fileService.deleteFile(file.getId(), userId);
70+
- }
71+
+ if (!addedIds.isEmpty()) {
72+
+ List<FileAttachment> newAttachments = validateAndFindAttachments(addedIds);
73+
+ newAttachments.forEach(attachment ->
74+
+ attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.POST, post.getId()))
75+
+ );
76+
}
77+
- attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(EntityType.POST, post.getId());
78+
+
79+
+ // 최신 매핑 다시 조회
80+
+ return attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId())
81+
+ .stream()
82+
+ .map(AttachmentMapping::getFileAttachment)
83+
+ .toList();
84+
}
85+
86+
/**
87+
@@ -231,15 +226,7 @@ public class PostService {
88+
}
89+
90+
// 첨부 파일 삭제
91+
- List<AttachmentMapping> mappings =
92+
- attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId());
93+
- for (AttachmentMapping mapping : mappings) {
94+
- FileAttachment fileAttachment = mapping.getFileAttachment();
95+
- if (fileAttachment != null) {
96+
- fileService.deleteFile(fileAttachment.getId(), userId);
97+
- }
98+
- }
99+
- attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(EntityType.POST, post.getId());
100+
+ attachmentMappingService.deleteAttachments(EntityType.POST, post.getId(), userId);
101+
102+
// Post 삭제
103+
post.remove();
104+
diff --git a/src/main/java/com/back/domain/file/service/AttachmentMappingService.java b/src/main/java/com/back/domain/file/service/AttachmentMappingService.java
105+
index 6b38b8d..12dbb0d 100644
106+
--- a/src/main/java/com/back/domain/file/service/AttachmentMappingService.java
107+
+++ b/src/main/java/com/back/domain/file/service/AttachmentMappingService.java
108+
@@ -109,6 +109,31 @@ public class AttachmentMappingService {
109+
attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(entityType, entityId);
110+
}
111+
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+
+
137+
private void s3Delete(String fileName) {
138+
amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
139+
}
140+
diff --git a/src/main/java/com/back/domain/user/account/service/AccountService.java b/src/main/java/com/back/domain/user/account/service/AccountService.java
141+
index 268af79..d07eba8 100644
142+
--- a/src/main/java/com/back/domain/user/account/service/AccountService.java
143+
+++ b/src/main/java/com/back/domain/user/account/service/AccountService.java
144+
@@ -5,12 +5,8 @@ import com.back.domain.board.comment.repository.CommentRepository;
145+
import com.back.domain.board.common.dto.PageResponse;
146+
import com.back.domain.board.post.dto.PostListResponse;
147+
import com.back.domain.board.post.repository.PostRepository;
148+
-import com.back.domain.file.entity.AttachmentMapping;
149+
import com.back.domain.file.entity.EntityType;
150+
-import com.back.domain.file.entity.FileAttachment;
151+
-import com.back.domain.file.repository.AttachmentMappingRepository;
152+
-import com.back.domain.file.repository.FileAttachmentRepository;
153+
-import com.back.domain.file.service.FileService;
154+
+import com.back.domain.file.service.AttachmentMappingService;
155+
import com.back.domain.user.account.dto.ChangePasswordRequest;
156+
import com.back.domain.user.account.dto.UserProfileRequest;
157+
import com.back.domain.user.account.dto.UserDetailResponse;
158+
@@ -40,9 +36,7 @@ public class AccountService {
159+
private final UserProfileRepository userProfileRepository;
160+
private final CommentRepository commentRepository;
161+
private final PostRepository postRepository;
162+
- private final FileAttachmentRepository fileAttachmentRepository;
163+
- private final AttachmentMappingRepository attachmentMappingRepository;
164+
- private final FileService fileService;
165+
+ private final AttachmentMappingService attachmentMappingService;
166+
private final PasswordEncoder passwordEncoder;
167+
168+
/**
169+
@@ -76,25 +70,17 @@ public class AccountService {
170+
throw new CustomException(ErrorCode.NICKNAME_DUPLICATED);
171+
}
172+
173+
-
174+
// UserProfile 업데이트
175+
UserProfile profile = user.getUserProfile();
176+
profile.setNickname(request.nickname());
177+
profile.setBio(request.bio());
178+
profile.setBirthDate(request.birthDate());
179+
180+
- // TODO: 프로필 이미지 및 매핑 업데이트 리팩토링 필요
181+
// 프로필 이미지 변경이 있는 경우만 수행
182+
String newUrl = request.profileImageUrl();
183+
String oldUrl = profile.getProfileImageUrl();
184+
if (!Objects.equals(newUrl, oldUrl)) {
185+
- // 외부 이미지(S3 외부 URL)는 매핑 로직 제외
186+
- if (isExternalImageUrl(newUrl)) {
187+
- // 기존 매핑만 제거 (소셜 이미지로 바뀌면 내부 매핑 필요 없음)
188+
- removeExistingMapping(userId);
189+
- } else {
190+
- updateProfileImage(userId, newUrl);
191+
- }
192+
+ attachmentMappingService.replaceAttachmentByUrl(EntityType.PROFILE, profile.getId(), userId, newUrl);
193+
profile.setProfileImageUrl(newUrl);
194+
}
195+
196+
@@ -102,61 +88,6 @@ public class AccountService {
197+
return UserDetailResponse.from(user);
198+
}
199+
200+
- /**
201+
- * 내부 저장소(S3) 이미지 교체 로직
202+
- * - 기존 매핑 및 파일 삭제 후 새 매핑 생성
203+
- */
204+
- private void updateProfileImage(Long userId, String newImageUrl) {
205+
-
206+
- // 기존 매핑 제거
207+
- removeExistingMapping(userId);
208+
-
209+
- // 새 이미지가 없는 경우
210+
- if (newImageUrl == null || newImageUrl.isBlank()) {
211+
- return;
212+
- }
213+
-
214+
- // 새 파일 조회 및 검증
215+
- FileAttachment newAttachment = fileAttachmentRepository
216+
- .findByPublicURL(newImageUrl)
217+
- .orElseThrow(() -> new CustomException(ErrorCode.FILE_NOT_FOUND));
218+
-
219+
- if (!newAttachment.getUser().getId().equals(userId)) {
220+
- throw new CustomException(ErrorCode.FILE_ACCESS_DENIED);
221+
- }
222+
-
223+
- // 새 매핑 생성 및 저장
224+
- AttachmentMapping newMapping = new AttachmentMapping(newAttachment, EntityType.PROFILE, userId);
225+
- attachmentMappingRepository.save(newMapping);
226+
- }
227+
-
228+
- /**
229+
- * 기존 프로필 이미지 매핑 및 파일 삭제
230+
- */
231+
- private void removeExistingMapping(Long userId) {
232+
- attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.PROFILE, userId)
233+
- .ifPresent(mapping -> {
234+
- FileAttachment oldAttachment = mapping.getFileAttachment();
235+
- if (oldAttachment != null) {
236+
- fileService.deleteFile(oldAttachment.getId(), userId);
237+
- }
238+
- attachmentMappingRepository.delete(mapping);
239+
- });
240+
- }
241+
-
242+
- /**
243+
- * 외부 이미지 URL 판별
244+
- * - 우리 S3 또는 CDN이 아니면 true
245+
- * - 필요 시 application.yml에서 환경변수로 관리
246+
- */
247+
- private boolean isExternalImageUrl(String url) {
248+
- if (url == null || url.isBlank()) return true;
249+
-
250+
- // TODO: 하드 코딩 제거
251+
- return !(url.startsWith("https://team5-s3-1.s3.ap-northeast-2.amazonaws.com")
252+
- || url.contains("cdn.example.com"));
253+
- }
254+
-
255+
/**
256+
* 비밀번호 변경 서비스
257+
* 1. 사용자 조회 및 상태 검증
258+
@@ -196,9 +127,6 @@ public class AccountService {
259+
// 사용자 조회 및 상태 검증
260+
User user = getValidUser(userId);
261+
262+
- // 프로필 이미지 및 매핑 삭제
263+
- removeExistingMapping(userId);
264+
-
265+
// 상태 변경 (soft delete)
266+
user.setUserStatus(UserStatus.DELETED);
267+
268+
@@ -211,6 +139,9 @@ public class AccountService {
269+
// 개인정보 마스킹
270+
UserProfile profile = user.getUserProfile();
271+
if (profile != null) {
272+
+ // 프로필 이미지 및 매핑 삭제
273+
+ attachmentMappingService.deleteAttachments(EntityType.PROFILE, profile.getId(), userId);
274+
+
275+
profile.setNickname("탈퇴한 회원");
276+
profile.setProfileImageUrl(null);
277+
profile.setBio(null);
278+
diff --git a/src/test/java/com/back/domain/board/post/service/PostServiceTest.java b/src/test/java/com/back/domain/board/post/service/PostServiceTest.java
279+
index 5b16103..ab9b3aa 100644
280+
--- a/src/test/java/com/back/domain/board/post/service/PostServiceTest.java
281+
+++ b/src/test/java/com/back/domain/board/post/service/PostServiceTest.java
282+
@@ -1,5 +1,6 @@
283+
package com.back.domain.board.post.service;
284+
285+
+import com.amazonaws.services.s3.AmazonS3;
286+
import com.back.domain.board.common.dto.PageResponse;
287+
import com.back.domain.board.post.entity.Post;
288+
import com.back.domain.board.post.entity.PostCategory;
289+
@@ -15,7 +16,7 @@ import com.back.domain.file.entity.EntityType;
290+
import com.back.domain.file.entity.FileAttachment;
291+
import com.back.domain.file.repository.AttachmentMappingRepository;
292+
import com.back.domain.file.repository.FileAttachmentRepository;
293+
-import com.back.domain.file.service.FileService;
294+
+import com.back.domain.file.service.AttachmentMappingService;
295+
import com.back.domain.user.common.entity.User;
296+
import com.back.domain.user.common.entity.UserProfile;
297+
import com.back.domain.user.common.enums.UserStatus;
298+
@@ -26,6 +27,7 @@ import org.junit.jupiter.api.DisplayName;
299+
import org.junit.jupiter.api.Test;
300+
import org.springframework.beans.factory.annotation.Autowired;
301+
import org.springframework.boot.test.context.SpringBootTest;
302+
+import org.springframework.boot.test.mock.mockito.MockBean;
303+
import org.springframework.data.domain.PageRequest;
304+
import org.springframework.data.domain.Pageable;
305+
import org.springframework.data.domain.Sort;
306+
@@ -61,8 +63,11 @@ class PostServiceTest {
307+
@Autowired
308+
private AttachmentMappingRepository attachmentMappingRepository;
309+
310+
- @MockitoBean
311+
- private FileService fileService;
312+
+ @Autowired
313+
+ private AttachmentMappingService attachmentMappingService;
314+
+
315+
+ @MockBean
316+
+ private AmazonS3 amazonS3; // S3 호출 차단용 mock
317+
318+
// ====================== 게시글 생성 테스트 ======================
319+
320+
diff --git a/src/test/java/com/back/domain/user/account/service/AccountServiceTest.java b/src/test/java/com/back/domain/user/account/service/AccountServiceTest.java
321+
index ab94f70..e0ec982 100644
322+
--- a/src/test/java/com/back/domain/user/account/service/AccountServiceTest.java
323+
+++ b/src/test/java/com/back/domain/user/account/service/AccountServiceTest.java
324+
@@ -1,5 +1,6 @@
325+
package com.back.domain.user.account.service;
326+
327+
+import com.amazonaws.services.s3.AmazonS3;
328+
import com.back.domain.board.comment.dto.MyCommentResponse;
329+
import com.back.domain.board.comment.entity.Comment;
330+
import com.back.domain.board.comment.repository.CommentRepository;
331+
@@ -14,7 +15,7 @@ import com.back.domain.file.entity.EntityType;
332+
import com.back.domain.file.entity.FileAttachment;
333+
import com.back.domain.file.repository.AttachmentMappingRepository;
334+
import com.back.domain.file.repository.FileAttachmentRepository;
335+
-import com.back.domain.file.service.FileService;
336+
+import com.back.domain.file.service.AttachmentMappingService;
337+
import com.back.domain.user.account.dto.ChangePasswordRequest;
338+
import com.back.domain.user.account.dto.UserProfileRequest;
339+
import com.back.domain.user.account.dto.UserDetailResponse;
340+
@@ -28,6 +29,7 @@ import org.junit.jupiter.api.DisplayName;
341+
import org.junit.jupiter.api.Test;
342+
import org.springframework.beans.factory.annotation.Autowired;
343+
import org.springframework.boot.test.context.SpringBootTest;
344+
+import org.springframework.boot.test.mock.mockito.MockBean;
345+
import org.springframework.data.domain.PageRequest;
346+
import org.springframework.data.domain.Pageable;
347+
import org.springframework.data.domain.Sort;
348+
@@ -73,8 +75,11 @@ class AccountServiceTest {
349+
@Autowired
350+
private PasswordEncoder passwordEncoder;
351+
352+
- @MockitoBean
353+
- private FileService fileService;
354+
+ @Autowired
355+
+ private AttachmentMappingService attachmentMappingService;
356+
+
357+
+ @MockBean
358+
+ private AmazonS3 amazonS3; // S3 호출 차단용 mock
359+
360+
private MultipartFile mockMultipartFile(String filename) {
361+
return new MockMultipartFile(filename, filename, "image/png", new byte[]{1, 2, 3});
362+
@@ -169,7 +174,7 @@ class AccountServiceTest {
363+
assertThat(response.profile().nickname()).isEqualTo("새닉네임");
364+
365+
// 새 매핑이 존재하고 기존 매핑은 삭제되었는지 검증
366+
- List<AttachmentMapping> mappings = attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.PROFILE, user.getId());
367+
+ List<AttachmentMapping> mappings = attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.PROFILE, user.getUserProfile().getId());
368+
assertThat(mappings).hasSize(1);
369+
assertThat(mappings.get(0).getFileAttachment().getPublicURL()).isEqualTo(newAttachment.getPublicURL());
370+
371+
@@ -364,7 +369,7 @@ class AccountServiceTest {
372+
// 프로필 이미지 매핑 설정
373+
FileAttachment attachment = new FileAttachment("profile_uuid_img.png", mockMultipartFile("profile.png"), user, "https://cdn.example.com/profile.png");
374+
fileAttachmentRepository.save(attachment);
375+
- attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.PROFILE, user.getId()));
376+
+ attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.PROFILE, user.getUserProfile().getId()));
377+
378+
// when: 탈퇴 처리
379+
accountService.deleteUser(user.getId());
380+
@@ -385,7 +390,7 @@ class AccountServiceTest {
381+
assertThat(profile.getBirthDate()).isNull();
382+
383+
// 프로필 이미지 및 매핑 삭제 검증
384+
- assertThat(attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.PROFILE, user.getId())).isEmpty();
385+
+ assertThat(attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.PROFILE, user.getUserProfile().getId())).isEmpty();
386+
assertThat(fileAttachmentRepository.findByPublicURL("https://cdn.example.com/profile.png")).isEmpty();
387+
}
388+

0 commit comments

Comments
 (0)