Skip to content

[Feat] 매체 목록 API에 offset/limit 페이지네이션 추가#59

Merged
LeeJeongHeon02 merged 2 commits into
devfrom
feat/media-unit-list-pagination
Jul 21, 2026
Merged

[Feat] 매체 목록 API에 offset/limit 페이지네이션 추가#59
LeeJeongHeon02 merged 2 commits into
devfrom
feat/media-unit-list-pagination

Conversation

@LeeJeongHeon02

@LeeJeongHeon02 LeeJeongHeon02 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

배경

매체 목록 조회(GET /api/v1/media-units)에서 매체 사진(photoUrl)이 고해상도 원본이라, 목록 전체를 한 번에 내려주면 트래픽이 큽니다. 이미지 자체를 리사이즈하는 방식(백엔드가 이미지 파일을 소유하지 않아 SSRF/메모리 이슈 등 리스크가 더 큼) 대신, 화면에 실제로 보일 만큼만 페이지 단위로 불러오는 쪽으로 결정했습니다.

변경 내용

  • offset/limit 쿼리 파라미터 추가. 생략 시 offset=0, limit=10, 최대 limit=50(범위를 벗어나면 자동 clamp - RealtimeGraphService의 기존 clamp 패턴과 동일)
  • MediaUnitListResponsehasMore 추가 - 무한 스크롤 종료 시점 판단용
  • 페이지네이션을 먼저 적용한 뒤 기간 충돌 조회(findConflictingMediaUnitIds)를 실행하도록 순서를 바꿔, 이 쿼리도 페이지 분량의 ID만 조회하도록 함께 개선
  • Swagger 문서, docs/campaign-registration-api-spec.md 갱신

프론트 사용 예

  • 첫 진입: limit=10
  • 스크롤 시마다: 직전 offset + limit을 다음 offset으로, limit=6으로 반복 호출
  • hasMore: false가 나오면 더 불러올 매체 없음

검증

페이지네이션 케이스(기본 10개+hasMore, offset 적용한 다음 페이지, limit 최대치 clamp) 테스트 추가. 전체 295개 테스트 통과.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • 새로운 기능

    • 매체 목록 조회에 페이지네이션을 지원합니다.
    • offsetlimit으로 원하는 구간을 조회할 수 있으며, 기본값은 각각 0과 10입니다.
    • 한 번에 최대 50개까지 조회할 수 있고, 범위를 초과하면 자동 조정됩니다.
    • 응답에 hasMore가 포함되어 추가 데이터 존재 여부를 확인할 수 있습니다.
    • 결과가 없을 경우 빈 목록과 함께 hasMore: false를 반환합니다.
  • 문서

    • 매체 목록 API의 페이지네이션 및 무한 스크롤 사용 방법을 명확히 안내합니다.

매체 사진(photoUrl)이 고해상도 원본이라 목록을 한 번에 전부 내려주면
트래픽이 크다. 이미지 자체를 리사이즈하는 대신, 화면에 실제로 보일 만큼만
불러오도록 offset/limit 기반 페이지네이션을 추가했다 (예: 첫 진입 limit=10,
스크롤마다 offset을 누적하며 limit=6으로 추가 로드하는 무한 스크롤 전제).

- MediaUnitListResponse에 hasMore 추가
- MediaUnitQueryService: RealtimeGraphService와 동일한 clamp 패턴
  (limit 기본 10, 최대 50, 범위 밖이면 자동 clamp). 페이지네이션을 먼저
  적용한 뒤 기간 충돌 조회(findConflictingMediaUnitIds)를 페이지 분량의
  ID로만 실행하도록 순서를 바꿔 쿼리 규모도 줄였다
- Swagger 문서, campaign-registration-api-spec.md 갱신

검증: 페이지네이션 케이스(기본 10개+hasMore, offset 적용한 다음 페이지,
limit 최대치 clamp) 테스트 추가, 전체 295개 테스트 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@LeeJeongHeon02, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 42 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 61264e3a-7515-45de-83cd-3757e2aa4a1f

📥 Commits

Reviewing files that changed from the base of the PR and between 62264e8 and 0f2c99d.

📒 Files selected for processing (1)
  • src/test/java/com/shinhan/klljs/domain/media/service/MediaUnitQueryServiceTest.java
📝 Walkthrough

Walkthrough

매체 목록 조회 API에 offset/limit 기반 페이지네이션과 hasMore 응답이 추가되었습니다. 컨트롤러와 Swagger 계약이 갱신되었고, 서비스는 페이지 단위 조회 및 limit 클램프를 적용하며 관련 테스트가 보강되었습니다.

Changes

매체 목록 페이지네이션

Layer / File(s) Summary
페이지네이션 API 계약
docs/campaign-registration-api-spec.md, src/main/java/com/shinhan/klljs/domain/media/dto/MediaUnitListResponse.java, src/main/java/com/shinhan/klljs/domain/media/controller/MediaUnitControllerDocs.java
매체 목록 API에 offset/limit 요청 파라미터와 hasMore 응답 필드가 추가되고 기본값, 최대값, 클램프 동작이 문서화되었습니다.
페이지 단위 조회 처리
src/main/java/com/shinhan/klljs/domain/media/service/MediaUnitQueryService.java
조회 결과에 offset과 limit을 적용하고, 페이지 범위의 기간 충돌 정보를 계산하며, 빈 결과와 다음 페이지 여부를 응답에 반영합니다.
엔드포인트 연결 및 검증
src/main/java/com/shinhan/klljs/domain/media/controller/MediaUnitController.java, src/test/java/com/shinhan/klljs/domain/media/service/MediaUnitQueryServiceTest.java
컨트롤러가 페이지네이션 파라미터를 서비스에 전달하며, 기본 10건 조회, 다음 페이지, 최대 50건 클램프와 hasMore 동작을 검증합니다.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MediaUnitController
  participant MediaUnitQueryService
  participant MediaUnitListResponse
  Client->>MediaUnitController: offset, limit 포함 목록 조회 요청
  MediaUnitController->>MediaUnitQueryService: 필터와 페이지네이션 파라미터 전달
  MediaUnitQueryService->>MediaUnitQueryService: 파라미터 보정 및 페이지 생성
  MediaUnitQueryService->>MediaUnitListResponse: mediaUnits와 hasMore 생성
  MediaUnitQueryService-->>MediaUnitController: 페이지 응답 반환
  MediaUnitController-->>Client: mediaUnits와 hasMore 응답
Loading

Possibly related PRs

  • Shinhan-KLLJS/backend#35: 동일한 getMediaUnits Swagger 문서를 수정하지만, 기존 검색 파라미터 예시 메타데이터를 추가한 변경입니다.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 매체 목록 API에 offset/limit 기반 페이지네이션을 추가한 변경을 정확하고 간결하게 요약합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/media-unit-list-pagination

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/shinhan/klljs/domain/media/service/MediaUnitQueryService.java`:
- Around line 73-101: 현재 메모리 기반 페이징 구현은 변경하지 말고, 향후 활성 매체 수 증가에 대비해 DB 레벨 페이징과
검색 최적화 전환 작업을 백로그에 추가하세요. MediaUnitQueryService의 filtered 및
stream().skip().limit() 흐름을 참고해 Pageable 기반 조회와 Full Text Search 인덱스 검토 항목을
기록하세요.

In
`@src/test/java/com/shinhan/klljs/domain/media/service/MediaUnitQueryServiceTest.java`:
- Around line 97-109: Extend MediaUnitQueryServiceTest with boundary tests for
negative pagination parameters and offsets beyond the available data, using the
existing getMediaUnits and persistence helpers. Verify negative offset and limit
are clamped to valid minimums with the expected first-page result, and verify an
offset beyond the total count returns an empty mediaUnits list with hasMore
false.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1bae3922-c409-47cc-bc5d-b6b8cdc43f48

📥 Commits

Reviewing files that changed from the base of the PR and between 480ce62 and 62264e8.

📒 Files selected for processing (6)
  • docs/campaign-registration-api-spec.md
  • src/main/java/com/shinhan/klljs/domain/media/controller/MediaUnitController.java
  • src/main/java/com/shinhan/klljs/domain/media/controller/MediaUnitControllerDocs.java
  • src/main/java/com/shinhan/klljs/domain/media/dto/MediaUnitListResponse.java
  • src/main/java/com/shinhan/klljs/domain/media/service/MediaUnitQueryService.java
  • src/test/java/com/shinhan/klljs/domain/media/service/MediaUnitQueryServiceTest.java

Comment on lines +73 to +101
return new MediaUnitListResponse(List.of(), false);
}

List<Long> mediaUnitIds = filtered.stream().map(MediaUnit::getId).toList();
// 매체 사진(photoUrl)이 고해상도 원본이라 한 번에 다 내려주면 트래픽이 크다 - 화면에 보일
// 만큼만 페이지로 잘라서 응답한다(무한 스크롤). 매체 수가 많아져도 findConflictingMediaUnitIds가
// 이 페이지의 ID만 조회하도록, 페이지네이션을 먼저 적용한 뒤 기간 충돌을 계산한다.
int offset = clampOffset(offsetParam);
int limit = clampLimit(limitParam);
List<MediaUnit> page = filtered.stream().skip(offset).limit(limit).toList();
boolean hasMore = offset + page.size() < filtered.size();

if (page.isEmpty()) {
return new MediaUnitListResponse(List.of(), false);
}

List<Long> pageMediaUnitIds = page.stream().map(MediaUnit::getId).toList();
Set<Long> conflictingIds = new HashSet<>(campaignRepository.findConflictingMediaUnitIds(
mediaUnitIds,
pageMediaUnitIds,
CampaignStatus.REGISTRATION_FAILED,
executionStartDate,
executionEndDate
));

List<MediaUnitSummary> summaries = filtered.stream()
List<MediaUnitSummary> summaries = page.stream()
.map(media -> MediaUnitSummary.from(media, !conflictingIds.contains(media.getId())))
.toList();
return new MediaUnitListResponse(summaries);
return new MediaUnitListResponse(summaries, hasMore);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial

메모리 기반 페이징 처리 관련 운영 조언

현재 MVP 특성상 매체 수가 적어 전체 데이터를 메모리에 로드한 뒤 stream().skip().limit()으로 페이징을 처리하고 있습니다. 이 방식은 현재로서는 안전하지만, 향후 활성 매체 수가 급증할 경우 다음과 같은 문제가 발생할 수 있습니다.

  • 모든 엔티티를 영속성 컨텍스트 및 메모리에 적재함에 따른 힙 메모리 사용량 증가
  • 가비지 컬렉션(GC) 빈도 증가로 인한 응답 지연

당장은 변경할 필요가 없으나, 서비스 규모가 커지면 DB 레벨의 페이징(Spring Data JPA의 Pageable) 및 검색 최적화(Full Text Search 인덱스 등)로 전환하는 것을 백로그에 추가하여 관리하시기를 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/shinhan/klljs/domain/media/service/MediaUnitQueryService.java`
around lines 73 - 101, 현재 메모리 기반 페이징 구현은 변경하지 말고, 향후 활성 매체 수 증가에 대비해 DB 레벨 페이징과
검색 최적화 전환 작업을 백로그에 추가하세요. MediaUnitQueryService의 filtered 및
stream().skip().limit() 흐름을 참고해 Pageable 기반 조회와 Full Text Search 인덱스 검토 항목을
기록하세요.

CodeRabbit 지적 검증 결과 clampOffset/clampLimit과 빈 페이지 처리 로직은
이미 올바르게 동작했지만(코드 트레이싱으로 확인), 회귀 방지를 위해 테스트로
고정해둔다.

- offset=-5, limit=-3 -> offset 0, limit 1로 clamp되어 첫 항목 하나만 반환
- offset이 전체 개수를 넘으면 빈 목록 + hasMore=false

DB 레벨 페이징 전환 제안은 코드 변경 없이 스킵 - MediaUnitQueryService
클래스 주석에 이미 "매체 수가 작은 MVP" 전제가 문서화되어 있고, 트래픽이
커지면 그때 전환한다는 계획도 이미 있다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@LeeJeongHeon02
LeeJeongHeon02 merged commit 8dabade into dev Jul 21, 2026
1 of 2 checks passed
@LeeJeongHeon02
LeeJeongHeon02 deleted the feat/media-unit-list-pagination branch July 21, 2026 03:26
@LeeJeongHeon02 LeeJeongHeon02 self-assigned this Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant