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
3 changes: 0 additions & 3 deletions src/main/java/com/back/domain/file/entity/FileAttachment.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ public class FileAttachment extends BaseEntity {
@JoinColumn(name = "uploaded_by")
private User user;

@OneToOne(mappedBy = "fileAttachment", fetch = FetchType.LAZY)
private AttachmentMapping attachmentMapping;

public FileAttachment(
String storedName,
MultipartFile multipartFile,
Expand Down
5 changes: 0 additions & 5 deletions src/main/java/com/back/domain/file/entity/MimeType.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.back.domain.file.service;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.DeleteObjectRequest;
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.global.exception.CustomException;
import com.back.global.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
public class AttachmentMappingService {
@Value("${cloud.aws.s3.bucket}")
private String bucket;
private final AmazonS3 amazonS3;
private final AttachmentMappingRepository attachmentMappingRepository;
private final FileAttachmentRepository fileAttachmentRepository;

/**
* 특정 엔티티의 첨부파일 매핑 갱신 (게시글, 프로필 등 공통 사용)
* 기존 매핑 및 파일 삭제 후, 새 첨부파일 목록으로 교체
*
* @param entityType 엔티티 종류 (POST, PROFILE 등)
* @param entityId 엔티티 ID
* @param userId 파일 업로더 검증용
* @param newAttachmentIds 새 파일 ID 리스트 (null 또는 빈 리스트면 삭제만 수행)
*/
@Transactional
public void replaceAttachments(
EntityType entityType,
Long entityId,
Long userId,
List<Long> newAttachmentIds
) {
// 기존 매핑 및 파일 삭제
deleteAttachments(entityType, entityId, userId);

if(newAttachmentIds == null || newAttachmentIds.isEmpty()) {
return;
}

List<FileAttachment> attachments = fileAttachmentRepository.findAllById(newAttachmentIds);
if(attachments.size() != newAttachmentIds.size()) {
throw new CustomException(ErrorCode.FILE_NOT_FOUND);
}

for (FileAttachment attachment : attachments) {
if (!attachment.getUser().getId().equals(userId)) {
throw new CustomException(ErrorCode.FILE_ACCESS_DENIED);
}
attachmentMappingRepository.save(new AttachmentMapping(attachment, entityType, entityId));
}
}

// URL로 갱신하는 경우
@Transactional
public void replaceAttachmentByUrl(
EntityType entityType,
Long entityId,
Long userId,
String newImageUrl
) {
deleteAttachments(entityType, entityId, userId);

if (newImageUrl == null || newImageUrl.isBlank()) return;

FileAttachment attachment = fileAttachmentRepository
.findByPublicURL(newImageUrl)
.orElseThrow(() -> new CustomException(ErrorCode.FILE_NOT_FOUND));

if (!attachment.getUser().getId().equals(userId)) {
throw new CustomException(ErrorCode.FILE_ACCESS_DENIED);
}

attachmentMappingRepository.save(new AttachmentMapping(attachment, entityType, entityId));
}

/**
* 특정 EntityType과 entityId에 연결된 첨부 파일을 모두 삭제
* - S3 객체 삭제
* - 매핑 테이블 + 파일 정보 삭제
*/
@Transactional
public void deleteAttachments(EntityType entityType, Long entityId, Long userId) {
List<AttachmentMapping> mappings = attachmentMappingRepository.findAllByEntityTypeAndEntityId(
entityType,
entityId
);

for(AttachmentMapping mapping : mappings) {
FileAttachment attachment = mapping.getFileAttachment();

if(attachment != null) {
// S3 오브젝트 삭제
s3Delete(attachment.getStoredName());
}
}

// 매핑 테이블 + 파일 정보 삭제
attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(entityType, entityId);
}

private void s3Delete(String fileName) {
amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;


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

@SpringBootTest
@Transactional
@ActiveProfiles("test")
class AttachmentMappingRepositoryTest {
@Autowired
private FileAttachmentRepository fileAttachmentRepository;
Expand Down Expand Up @@ -45,9 +47,6 @@ class AttachmentMappingRepositoryTest {
// when
attachmentMappingRepository.deleteAllByEntityTypeAndEntityId(EntityType.POST, 1L);

em.flush(); // 즉시 DB에 변경사항 반영
em.clear(); // 영속성 컨텍스트 초기화

// then
assertThat(fileAttachmentRepository.findAll().size()).isEqualTo(0);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.back.domain.file.service;

import com.back.domain.file.config.S3MockConfig;
import com.back.domain.file.dto.FileUploadResponseDto;
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.user.common.entity.User;
import com.back.domain.user.common.entity.UserProfile;
import com.back.domain.user.common.enums.UserStatus;
import com.back.domain.user.common.repository.UserRepository;
import io.findify.s3mock.S3Mock;
import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.List;

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

@Import(S3MockConfig.class)
@SpringBootTest
@Transactional
@ActiveProfiles("test")
class AttachmentMappingServiceTest {
@Autowired
private S3Mock s3Mock;

@Autowired
private FileService fileService;

@Autowired
private AttachmentMappingService attachmentMappingService;

@Autowired
private UserRepository userRepository;

@Autowired
private FileAttachmentRepository fileAttachmentRepository;

@Autowired
private AttachmentMappingRepository attachmentMappingRepository;

@Autowired
private PasswordEncoder passwordEncoder;

@AfterEach
public void tearDown() {
s3Mock.stop();
}

@Test
void deleteAttachments_success() throws Exception {
// given
User user = User.createUser("writer", "[email protected]", passwordEncoder.encode("P@ssw0rd!"));
user.setUserProfile(new UserProfile(user, "홍길동", null, "소개글", LocalDate.of(2000, 1, 1), 1000));
user.setUserStatus(UserStatus.ACTIVE);
userRepository.save(user);

String path = "test.png";
String contentType = "image/png";

MockMultipartFile file = new MockMultipartFile("test", path, contentType, "test".getBytes());

FileUploadResponseDto res = fileService.uploadFile(file, user.getId());
FileAttachment fileAttachment = fileAttachmentRepository.findById(res.getAttachmentId()).orElse(null);

AttachmentMapping attachmentMapping = new AttachmentMapping(fileAttachment, EntityType.POST, 1L);
attachmentMappingRepository.save(attachmentMapping);

// when
attachmentMappingService.deleteAttachments(EntityType.POST, 1L, user.getId());

// then
assertThat(attachmentMappingRepository.findAllByEntityTypeAndEntityId(EntityType.POST, 1L)
.size()).isEqualTo(0);
assertThat(fileAttachmentRepository.findAll().size()).isEqualTo(0);
}

@Test
void replaceAttachments_success() throws Exception {
// given
User user = User.createUser("writer", "[email protected]", passwordEncoder.encode("P@ssw0rd!"));
user.setUserProfile(new UserProfile(user, "홍길동", null, "소개글", LocalDate.of(2000, 1, 1), 1000));
user.setUserStatus(UserStatus.ACTIVE);
userRepository.save(user);

// 기존(삭제할) 파일 정보
String path = "test.png";
String contentType = "image/png";
MockMultipartFile oldFile = new MockMultipartFile("test", path, contentType, "test".getBytes());
Long oldAttachmentId = fileService.uploadFile(oldFile, user.getId()).getAttachmentId();

// 새 파일 정보
String newPath = "newTest.png";
MockMultipartFile newFile = new MockMultipartFile("newTest", newPath, contentType, "newTest".getBytes());
Long newAttachmentId = fileService.uploadFile(newFile, user.getId()).getAttachmentId();

FileAttachment fileAttachment = fileAttachmentRepository.findById(oldAttachmentId).orElse(null);
AttachmentMapping attachmentMapping = new AttachmentMapping(fileAttachment, EntityType.POST, 1L);
attachmentMappingRepository.save(attachmentMapping);

// when
attachmentMappingService.replaceAttachments(EntityType.POST, 1L, user.getId(), List.of(newAttachmentId));

// then
AttachmentMapping findMapping = attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.POST, 1L).orElse(null);
assertThat(findMapping.getFileAttachment().getId()).isEqualTo(newAttachmentId);
}

@Test
void replaceAttachmentsUrl_success() throws Exception {
// given
User user = User.createUser("writer", "[email protected]", passwordEncoder.encode("P@ssw0rd!"));
user.setUserProfile(new UserProfile(user, "홍길동", null, "소개글", LocalDate.of(2000, 1, 1), 1000));
user.setUserStatus(UserStatus.ACTIVE);
userRepository.save(user);

// 기존(삭제할) 파일 정보
String path = "test.png";
String contentType = "image/png";
MockMultipartFile oldFile = new MockMultipartFile("test", path, contentType, "test".getBytes());
Long oldAttachmentId = fileService.uploadFile(oldFile, user.getId()).getAttachmentId();

// 새 파일 정보
String newPath = "newTest.png";
MockMultipartFile newFile = new MockMultipartFile("newTest", newPath, contentType, "newTest".getBytes());
Long newAttachmentId = fileService.uploadFile(newFile, user.getId()).getAttachmentId();

FileAttachment fileAttachment = fileAttachmentRepository.findById(oldAttachmentId).orElse(null);
AttachmentMapping attachmentMapping = new AttachmentMapping(fileAttachment, EntityType.POST, 1L);
attachmentMappingRepository.save(attachmentMapping);

// when
String newPublicURL = fileAttachmentRepository
.findById(newAttachmentId)
.orElse(null)
.getPublicURL();

attachmentMappingService.replaceAttachmentByUrl(EntityType.POST, 1L, user.getId(), newPublicURL);

// then
AttachmentMapping findMapping = attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.POST, 1L).orElse(null);
assertThat(findMapping.getFileAttachment().getId()).isEqualTo(newAttachmentId);
}

}