Skip to content

Commit da3f1a1

Browse files
authored
[refactor] 나만의 bar 페이지네이션을 커서 기반으로 리팩토링 #49 (#51)
* refactor: MyBar 목록 API 페이징 방식을 오프셋에서 커서 기반으로 변경 - 응답 DTO인 `MyBarListResponseDto`에서 `nextPage` 필드를 제거하고, 대신 `nextCursor` 필드를 추가 * feat: MyBarRepository에 커서 기반 페이징 메서드 추가 - `MyBarRepository`에 커서 기반 페이징을 위한 새로운 쿼리 메서드 `findSliceByCursor`를 추가 * refactor: MyBarService에 커서 기반 페이징 로직 구현 및 적용 - MyBar 목록 조회 API의 페이징 방식을 기존의 오프셋(offset) 기반에서 커서(cursor) 기반으로 변경 - `getMyBar` 메서드의 파라미터를 `page`, `pageSize`에서 `cursor`, `limit`로 수정하여 커서 기반 페이징에 적합하게 변경 - `Cursor` 클래스를 새로 추가하여 커서의 인코딩/디코딩 로직을 구현 커서는 `epochMillis`와 `id`를 조합하여 Base64로 인코딩 - 첫 페이지 요청(`cursor`가 null 또는 빈 문자열)과 다음 페이지 요청(`cursor`가 존재)을 분리하여 처리하는 로직을 추가 * refactor: MyBarController의 목록 조회 API 페이징 방식 변경 - 마이바(MyBar) 목록을 조회하는 GET API의 페이징 방식을 오프셋(offset) 기반에서 커서(cursor) 기반으로 리팩토링 - 기존의 `@RequestParam`인 `page`와 `pageSize`를 `cursor`와 `limit`로 변경 - `cursor`는 `String` 타입으로, 다음 페이지를 식별하는 데 사용 * refactor: MyBarListResponseDto의 커서 필드를 분리하여 재구성 - `MyBarListResponseDto`의 `nextCursor` 필드를 제거하고, `nextKeptAt`과 `nextId` 두 개의 필드로 분리 - 기존의 Base64로 인코딩된 문자열 커서 대신, 서버 내부에서 직접 사용할 수 있는 `LocalDateTime`과 `Long` 타입의 필드를 제공 * refactor: MyBarService의 커서 기반 페이징 로직 재구성 (간소화) - `MyBarService`의 `getMyBar` 메서드에서 커서 기반 페이징 로직을 간소화 - 기존의 Base64 인코딩/디코딩 방식을 제거하고, 커서 값을 `lastKeptAt` (LocalDateTime)과 `lastId` (Long) 두 개의 파라미터로 직접 받도록 변경 * refactor: MyBar 목록 조회 API 파라미터를 커서 값으로 직접 변경 - MyBar 목록 조회 API의 `@GetMapping` 메서드 파라미터를 커서 기반 페이징에 맞게 변경 - 기존의 단일 `cursor` 문자열 필드 대신, `lastKeptAt` (LocalDateTime)과 `lastId` (Long)를 직접 받도록 수정 - `lastKeptAt` 파라미터에 `@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)`을 적용하여 ISO 8601 형식의 시간 데이터를 자동으로 변환
1 parent dc9d638 commit da3f1a1

File tree

4 files changed

+52
-13
lines changed

4 files changed

+52
-13
lines changed

src/main/java/com/back/domain/mybar/controller/MyBarController.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1010
import org.springframework.validation.annotation.Validated;
1111
import org.springframework.web.bind.annotation.*;
12+
import org.springframework.format.annotation.DateTimeFormat;
13+
import java.time.LocalDateTime;
1214

1315
@RestController
1416
@RequestMapping("/me/bar")
@@ -21,10 +23,12 @@ public class MyBarController {
2123
@GetMapping
2224
public RsData<MyBarListResponseDto> getMyBarList(
2325
@AuthenticationPrincipal(expression = "id") Long userId,
24-
@RequestParam(defaultValue = "0") @Min(0) int page,
25-
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int pageSize
26+
@RequestParam(required = false)
27+
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime lastKeptAt,
28+
@RequestParam(required = false) Long lastId,
29+
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int limit
2630
) {
27-
MyBarListResponseDto body = myBarService.getMyBar(userId, page, pageSize);
31+
MyBarListResponseDto body = myBarService.getMyBar(userId, lastKeptAt, lastId, limit);
2832
return RsData.successOf(body); // code=200, message="success"
2933
}
3034

src/main/java/com/back/domain/mybar/dto/MyBarListResponseDto.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
import lombok.AllArgsConstructor;
44
import lombok.Getter;
55

6+
import java.time.LocalDateTime;
67
import java.util.List;
78

89
@Getter
910
@AllArgsConstructor
1011
public class MyBarListResponseDto {
1112
private List<MyBarItemResponseDto> items;
12-
private boolean hasNext; // 다음 페이지 존재 여부
13-
private Integer nextPage; // 다음 페이지 번호(없으면 null)
14-
}
13+
private boolean hasNext; // 다음 페이지 존재 여부
14+
private LocalDateTime nextKeptAt; // 다음 페이지 시작용 keptAt
15+
private Long nextId; // 다음 페이지 시작용 id
16+
}

src/main/java/com/back/domain/mybar/repository/MyBarRepository.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,23 @@
1010
import org.springframework.stereotype.Repository;
1111

1212
import java.util.Optional;
13+
import java.util.List;
14+
import java.time.LocalDateTime;
1315

1416
@Repository
1517
public interface MyBarRepository extends JpaRepository<MyBar, Long> {
1618
/** 나만의 bar(킵) 목록: ACTIVE만, id desc */
1719
Page<MyBar> findByUser_IdAndStatusOrderByKeptAtDescIdDesc(Long userId, KeepStatus status, Pageable pageable);
1820

21+
@Query("""
22+
select m from MyBar m
23+
where m.user.id = :userId
24+
and m.status = :status
25+
and (m.keptAt < :keptAt or (m.keptAt = :keptAt and m.id < :id))
26+
order by m.keptAt desc, m.id desc
27+
""")
28+
List<MyBar> findSliceByCursor(Long userId, KeepStatus status, LocalDateTime keptAt, Long id, Pageable pageable);
29+
1930
/** 프로필/요약용: ACTIVE 개수 */
2031
long countByUser_IdAndStatus(Long userId, KeepStatus status);
2132

src/main/java/com/back/domain/mybar/service/MyBarService.java

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import lombok.RequiredArgsConstructor;
1111
import org.springframework.data.domain.Page;
1212
import org.springframework.data.domain.PageRequest;
13+
import org.springframework.data.domain.Pageable;
1314
import org.springframework.stereotype.Service;
1415
import org.springframework.transaction.annotation.Transactional;
1516

@@ -26,18 +27,38 @@ public class MyBarService {
2627
private final UserRepository userRepository;
2728
private final CocktailRepository cocktailRepository;
2829

30+
// 커서: lastKeptAt + lastId를 그대로 파라미터로 사용
2931
@Transactional(readOnly = true)
30-
public MyBarListResponseDto getMyBar(Long userId, int page, int pageSize) {
31-
Page<MyBar> myBarPage = myBarRepository.findByUser_IdAndStatusOrderByKeptAtDescIdDesc(userId, KeepStatus.ACTIVE, PageRequest.of(page, pageSize));
32+
public MyBarListResponseDto getMyBar(Long userId, LocalDateTime lastKeptAt, Long lastId, int limit) {
33+
int safeLimit = Math.max(1, Math.min(limit, 100));
34+
int fetchSize = safeLimit + 1; // 다음 페이지 여부 판단용으로 1개 더 조회
35+
36+
List<MyBar> rows;
37+
Pageable pageable = PageRequest.of(0, fetchSize);
38+
39+
if (lastKeptAt == null || lastId == null) {
40+
Page<MyBar> page0 = myBarRepository
41+
.findByUser_IdAndStatusOrderByKeptAtDescIdDesc(userId, KeepStatus.ACTIVE, pageable);
42+
rows = page0.getContent();
43+
} else {
44+
rows = myBarRepository.findSliceByCursor(userId, KeepStatus.ACTIVE, lastKeptAt, lastId, pageable);
45+
}
46+
47+
boolean hasNext = rows.size() > safeLimit;
48+
if (hasNext) rows = rows.subList(0, safeLimit);
3249

33-
List<MyBar> myBars = myBarPage.getContent();
3450
List<MyBarItemResponseDto> items = new ArrayList<>();
35-
for (MyBar myBar : myBars) items.add(MyBarItemResponseDto.from(myBar));
51+
for (MyBar myBar : rows) items.add(MyBarItemResponseDto.from(myBar));
3652

37-
boolean hasNext = myBarPage.hasNext();
38-
Integer nextPage = hasNext ? myBarPage.getNumber() + 1 : null;
53+
LocalDateTime nextKeptAt = null;
54+
Long nextId = null;
55+
if (hasNext && !rows.isEmpty()) {
56+
MyBar last = rows.get(rows.size() - 1);
57+
nextKeptAt = last.getKeptAt();
58+
nextId = last.getId();
59+
}
3960

40-
return new MyBarListResponseDto(items, hasNext, nextPage);
61+
return new MyBarListResponseDto(items, hasNext, nextKeptAt, nextId);
4162
}
4263

4364
@Transactional
@@ -75,3 +96,4 @@ public void unkeep(Long userId, Long cocktailId) {
7596
myBarRepository.softDeleteByUserAndCocktail(userId, cocktailId);
7697
}
7798
}
99+

0 commit comments

Comments
 (0)