Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
388 changes: 388 additions & 0 deletions commits.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,388 @@
commit f76663028be113cb49f058a173729621d0744a78
Author: joyewon0705 <[email protected]>
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<FileAttachment> updatePostAttachments(Post post, List<Long> newImageIds, Long userId) {
List<Long> newIds = (newImageIds != null) ? newImageIds : List.of();

+ // 기존 매핑 조회
List<AttachmentMapping> existingMappings =
attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, post.getId());
List<Long> 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<FileAttachment> attachments = validateAndFindAttachments(newIds);
- attachments.forEach(attachment ->
- attachmentMappingRepository.save(new AttachmentMapping(attachment, EntityType.POST, post.getId()))
- );
- return attachments;
- }
+ // 새로 추가된 첨부만 매핑 생성
+ List<Long> addedIds = newIds.stream()
+ .filter(id -> !existingIds.contains(id))
+ .toList();

- /**
- * 게시글 첨부파일 삭제 (S3 + 매핑)
- */
- private void deletePostAttachments(Post post, Long userId) {
- List<AttachmentMapping> 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<FileAttachment> 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<AttachmentMapping> 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<Long> newIds) {
+ List<AttachmentMapping> 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<AttachmentMapping> mappings = attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.PROFILE, user.getId());
+ List<AttachmentMapping> 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();
}

Loading