Skip to content

Commit 140eef3

Browse files
authored
refactor: 태그 저장과 이미지 저장 트랜잭션 분리 (#117)
* refactor: 태그 저장과 이미지 저장 트랜잭션 분리 * style: 메서드 이름 수정 * feat: 회원 탈퇴 트랜잭션 추가
1 parent b2c25ed commit 140eef3

File tree

13 files changed

+465
-2
lines changed

13 files changed

+465
-2
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.capturecat.core.api.image.dto;
2+
3+
import java.util.List;
4+
5+
import com.capturecat.core.domain.image.dto.ImageSaveRequest;
6+
import com.capturecat.core.domain.tag.Tag;
7+
8+
public class ImageRequestDto {
9+
10+
public record UploadItem(
11+
String fileName,
12+
long fileSize,
13+
String captureDate,
14+
List<String> tagNames
15+
) {
16+
17+
}
18+
19+
public record ImageCreateData(
20+
ImageSaveRequest imageSaveRequest,
21+
List<Tag> tags
22+
) {
23+
24+
}
25+
}

capturecat-core/src/main/java/com/capturecat/core/domain/image/Image.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import lombok.NoArgsConstructor;
1818

1919
import com.capturecat.core.domain.BaseTimeEntity;
20+
import com.capturecat.core.domain.image.dto.ImageSaveRequest;
2021
import com.capturecat.core.domain.user.User;
2122
import com.capturecat.core.support.error.CoreException;
2223
import com.capturecat.core.support.error.ErrorType;
@@ -54,6 +55,16 @@ public Image(Long id, String fileName, String fileUrl, long size, LocalDate capt
5455
this.user = user;
5556
}
5657

58+
public static Image create(ImageSaveRequest imageSaveRequest, User user) {
59+
return Image.builder()
60+
.fileName(imageSaveRequest.fileName())
61+
.fileUrl(imageSaveRequest.fileUrl())
62+
.size(imageSaveRequest.size())
63+
.captureDate(imageSaveRequest.captureDate())
64+
.user(user)
65+
.build();
66+
}
67+
5768
public void validateOwnership(User user) {
5869
if (isNotOwnedBy(user)) {
5970
throw new CoreException(ErrorType.IMAGE_ACCESS_DENIED);
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.capturecat.core.domain.image;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.function.Function;
7+
import java.util.stream.Collectors;
8+
9+
import org.springframework.transaction.annotation.Transactional;
10+
11+
import lombok.RequiredArgsConstructor;
12+
13+
import com.capturecat.core.api.image.dto.ImageRequestDto;
14+
import com.capturecat.core.domain.annotation.DomainService;
15+
import com.capturecat.core.domain.tag.ImageTag;
16+
import com.capturecat.core.domain.tag.ImageTagFactory;
17+
import com.capturecat.core.domain.tag.ImageTagRepository;
18+
import com.capturecat.core.domain.user.User;
19+
import com.capturecat.core.domain.user.UserRepository;
20+
import com.capturecat.core.service.auth.LoginUser;
21+
import com.capturecat.core.support.error.CoreException;
22+
import com.capturecat.core.support.error.ErrorType;
23+
24+
@DomainService
25+
@RequiredArgsConstructor
26+
public class ImageCreator {
27+
28+
private final UserRepository userRepository;
29+
private final ImageRepository imageRepository;
30+
private final ImageTagRepository imageTagRepository;
31+
private final ImageTagValidator imageTagValidator;
32+
private final ImageTagFactory imageTagFactory;
33+
34+
/**
35+
* 이미지와 태그를 함께 저장합니다.
36+
*/
37+
@Transactional
38+
public List<Image> createAll(LoginUser loginUser, List<ImageRequestDto.ImageCreateData> requests) {
39+
User user = userRepository.findByUsername(loginUser.getUsername())
40+
.orElseThrow(() -> new CoreException(ErrorType.USER_NOT_FOUND));
41+
42+
List<Image> savedImages = imageRepository.saveAll(requests.stream()
43+
.map(item -> Image.create(item.imageSaveRequest(), user))
44+
.toList());
45+
46+
Map<String, Image> savedImagesMap = savedImages.stream()
47+
.collect(Collectors.toMap(Image::getFileName, Function.identity()));
48+
49+
List<ImageTag> allImageTags = new ArrayList<>();
50+
for (ImageRequestDto.ImageCreateData request : requests) {
51+
Image image = savedImagesMap.get(request.imageSaveRequest().fileName());
52+
53+
imageTagValidator.validateTags(image, request.tags());
54+
55+
allImageTags.addAll(imageTagFactory.create(image, request.tags()));
56+
}
57+
imageTagRepository.saveAll(allImageTags);
58+
59+
return savedImages;
60+
}
61+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.capturecat.core.domain.image;
2+
3+
import java.util.HashSet;
4+
import java.util.List;
5+
import java.util.Set;
6+
7+
import org.springframework.stereotype.Component;
8+
9+
import lombok.RequiredArgsConstructor;
10+
11+
import com.capturecat.core.domain.tag.ImageTagRepository;
12+
import com.capturecat.core.domain.tag.Tag;
13+
import com.capturecat.core.support.error.CoreException;
14+
import com.capturecat.core.support.error.ErrorType;
15+
16+
@Component
17+
@RequiredArgsConstructor
18+
public class ImageTagValidator {
19+
20+
private static final int TAG_MAX_COUNT = 4;
21+
22+
private final ImageTagRepository imageTagRepository;
23+
24+
/**
25+
* 이미지에 태그를 추가하기 전 모든 유효성 규칙을 검증하는 기본 메서드입니다.
26+
*/
27+
public void validateTags(Image image, List<Tag> tags) {
28+
validateDuplicateTags(tags);
29+
validateExistingTagsOnImage(image, tags);
30+
validateTagCount(image, tags);
31+
}
32+
33+
/**
34+
* 제공된 태그 리스트 내에 중복된 태그가 있는지 확인합니다.
35+
*/
36+
private void validateDuplicateTags(List<Tag> tags) {
37+
Set<Tag> uniqueTags = new HashSet<>(tags);
38+
if (uniqueTags.size() < tags.size()) {
39+
throw new CoreException(ErrorType.DUPLICATE_TAG_NAMES);
40+
}
41+
}
42+
43+
/**
44+
* 제공된 태그들이 이미지에 이미 등록되어 있는지 확인합니다.
45+
*/
46+
private void validateExistingTagsOnImage(Image image, List<Tag> tags) {
47+
if (tags.isEmpty()) {
48+
return;
49+
}
50+
51+
if (imageTagRepository.existsByImageAndTagIn(image, tags)) {
52+
throw new CoreException(ErrorType.ALREADY_REGISTERED_TAGS);
53+
}
54+
}
55+
56+
/**
57+
* 이미지에 추가될 태그의 개수 관련 규칙만 검증합니다.
58+
*/
59+
private void validateTagCount(Image image, List<Tag> tags) {
60+
int newTagsCount = tags.size();
61+
if (newTagsCount > TAG_MAX_COUNT) {
62+
throw new CoreException(ErrorType.TOO_MANY_TAGS);
63+
}
64+
65+
long existingTagCount = imageTagRepository.countByImage(image);
66+
if (existingTagCount + newTagsCount > TAG_MAX_COUNT) {
67+
throw new CoreException(ErrorType.TOO_MANY_TAGS);
68+
}
69+
}
70+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.capturecat.core.domain.image.dto;
2+
3+
import java.time.LocalDate;
4+
import java.util.List;
5+
6+
import lombok.Builder;
7+
8+
@Builder
9+
public record ImageSaveRequest(
10+
String fileName,
11+
String fileUrl,
12+
long size,
13+
LocalDate captureDate,
14+
List<String> tagNames) {
15+
}

capturecat-core/src/main/java/com/capturecat/core/domain/tag/ImageTagRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public interface ImageTagRepository extends JpaRepository<ImageTag, Long> {
2424

2525
boolean existsByTag(Tag tag);
2626

27+
boolean existsByImageAndTagIn(Image image, List<Tag> tags);
28+
2729
void deleteAllByImage(Image image);
2830

2931
@Modifying

capturecat-core/src/main/java/com/capturecat/core/domain/tag/Tag.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import jakarta.persistence.Id;
77

88
import lombok.AccessLevel;
9+
import lombok.EqualsAndHashCode;
910
import lombok.Getter;
1011
import lombok.NoArgsConstructor;
1112
import lombok.ToString;
@@ -16,6 +17,7 @@
1617
@NoArgsConstructor(access = AccessLevel.PROTECTED)
1718
@Getter
1819
@ToString
20+
@EqualsAndHashCode(of = "id", callSuper = false)
1921
public class Tag extends BaseTimeEntity {
2022

2123
@Id

capturecat-core/src/main/java/com/capturecat/core/service/image/ImageService.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import java.util.ArrayList;
44
import java.util.List;
5+
import java.util.Map;
6+
import java.util.function.Function;
7+
import java.util.stream.Collectors;
58

69
import org.springframework.data.domain.Pageable;
710
import org.springframework.data.domain.Slice;
@@ -14,11 +17,14 @@
1417
import com.capturecat.client.upload.DeleteException;
1518
import com.capturecat.client.upload.FileUploader;
1619
import com.capturecat.client.upload.UploadException;
20+
import com.capturecat.core.api.image.dto.ImageRequestDto;
1721
import com.capturecat.core.api.image.dto.UploadItemRequest;
1822
import com.capturecat.core.domain.bookmark.Bookmark;
1923
import com.capturecat.core.domain.bookmark.BookmarkRepository;
2024
import com.capturecat.core.domain.image.Image;
25+
import com.capturecat.core.domain.image.ImageCreator;
2126
import com.capturecat.core.domain.image.ImageRepository;
27+
import com.capturecat.core.domain.image.dto.ImageSaveRequest;
2228
import com.capturecat.core.domain.tag.ImageTag;
2329
import com.capturecat.core.domain.tag.ImageTagFactory;
2430
import com.capturecat.core.domain.tag.ImageTagRepository;
@@ -48,6 +54,7 @@ public class ImageService {
4854
private final TagRepository tagRepository;
4955
private final UserRepository userRepository;
5056
private final BookmarkRepository bookmarkRepository;
57+
private final ImageCreator imageCreator;
5158

5259
@Transactional
5360
// TODO: UploadItemRequest의 api 패키지 의존성 제거 고민하기 및 트랜잭션 분리
@@ -94,6 +101,41 @@ public void save(List<UploadItemRequest> uploadItems, List<MultipartFile> files,
94101
imageTagRepository.saveAll(allImageTags);
95102
}
96103

104+
public void createImages(List<ImageRequestDto.UploadItem> uploadItems, LoginUser loginUser) {
105+
// 1. 태그 등록
106+
List<String> allTagNames = uploadItems.stream()
107+
.flatMap(item -> item.tagNames().stream())
108+
.distinct()
109+
.toList();
110+
111+
Map<String, Tag> registeredTags = tagRegister.registerTagsFor(allTagNames).stream()
112+
.collect(Collectors.toMap(Tag::getName, Function.identity()));
113+
114+
// 2. 이미지 정보와 태그 엔티티 매핑
115+
List<ImageRequestDto.ImageCreateData> requests = uploadItems.stream()
116+
.map(each -> {
117+
// TODO: Pre-signed URL 및 fileURL 생성 필요
118+
ImageSaveRequest imageSaveRequest = ImageSaveRequest.builder()
119+
.fileName(each.fileName())
120+
.fileUrl("")
121+
.size(each.fileSize())
122+
.captureDate(DateTimeConverter.convert(each.captureDate()))
123+
.tagNames(each.tagNames())
124+
.build();
125+
126+
List<Tag> tags = each.tagNames().stream()
127+
.map(registeredTags::get)
128+
.toList();
129+
130+
return new ImageRequestDto.ImageCreateData(imageSaveRequest, tags);
131+
}).toList();
132+
133+
// 3. 이미지 및 이미지 태그 저장
134+
imageCreator.createAll(loginUser, requests);
135+
136+
// TODO: 응답 리턴(생성된 이미지 + pre-signed URL)
137+
}
138+
97139
@Transactional
98140
public List<TagResponse> addTagsToImage(Long imageId, List<String> tagNames, LoginUser loginUser) {
99141
User user = userRepository.findByUsername(loginUser.getUsername())

capturecat-core/src/main/java/com/capturecat/core/service/user/UserService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import com.capturecat.core.api.user.dto.UserRespDto;
1414
import com.capturecat.core.api.user.dto.UserRespDto.JoinRespDto;
1515
import com.capturecat.core.domain.bookmark.BookmarkRepository;
16-
import com.capturecat.core.domain.image.Image;
1716
import com.capturecat.core.domain.image.ImageRepository;
1817
import com.capturecat.core.domain.tag.ImageTagRepository;
1918
import com.capturecat.core.domain.user.User;
@@ -97,6 +96,7 @@ public void updateTutorialCompleted(LoginUser loginUser) {
9796
* 2) 탈퇴 사유 저장 - 실패해도 1,2 롤백 X (별도 TX)
9897
* 3) 회원 관련 데이터 삭제
9998
*/
99+
@Transactional
100100
public String withdraw(LoginUser loginUser, String reason) {
101101
User user = userRepository.findByUsername(loginUser.getUsername()) //email
102102
.orElseThrow(() -> new CoreException(ErrorType.USER_NOT_FOUND));

capturecat-core/src/main/java/com/capturecat/core/support/handler/CoreExceptionHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public ResponseEntity<ApiResponse<?>> handleMethodArgumentNotValidException(
3939

4040
@ExceptionHandler(MissingServletRequestParameterException.class)
4141
public ResponseEntity<ApiResponse<?>> handle(MissingServletRequestParameterException ex) {
42-
log.warn("Missing request parameter: {}", ex.getMessage(), ex);
42+
log.warn("Missing imageSaveRequest parameter: {}", ex.getMessage(), ex);
4343
return ResponseEntity.badRequest()
4444
.body(ApiResponse.error(ErrorType.MISSING_PARAMETER, (Object) ex.getParameterName()));
4545
}

0 commit comments

Comments
 (0)