Skip to content

Commit 9436ad8

Browse files
authored
Fix: AttachmentMapping 관련 기능 수정
* Fix: 파일 업로드 엔티티 구조 변경 - AttachmentMapping, FileAttachment 연관관계 주인을 다시 FileAttachment로 변경 - FileAttachment가 AttachmentMapping 참조를 안하도록 변경 * Feat: AttachmentMappingService 생성 EntityType에 해당하는 도메인에서 참조할 수 있는 비즈니스 로직 추가 - 각 entityType, entityId를 통해 AttachmentMapping을 수정/삭제 할 수 있도록 변경 - 이전에 추가하지 못했던 S3 이미지도 해당 로직을 통해 연동 가능 * Fix: AttachmentMapping 필드 변경 - FileAttachment의 nullable 여부 false로 변경 * Chore: 주석추가 * Chore: 안 쓰는 코드 제거 - MimeType 삭제 - AttachmentRepository em.flush, em.clear 삭제 * Fix: deleteAttachments 로직 수정 - 기존 1. S3 오브젝트 + 파일 정보 제거 2. 매핑 테이블 제거 - 변경 1. S3 오브젝트 제거 2. 매핑 테이블 + 파일 정보 제거 * Test: AttachmentMappingServiceTest 구현
1 parent 67c7c2b commit 9436ad8

File tree

5 files changed

+275
-11
lines changed

5 files changed

+275
-11
lines changed

src/main/java/com/back/domain/file/entity/FileAttachment.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@ public class FileAttachment extends BaseEntity {
2626
@JoinColumn(name = "uploaded_by")
2727
private User user;
2828

29-
@OneToOne(mappedBy = "fileAttachment", fetch = FetchType.LAZY)
30-
private AttachmentMapping attachmentMapping;
31-
3229
public FileAttachment(
3330
String storedName,
3431
MultipartFile multipartFile,

src/main/java/com/back/domain/file/entity/MimeType.java

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package com.back.domain.file.service;
2+
3+
import com.amazonaws.services.s3.AmazonS3;
4+
import com.amazonaws.services.s3.model.DeleteObjectRequest;
5+
import com.back.domain.file.entity.AttachmentMapping;
6+
import com.back.domain.file.entity.EntityType;
7+
import com.back.domain.file.entity.FileAttachment;
8+
import com.back.domain.file.repository.AttachmentMappingRepository;
9+
import com.back.domain.file.repository.FileAttachmentRepository;
10+
import com.back.global.exception.CustomException;
11+
import com.back.global.exception.ErrorCode;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.beans.factory.annotation.Value;
14+
import org.springframework.stereotype.Service;
15+
import org.springframework.transaction.annotation.Transactional;
16+
17+
import java.util.List;
18+
19+
@Service
20+
@RequiredArgsConstructor
21+
public class AttachmentMappingService {
22+
@Value("${cloud.aws.s3.bucket}")
23+
private String bucket;
24+
private final AmazonS3 amazonS3;
25+
private final AttachmentMappingRepository attachmentMappingRepository;
26+
private final FileAttachmentRepository fileAttachmentRepository;
27+
28+
/**
29+
* 특정 엔티티의 첨부파일 매핑 갱신 (게시글, 프로필 등 공통 사용)
30+
* 기존 매핑 및 파일 삭제 후, 새 첨부파일 목록으로 교체
31+
*
32+
* @param entityType 엔티티 종류 (POST, PROFILE 등)
33+
* @param entityId 엔티티 ID
34+
* @param userId 파일 업로더 검증용
35+
* @param newAttachmentIds 새 파일 ID 리스트 (null 또는 빈 리스트면 삭제만 수행)
36+
*/
37+
@Transactional
38+
public void replaceAttachments(
39+
EntityType entityType,
40+
Long entityId,
41+
Long userId,
42+
List<Long> newAttachmentIds
43+
) {
44+
// 기존 매핑 및 파일 삭제
45+
deleteAttachments(entityType, entityId, userId);
46+
47+
if(newAttachmentIds == null || newAttachmentIds.isEmpty()) {
48+
return;
49+
}
50+
51+
List<FileAttachment> attachments = fileAttachmentRepository.findAllById(newAttachmentIds);
52+
if(attachments.size() != newAttachmentIds.size()) {
53+
throw new CustomException(ErrorCode.FILE_NOT_FOUND);
54+
}
55+
56+
for (FileAttachment attachment : attachments) {
57+
if (!attachment.getUser().getId().equals(userId)) {
58+
throw new CustomException(ErrorCode.FILE_ACCESS_DENIED);
59+
}
60+
attachmentMappingRepository.save(new AttachmentMapping(attachment, entityType, entityId));
61+
}
62+
}
63+
64+
// URL로 갱신하는 경우
65+
@Transactional
66+
public void replaceAttachmentByUrl(
67+
EntityType entityType,
68+
Long entityId,
69+
Long userId,
70+
String newImageUrl
71+
) {
72+
deleteAttachments(entityType, entityId, userId);
73+
74+
if (newImageUrl == null || newImageUrl.isBlank()) return;
75+
76+
FileAttachment attachment = fileAttachmentRepository
77+
.findByPublicURL(newImageUrl)
78+
.orElseThrow(() -> new CustomException(ErrorCode.FILE_NOT_FOUND));
79+
80+
if (!attachment.getUser().getId().equals(userId)) {
81+
throw new CustomException(ErrorCode.FILE_ACCESS_DENIED);
82+
}
83+
84+
attachmentMappingRepository.save(new AttachmentMapping(attachment, entityType, entityId));
85+
}
86+
87+
/**
88+
* 특정 EntityType과 entityId에 연결된 첨부 파일을 모두 삭제
89+
* - S3 객체 삭제
90+
* - 매핑 테이블 + 파일 정보 삭제
91+
*/
92+
@Transactional
93+
public void deleteAttachments(EntityType entityType, Long entityId, Long userId) {
94+
List<AttachmentMapping> mappings = attachmentMappingRepository.findAllByEntityTypeAndEntityId(
95+
entityType,
96+
entityId
97+
);
98+
99+
for(AttachmentMapping mapping : mappings) {
100+
FileAttachment attachment = mapping.getFileAttachment();
101+
102+
if(attachment != null) {
103+
// S3 오브젝트 삭제
104+
s3Delete(attachment.getStoredName());
105+
}
106+
}
107+
108+
// 매핑 테이블 + 파일 정보 삭제
109+
attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(entityType, entityId);
110+
}
111+
112+
private void s3Delete(String fileName) {
113+
amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
114+
}
115+
}
116+

src/test/java/com/back/domain/file/repository/AttachmentMappingRepositoryTest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
import org.springframework.beans.factory.annotation.Autowired;
99
import org.springframework.boot.test.context.SpringBootTest;
1010
import org.springframework.mock.web.MockMultipartFile;
11+
import org.springframework.test.context.ActiveProfiles;
1112
import org.springframework.transaction.annotation.Transactional;
1213

1314

1415
import static org.assertj.core.api.Assertions.assertThat;
1516

1617
@SpringBootTest
1718
@Transactional
19+
@ActiveProfiles("test")
1820
class AttachmentMappingRepositoryTest {
1921
@Autowired
2022
private FileAttachmentRepository fileAttachmentRepository;
@@ -45,9 +47,6 @@ class AttachmentMappingRepositoryTest {
4547
// when
4648
attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(EntityType.POST, 1L);
4749

48-
em.flush(); // 즉시 DB에 변경사항 반영
49-
em.clear(); // 영속성 컨텍스트 초기화
50-
5150
// then
5251
assertThat(fileAttachmentRepository.findAll().size()).isEqualTo(0);
5352
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package com.back.domain.file.service;
2+
3+
import com.back.domain.file.config.S3MockConfig;
4+
import com.back.domain.file.dto.FileUploadResponseDto;
5+
import com.back.domain.file.entity.AttachmentMapping;
6+
import com.back.domain.file.entity.EntityType;
7+
import com.back.domain.file.entity.FileAttachment;
8+
import com.back.domain.file.repository.AttachmentMappingRepository;
9+
import com.back.domain.file.repository.FileAttachmentRepository;
10+
import com.back.domain.user.common.entity.User;
11+
import com.back.domain.user.common.entity.UserProfile;
12+
import com.back.domain.user.common.enums.UserStatus;
13+
import com.back.domain.user.common.repository.UserRepository;
14+
import io.findify.s3mock.S3Mock;
15+
import jakarta.persistence.EntityManager;
16+
import org.junit.jupiter.api.AfterEach;
17+
import org.junit.jupiter.api.Test;
18+
import org.springframework.beans.factory.annotation.Autowired;
19+
import org.springframework.boot.test.context.SpringBootTest;
20+
import org.springframework.context.annotation.Import;
21+
import org.springframework.mock.web.MockMultipartFile;
22+
import org.springframework.security.crypto.password.PasswordEncoder;
23+
import org.springframework.test.context.ActiveProfiles;
24+
import org.springframework.transaction.annotation.Transactional;
25+
26+
import java.time.LocalDate;
27+
import java.util.List;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
31+
@Import(S3MockConfig.class)
32+
@SpringBootTest
33+
@Transactional
34+
@ActiveProfiles("test")
35+
class AttachmentMappingServiceTest {
36+
@Autowired
37+
private S3Mock s3Mock;
38+
39+
@Autowired
40+
private FileService fileService;
41+
42+
@Autowired
43+
private AttachmentMappingService attachmentMappingService;
44+
45+
@Autowired
46+
private UserRepository userRepository;
47+
48+
@Autowired
49+
private FileAttachmentRepository fileAttachmentRepository;
50+
51+
@Autowired
52+
private AttachmentMappingRepository attachmentMappingRepository;
53+
54+
@Autowired
55+
private PasswordEncoder passwordEncoder;
56+
57+
@AfterEach
58+
public void tearDown() {
59+
s3Mock.stop();
60+
}
61+
62+
@Test
63+
void deleteAttachments_success() throws Exception {
64+
// given
65+
User user = User.createUser("writer", "[email protected]", passwordEncoder.encode("P@ssw0rd!"));
66+
user.setUserProfile(new UserProfile(user, "홍길동", null, "소개글", LocalDate.of(2000, 1, 1), 1000));
67+
user.setUserStatus(UserStatus.ACTIVE);
68+
userRepository.save(user);
69+
70+
String path = "test.png";
71+
String contentType = "image/png";
72+
73+
MockMultipartFile file = new MockMultipartFile("test", path, contentType, "test".getBytes());
74+
75+
FileUploadResponseDto res = fileService.uploadFile(file, user.getId());
76+
FileAttachment fileAttachment = fileAttachmentRepository.findById(res.getAttachmentId()).orElse(null);
77+
78+
AttachmentMapping attachmentMapping = new AttachmentMapping(fileAttachment, EntityType.POST, 1L);
79+
attachmentMappingRepository.save(attachmentMapping);
80+
81+
// when
82+
attachmentMappingService.deleteAttachments(EntityType.POST, 1L, user.getId());
83+
84+
// then
85+
assertThat(attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, 1L)
86+
.size()).isEqualTo(0);
87+
assertThat(fileAttachmentRepository.findAll().size()).isEqualTo(0);
88+
}
89+
90+
@Test
91+
void replaceAttachments_success() throws Exception {
92+
// given
93+
User user = User.createUser("writer", "[email protected]", passwordEncoder.encode("P@ssw0rd!"));
94+
user.setUserProfile(new UserProfile(user, "홍길동", null, "소개글", LocalDate.of(2000, 1, 1), 1000));
95+
user.setUserStatus(UserStatus.ACTIVE);
96+
userRepository.save(user);
97+
98+
// 기존(삭제할) 파일 정보
99+
String path = "test.png";
100+
String contentType = "image/png";
101+
MockMultipartFile oldFile = new MockMultipartFile("test", path, contentType, "test".getBytes());
102+
Long oldAttachmentId = fileService.uploadFile(oldFile, user.getId()).getAttachmentId();
103+
104+
// 새 파일 정보
105+
String newPath = "newTest.png";
106+
MockMultipartFile newFile = new MockMultipartFile("newTest", newPath, contentType, "newTest".getBytes());
107+
Long newAttachmentId = fileService.uploadFile(newFile, user.getId()).getAttachmentId();
108+
109+
FileAttachment fileAttachment = fileAttachmentRepository.findById(oldAttachmentId).orElse(null);
110+
AttachmentMapping attachmentMapping = new AttachmentMapping(fileAttachment, EntityType.POST, 1L);
111+
attachmentMappingRepository.save(attachmentMapping);
112+
113+
// when
114+
attachmentMappingService.replaceAttachments(EntityType.POST, 1L, user.getId(), List.of(newAttachmentId));
115+
116+
// then
117+
AttachmentMapping findMapping = attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.POST, 1L).orElse(null);
118+
assertThat(findMapping.getFileAttachment().getId()).isEqualTo(newAttachmentId);
119+
}
120+
121+
@Test
122+
void replaceAttachmentsUrl_success() throws Exception {
123+
// given
124+
User user = User.createUser("writer", "[email protected]", passwordEncoder.encode("P@ssw0rd!"));
125+
user.setUserProfile(new UserProfile(user, "홍길동", null, "소개글", LocalDate.of(2000, 1, 1), 1000));
126+
user.setUserStatus(UserStatus.ACTIVE);
127+
userRepository.save(user);
128+
129+
// 기존(삭제할) 파일 정보
130+
String path = "test.png";
131+
String contentType = "image/png";
132+
MockMultipartFile oldFile = new MockMultipartFile("test", path, contentType, "test".getBytes());
133+
Long oldAttachmentId = fileService.uploadFile(oldFile, user.getId()).getAttachmentId();
134+
135+
// 새 파일 정보
136+
String newPath = "newTest.png";
137+
MockMultipartFile newFile = new MockMultipartFile("newTest", newPath, contentType, "newTest".getBytes());
138+
Long newAttachmentId = fileService.uploadFile(newFile, user.getId()).getAttachmentId();
139+
140+
FileAttachment fileAttachment = fileAttachmentRepository.findById(oldAttachmentId).orElse(null);
141+
AttachmentMapping attachmentMapping = new AttachmentMapping(fileAttachment, EntityType.POST, 1L);
142+
attachmentMappingRepository.save(attachmentMapping);
143+
144+
// when
145+
String newPublicURL = fileAttachmentRepository
146+
.findById(newAttachmentId)
147+
.orElse(null)
148+
.getPublicURL();
149+
150+
attachmentMappingService.replaceAttachmentByUrl(EntityType.POST, 1L, user.getId(), newPublicURL);
151+
152+
// then
153+
AttachmentMapping findMapping = attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.POST, 1L).orElse(null);
154+
assertThat(findMapping.getFileAttachment().getId()).isEqualTo(newAttachmentId);
155+
}
156+
157+
}

0 commit comments

Comments
 (0)