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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +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.user.common.entity.User;
import com.back.domain.user.common.repository.UserRepository;
import com.back.global.exception.CustomException;
Expand All @@ -40,6 +41,7 @@ public class PostService {
private final PostCategoryRepository postCategoryRepository;
private final FileAttachmentRepository fileAttachmentRepository;
private final AttachmentMappingRepository attachmentMappingRepository;
private final FileService fileService;

/**
* 게시글 생성 서비스
Expand Down Expand Up @@ -183,7 +185,15 @@ public void deletePost(Long postId, Long userId) {
throw new CustomException(ErrorCode.POST_NO_PERMISSION);
}

// AttachmentMapping 매핑 제거
// 첨부 파일 삭제
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());

// Post 삭제
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
@Getter
@NoArgsConstructor
public class AttachmentMapping extends BaseEntity {
@OneToOne(fetch = FetchType.LAZY, mappedBy = "attachmentMapping", cascade = CascadeType.ALL, orphanRemoval = true)
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "attachment_id", nullable = false)
private FileAttachment fileAttachment;

@Enumerated(EnumType.STRING)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@NoArgsConstructor
Expand All @@ -29,8 +26,7 @@ public class FileAttachment extends BaseEntity {
@JoinColumn(name = "uploaded_by")
private User user;

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

public FileAttachment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

public interface AttachmentMappingRepository extends JpaRepository<AttachmentMapping, Long> {
List<AttachmentMapping> findAllByEntityTypeAndEntityId(EntityType entityType, Long entityId);
Optional<AttachmentMapping> findByEntityTypeAndEntityId(EntityType entityType, Long entityId);

void deleteAllByEntityTypeAndEntityId(EntityType entityType, Long entityId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
import com.back.domain.file.entity.FileAttachment;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface FileAttachmentRepository extends JpaRepository<FileAttachment, Long> {
Optional<FileAttachment> findByPublicURL(String publicUrl);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import com.back.domain.board.post.dto.PostListResponse;
import com.back.domain.user.account.controller.docs.AccountControllerDocs;
import com.back.domain.user.account.dto.ChangePasswordRequest;
import com.back.domain.user.account.dto.UpdateUserProfileRequest;
import com.back.domain.user.account.dto.UserProfileRequest;
import com.back.domain.user.account.dto.UserDetailResponse;
import com.back.domain.user.account.service.AccountService;
import com.back.global.common.dto.RsData;
Expand Down Expand Up @@ -42,7 +42,7 @@ public ResponseEntity<RsData<UserDetailResponse>> getMyInfo(
@PatchMapping
public ResponseEntity<RsData<UserDetailResponse>> updateMyProfile(
@AuthenticationPrincipal CustomUserDetails user,
@Valid @RequestBody UpdateUserProfileRequest request
@Valid @RequestBody UserProfileRequest request
) {
UserDetailResponse updated = accountService.updateUserProfile(user.getUserId(), request);
return ResponseEntity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import com.back.domain.board.common.dto.PageResponse;
import com.back.domain.board.post.dto.PostListResponse;
import com.back.domain.user.account.dto.ChangePasswordRequest;
import com.back.domain.user.account.dto.UpdateUserProfileRequest;
import com.back.domain.user.account.dto.UserProfileRequest;
import com.back.domain.user.account.dto.UserDetailResponse;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
Expand Down Expand Up @@ -307,7 +307,7 @@ ResponseEntity<RsData<UserDetailResponse>> getMyInfo(
})
ResponseEntity<RsData<UserDetailResponse>> updateMyProfile(
@AuthenticationPrincipal CustomUserDetails user,
@Valid @RequestBody UpdateUserProfileRequest request
@Valid @RequestBody UserProfileRequest request
);

@Operation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* @param bio 사용자의 자기소개
* @param birthDate 사용자의 생년월일
*/
public record UpdateUserProfileRequest(
public record UserProfileRequest(
@NotBlank @Size(max = 20) String nickname,
String profileImageUrl,
@Size(max = 255) String bio,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@
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.user.account.dto.ChangePasswordRequest;
import com.back.domain.user.account.dto.UpdateUserProfileRequest;
import com.back.domain.user.account.dto.UserProfileRequest;
import com.back.domain.user.account.dto.UserDetailResponse;
import com.back.domain.user.common.entity.User;
import com.back.domain.user.common.entity.UserProfile;
Expand All @@ -23,6 +29,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Service
@RequiredArgsConstructor
@Transactional
Expand All @@ -31,6 +39,9 @@ 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 PasswordEncoder passwordEncoder;

/**
Expand All @@ -54,7 +65,7 @@ public UserDetailResponse getUserInfo(Long userId) {
* 3. UserProfile 업데이트
* 4. UserDetailResponse 변환 및 반환
*/
public UserDetailResponse updateUserProfile(Long userId, UpdateUserProfileRequest request) {
public UserDetailResponse updateUserProfile(Long userId, UserProfileRequest request) {

// 사용자 조회 및 상태 검증
User user = getValidUser(userId);
Expand All @@ -71,10 +82,48 @@ public UserDetailResponse updateUserProfile(Long userId, UpdateUserProfileReques
profile.setBio(request.bio());
profile.setBirthDate(request.birthDate());

// 프로필 이미지 및 매핑 업데이트
updateProfileImage(userId, request.profileImageUrl());

// UserDetailResponse로 변환하여 반환
return UserDetailResponse.from(user);
}

/**
* 프로필 이미지 및 매핑 교체 로직
* - 기존 이미지(S3 + FileAttachment + Mapping) 삭제 후 새 매핑 생성
*/
private void updateProfileImage(Long userId, String newImageUrl) {

// 기존 매핑 및 파일 삭제
attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.PROFILE, userId)
.ifPresent(existingMapping -> {
FileAttachment oldAttachment = existingMapping.getFileAttachment();
if (oldAttachment != null) {
fileService.deleteFile(oldAttachment.getId(), userId);
}
attachmentMappingRepository.delete(existingMapping);
});

// 새 이미지가 없는 경우
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);
}

/**
* 비밀번호 변경 서비스
* 1. 사용자 조회 및 상태 검증
Expand Down Expand Up @@ -114,6 +163,9 @@ public void deleteUser(Long userId) {
// 사용자 조회 및 상태 검증
User user = getValidUser(userId);

// 프로필 이미지 및 매핑 삭제
deleteProfileImage(userId);

// 상태 변경 (soft delete)
user.setUserStatus(UserStatus.DELETED);

Expand All @@ -133,6 +185,20 @@ public void deleteUser(Long userId) {
}
}

/**
* 프로필 이미지 및 매핑 삭제
*/
private void deleteProfileImage(Long userId) {
attachmentMappingRepository.findByEntityTypeAndEntityId(EntityType.PROFILE, userId)
.ifPresent(mapping -> {
FileAttachment attachment = mapping.getFileAttachment();
if (attachment != null) {
fileService.deleteFile(attachment.getId(), userId);
}
attachmentMappingRepository.delete(mapping);
});
}

/**
* 내 게시글 목록 조회 서비스
* 1. 사용자 조회 및 상태 검증
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.back.domain.board.post.repository.PostRepository;
import com.back.domain.file.entity.FileAttachment;
import com.back.domain.file.repository.FileAttachmentRepository;
import com.back.domain.file.service.FileService;
import com.back.domain.user.common.entity.User;
import com.back.domain.user.common.entity.UserProfile;
import com.back.domain.user.common.enums.UserStatus;
Expand All @@ -19,6 +20,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.crypto.password.PasswordEncoder;
Expand Down Expand Up @@ -64,6 +66,9 @@ class PostControllerTest {
@Autowired
private ObjectMapper objectMapper;

@MockBean
private FileService fileService;

private String generateAccessToken(User user) {
return testJwtTokenProvider.createAccessToken(user.getId(), user.getUsername(), user.getRole().name());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +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.user.common.entity.User;
import com.back.domain.user.common.entity.UserProfile;
import com.back.domain.user.common.enums.UserStatus;
Expand All @@ -25,6 +26,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;
Expand Down Expand Up @@ -59,6 +61,9 @@ class PostServiceTest {
@Autowired
private AttachmentMappingRepository attachmentMappingRepository;

@MockBean
private FileService fileService;

// ====================== 게시글 생성 테스트 ======================

@Test
Expand Down Expand Up @@ -91,14 +96,28 @@ void createPost_success_withCategoriesAndImages() {
// when
PostResponse response = postService.createPost(request, user.getId());

// then
// then — DTO 검증
assertThat(response.title()).isEqualTo("제목");
assertThat(response.content()).isEqualTo("내용");
assertThat(response.author().nickname()).isEqualTo("작성자");
assertThat(response.categories()).hasSize(1);
assertThat(response.categories().getFirst().name()).isEqualTo("공지");
assertThat(response.images()).hasSize(2);
assertThat(response.images().getFirst().id()).isEqualTo(img1.getId());

// then — DB 매핑 검증
List<AttachmentMapping> mappings = attachmentMappingRepository
.findAllByEntityTypeAndEntityId(EntityType.POST, response.postId());

assertThat(mappings).hasSize(2); // 이미지 2개 → 매핑 2개
assertThat(mappings)
.allSatisfy(mapping -> {
assertThat(mapping.getEntityType()).isEqualTo(EntityType.POST);
assertThat(mapping.getEntityId()).isEqualTo(response.postId());
assertThat(mapping.getFileAttachment()).isNotNull();
assertThat(mapping.getFileAttachment().getId())
.isIn(img1.getId(), img2.getId());
});
}

@Test
Expand Down Expand Up @@ -199,11 +218,25 @@ void getPost_success() {
PostCategory category = new PostCategory("공지", CategoryType.SUBJECT);
postCategoryRepository.save(category);

// 게시글 생성
Post post = new Post(user, "조회용 제목", "조회용 내용", null);
post.updateCategories(List.of(category));
postRepository.save(post);

// when
// 첨부 이미지 추가
MockMultipartFile file1 = new MockMultipartFile("file", "img1.png", "image/png", "dummy".getBytes());
FileAttachment attachment1 = new FileAttachment("stored_img1.png", file1, user, "https://cdn.example.com/img1.png");
fileAttachmentRepository.save(attachment1);

MockMultipartFile file2 = new MockMultipartFile("file", "img2.png", "image/png", "dummy".getBytes());
FileAttachment attachment2 = new FileAttachment("stored_img2.png", file2, user, "https://cdn.example.com/img2.png");
fileAttachmentRepository.save(attachment2);

// 매핑 저장 (EntityType.POST)
attachmentMappingRepository.save(new AttachmentMapping(attachment1, EntityType.POST, post.getId()));
attachmentMappingRepository.save(new AttachmentMapping(attachment2, EntityType.POST, post.getId()));

// when: 비로그인 상태에서 조회
PostDetailResponse response = postService.getPost(post.getId(), null);

// then
Expand All @@ -212,9 +245,19 @@ void getPost_success() {
assertThat(response.content()).isEqualTo("조회용 내용");
assertThat(response.author().nickname()).isEqualTo("독자");
assertThat(response.categories()).extracting("name").containsExactly("공지");

// 첨부 이미지 검증
assertThat(response.images()).hasSize(2);
assertThat(response.images())
.extracting("url")
.containsExactlyInAnyOrder("https://cdn.example.com/img1.png", "https://cdn.example.com/img2.png");

// 기본 상호작용 상태 검증
assertThat(response.likeCount()).isZero();
assertThat(response.bookmarkCount()).isZero();
assertThat(response.commentCount()).isZero();
assertThat(response.likedByMe()).isFalse();
assertThat(response.bookmarkedByMe()).isFalse();
}

@Test
Expand Down
Loading