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 @@ -33,7 +33,16 @@ public static String dailyGoal(Long memberId, Long tripId, Long dailyGoalId) {
return "member:" + memberId + ":trip:" + tripId + ":dailyGoal:" + dailyGoalId;
}

public static String studyLogs(Long memberId, Long tripId, int page, int size) {
return "member:" + memberId + ":trip:" + tripId + ":page:" + page + ":size:" + size;
public static String studyLogs(Long memberId, Long tripId, int page, int size, String order) {
return "member:"
+ memberId
+ ":trip:"
+ tripId
+ ":page:"
+ page
+ ":size:"
+ size
+ ":order:"
+ order.toLowerCase();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@
import com.ject.studytrip.pomodoro.domain.model.Pomodoro;
import com.ject.studytrip.stamp.application.service.StampCommandService;
import com.ject.studytrip.stamp.domain.model.Stamp;
import com.ject.studytrip.studylog.application.dto.PresignedStudyLogImageInfo;
import com.ject.studytrip.studylog.application.dto.StudyLogDetail;
import com.ject.studytrip.studylog.application.dto.StudyLogInfo;
import com.ject.studytrip.studylog.application.dto.StudyLogSliceInfo;
import com.ject.studytrip.studylog.application.dto.*;
import com.ject.studytrip.studylog.application.service.*;
import com.ject.studytrip.studylog.domain.model.StudyLog;
import com.ject.studytrip.studylog.domain.model.StudyLogDailyMission;
Expand Down Expand Up @@ -90,15 +87,16 @@ public StudyLogInfo createStudyLog(
@Cacheable(
cacheNames = STUDY_LOGS,
key =
"T(com.ject.studytrip.global.common.factory.CacheKeyFactory).studyLogs(#memberId, #tripId, #page, #size)")
"T(com.ject.studytrip.global.common.factory.CacheKeyFactory).studyLogs(#memberId, #tripId, #page, #size, #order)")
@Transactional(readOnly = true)
public StudyLogSliceInfo getStudyLogsByTrip(Long memberId, Long tripId, int page, int size) {
public StudyLogSliceInfo getStudyLogsByTrip(
Long memberId, Long tripId, int page, int size, String order) {
// 1. 유효성 검증 및 엔티티 조회
Trip trip = tripQueryService.getValidTrip(memberId, tripId);

// 2. 페이징된 학습 로그 목록 조회
Slice<StudyLog> studyLogSlice =
studyLogQueryService.getStudyLogsSliceByTripId(trip.getId(), page, size);
studyLogQueryService.getStudyLogsSliceByTripId(trip.getId(), page, size, order);

// 3. 학습 로그 상세 정보 구성
return buildStudyLogDetailsSlice(studyLogSlice);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ public long getActiveStudyLogCountByMemberId(Long memberId) {
return studyLogQueryRepository.countActiveStudyLogsByMemberId(memberId);
}

public Slice<StudyLog> getStudyLogsSliceByTripId(Long tripId, int page, int size) {
return studyLogQueryRepository.findSliceByTripIdOrderByCreatedAtDesc(
tripId, PageRequest.of(page, size));
public Slice<StudyLog> getStudyLogsSliceByTripId(
Long tripId, int page, int size, String order) {
return studyLogQueryRepository.findSliceByTripId(tripId, PageRequest.of(page, size), order);
}

public StudyLog getValidStudyLog(Long studyLogId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
public interface StudyLogQueryRepository {
long countActiveStudyLogsByMemberId(Long memberId);

Slice<StudyLog> findSliceByTripIdOrderByCreatedAtDesc(Long tripId, Pageable pageable);
Slice<StudyLog> findSliceByTripId(Long tripId, Pageable pageable, String order);

long deleteAllByDeletedAtIsNotNull();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.ject.studytrip.studylog.domain.repository.StudyLogQueryRepository;
import com.ject.studytrip.trip.domain.model.QDailyGoal;
import com.ject.studytrip.trip.domain.model.QTripReportStudyLog;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
Expand Down Expand Up @@ -38,15 +39,15 @@ public long countActiveStudyLogsByMemberId(Long memberId) {
}

@Override
public Slice<StudyLog> findSliceByTripIdOrderByCreatedAtDesc(Long tripId, Pageable pageable) {
public Slice<StudyLog> findSliceByTripId(Long tripId, Pageable pageable, String order) {
List<StudyLog> content =
queryFactory
.selectFrom(studyLog)
.join(studyLog.dailyGoal, dailyGoal)
.where(dailyGoal.trip.id.eq(tripId), dailyGoal.deletedAt.isNull())
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1)
.orderBy(studyLog.createdAt.desc())
.orderBy(orderSpecifiers(order))
.fetch();

List<StudyLog> result = content;
Expand Down Expand Up @@ -124,4 +125,10 @@ public Slice<StudyLog> findSliceByTripReportIdOrderByCreatedAtDesc(

return new SliceImpl<>(result, pageable, hasNext);
}

private OrderSpecifier<?>[] orderSpecifiers(String order) {
return (order.equalsIgnoreCase("OLDEST"))
? new OrderSpecifier<?>[] {studyLog.createdAt.asc(), studyLog.id.asc()}
: new OrderSpecifier<?>[] {studyLog.createdAt.desc(), studyLog.id.desc()};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -49,15 +50,22 @@ public ResponseEntity<StandardResponse> createStudyLog(

@Operation(
summary = "여행의 학습 로그 목록 조회",
description = "특정 여행의 학습 로그 목록을 조회하는 API 입니다. 슬라이스를 적용하고 최신순으로 정렬합니다.")
description =
"특정 여행의 학습 로그 목록을 조회하는 API 입니다. 슬라이스를 적용하고 정렬 옵션 LATEST(최신순)/OLDEST(과거순)을 적용합니다.")
@GetMapping("/api/trips/{tripId}/study-logs")
public ResponseEntity<StandardResponse> loadStudyLogsByTrip(
@AuthenticationPrincipal String memberId,
@PathVariable @NotNull(message = "여행 ID는 필수 요청 파라미터입니다.") Long tripId,
@RequestParam(name = "page", defaultValue = "0") @Min(0) int page,
@RequestParam(name = "size", defaultValue = "5") @Min(1) @Max(10) int size) {
@RequestParam(name = "size", defaultValue = "5") @Min(1) @Max(10) int size,
@RequestParam(name = "order", defaultValue = "LATEST")
@Pattern(
regexp = "LATEST|OLDEST",
message = "정렬은 최신순(LATEST) 또는 과거순(OLDEST)만 허용됩니다.")
String order) {
StudyLogSliceInfo result =
studyLogFacade.getStudyLogsByTrip(Long.valueOf(memberId), tripId, page, size);
studyLogFacade.getStudyLogsByTrip(
Long.valueOf(memberId), tripId, page, size, order);

return ResponseEntity.status(HttpStatus.OK)
.body(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public TripRetrospectDetail getTripRetrospect(Long memberId, Long tripId, int pa
Member member = memberQueryService.getValidMember(memberId);
Trip trip = tripQueryService.getValidCompletedTrip(member.getId(), tripId); // 완료된 여행
Slice<StudyLog> studyLogSlice =
studyLogQueryService.getStudyLogsSliceByTripId(trip.getId(), page, size);
studyLogQueryService.getStudyLogsSliceByTripId(trip.getId(), page, size, "LATEST");

long studyLogCount = studyLogQueryService.getStudyLogCountByTripId(trip.getId());
long totalFocusHours = pomodoroQueryService.getTotalFocusHoursByTripId(trip.getId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,29 +93,57 @@ class getStudyLogsSliceByTripId {

@Test
@DisplayName("특정 여행의 학습 로그 목록을 페이징 처리와 최신순으로 정렬하고 반환한다")
void shouldReturnStudyLogsByTripIdWithSlice() {
void shouldReturnStudyLogsByTripIdWithLatestOrder() {
// given
Long tripId = courseTrip.getId();
List<StudyLog> studyLogs = List.of(studyLog1, studyLog2);

int page = 0;
int size = 5;
String order = "LATEST";
Pageable pageable = PageRequest.of(page, size);

Slice<StudyLog> mockSlice = new SliceImpl<>(studyLogs, pageable, false);

given(studyLogQueryRepository.findSliceByTripIdOrderByCreatedAtDesc(tripId, pageable))
given(studyLogQueryRepository.findSliceByTripId(tripId, pageable, order))
.willReturn(mockSlice);

// when
Slice<StudyLog> result =
studyLogQueryService.getStudyLogsSliceByTripId(tripId, page, size);
studyLogQueryService.getStudyLogsSliceByTripId(tripId, page, size, order);

// then
assertThat(result.getContent().size()).isEqualTo(studyLogs.size());
assertThat(result.getContent().get(0)).isEqualTo(studyLog1);
assertThat(result.getContent().get(1)).isEqualTo(studyLog2);
}

@Test
@DisplayName("특정 여행의 학습 로그 목록을 페이징 처리와 과거순으로 정렬하고 반환한다")
void shouldReturnStudyLogsByTripIdWithOldestOrder() {
// given
Long tripId = courseTrip.getId();
List<StudyLog> studyLogs = List.of(studyLog2, studyLog1); // 과거순이므로 순서 반대

int page = 0;
int size = 5;
String order = "OLDEST";
Pageable pageable = PageRequest.of(page, size);

Slice<StudyLog> mockSlice = new SliceImpl<>(studyLogs, pageable, false);

given(studyLogQueryRepository.findSliceByTripId(tripId, pageable, order))
.willReturn(mockSlice);

// when
Slice<StudyLog> result =
studyLogQueryService.getStudyLogsSliceByTripId(tripId, page, size, order);

// then
assertThat(result.getContent().size()).isEqualTo(studyLogs.size());
assertThat(result.getContent().get(0)).isEqualTo(studyLog2);
assertThat(result.getContent().get(1)).isEqualTo(studyLog1);
}
}

@Nested
Expand Down Expand Up @@ -218,7 +246,7 @@ void shouldReturnCountWhenStudyLogExistsForTrip() {
}

@Nested
@DisplayName("getStudyLogsSliceByTripId 메서드는")
@DisplayName("getStudyLogsSliceByTripReportId 메서드는")
class GetStudyLogsSliceByTripReportId {

@Test
Expand All @@ -235,13 +263,14 @@ void shouldReturnStudyLogsByTripReportIdWithSlice() {
Slice<StudyLog> mockSlice = new SliceImpl<>(studyLogs, pageable, false);

given(
studyLogQueryRepository.findSliceByTripIdOrderByCreatedAtDesc(
studyLogQueryRepository.findSliceByTripReportIdOrderByCreatedAtDesc(
tripReport.getId(), pageable))
.willReturn(mockSlice);

// when
Slice<StudyLog> result =
studyLogQueryService.getStudyLogsSliceByTripId(tripReport.getId(), page, size);
studyLogQueryService.getStudyLogsSliceByTripReportId(
tripReport.getId(), page, size);

// then
assertThat(result.getContent().size()).isEqualTo(studyLogs.size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -567,13 +567,16 @@ void shouldReturnBadRequestWhenSelectedMissionIsAlreadyCompleted() throws Except
class ListStudyLogs {
private static final String DEFAULT_PAGE = "0";
private static final String DEFAULT_PAGE_SIZE = "5";
private static final String DEFAULT_ORDER = "LATEST";

private ResultActions getResultActions(
String token, Object tripId, String page, String size) throws Exception {
String token, Object tripId, String page, String size, String order)
throws Exception {
return mockMvc.perform(
get("/api/trips/{tripId}/study-logs", tripId)
.param("page", page)
.param("size", size)
.param("order", order)
.header(HttpHeaders.AUTHORIZATION, TokenFixture.TOKEN_PREFIX + token));
}

Expand All @@ -588,7 +591,39 @@ void shouldLoadStudyLogsByTripWithSlicePaging() throws Exception {

// when
ResultActions resultActions =
getResultActions(token, courseTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE);
getResultActions(
token,
courseTrip.getId(),
DEFAULT_PAGE,
DEFAULT_PAGE_SIZE,
DEFAULT_ORDER);

// then
resultActions
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.studyLogs").isNotEmpty())
.andExpect(jsonPath("$.data.hasNext").value(false))
.andExpect(jsonPath("$.data.studyLogs[0].studyLogId").value(studyLog.getId()))
.andExpect(jsonPath("$.data.studyLogs[0].dailyMissions").isNotEmpty())
.andExpect(
jsonPath("$.data.studyLogs[0].dailyMissions")
.value(Matchers.hasSize(studyLogDailyMissions.size())));
}

@Test
@DisplayName("order 파라미터를 OLDEST로 지정하면 과거순으로 정렬된 학습 로그 목록을 반환한다")
void shouldLoadStudyLogsByTripWithOldestOrder() throws Exception {
// given
StudyLog studyLog = studyLogTestHelper.saveStudyLog(member, dailyGoal);
List<StudyLogDailyMission> studyLogDailyMissions =
studyLogDailyMissionTestHelper.saveStudyLogDailyMissions(
studyLog, dailyMission);

// when
ResultActions resultActions =
getResultActions(
token, courseTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE, "OLDEST");

// then
resultActions
Expand All @@ -603,12 +638,33 @@ void shouldLoadStudyLogsByTripWithSlicePaging() throws Exception {
.value(Matchers.hasSize(studyLogDailyMissions.size())));
}

@Test
@DisplayName("order 파라미터가 유효하지 않은 값이면 400 Bad Request를 반환한다")
void shouldReturnBadRequestWhenOrderIsInvalid() throws Exception {
// when
ResultActions resultActions =
getResultActions(
token, courseTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE, "INVALID");

// then
resultActions
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(
jsonPath("$.status")
.value(
CommonErrorCode.METHOD_ARGUMENT_NOT_VALID
.getStatus()
.value()));
}

@Test
@DisplayName("인증되지 않은 사용자일 경우 401 Unauthorized를 반환한다")
void shouldReturnUnauthorizedWhenUnauthenticated() throws Exception {
// when
ResultActions resultActions =
getResultActions("", courseTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE);
getResultActions(
"", courseTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE, DEFAULT_ORDER);

// then
resultActions
Expand All @@ -627,7 +683,7 @@ void shouldReturnBadRequestWhenTripIdTypeMismatch() throws Exception {

// when
ResultActions resultActions =
getResultActions(token, tripId, DEFAULT_PAGE, DEFAULT_PAGE_SIZE);
getResultActions(token, tripId, DEFAULT_PAGE, DEFAULT_PAGE_SIZE, DEFAULT_ORDER);

// then
resultActions
Expand All @@ -649,7 +705,8 @@ void shouldReturnBadRequestWhenWhenPagingParameterTypeMismatch() throws Exceptio
String size = "test";

// when
ResultActions resultActions = getResultActions(token, courseTrip, page, size);
ResultActions resultActions =
getResultActions(token, courseTrip, page, size, DEFAULT_ORDER);

// then
resultActions
Expand All @@ -671,7 +728,8 @@ void shouldReturnBadRequestWhenWhenPagingParameterIsInvalid() throws Exception {
String size = "100";

// when
ResultActions resultActions = getResultActions(token, courseTrip.getId(), page, size);
ResultActions resultActions =
getResultActions(token, courseTrip.getId(), page, size, DEFAULT_ORDER);

// then
resultActions
Expand All @@ -693,7 +751,7 @@ void shouldReturnNotFoundWhenInvalidTripId() throws Exception {

// when
ResultActions resultActions =
getResultActions(token, tripId, DEFAULT_PAGE, DEFAULT_PAGE_SIZE);
getResultActions(token, tripId, DEFAULT_PAGE, DEFAULT_PAGE_SIZE, DEFAULT_ORDER);

// when & then
resultActions
Expand All @@ -713,7 +771,8 @@ void shouldReturnForbiddenWhenNotTripOwner() throws Exception {

// when
ResultActions resultActions =
getResultActions(token, newTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE);
getResultActions(
token, newTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE, DEFAULT_ORDER);

// then
resultActions
Expand All @@ -732,7 +791,8 @@ void shouldReturnBadRequestWhenAlreadyTrip() throws Exception {

// when
ResultActions resultActions =
getResultActions(token, deleted.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE);
getResultActions(
token, deleted.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE, DEFAULT_ORDER);

// then
resultActions
Expand Down