Skip to content

Commit ca50184

Browse files
committed
refactor: 학습 로그 목록 조회 정렬 로직 리팩토링 (#93)
* refactor: 학습 로그 목록 조회 API에 order 요청 파라미터 추가 * refactor: 학습 로그 목록 조회 쿼리에 LATEST(최신순)/OLDEST(과거순) 정렬 분기 적용 * refactor: 여행 리포트 조회 시 관련 학습 로그 목록은 최신순으로 조회하도록 수정 * test: 학습 로그 목록 조회 통합/단위 테스트 수정
1 parent 08eb7b4 commit ca50184

8 files changed

Lines changed: 133 additions & 31 deletions

File tree

src/main/java/com/ject/studytrip/studylog/application/facade/StudyLogFacade.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,7 @@
1414
import com.ject.studytrip.pomodoro.domain.model.Pomodoro;
1515
import com.ject.studytrip.stamp.application.service.StampCommandService;
1616
import com.ject.studytrip.stamp.domain.model.Stamp;
17-
import com.ject.studytrip.studylog.application.dto.PresignedStudyLogImageInfo;
18-
import com.ject.studytrip.studylog.application.dto.StudyLogDetail;
19-
import com.ject.studytrip.studylog.application.dto.StudyLogInfo;
20-
import com.ject.studytrip.studylog.application.dto.StudyLogSliceInfo;
17+
import com.ject.studytrip.studylog.application.dto.*;
2118
import com.ject.studytrip.studylog.application.service.*;
2219
import com.ject.studytrip.studylog.domain.model.StudyLog;
2320
import com.ject.studytrip.studylog.domain.model.StudyLogDailyMission;
@@ -92,13 +89,14 @@ public StudyLogInfo createStudyLog(
9289
key =
9390
"T(com.ject.studytrip.global.common.factory.CacheKeyFactory).studyLogs(#memberId, #tripId, #page, #size)")
9491
@Transactional(readOnly = true)
95-
public StudyLogSliceInfo getStudyLogsByTrip(Long memberId, Long tripId, int page, int size) {
92+
public StudyLogSliceInfo getStudyLogsByTrip(
93+
Long memberId, Long tripId, int page, int size, String order) {
9694
// 1. 유효성 검증 및 엔티티 조회
9795
Trip trip = tripQueryService.getValidTrip(memberId, tripId);
9896

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

103101
// 3. 학습 로그 상세 정보 구성
104102
return buildStudyLogDetailsSlice(studyLogSlice);

src/main/java/com/ject/studytrip/studylog/application/service/StudyLogQueryService.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ public long getActiveStudyLogCountByMemberId(Long memberId) {
2222
return studyLogQueryRepository.countActiveStudyLogsByMemberId(memberId);
2323
}
2424

25-
public Slice<StudyLog> getStudyLogsSliceByTripId(Long tripId, int page, int size) {
26-
return studyLogQueryRepository.findSliceByTripIdOrderByCreatedAtDesc(
27-
tripId, PageRequest.of(page, size));
25+
public Slice<StudyLog> getStudyLogsSliceByTripId(
26+
Long tripId, int page, int size, String order) {
27+
return studyLogQueryRepository.findSliceByTripId(tripId, PageRequest.of(page, size), order);
2828
}
2929

3030
public StudyLog getValidStudyLog(Long studyLogId) {

src/main/java/com/ject/studytrip/studylog/domain/repository/StudyLogQueryRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
public interface StudyLogQueryRepository {
88
long countActiveStudyLogsByMemberId(Long memberId);
99

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

1212
long deleteAllByDeletedAtIsNotNull();
1313

src/main/java/com/ject/studytrip/studylog/infra/querydsl/StudyLogQueryRepositoryAdapter.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.ject.studytrip.studylog.domain.repository.StudyLogQueryRepository;
77
import com.ject.studytrip.trip.domain.model.QDailyGoal;
88
import com.ject.studytrip.trip.domain.model.QTripReportStudyLog;
9+
import com.querydsl.core.types.OrderSpecifier;
910
import com.querydsl.jpa.JPAExpressions;
1011
import com.querydsl.jpa.impl.JPAQueryFactory;
1112
import java.util.List;
@@ -38,15 +39,15 @@ public long countActiveStudyLogsByMemberId(Long memberId) {
3839
}
3940

4041
@Override
41-
public Slice<StudyLog> findSliceByTripIdOrderByCreatedAtDesc(Long tripId, Pageable pageable) {
42+
public Slice<StudyLog> findSliceByTripId(Long tripId, Pageable pageable, String order) {
4243
List<StudyLog> content =
4344
queryFactory
4445
.selectFrom(studyLog)
4546
.join(studyLog.dailyGoal, dailyGoal)
4647
.where(dailyGoal.trip.id.eq(tripId), dailyGoal.deletedAt.isNull())
4748
.offset(pageable.getOffset())
4849
.limit(pageable.getPageSize() + 1)
49-
.orderBy(studyLog.createdAt.desc())
50+
.orderBy(orderSpecifiers(order))
5051
.fetch();
5152

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

125126
return new SliceImpl<>(result, pageable, hasNext);
126127
}
128+
129+
private OrderSpecifier<?>[] orderSpecifiers(String order) {
130+
return (order.equalsIgnoreCase("OLDEST"))
131+
? new OrderSpecifier<?>[] {studyLog.createdAt.asc(), studyLog.id.asc()}
132+
: new OrderSpecifier<?>[] {studyLog.createdAt.desc(), studyLog.id.desc()};
133+
}
127134
}

src/main/java/com/ject/studytrip/studylog/presentation/controller/StudyLogController.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import jakarta.validation.constraints.Max;
1818
import jakarta.validation.constraints.Min;
1919
import jakarta.validation.constraints.NotNull;
20+
import jakarta.validation.constraints.Pattern;
2021
import lombok.RequiredArgsConstructor;
2122
import org.springframework.http.HttpStatus;
2223
import org.springframework.http.ResponseEntity;
@@ -49,15 +50,22 @@ public ResponseEntity<StandardResponse> createStudyLog(
4950

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

6270
return ResponseEntity.status(HttpStatus.OK)
6371
.body(

src/main/java/com/ject/studytrip/trip/application/facade/TripReportFacade.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public TripRetrospectDetail getTripRetrospect(Long memberId, Long tripId, int pa
5151
Member member = memberQueryService.getValidMember(memberId);
5252
Trip trip = tripQueryService.getValidCompletedTrip(member.getId(), tripId); // 완료된 여행
5353
Slice<StudyLog> studyLogSlice =
54-
studyLogQueryService.getStudyLogsSliceByTripId(trip.getId(), page, size);
54+
studyLogQueryService.getStudyLogsSliceByTripId(trip.getId(), page, size, "LATEST");
5555

5656
long studyLogCount = studyLogQueryService.getStudyLogCountByTripId(trip.getId());
5757
long totalFocusHours = pomodoroQueryService.getTotalFocusHoursByTripId(trip.getId());

src/test/java/com/ject/studytrip/studylog/application/service/StudyLogQueryServiceTest.java

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,29 +93,57 @@ class getStudyLogsSliceByTripId {
9393

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

101101
int page = 0;
102102
int size = 5;
103+
String order = "LATEST";
103104
Pageable pageable = PageRequest.of(page, size);
104105

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

107-
given(studyLogQueryRepository.findSliceByTripIdOrderByCreatedAtDesc(tripId, pageable))
108+
given(studyLogQueryRepository.findSliceByTripId(tripId, pageable, order))
108109
.willReturn(mockSlice);
109110

110111
// when
111112
Slice<StudyLog> result =
112-
studyLogQueryService.getStudyLogsSliceByTripId(tripId, page, size);
113+
studyLogQueryService.getStudyLogsSliceByTripId(tripId, page, size, order);
113114

114115
// then
115116
assertThat(result.getContent().size()).isEqualTo(studyLogs.size());
116117
assertThat(result.getContent().get(0)).isEqualTo(studyLog1);
117118
assertThat(result.getContent().get(1)).isEqualTo(studyLog2);
118119
}
120+
121+
@Test
122+
@DisplayName("특정 여행의 학습 로그 목록을 페이징 처리와 과거순으로 정렬하고 반환한다")
123+
void shouldReturnStudyLogsByTripIdWithOldestOrder() {
124+
// given
125+
Long tripId = courseTrip.getId();
126+
List<StudyLog> studyLogs = List.of(studyLog2, studyLog1); // 과거순이므로 순서 반대
127+
128+
int page = 0;
129+
int size = 5;
130+
String order = "OLDEST";
131+
Pageable pageable = PageRequest.of(page, size);
132+
133+
Slice<StudyLog> mockSlice = new SliceImpl<>(studyLogs, pageable, false);
134+
135+
given(studyLogQueryRepository.findSliceByTripId(tripId, pageable, order))
136+
.willReturn(mockSlice);
137+
138+
// when
139+
Slice<StudyLog> result =
140+
studyLogQueryService.getStudyLogsSliceByTripId(tripId, page, size, order);
141+
142+
// then
143+
assertThat(result.getContent().size()).isEqualTo(studyLogs.size());
144+
assertThat(result.getContent().get(0)).isEqualTo(studyLog2);
145+
assertThat(result.getContent().get(1)).isEqualTo(studyLog1);
146+
}
119147
}
120148

121149
@Nested
@@ -218,7 +246,7 @@ void shouldReturnCountWhenStudyLogExistsForTrip() {
218246
}
219247

220248
@Nested
221-
@DisplayName("getStudyLogsSliceByTripId 메서드는")
249+
@DisplayName("getStudyLogsSliceByTripReportId 메서드는")
222250
class GetStudyLogsSliceByTripReportId {
223251

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

237265
given(
238-
studyLogQueryRepository.findSliceByTripIdOrderByCreatedAtDesc(
266+
studyLogQueryRepository.findSliceByTripReportIdOrderByCreatedAtDesc(
239267
tripReport.getId(), pageable))
240268
.willReturn(mockSlice);
241269

242270
// when
243271
Slice<StudyLog> result =
244-
studyLogQueryService.getStudyLogsSliceByTripId(tripReport.getId(), page, size);
272+
studyLogQueryService.getStudyLogsSliceByTripReportId(
273+
tripReport.getId(), page, size);
245274

246275
// then
247276
assertThat(result.getContent().size()).isEqualTo(studyLogs.size());

src/test/java/com/ject/studytrip/studylog/presentation/controller/StudyLogControllerIntegrationTest.java

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -567,13 +567,16 @@ void shouldReturnBadRequestWhenSelectedMissionIsAlreadyCompleted() throws Except
567567
class ListStudyLogs {
568568
private static final String DEFAULT_PAGE = "0";
569569
private static final String DEFAULT_PAGE_SIZE = "5";
570+
private static final String DEFAULT_ORDER = "LATEST";
570571

571572
private ResultActions getResultActions(
572-
String token, Object tripId, String page, String size) throws Exception {
573+
String token, Object tripId, String page, String size, String order)
574+
throws Exception {
573575
return mockMvc.perform(
574576
get("/api/trips/{tripId}/study-logs", tripId)
575577
.param("page", page)
576578
.param("size", size)
579+
.param("order", order)
577580
.header(HttpHeaders.AUTHORIZATION, TokenFixture.TOKEN_PREFIX + token));
578581
}
579582

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

589592
// when
590593
ResultActions resultActions =
591-
getResultActions(token, courseTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE);
594+
getResultActions(
595+
token,
596+
courseTrip.getId(),
597+
DEFAULT_PAGE,
598+
DEFAULT_PAGE_SIZE,
599+
DEFAULT_ORDER);
600+
601+
// then
602+
resultActions
603+
.andExpect(status().isOk())
604+
.andExpect(jsonPath("$.success").value(true))
605+
.andExpect(jsonPath("$.data.studyLogs").isNotEmpty())
606+
.andExpect(jsonPath("$.data.hasNext").value(false))
607+
.andExpect(jsonPath("$.data.studyLogs[0].studyLogId").value(studyLog.getId()))
608+
.andExpect(jsonPath("$.data.studyLogs[0].dailyMissions").isNotEmpty())
609+
.andExpect(
610+
jsonPath("$.data.studyLogs[0].dailyMissions")
611+
.value(Matchers.hasSize(studyLogDailyMissions.size())));
612+
}
613+
614+
@Test
615+
@DisplayName("order 파라미터를 OLDEST로 지정하면 과거순으로 정렬된 학습 로그 목록을 반환한다")
616+
void shouldLoadStudyLogsByTripWithOldestOrder() throws Exception {
617+
// given
618+
StudyLog studyLog = studyLogTestHelper.saveStudyLog(member, dailyGoal);
619+
List<StudyLogDailyMission> studyLogDailyMissions =
620+
studyLogDailyMissionTestHelper.saveStudyLogDailyMissions(
621+
studyLog, dailyMission);
622+
623+
// when
624+
ResultActions resultActions =
625+
getResultActions(
626+
token, courseTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE, "OLDEST");
592627

593628
// then
594629
resultActions
@@ -603,12 +638,33 @@ void shouldLoadStudyLogsByTripWithSlicePaging() throws Exception {
603638
.value(Matchers.hasSize(studyLogDailyMissions.size())));
604639
}
605640

641+
@Test
642+
@DisplayName("order 파라미터가 유효하지 않은 값이면 400 Bad Request를 반환한다")
643+
void shouldReturnBadRequestWhenOrderIsInvalid() throws Exception {
644+
// when
645+
ResultActions resultActions =
646+
getResultActions(
647+
token, courseTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE, "INVALID");
648+
649+
// then
650+
resultActions
651+
.andExpect(status().isBadRequest())
652+
.andExpect(jsonPath("$.success").value(false))
653+
.andExpect(
654+
jsonPath("$.status")
655+
.value(
656+
CommonErrorCode.METHOD_ARGUMENT_NOT_VALID
657+
.getStatus()
658+
.value()));
659+
}
660+
606661
@Test
607662
@DisplayName("인증되지 않은 사용자일 경우 401 Unauthorized를 반환한다")
608663
void shouldReturnUnauthorizedWhenUnauthenticated() throws Exception {
609664
// when
610665
ResultActions resultActions =
611-
getResultActions("", courseTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE);
666+
getResultActions(
667+
"", courseTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE, DEFAULT_ORDER);
612668

613669
// then
614670
resultActions
@@ -627,7 +683,7 @@ void shouldReturnBadRequestWhenTripIdTypeMismatch() throws Exception {
627683

628684
// when
629685
ResultActions resultActions =
630-
getResultActions(token, tripId, DEFAULT_PAGE, DEFAULT_PAGE_SIZE);
686+
getResultActions(token, tripId, DEFAULT_PAGE, DEFAULT_PAGE_SIZE, DEFAULT_ORDER);
631687

632688
// then
633689
resultActions
@@ -649,7 +705,8 @@ void shouldReturnBadRequestWhenWhenPagingParameterTypeMismatch() throws Exceptio
649705
String size = "test";
650706

651707
// when
652-
ResultActions resultActions = getResultActions(token, courseTrip, page, size);
708+
ResultActions resultActions =
709+
getResultActions(token, courseTrip, page, size, DEFAULT_ORDER);
653710

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

673730
// when
674-
ResultActions resultActions = getResultActions(token, courseTrip.getId(), page, size);
731+
ResultActions resultActions =
732+
getResultActions(token, courseTrip.getId(), page, size, DEFAULT_ORDER);
675733

676734
// then
677735
resultActions
@@ -693,7 +751,7 @@ void shouldReturnNotFoundWhenInvalidTripId() throws Exception {
693751

694752
// when
695753
ResultActions resultActions =
696-
getResultActions(token, tripId, DEFAULT_PAGE, DEFAULT_PAGE_SIZE);
754+
getResultActions(token, tripId, DEFAULT_PAGE, DEFAULT_PAGE_SIZE, DEFAULT_ORDER);
697755

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

714772
// when
715773
ResultActions resultActions =
716-
getResultActions(token, newTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE);
774+
getResultActions(
775+
token, newTrip.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE, DEFAULT_ORDER);
717776

718777
// then
719778
resultActions
@@ -732,7 +791,8 @@ void shouldReturnBadRequestWhenAlreadyTrip() throws Exception {
732791

733792
// when
734793
ResultActions resultActions =
735-
getResultActions(token, deleted.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE);
794+
getResultActions(
795+
token, deleted.getId(), DEFAULT_PAGE, DEFAULT_PAGE_SIZE, DEFAULT_ORDER);
736796

737797
// then
738798
resultActions

0 commit comments

Comments
 (0)