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 @@ -96,6 +96,20 @@ public SliceResponse<AlbumImageListResponse> albumImagesGet(
return imageService.getAlbumImages(albumId, lastImageId, size, parameter, direction);
}

@GetMapping("/temp-albums/{tempAlbumId}/images")
@Operation(summary = "임시 앨범 이미지 목록 조회", description = "임시 앨범의 이미지 목록을 조회합니다.")
public SliceResponse<TempAlbumImageListResponse> tempAlbumImagesGet(
@PathVariable Long tempAlbumId,
@Parameter(description = "이전 페이지의 마지막 임시 앨범 이미지 ID (첫 요청 시 생략)")
@RequestParam(required = false)
Long tempAlbumImageId,
@Parameter(description = "페이지당 조회할 임시 앨범 이미지의 수") @RequestParam @PageSize Integer size,
@Parameter(description = "정렬 방향 (ASC: 오래된순, DESC: 최신순)")
@RequestParam(defaultValue = "DESC")
SortDirection direction) {
return imageService.getTempAlbumImages(tempAlbumId, tempAlbumImageId, size, direction);
}

@GetMapping("/events/{eventId}/images")
@Operation(summary = "이벤트 이미지 목록 조회", description = "이벤트 이미지 목록을 조회합니다.")
public SliceResponse<EventImageListResponse> eventImagesGet(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.cherrypic.domain.image.dto.response;

import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;

public record TempAlbumImageListResponse(
@Schema(description = "임시 앨범 이미지 ID", example = "1") Long tempAlbumImageId,
@Schema(description = "임시 앨범 이미지 url", example = "https://example.jpg")
String tempAlbumImageUrl,
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd",
timezone = "Asia/Seoul")
@Schema(description = "필터링 기준 날짜", example = "2025-10-01")
LocalDateTime date) {}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ SliceResponse<AlbumImageListResponse> getAlbumImages(
SortParameter parameter,
SortDirection direction);

SliceResponse<TempAlbumImageListResponse> getTempAlbumImages(
Long tempAlbumId, Long lasTempAlbumImageId, int size, SortDirection direction);

SliceResponse<EventImageListResponse> getEventImages(
Long eventId,
Long lastImageId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,22 @@ public SliceResponse<AlbumImageListResponse> getAlbumImages(
return SliceResponse.from(result);
}

@Override
@Transactional(readOnly = true)
public SliceResponse<TempAlbumImageListResponse> getTempAlbumImages(
Long tempAlbumId, Long lasTempAlbumImageId, int size, SortDirection direction) {
final Member currentMember = memberUtil.getCurrentMember();
final TempAlbum tempAlbum = getTempAlbumById(tempAlbumId);

validateTempAlbumOwner(tempAlbum, currentMember);

Slice<TempAlbumImageListResponse> result =
tempAlbumImageRepository.findAllByTempAlbumId(
tempAlbum.getId(), lasTempAlbumImageId, size, direction);

return SliceResponse.from(result);
}

@Override
@Transactional(readOnly = true)
public SliceResponse<EventImageListResponse> getEventImages(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

public interface TempAlbumImageRepository extends JpaRepository<TempAlbumImage, Long> {
public interface TempAlbumImageRepository
extends JpaRepository<TempAlbumImage, Long>, TempAlbumImageRepositoryCustom {

@Modifying(clearAutomatically = true)
@Query("delete from TempAlbumImage i where i.tempAlbum.id = :tempAlbumId")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.cherrypic.domain.tempalbum.repository;

import org.cherrypic.domain.image.dto.response.TempAlbumImageListResponse;
import org.cherrypic.global.pagination.SortDirection;
import org.springframework.data.domain.Slice;

public interface TempAlbumImageRepositoryCustom {

Slice<TempAlbumImageListResponse> findAllByTempAlbumId(
Long tempAlbumId, Long lastTempAlbumImageId, int size, SortDirection direction);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.cherrypic.domain.tempalbum.repository;

import static org.cherrypic.tempalbum.entity.QTempAlbumImage.tempAlbumImage;

import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.cherrypic.domain.image.dto.response.TempAlbumImageListResponse;
import org.cherrypic.global.pagination.SortDirection;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class TempAlbumImageRepositoryImpl implements TempAlbumImageRepositoryCustom {

private final JPAQueryFactory queryFactory;

@Override
public Slice<TempAlbumImageListResponse> findAllByTempAlbumId(
Long tempAlbumId, Long lastTempAlbumImageId, int size, SortDirection direction) {

List<TempAlbumImageListResponse> results =
queryFactory
.select(
Projections.constructor(
TempAlbumImageListResponse.class,
tempAlbumImage.id,
tempAlbumImage.url,
tempAlbumImage.createdAt))
.from(tempAlbumImage)
.where(
tempAlbumImage.tempAlbum.id.eq(tempAlbumId),
lastTempAlbumImageIdCondition(lastTempAlbumImageId, direction))
.orderBy(
direction == SortDirection.DESC
? tempAlbumImage.id.desc()
: tempAlbumImage.id.asc())
.limit((long) size + 1)
.fetch();

return checkLastPage(size, results);
}

private BooleanExpression lastTempAlbumImageIdCondition(
Long tempAlbumImageId, SortDirection direction) {
if (tempAlbumImageId == null) {
return null;
}

return direction == SortDirection.DESC
? tempAlbumImage.id.lt(tempAlbumImageId)
: tempAlbumImage.id.gt(tempAlbumImageId);
}

private <T> Slice<T> checkLastPage(int pageSize, List<T> results) {
boolean hasNext = false;

if (results.size() > pageSize) {
hasNext = true;
results.remove(pageSize);
}

return new SliceImpl<>(results, PageRequest.of(0, pageSize), hasNext);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,216 @@ class 앨범_이미지_목록_조회_요청시 {
}
}

@Nested
class 임시_앨범_이미지_목록_조회_요청시 {

@Test
void 정렬_조건이_ASC이면_tempAlbumImageId를_오름차순으로_응답한다() throws Exception {
// given
List<TempAlbumImageListResponse> images =
List.of(
new TempAlbumImageListResponse(
1L, "testImageUrl1", LocalDateTime.of(2025, 1, 1, 0, 0)),
new TempAlbumImageListResponse(
2L, "testImageUrl2", LocalDateTime.of(2025, 1, 2, 0, 0)));

given(imageService.getTempAlbumImages(1L, null, 2, SortDirection.ASC))
.willReturn(new SliceResponse<>(images, true));

// when & then
ResultActions perform =
mockMvc.perform(
get("/temp-albums/1/images")
.param("size", "2")
.param("direction", "ASC"));

perform.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.status").value(HttpStatus.OK.value()))
.andExpect(jsonPath("$.data.content[0].tempAlbumImageId").value(1))
.andExpect(jsonPath("$.data.content[1].tempAlbumImageId").value(2))
.andExpect(jsonPath("$.data.isLast").value(true));
}

@Test
void 정렬_파라미터가_UPLOAD이고_정렬_조건이_DESC면_imageId를_내림차순으로_응답한다() throws Exception {
// given
List<TempAlbumImageListResponse> images =
List.of(
new TempAlbumImageListResponse(
2L, "testImageUrl1", LocalDateTime.of(2025, 1, 1, 0, 0)),
new TempAlbumImageListResponse(
1L, "testImageUrl2", LocalDateTime.of(2025, 1, 2, 0, 0)));

given(imageService.getTempAlbumImages(1L, null, 2, SortDirection.DESC))
.willReturn(new SliceResponse<>(images, true));

// when & then
ResultActions perform =
mockMvc.perform(
get("/temp-albums/1/images")
.param("size", "2")
.param("direction", "DESC"));

perform.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.status").value(HttpStatus.OK.value()))
.andExpect(jsonPath("$.data.content[0].tempAlbumImageId").value(2))
.andExpect(jsonPath("$.data.content[1].tempAlbumImageId").value(1))
.andExpect(jsonPath("$.data.isLast").value(true));
}

@Test
void 마지막_페이지인_경우_isLast를_true로_응답한다() throws Exception {
// given
List<TempAlbumImageListResponse> images =
List.of(
new TempAlbumImageListResponse(
1L, "testImageUrl1", LocalDateTime.of(2025, 1, 1, 0, 0)));

given(imageService.getTempAlbumImages(1L, null, 1, SortDirection.ASC))
.willReturn(new SliceResponse<>(images, true));

// when & then
ResultActions perform =
mockMvc.perform(
get("/temp-albums/1/images")
.param("size", "1")
.param("direction", "ASC"));

perform.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.status").value(HttpStatus.OK.value()))
.andExpect(jsonPath("$.data.content[0].tempAlbumImageId").value(1))
.andExpect(jsonPath("$.data.isLast").value(true));
}

@Test
void 마지막_페이지가_아닌_경우_isLast를_false로_응답한다() throws Exception {
// given
List<TempAlbumImageListResponse> images =
List.of(
new TempAlbumImageListResponse(
1L, "testImageUrl1", LocalDateTime.of(2025, 1, 1, 0, 0)),
new TempAlbumImageListResponse(
2L, "testImageUrl2", LocalDateTime.of(2025, 1, 2, 0, 0)));

given(imageService.getTempAlbumImages(1L, null, 1, SortDirection.ASC))
.willReturn(new SliceResponse<>(images, false));

// when & then
ResultActions perform =
mockMvc.perform(
get("/temp-albums/1/images")
.param("size", "1")
.param("direction", "ASC"));

perform.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.status").value(HttpStatus.OK.value()))
.andExpect(jsonPath("$.data.content[0].tempAlbumImageId").value(1))
.andExpect(jsonPath("$.data.content[1].tempAlbumImageId").value(2))
.andExpect(jsonPath("$.data.isLast").value(false));
}

@Test
void 임시_앨범_이미지가_없는_경우_빈_리스트를_응답한다() throws Exception {
// given
List<TempAlbumImageListResponse> images = List.of();

given(imageService.getTempAlbumImages(1L, null, 1, SortDirection.ASC))
.willReturn(new SliceResponse<>(images, true));

// when & then
ResultActions perform =
mockMvc.perform(
get("/temp-albums/1/images")
.param("size", "1")
.param("parameter", "UPLOAD")
.param("direction", "ASC"));

perform.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.status").value(HttpStatus.OK.value()))
.andExpect(jsonPath("$.data.content").isEmpty())
.andExpect(jsonPath("$.data.isLast").value(true));
}

@Test
void 임시_앨범이_존재하지_않을_경우_예외가_발생한다() throws Exception {
// given
given(imageService.getTempAlbumImages(999L, null, 2, SortDirection.ASC))
.willThrow(new CustomException(TempAlbumErrorCode.TEMP_ALBUM_NOT_FOUND));

// when & then
ResultActions perform =
mockMvc.perform(
get("/temp-albums/999/images")
.param("size", "2")
.param("direction", "ASC"));

perform.andExpect(status().isNotFound())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.status").value(HttpStatus.NOT_FOUND.value()))
.andExpect(jsonPath("$.data.code").value("TEMP_ALBUM_NOT_FOUND"))
.andExpect(jsonPath("$.data.message").value("임시 앨범이 존재하지 않습니다."));
}

@Test
void 임시_앨범_소유자가_아닌_경우_예외가_발생한다() throws Exception {
// given
given(imageService.getTempAlbumImages(1L, null, 2, SortDirection.ASC))
.willThrow(new CustomException(TempAlbumErrorCode.NOT_TEMP_ALBUM_OWNER));

// when & then
ResultActions perform =
mockMvc.perform(
get("/temp-albums/1/images")
.param("size", "2")
.param("direction", "ASC"));

perform.andExpect(status().isForbidden())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.status").value(HttpStatus.FORBIDDEN.value()))
.andExpect(jsonPath("$.data.code").value("NOT_TEMP_ALBUM_OWNER"))
.andExpect(jsonPath("$.data.message").value("임시 앨범 소유자가 아닌 경우 권한이 없습니다."));
}

@ParameterizedTest
@ValueSource(strings = {"-1", "-999", "0"})
void 페이지_크기를_0_이하로_설정하면_예외가_발생한다(String pageSize) throws Exception {
// when & then
ResultActions perform =
mockMvc.perform(
get("/temp-albums/1/images")
.param("size", pageSize)
.param("direction", "ASC"));

perform.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value()))
.andExpect(jsonPath("$.data.code").value("ConstraintViolationException"))
.andExpect(jsonPath("$.data.message").value("페이지 크기는 0보다 큰 값만 가능합니다."));
}

@ParameterizedTest
@ValueSource(strings = {"ASCC", "DESCC", "OLDEST", "NEWEST"})
void 존재하지_않는_정렬_기준을_입력한_경우_예외가_발생한다(String sort) throws Exception {
// when & then
ResultActions perform =
mockMvc.perform(
get("/temp-albums/1/images")
.param("size", "1")
.param("direction", sort));

perform.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value()))
.andExpect(jsonPath("$.data.code").value("METHOD_ARGUMENT_TYPE_MISMATCH"))
.andExpect(jsonPath("$.data.message").value("요청한 값의 타입이 잘못되어 처리할 수 없습니다."));
}
}

@Nested
class 이벤트_이미지_목록_조회_요청시 {

Expand Down
Loading