From 2751d8606c5c6e0fa9a8a0e219c9ca6d46ebc5c5 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Thu, 16 Oct 2025 01:16:42 +0900 Subject: [PATCH] =?UTF-8?q?Fix:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EB=B0=8F?= =?UTF-8?q?=20=ED=94=84=EB=A1=9C=ED=95=84=20=ED=8C=8C=EC=9D=BC=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- commits.txt | 388 ++++++++++++++++++ .../board/post/service/PostService.java | 55 +-- .../service/AttachmentMappingService.java | 25 ++ .../user/account/service/AccountService.java | 81 +--- .../board/post/service/PostServiceTest.java | 11 +- .../account/service/AccountServiceTest.java | 19 +- 6 files changed, 460 insertions(+), 119 deletions(-) create mode 100644 commits.txt diff --git a/commits.txt b/commits.txt new file mode 100644 index 00000000..87374823 --- /dev/null +++ b/commits.txt @@ -0,0 +1,388 @@ +commit f76663028be113cb49f058a173729621d0744a78 +Author: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> +Date: Thu Oct 16 01:16:42 2025 +0900 + + Fix: 게시글 및 프로필 파일 관련 로직 보완 + +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 +index becb576..4869b37 100644 +--- a/src/main/java/com/back/domain/board/post/service/PostService.java ++++ b/src/main/java/com/back/domain/board/post/service/PostService.java +@@ -17,7 +17,7 @@ import com.back.domain.file.entity.EntityType; + import com.back.domain.file.entity.FileAttachment; + import com.back.domain.file.repository.AttachmentMappingRepository; + import com.back.domain.file.repository.FileAttachmentRepository; +-import com.back.domain.file.service.FileService; ++import com.back.domain.file.service.AttachmentMappingService; + import com.back.domain.user.common.entity.User; + import com.back.domain.user.common.repository.UserRepository; + import com.back.global.exception.CustomException; +@@ -41,7 +41,7 @@ public class PostService { + private final PostCategoryRepository postCategoryRepository; + private final FileAttachmentRepository fileAttachmentRepository; + private final AttachmentMappingRepository attachmentMappingRepository; +- private final FileService fileService; ++ private final AttachmentMappingService attachmentMappingService; + + /** + * 게시글 생성 서비스 +@@ -169,6 +169,7 @@ public class PostService { + private List updatePostAttachments(Post post, List newImageIds, Long userId) { + List newIds = (newImageIds != null) ? newImageIds : List.of(); + ++ // 기존 매핑 조회 + List existingMappings = + attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId()); + List existingIds = existingMappings.stream() +@@ -182,32 +183,26 @@ public class PostService { + .toList(); + } + +- // 기존 첨부 삭제 +- deletePostAttachments(post, userId); ++ // 기존 중 newIds에 없는 첨부만 삭제 ++ attachmentMappingService.deleteRemovedAttachments(EntityType.POST, post.getId(), userId, newIds); + +- // 새 첨부 매핑 등록 +- if (newIds.isEmpty()) return List.of(); +- +- List attachments = validateAndFindAttachments(newIds); +- attachments.forEach(attachment -> +- attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.POST, post.getId())) +- ); +- return attachments; +- } ++ // 새로 추가된 첨부만 매핑 생성 ++ List addedIds = newIds.stream() ++ .filter(id -> !existingIds.contains(id)) ++ .toList(); + +- /** +- * 게시글 첨부파일 삭제 (S3 + 매핑) +- */ +- private void deletePostAttachments(Post post, Long userId) { +- List mappings = +- attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId()); +- for (AttachmentMapping mapping : mappings) { +- FileAttachment file = mapping.getFileAttachment(); +- if (file != null) { +- fileService.deleteFile(file.getId(), userId); +- } ++ if (!addedIds.isEmpty()) { ++ List newAttachments = validateAndFindAttachments(addedIds); ++ newAttachments.forEach(attachment -> ++ attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.POST, post.getId())) ++ ); + } +- attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(EntityType.POST, post.getId()); ++ ++ // 최신 매핑 다시 조회 ++ return attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId()) ++ .stream() ++ .map(AttachmentMapping::getFileAttachment) ++ .toList(); + } + + /** +@@ -231,15 +226,7 @@ public class PostService { + } + + // 첨부 파일 삭제 +- List mappings = +- attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId()); +- for (AttachmentMapping mapping : mappings) { +- FileAttachment fileAttachment = mapping.getFileAttachment(); +- if (fileAttachment != null) { +- fileService.deleteFile(fileAttachment.getId(), userId); +- } +- } +- attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(EntityType.POST, post.getId()); ++ attachmentMappingService.deleteAttachments(EntityType.POST, post.getId(), userId); + + // Post 삭제 + post.remove(); +diff --git a/src/main/java/com/back/domain/file/service/AttachmentMappingService.java b/src/main/java/com/back/domain/file/service/AttachmentMappingService.java +index 6b38b8d..12dbb0d 100644 +--- a/src/main/java/com/back/domain/file/service/AttachmentMappingService.java ++++ b/src/main/java/com/back/domain/file/service/AttachmentMappingService.java +@@ -109,6 +109,31 @@ public class AttachmentMappingService { + attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(entityType, entityId); + } + ++ /** ++ * 기존 매핑 중 새 요청(newIds)에 없는 첨부만 삭제 ++ * - S3 객체 삭제 ++ * - 매핑 테이블 + 파일 정보 삭제 ++ */ ++ @Transactional ++ public void deleteRemovedAttachments(EntityType entityType, Long entityId, Long userId, List newIds) { ++ List mappings = ++ attachmentMappingRepository.findAllByEntityTypeAndEntityId(entityType, entityId); ++ ++ for (AttachmentMapping mapping : mappings) { ++ FileAttachment attachment = mapping.getFileAttachment(); ++ ++ if (attachment == null) continue; ++ ++ Long attachmentId = attachment.getId(); ++ ++ // 새 요청에 포함되지 않은 첨부만 삭제 ++ if (!newIds.contains(attachmentId)) { ++ s3Delete(attachment.getStoredName()); ++ attachmentMappingRepository.delete(mapping); ++ } ++ } ++ } ++ + private void s3Delete(String fileName) { + amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName)); + } +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 +index 268af79..d07eba8 100644 +--- a/src/main/java/com/back/domain/user/account/service/AccountService.java ++++ b/src/main/java/com/back/domain/user/account/service/AccountService.java +@@ -5,12 +5,8 @@ import com.back.domain.board.comment.repository.CommentRepository; + import com.back.domain.board.common.dto.PageResponse; + import com.back.domain.board.post.dto.PostListResponse; + import com.back.domain.board.post.repository.PostRepository; +-import com.back.domain.file.entity.AttachmentMapping; + import com.back.domain.file.entity.EntityType; +-import com.back.domain.file.entity.FileAttachment; +-import com.back.domain.file.repository.AttachmentMappingRepository; +-import com.back.domain.file.repository.FileAttachmentRepository; +-import com.back.domain.file.service.FileService; ++import com.back.domain.file.service.AttachmentMappingService; + import com.back.domain.user.account.dto.ChangePasswordRequest; + import com.back.domain.user.account.dto.UserProfileRequest; + import com.back.domain.user.account.dto.UserDetailResponse; +@@ -40,9 +36,7 @@ public class AccountService { + private final UserProfileRepository userProfileRepository; + private final CommentRepository commentRepository; + private final PostRepository postRepository; +- private final FileAttachmentRepository fileAttachmentRepository; +- private final AttachmentMappingRepository attachmentMappingRepository; +- private final FileService fileService; ++ private final AttachmentMappingService attachmentMappingService; + private final PasswordEncoder passwordEncoder; + + /** +@@ -76,25 +70,17 @@ public class AccountService { + throw new CustomException(ErrorCode.NICKNAME_DUPLICATED); + } + +- + // UserProfile 업데이트 + UserProfile profile = user.getUserProfile(); + profile.setNickname(request.nickname()); + profile.setBio(request.bio()); + profile.setBirthDate(request.birthDate()); + +- // TODO: 프로필 이미지 및 매핑 업데이트 리팩토링 필요 + // 프로필 이미지 변경이 있는 경우만 수행 + String newUrl = request.profileImageUrl(); + String oldUrl = profile.getProfileImageUrl(); + if (!Objects.equals(newUrl, oldUrl)) { +- // 외부 이미지(S3 외부 URL)는 매핑 로직 제외 +- if (isExternalImageUrl(newUrl)) { +- // 기존 매핑만 제거 (소셜 이미지로 바뀌면 내부 매핑 필요 없음) +- removeExistingMapping(userId); +- } else { +- updateProfileImage(userId, newUrl); +- } ++ attachmentMappingService.replaceAttachmentByUrl(EntityType.PROFILE, profile.getId(), userId, newUrl); + profile.setProfileImageUrl(newUrl); + } + +@@ -102,61 +88,6 @@ public class AccountService { + return UserDetailResponse.from(user); + } + +- /** +- * 내부 저장소(S3) 이미지 교체 로직 +- * - 기존 매핑 및 파일 삭제 후 새 매핑 생성 +- */ +- private void updateProfileImage(Long userId, String newImageUrl) { +- +- // 기존 매핑 제거 +- removeExistingMapping(userId); +- +- // 새 이미지가 없는 경우 +- if (newImageUrl == null || newImageUrl.isBlank()) { +- return; +- } +- +- // 새 파일 조회 및 검증 +- FileAttachment newAttachment = fileAttachmentRepository +- .findByPublicURL(newImageUrl) +- .orElseThrow(() -> new CustomException(ErrorCode.FILE_NOT_FOUND)); +- +- if (!newAttachment.getUser().getId().equals(userId)) { +- throw new CustomException(ErrorCode.FILE_ACCESS_DENIED); +- } +- +- // 새 매핑 생성 및 저장 +- AttachmentMapping newMapping = new AttachmentMapping(newAttachment, EntityType.PROFILE, userId); +- attachmentMappingRepository.save(newMapping); +- } +- +- /** +- * 기존 프로필 이미지 매핑 및 파일 삭제 +- */ +- private void removeExistingMapping(Long userId) { +- attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.PROFILE, userId) +- .ifPresent(mapping -> { +- FileAttachment oldAttachment = mapping.getFileAttachment(); +- if (oldAttachment != null) { +- fileService.deleteFile(oldAttachment.getId(), userId); +- } +- attachmentMappingRepository.delete(mapping); +- }); +- } +- +- /** +- * 외부 이미지 URL 판별 +- * - 우리 S3 또는 CDN이 아니면 true +- * - 필요 시 application.yml에서 환경변수로 관리 +- */ +- private boolean isExternalImageUrl(String url) { +- if (url == null || url.isBlank()) return true; +- +- // TODO: 하드 코딩 제거 +- return !(url.startsWith("https://team5-s3-1.s3.ap-northeast-2.amazonaws.com") +- || url.contains("cdn.example.com")); +- } +- + /** + * 비밀번호 변경 서비스 + * 1. 사용자 조회 및 상태 검증 +@@ -196,9 +127,6 @@ public class AccountService { + // 사용자 조회 및 상태 검증 + User user = getValidUser(userId); + +- // 프로필 이미지 및 매핑 삭제 +- removeExistingMapping(userId); +- + // 상태 변경 (soft delete) + user.setUserStatus(UserStatus.DELETED); + +@@ -211,6 +139,9 @@ public class AccountService { + // 개인정보 마스킹 + UserProfile profile = user.getUserProfile(); + if (profile != null) { ++ // 프로필 이미지 및 매핑 삭제 ++ attachmentMappingService.deleteAttachments(EntityType.PROFILE, profile.getId(), userId); ++ + profile.setNickname("탈퇴한 회원"); + profile.setProfileImageUrl(null); + profile.setBio(null); +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 +index 5b16103..ab9b3aa 100644 +--- a/src/test/java/com/back/domain/board/post/service/PostServiceTest.java ++++ b/src/test/java/com/back/domain/board/post/service/PostServiceTest.java +@@ -1,5 +1,6 @@ + package com.back.domain.board.post.service; + ++import com.amazonaws.services.s3.AmazonS3; + import com.back.domain.board.common.dto.PageResponse; + import com.back.domain.board.post.entity.Post; + import com.back.domain.board.post.entity.PostCategory; +@@ -15,7 +16,7 @@ import com.back.domain.file.entity.EntityType; + import com.back.domain.file.entity.FileAttachment; + import com.back.domain.file.repository.AttachmentMappingRepository; + import com.back.domain.file.repository.FileAttachmentRepository; +-import com.back.domain.file.service.FileService; ++import com.back.domain.file.service.AttachmentMappingService; + import com.back.domain.user.common.entity.User; + import com.back.domain.user.common.entity.UserProfile; + import com.back.domain.user.common.enums.UserStatus; +@@ -26,6 +27,7 @@ import org.junit.jupiter.api.DisplayName; + import org.junit.jupiter.api.Test; + import org.springframework.beans.factory.annotation.Autowired; + import org.springframework.boot.test.context.SpringBootTest; ++import org.springframework.boot.test.mock.mockito.MockBean; + import org.springframework.data.domain.PageRequest; + import org.springframework.data.domain.Pageable; + import org.springframework.data.domain.Sort; +@@ -61,8 +63,11 @@ class PostServiceTest { + @Autowired + private AttachmentMappingRepository attachmentMappingRepository; + +- @MockitoBean +- private FileService fileService; ++ @Autowired ++ private AttachmentMappingService attachmentMappingService; ++ ++ @MockBean ++ private AmazonS3 amazonS3; // S3 호출 차단용 mock + + // ====================== 게시글 생성 테스트 ====================== + +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 +index ab94f70..e0ec982 100644 +--- a/src/test/java/com/back/domain/user/account/service/AccountServiceTest.java ++++ b/src/test/java/com/back/domain/user/account/service/AccountServiceTest.java +@@ -1,5 +1,6 @@ + package com.back.domain.user.account.service; + ++import com.amazonaws.services.s3.AmazonS3; + import com.back.domain.board.comment.dto.MyCommentResponse; + import com.back.domain.board.comment.entity.Comment; + import com.back.domain.board.comment.repository.CommentRepository; +@@ -14,7 +15,7 @@ import com.back.domain.file.entity.EntityType; + import com.back.domain.file.entity.FileAttachment; + import com.back.domain.file.repository.AttachmentMappingRepository; + import com.back.domain.file.repository.FileAttachmentRepository; +-import com.back.domain.file.service.FileService; ++import com.back.domain.file.service.AttachmentMappingService; + import com.back.domain.user.account.dto.ChangePasswordRequest; + import com.back.domain.user.account.dto.UserProfileRequest; + import com.back.domain.user.account.dto.UserDetailResponse; +@@ -28,6 +29,7 @@ import org.junit.jupiter.api.DisplayName; + import org.junit.jupiter.api.Test; + import org.springframework.beans.factory.annotation.Autowired; + import org.springframework.boot.test.context.SpringBootTest; ++import org.springframework.boot.test.mock.mockito.MockBean; + import org.springframework.data.domain.PageRequest; + import org.springframework.data.domain.Pageable; + import org.springframework.data.domain.Sort; +@@ -73,8 +75,11 @@ class AccountServiceTest { + @Autowired + private PasswordEncoder passwordEncoder; + +- @MockitoBean +- private FileService fileService; ++ @Autowired ++ private AttachmentMappingService attachmentMappingService; ++ ++ @MockBean ++ private AmazonS3 amazonS3; // S3 호출 차단용 mock + + private MultipartFile mockMultipartFile(String filename) { + return new MockMultipartFile(filename, filename, "image/png", new byte[]{1, 2, 3}); +@@ -169,7 +174,7 @@ class AccountServiceTest { + assertThat(response.profile().nickname()).isEqualTo("새닉네임"); + + // 새 매핑이 존재하고 기존 매핑은 삭제되었는지 검증 +- List mappings = attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.PROFILE, user.getId()); ++ List mappings = attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.PROFILE, user.getUserProfile().getId()); + assertThat(mappings).hasSize(1); + assertThat(mappings.get(0).getFileAttachment().getPublicURL()).isEqualTo(newAttachment.getPublicURL()); + +@@ -364,7 +369,7 @@ class AccountServiceTest { + // 프로필 이미지 매핑 설정 + FileAttachment attachment = new FileAttachment("profile_uuid_img.png", mockMultipartFile("profile.png"), user, "https://cdn.example.com/profile.png"); + fileAttachmentRepository.save(attachment); +- attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.PROFILE, user.getId())); ++ attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.PROFILE, user.getUserProfile().getId())); + + // when: 탈퇴 처리 + accountService.deleteUser(user.getId()); +@@ -385,7 +390,7 @@ class AccountServiceTest { + assertThat(profile.getBirthDate()).isNull(); + + // 프로필 이미지 및 매핑 삭제 검증 +- assertThat(attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.PROFILE, user.getId())).isEmpty(); ++ assertThat(attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.PROFILE, user.getUserProfile().getId())).isEmpty(); + assertThat(fileAttachmentRepository.findByPublicURL("https://cdn.example.com/profile.png")).isEmpty(); + } + 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 index becb576d..4869b376 100644 --- a/src/main/java/com/back/domain/board/post/service/PostService.java +++ b/src/main/java/com/back/domain/board/post/service/PostService.java @@ -17,7 +17,7 @@ import com.back.domain.file.entity.FileAttachment; import com.back.domain.file.repository.AttachmentMappingRepository; import com.back.domain.file.repository.FileAttachmentRepository; -import com.back.domain.file.service.FileService; +import com.back.domain.file.service.AttachmentMappingService; import com.back.domain.user.common.entity.User; import com.back.domain.user.common.repository.UserRepository; import com.back.global.exception.CustomException; @@ -41,7 +41,7 @@ public class PostService { private final PostCategoryRepository postCategoryRepository; private final FileAttachmentRepository fileAttachmentRepository; private final AttachmentMappingRepository attachmentMappingRepository; - private final FileService fileService; + private final AttachmentMappingService attachmentMappingService; /** * 게시글 생성 서비스 @@ -169,6 +169,7 @@ public PostResponse updatePost(Long postId, PostRequest request, Long userId) { private List updatePostAttachments(Post post, List newImageIds, Long userId) { List newIds = (newImageIds != null) ? newImageIds : List.of(); + // 기존 매핑 조회 List existingMappings = attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId()); List existingIds = existingMappings.stream() @@ -182,32 +183,26 @@ private List updatePostAttachments(Post post, List newImag .toList(); } - // 기존 첨부 삭제 - deletePostAttachments(post, userId); + // 기존 중 newIds에 없는 첨부만 삭제 + attachmentMappingService.deleteRemovedAttachments(EntityType.POST, post.getId(), userId, newIds); - // 새 첨부 매핑 등록 - if (newIds.isEmpty()) return List.of(); - - List attachments = validateAndFindAttachments(newIds); - attachments.forEach(attachment -> - attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.POST, post.getId())) - ); - return attachments; - } + // 새로 추가된 첨부만 매핑 생성 + List addedIds = newIds.stream() + .filter(id -> !existingIds.contains(id)) + .toList(); - /** - * 게시글 첨부파일 삭제 (S3 + 매핑) - */ - private void deletePostAttachments(Post post, Long userId) { - List mappings = - attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId()); - for (AttachmentMapping mapping : mappings) { - FileAttachment file = mapping.getFileAttachment(); - if (file != null) { - fileService.deleteFile(file.getId(), userId); - } + if (!addedIds.isEmpty()) { + List newAttachments = validateAndFindAttachments(addedIds); + newAttachments.forEach(attachment -> + attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.POST, post.getId())) + ); } - attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(EntityType.POST, post.getId()); + + // 최신 매핑 다시 조회 + return attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId()) + .stream() + .map(AttachmentMapping::getFileAttachment) + .toList(); } /** @@ -231,15 +226,7 @@ public void deletePost(Long postId, Long userId) { } // 첨부 파일 삭제 - List mappings = - attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId()); - for (AttachmentMapping mapping : mappings) { - FileAttachment fileAttachment = mapping.getFileAttachment(); - if (fileAttachment != null) { - fileService.deleteFile(fileAttachment.getId(), userId); - } - } - attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(EntityType.POST, post.getId()); + attachmentMappingService.deleteAttachments(EntityType.POST, post.getId(), userId); // Post 삭제 post.remove(); diff --git a/src/main/java/com/back/domain/file/service/AttachmentMappingService.java b/src/main/java/com/back/domain/file/service/AttachmentMappingService.java index 6b38b8d7..12dbb0d4 100644 --- a/src/main/java/com/back/domain/file/service/AttachmentMappingService.java +++ b/src/main/java/com/back/domain/file/service/AttachmentMappingService.java @@ -109,6 +109,31 @@ public void deleteAttachments(EntityType entityType, Long entityId, Long userId) attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(entityType, entityId); } + /** + * 기존 매핑 중 새 요청(newIds)에 없는 첨부만 삭제 + * - S3 객체 삭제 + * - 매핑 테이블 + 파일 정보 삭제 + */ + @Transactional + public void deleteRemovedAttachments(EntityType entityType, Long entityId, Long userId, List newIds) { + List mappings = + attachmentMappingRepository.findAllByEntityTypeAndEntityId(entityType, entityId); + + for (AttachmentMapping mapping : mappings) { + FileAttachment attachment = mapping.getFileAttachment(); + + if (attachment == null) continue; + + Long attachmentId = attachment.getId(); + + // 새 요청에 포함되지 않은 첨부만 삭제 + if (!newIds.contains(attachmentId)) { + s3Delete(attachment.getStoredName()); + attachmentMappingRepository.delete(mapping); + } + } + } + private void s3Delete(String fileName) { amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName)); } 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 index 268af792..d07eba83 100644 --- a/src/main/java/com/back/domain/user/account/service/AccountService.java +++ b/src/main/java/com/back/domain/user/account/service/AccountService.java @@ -5,12 +5,8 @@ import com.back.domain.board.common.dto.PageResponse; import com.back.domain.board.post.dto.PostListResponse; import com.back.domain.board.post.repository.PostRepository; -import com.back.domain.file.entity.AttachmentMapping; import com.back.domain.file.entity.EntityType; -import com.back.domain.file.entity.FileAttachment; -import com.back.domain.file.repository.AttachmentMappingRepository; -import com.back.domain.file.repository.FileAttachmentRepository; -import com.back.domain.file.service.FileService; +import com.back.domain.file.service.AttachmentMappingService; import com.back.domain.user.account.dto.ChangePasswordRequest; import com.back.domain.user.account.dto.UserProfileRequest; import com.back.domain.user.account.dto.UserDetailResponse; @@ -40,9 +36,7 @@ public class AccountService { private final UserProfileRepository userProfileRepository; private final CommentRepository commentRepository; private final PostRepository postRepository; - private final FileAttachmentRepository fileAttachmentRepository; - private final AttachmentMappingRepository attachmentMappingRepository; - private final FileService fileService; + private final AttachmentMappingService attachmentMappingService; private final PasswordEncoder passwordEncoder; /** @@ -76,25 +70,17 @@ public UserDetailResponse updateUserProfile(Long userId, UserProfileRequest requ throw new CustomException(ErrorCode.NICKNAME_DUPLICATED); } - // UserProfile 업데이트 UserProfile profile = user.getUserProfile(); profile.setNickname(request.nickname()); profile.setBio(request.bio()); profile.setBirthDate(request.birthDate()); - // TODO: 프로필 이미지 및 매핑 업데이트 리팩토링 필요 // 프로필 이미지 변경이 있는 경우만 수행 String newUrl = request.profileImageUrl(); String oldUrl = profile.getProfileImageUrl(); if (!Objects.equals(newUrl, oldUrl)) { - // 외부 이미지(S3 외부 URL)는 매핑 로직 제외 - if (isExternalImageUrl(newUrl)) { - // 기존 매핑만 제거 (소셜 이미지로 바뀌면 내부 매핑 필요 없음) - removeExistingMapping(userId); - } else { - updateProfileImage(userId, newUrl); - } + attachmentMappingService.replaceAttachmentByUrl(EntityType.PROFILE, profile.getId(), userId, newUrl); profile.setProfileImageUrl(newUrl); } @@ -102,61 +88,6 @@ public UserDetailResponse updateUserProfile(Long userId, UserProfileRequest requ return UserDetailResponse.from(user); } - /** - * 내부 저장소(S3) 이미지 교체 로직 - * - 기존 매핑 및 파일 삭제 후 새 매핑 생성 - */ - private void updateProfileImage(Long userId, String newImageUrl) { - - // 기존 매핑 제거 - removeExistingMapping(userId); - - // 새 이미지가 없는 경우 - if (newImageUrl == null || newImageUrl.isBlank()) { - return; - } - - // 새 파일 조회 및 검증 - FileAttachment newAttachment = fileAttachmentRepository - .findByPublicURL(newImageUrl) - .orElseThrow(() -> new CustomException(ErrorCode.FILE_NOT_FOUND)); - - if (!newAttachment.getUser().getId().equals(userId)) { - throw new CustomException(ErrorCode.FILE_ACCESS_DENIED); - } - - // 새 매핑 생성 및 저장 - AttachmentMapping newMapping = new AttachmentMapping(newAttachment, EntityType.PROFILE, userId); - attachmentMappingRepository.save(newMapping); - } - - /** - * 기존 프로필 이미지 매핑 및 파일 삭제 - */ - private void removeExistingMapping(Long userId) { - attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.PROFILE, userId) - .ifPresent(mapping -> { - FileAttachment oldAttachment = mapping.getFileAttachment(); - if (oldAttachment != null) { - fileService.deleteFile(oldAttachment.getId(), userId); - } - attachmentMappingRepository.delete(mapping); - }); - } - - /** - * 외부 이미지 URL 판별 - * - 우리 S3 또는 CDN이 아니면 true - * - 필요 시 application.yml에서 환경변수로 관리 - */ - private boolean isExternalImageUrl(String url) { - if (url == null || url.isBlank()) return true; - - // TODO: 하드 코딩 제거 - return !(url.startsWith("https://team5-s3-1.s3.ap-northeast-2.amazonaws.com") - || url.contains("cdn.example.com")); - } - /** * 비밀번호 변경 서비스 * 1. 사용자 조회 및 상태 검증 @@ -196,9 +127,6 @@ public void deleteUser(Long userId) { // 사용자 조회 및 상태 검증 User user = getValidUser(userId); - // 프로필 이미지 및 매핑 삭제 - removeExistingMapping(userId); - // 상태 변경 (soft delete) user.setUserStatus(UserStatus.DELETED); @@ -211,6 +139,9 @@ public void deleteUser(Long userId) { // 개인정보 마스킹 UserProfile profile = user.getUserProfile(); if (profile != null) { + // 프로필 이미지 및 매핑 삭제 + attachmentMappingService.deleteAttachments(EntityType.PROFILE, profile.getId(), userId); + profile.setNickname("탈퇴한 회원"); profile.setProfileImageUrl(null); profile.setBio(null); 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 index 5b161033..ab9b3aaa 100644 --- a/src/test/java/com/back/domain/board/post/service/PostServiceTest.java +++ b/src/test/java/com/back/domain/board/post/service/PostServiceTest.java @@ -1,5 +1,6 @@ package com.back.domain.board.post.service; +import com.amazonaws.services.s3.AmazonS3; import com.back.domain.board.common.dto.PageResponse; import com.back.domain.board.post.entity.Post; import com.back.domain.board.post.entity.PostCategory; @@ -15,7 +16,7 @@ import com.back.domain.file.entity.FileAttachment; import com.back.domain.file.repository.AttachmentMappingRepository; import com.back.domain.file.repository.FileAttachmentRepository; -import com.back.domain.file.service.FileService; +import com.back.domain.file.service.AttachmentMappingService; import com.back.domain.user.common.entity.User; import com.back.domain.user.common.entity.UserProfile; import com.back.domain.user.common.enums.UserStatus; @@ -26,6 +27,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -61,8 +63,11 @@ class PostServiceTest { @Autowired private AttachmentMappingRepository attachmentMappingRepository; - @MockitoBean - private FileService fileService; + @Autowired + private AttachmentMappingService attachmentMappingService; + + @MockBean + private AmazonS3 amazonS3; // S3 호출 차단용 mock // ====================== 게시글 생성 테스트 ====================== 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 index ab94f709..7a40afac 100644 --- a/src/test/java/com/back/domain/user/account/service/AccountServiceTest.java +++ b/src/test/java/com/back/domain/user/account/service/AccountServiceTest.java @@ -1,5 +1,6 @@ package com.back.domain.user.account.service; +import com.amazonaws.services.s3.AmazonS3; import com.back.domain.board.comment.dto.MyCommentResponse; import com.back.domain.board.comment.entity.Comment; import com.back.domain.board.comment.repository.CommentRepository; @@ -14,7 +15,7 @@ import com.back.domain.file.entity.FileAttachment; import com.back.domain.file.repository.AttachmentMappingRepository; import com.back.domain.file.repository.FileAttachmentRepository; -import com.back.domain.file.service.FileService; +import com.back.domain.file.service.AttachmentMappingService; import com.back.domain.user.account.dto.ChangePasswordRequest; import com.back.domain.user.account.dto.UserProfileRequest; import com.back.domain.user.account.dto.UserDetailResponse; @@ -28,6 +29,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -73,8 +75,11 @@ class AccountServiceTest { @Autowired private PasswordEncoder passwordEncoder; - @MockitoBean - private FileService fileService; + @Autowired + private AttachmentMappingService attachmentMappingService; + + @MockBean + private AmazonS3 amazonS3; // S3 호출 차단용 mock private MultipartFile mockMultipartFile(String filename) { return new MockMultipartFile(filename, filename, "image/png", new byte[]{1, 2, 3}); @@ -152,7 +157,7 @@ void updateUserProfile_success() { // 기존 프로필 이미지 매핑 설정 FileAttachment oldAttachment = new FileAttachment("old_uuid_img.png", mockMultipartFile("old.png"), user, "https://cdn.example.com/old.png"); fileAttachmentRepository.save(oldAttachment); - attachmentMappingRepository.save(new AttachmentMapping(oldAttachment, EntityType.PROFILE, user.getId())); + attachmentMappingRepository.save(new AttachmentMapping(oldAttachment, EntityType.PROFILE, user.getUserProfile().getId())); // 새 프로필 이미지 업로드된 파일 가정 FileAttachment newAttachment = new FileAttachment("new_uuid_img.png", mockMultipartFile("new.png"), user, "https://cdn.example.com/new.png"); @@ -169,7 +174,7 @@ void updateUserProfile_success() { assertThat(response.profile().nickname()).isEqualTo("새닉네임"); // 새 매핑이 존재하고 기존 매핑은 삭제되었는지 검증 - List mappings = attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.PROFILE, user.getId()); + List mappings = attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.PROFILE, user.getUserProfile().getId()); assertThat(mappings).hasSize(1); assertThat(mappings.get(0).getFileAttachment().getPublicURL()).isEqualTo(newAttachment.getPublicURL()); @@ -364,7 +369,7 @@ void deleteUser_success() { // 프로필 이미지 매핑 설정 FileAttachment attachment = new FileAttachment("profile_uuid_img.png", mockMultipartFile("profile.png"), user, "https://cdn.example.com/profile.png"); fileAttachmentRepository.save(attachment); - attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.PROFILE, user.getId())); + attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.PROFILE, user.getUserProfile().getId())); // when: 탈퇴 처리 accountService.deleteUser(user.getId()); @@ -385,7 +390,7 @@ void deleteUser_success() { assertThat(profile.getBirthDate()).isNull(); // 프로필 이미지 및 매핑 삭제 검증 - assertThat(attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.PROFILE, user.getId())).isEmpty(); + assertThat(attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.PROFILE, user.getUserProfile().getId())).isEmpty(); assertThat(fileAttachmentRepository.findByPublicURL("https://cdn.example.com/profile.png")).isEmpty(); }