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
@@ -1,6 +1,9 @@
package kr.spot.review.application.command;

import java.util.List;
import kr.spot.IdGenerator;
import kr.spot.code.status.ErrorStatus;
import kr.spot.exception.GeneralException;
import kr.spot.ports.FileStoragePort;
import kr.spot.ports.GetWriterInfoPort;
import kr.spot.ports.dto.UploadResult;
Expand All @@ -22,6 +25,7 @@
public class ManageReviewService {

private static final String REVIEW_IMAGE_DIR = "reviews";
private static final int MAX_REVIEW_IMAGE_COUNT = 3;

private final IdGenerator idGenerator;
private final GetWriterInfoPort getWriterInfoPort;
Expand All @@ -30,13 +34,13 @@ public class ManageReviewService {
private final StudyAccessValidator studyAccessValidator;

public long createReview(long studyId, long memberId, CreateReviewRequest request,
MultipartFile imageFile) {
List<MultipartFile> imageFiles) {
studyAccessValidator.validateStudyMember(studyId, memberId);

WriterInfo writerInfo = getWriterInfo(memberId);
String imageUrl = uploadImage(imageFile);
List<String> imageUrls = uploadImages(imageFiles);
Content content = Content.of(request.activity(), request.learned(), request.encouragement(),
imageUrl);
imageUrls);

long reviewId = idGenerator.nextId();
Review review = Review.of(reviewId, studyId, writerInfo, content, request.isPrivate());
Expand All @@ -58,10 +62,25 @@ private WriterInfo getWriterInfo(long memberId) {
return WriterInfo.of(response.writerId(), response.nickname(), response.profileImageUrl());
}

private String uploadImage(MultipartFile imageFile) {
if (imageFile == null || imageFile.isEmpty()) {
return null;
private List<String> uploadImages(List<MultipartFile> imageFiles) {
if (imageFiles == null || imageFiles.isEmpty()) {
return List.of();
}

List<MultipartFile> validImageFiles = imageFiles.stream()
.filter(file -> file != null && !file.isEmpty())
.toList();

if (validImageFiles.size() > MAX_REVIEW_IMAGE_COUNT) {
throw new GeneralException(ErrorStatus._BAD_REQUEST);
}

return validImageFiles.stream()
.map(this::uploadImage)
.toList();
}

private String uploadImage(MultipartFile imageFile) {
UploadResult result = fileStoragePort.upload(imageFile, REVIEW_IMAGE_DIR);
return result.url();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,14 @@ private ContentResponse createContentResponse(Review review, Content content,
PRIVATE_CONTENT_MESSAGE,
PRIVATE_CONTENT_MESSAGE,
PRIVATE_CONTENT_MESSAGE,
null
List.of()
);
}
return ContentResponse.from(
content.getActivity(),
content.getLearned(),
content.getEncouragement(),
content.getImageUrl()
content.getImageUrls()
);
}
}
37 changes: 35 additions & 2 deletions modules/study/src/main/java/kr/spot/review/domain/vo/Content.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package kr.spot.review.domain.vo;

import jakarta.persistence.Embeddable;
import java.util.ArrayList;
import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
Expand All @@ -24,9 +26,40 @@ public class Content {
private String encouragement;

private String imageUrl;
private String imageUrl2;
private String imageUrl3;

public static Content of(String activity, String learned,
String encouragement, String imageUrl) {
return new Content(activity, learned, encouragement, imageUrl);
String encouragement, List<String> imageUrls) {
List<String> safeImageUrls = imageUrls == null ? List.of() : imageUrls;
return new Content(
activity,
learned,
encouragement,
getImageUrlByIndex(safeImageUrls, 0),
getImageUrlByIndex(safeImageUrls, 1),
getImageUrlByIndex(safeImageUrls, 2)
);
}

public List<String> getImageUrls() {
List<String> imageUrls = new ArrayList<>();
if (imageUrl != null) {
imageUrls.add(imageUrl);
}
if (imageUrl2 != null) {
imageUrls.add(imageUrl2);
}
if (imageUrl3 != null) {
imageUrls.add(imageUrl3);
}
return List.copyOf(imageUrls);
}

private static String getImageUrlByIndex(List<String> imageUrls, int index) {
if (index >= imageUrls.size()) {
return null;
}
return imageUrls.get(index);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList;
import java.util.List;
import kr.spot.ApiResponse;
import kr.spot.annotations.CurrentMember;
import kr.spot.code.status.SuccessStatus;
Expand Down Expand Up @@ -37,9 +39,11 @@ public ResponseEntity<ApiResponse<CreateReviewResponse>> createReview(
@PathVariable Long studyId,
@CurrentMember @Parameter(hidden = true) Long memberId,
@RequestPart CreateReviewRequest request,
@RequestPart(required = false) MultipartFile imageFile
@RequestPart(required = false) MultipartFile imageFile,
@RequestPart(required = false) List<MultipartFile> imageFiles
) {
long reviewId = manageReviewService.createReview(studyId, memberId, request, imageFile);
long reviewId = manageReviewService.createReview(studyId, memberId, request,
mergeImageFiles(imageFile, imageFiles));
return ResponseEntity.ok(
ApiResponse.onSuccess(SuccessStatus._CREATED, CreateReviewResponse.from(reviewId)));
}
Expand Down Expand Up @@ -78,4 +82,16 @@ public ResponseEntity<ApiResponse<Void>> removeReaction(
manageReviewReactionService.removeReaction(studyId, reviewId, memberId, reaction);
return ResponseEntity.ok(ApiResponse.onSuccess(SuccessStatus._NO_CONTENT));
}

private List<MultipartFile> mergeImageFiles(MultipartFile imageFile,
List<MultipartFile> imageFiles) {
List<MultipartFile> mergedFiles = new ArrayList<>();
if (imageFiles != null) {
mergedFiles.addAll(imageFiles);
}
if (imageFile != null) {
mergedFiles.add(imageFile);
}
return mergedFiles;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ public record ContentResponse(
String activity,
String learned,
String encouragement,
String imageUrl
List<String> imageUrls
) {

public static ContentResponse from(String activity, String learned, String encouragement,
String imageUrl) {
return new ContentResponse(activity, learned, encouragement, imageUrl);
List<String> imageUrls) {
return new ContentResponse(activity, learned, encouragement, imageUrls);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.List;
import kr.spot.IdGenerator;
import kr.spot.code.status.ErrorStatus;
import kr.spot.exception.GeneralException;
Expand Down Expand Up @@ -154,12 +156,82 @@ void should_create_review_with_image() {
.thenAnswer(invocation -> invocation.getArgument(0));

// when
manageReviewService.createReview(STUDY_ID, MEMBER_ID, request, imageFile);
manageReviewService.createReview(STUDY_ID, MEMBER_ID, request, List.of(imageFile));

// then
verify(fileStoragePort).upload(imageFile, "reviews");
verify(reviewRepository).save(reviewCaptor.capture());
assertThat(reviewCaptor.getValue().getContent().getImageUrl()).isEqualTo(uploadedImageUrl);
assertThat(reviewCaptor.getValue().getContent().getImageUrls()).containsExactly(uploadedImageUrl);
}

@Test
@DisplayName("이미지는 최대 3개까지만 등록할 수 있다")
void should_throw_exception_when_images_exceed_limit() {
// given
CreateReviewRequest request = createReviewRequest();
WriterInfoResponse writerInfoResponse = WriterInfoResponse.of(MEMBER_ID, WRITER_NAME,
WRITER_PROFILE_IMAGE_URL);
MultipartFile imageFile1 = new MockMultipartFile("image1", "1.jpg", "image/jpeg",
"1".getBytes());
MultipartFile imageFile2 = new MockMultipartFile("image2", "2.jpg", "image/jpeg",
"2".getBytes());
MultipartFile imageFile3 = new MockMultipartFile("image3", "3.jpg", "image/jpeg",
"3".getBytes());
MultipartFile imageFile4 = new MockMultipartFile("image4", "4.jpg", "image/jpeg",
"4".getBytes());

doNothing().when(studyAccessValidator).validateStudyMember(anyLong(), anyLong());
when(getWriterInfoPort.get(anyLong())).thenReturn(writerInfoResponse);

// when & then
assertThatThrownBy(() -> manageReviewService.createReview(STUDY_ID, MEMBER_ID, request,
List.of(imageFile1, imageFile2, imageFile3, imageFile4)))
.isInstanceOf(GeneralException.class)
.hasFieldOrPropertyWithValue("status", ErrorStatus._BAD_REQUEST);

verify(fileStoragePort, never()).upload(any(MultipartFile.class), anyString());
verify(reviewRepository, never()).save(any());
}

@Test
@DisplayName("이미지 3개를 모두 등록할 수 있다")
void should_create_review_with_three_images() {
// given
Long generatedId = 1L;
CreateReviewRequest request = createReviewRequest();
WriterInfoResponse writerInfoResponse = WriterInfoResponse.of(MEMBER_ID, WRITER_NAME,
WRITER_PROFILE_IMAGE_URL);
MultipartFile imageFile1 = new MockMultipartFile("image1", "1.jpg", "image/jpeg",
"1".getBytes());
MultipartFile imageFile2 = new MockMultipartFile("image2", "2.jpg", "image/jpeg",
"2".getBytes());
MultipartFile imageFile3 = new MockMultipartFile("image3", "3.jpg", "image/jpeg",
"3".getBytes());
String uploadedImageUrl1 = "https://s3.example.com/reviews/1.jpg";
String uploadedImageUrl2 = "https://s3.example.com/reviews/2.jpg";
String uploadedImageUrl3 = "https://s3.example.com/reviews/3.jpg";

doNothing().when(studyAccessValidator).validateStudyMember(anyLong(), anyLong());
when(idGenerator.nextId()).thenReturn(generatedId);
when(getWriterInfoPort.get(anyLong())).thenReturn(writerInfoResponse);
when(fileStoragePort.upload(any(MultipartFile.class), anyString()))
.thenReturn(
new UploadResult(uploadedImageUrl1, "1.jpg"),
new UploadResult(uploadedImageUrl2, "2.jpg"),
new UploadResult(uploadedImageUrl3, "3.jpg")
);
when(reviewRepository.save(any(Review.class)))
.thenAnswer(invocation -> invocation.getArgument(0));

// when
manageReviewService.createReview(STUDY_ID, MEMBER_ID, request,
List.of(imageFile1, imageFile2, imageFile3));

// then
verify(fileStoragePort, times(3)).upload(any(MultipartFile.class), anyString());
verify(reviewRepository).save(reviewCaptor.capture());
assertThat(reviewCaptor.getValue().getContent().getImageUrls())
.containsExactly(uploadedImageUrl1, uploadedImageUrl2, uploadedImageUrl3);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ void should_mask_content_for_non_study_member() {
assertThat(reviewResponse.content().activity()).isEqualTo(PRIVATE_CONTENT_MESSAGE);
assertThat(reviewResponse.content().learned()).isEqualTo(PRIVATE_CONTENT_MESSAGE);
assertThat(reviewResponse.content().encouragement()).isEqualTo(PRIVATE_CONTENT_MESSAGE);
assertThat(reviewResponse.content().imageUrl()).isNull();
assertThat(reviewResponse.content().imageUrls()).isEmpty();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kr.spot.review.common;

import java.util.List;
import kr.spot.review.domain.Review;
import kr.spot.review.domain.associations.ReviewReaction;
import kr.spot.review.domain.enums.Reaction;
Expand All @@ -21,6 +22,8 @@ public class ReviewFixture {
public static final String LEARNED = "다이나믹 프로그래밍의 개념을 이해했습니다.";
public static final String ENCOURAGEMENT = "오늘도 수고했어!";
public static final String IMAGE_URL = "https://example.com/review-image.jpg";
public static final String IMAGE_URL_2 = "https://example.com/review-image-2.jpg";
public static final String IMAGE_URL_3 = "https://example.com/review-image-3.jpg";

public static WriterInfo writerInfo() {
return WriterInfo.of(MEMBER_ID, WRITER_NAME, WRITER_PROFILE_IMAGE_URL);
Expand All @@ -31,11 +34,17 @@ public static WriterInfo writerInfo(Long memberId) {
}

public static Content content() {
return Content.of(ACTIVITY, LEARNED, ENCOURAGEMENT, IMAGE_URL);
return Content.of(ACTIVITY, LEARNED, ENCOURAGEMENT, List.of(IMAGE_URL));
}

public static Content content(String imageUrl) {
return Content.of(ACTIVITY, LEARNED, ENCOURAGEMENT, imageUrl);
return Content.of(ACTIVITY, LEARNED, ENCOURAGEMENT, imageUrl == null
? List.of()
: List.of(imageUrl));
}

public static Content content(List<String> imageUrls) {
return Content.of(ACTIVITY, LEARNED, ENCOURAGEMENT, imageUrls);
}

public static Review review() {
Expand Down
Loading