Skip to content

[NDGL-124] 인기 여행 목록 페이지 paging 구현 및 에러 뷰 추가#41

Merged
jihee-dev merged 2 commits intodevelopfrom
feature/NDGL-124/impl-popular-list-paging
Feb 27, 2026
Merged

[NDGL-124] 인기 여행 목록 페이지 paging 구현 및 에러 뷰 추가#41
jihee-dev merged 2 commits intodevelopfrom
feature/NDGL-124/impl-popular-list-paging

Conversation

@jihee-dev
Copy link
Copy Markdown
Member

@jihee-dev jihee-dev commented Feb 27, 2026

NDGL-124 인기 여행 목록 페이지 paging 구현 및 에러 뷰 추가


연관 문서

변경사항

  • CommonErrorView 추가
    • TemplateSearchScreen에서 사용중인 ErrorView를 CommonErrorView로 마이그레이션
  • 인기 여행 목록 paging 구현
  • 인기 여행 목록 에러 뷰 구현

테스트 체크 리스트

  • 여행 템플릿 검색 에러 뷰
  • 인기 여행 목록 paging
  • 인기 여행 목록 에러 뷰

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 여행 템플릿 목록에 무한 스크롤 기능 추가
    • 오류 발생 시 재시도 기능 제공
  • Bug Fixes

    • 오류 상태 처리 및 사용자 피드백 개선
    • 오류 메시지 표시 기능 추가
  • Refactor

    • 오류 화면 UI 통합으로 일관된 사용자 경험 제공

@jihee-dev jihee-dev self-assigned this Feb 27, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 27, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

새로운 재사용 가능한 공통 에러 UI 컴포넌트를 추가하고, 인기 여행 목록 기능에 페이지네이션 및 스낵바 지원을 구현합니다. 인기 여행 목록 계약은 상태 관리 개선을 위해 봉인된 클래스 상태로 리팩토링되며, 템플릿 검색 화면은 새로운 공통 에러 컴포넌트를 사용하도록 업데이트됩니다.

Changes

Cohort / File(s) Summary
공통 에러 UI 컴포넌트
core/ui/src/main/java/com/yapp/ndgl/core/ui/CommonErrorView.kt, core/ui/src/main/res/values/strings.xml
새로운 CommonErrorView Jetpack Compose 컴포넌트 추가. 이미지, 제목, 설명을 표시하는 에러 상태 UI 제공. 에러 메시지 문자열 리소스(common_err_unknown, common_error_title, common_error_description) 추가.
저장소 페이지네이션 지원
data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/TravelTemplateRepository.kt
getAllPopularTravelTemplates()와 getPopularTravelTemplates() 메서드에 page: Int = 0 파라미터 추가. API 호출 시 페이지 파라미터 전달.
상태 관리 리팩토링
feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListContract.kt
PopularTravelListState를 데이터 클래스에서 봉인된 클래스(Loading, Success, Error 변형)로 변환. PopularTravelListItem 타입 추가(Travel, Loading 변형). LoadMore, ClickRetry 인텐트 및 ShowSnackBar 사이드 이펙트 추가.
화면 및 스낵바 통합
feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListScreen.kt
snackbarHostState 파라미터 추가. 상태 기반 UI 구성 적용(Loading, Success, Error). 무한 스크롤 동작 구현. PopularTravelListContent, LoadingItem, ErrorView 컴포넌트 추가.
뷰모델 페이지네이션 로직
feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListViewModel.kt
초기 상태를 Loading으로 변경. isLoadingMore 플래그로 동시 로딩 방지. loadMore() 함수 추가로 페이지네이션 처리. LoadMore, ClickRetry 인텐트 처리. 에러 시 Error 상태로 감소 및 스낵바 이펙트 방출.
템플릿 검색 통합
feature/home/src/main/java/com/yapp/ndgl/feature/home/search/TemplateSearchScreen.kt, feature/home/src/main/res/values/strings.xml
로컬 ErrorView 제거. CommonErrorView 사용으로 변경. home_template_search_error_title, home_template_search_error_description 문자열 리소스 삭제.

Sequence Diagram

sequenceDiagram
    actor User
    participant Screen as PopularTravelListScreen
    participant ViewModel as PopularTravelListViewModel
    participant Repository as TravelTemplateRepository
    participant API as Backend API
    participant Snackbar as SnackbarHost

    User->>Screen: 목록 아이템 보기
    Screen->>ViewModel: 초기 상태 (Loading)
    ViewModel->>Repository: getAllPopularTravelTemplates(page=0)
    Repository->>API: 인기 여행 조회 (page=0)
    API-->>Repository: 여행 목록 반환
    ViewModel->>ViewModel: Success 상태 구성
    Screen->>Screen: PopularTravelListContent 렌더링
    
    User->>Screen: 목록 스크롤 (끝에 도달)
    Screen->>ViewModel: onLoadMore(nextPage=1)
    ViewModel->>ViewModel: isLoadingMore 확인
    ViewModel->>Repository: getPopularTravelTemplates(page=1)
    Repository->>API: 다음 페이지 조회 (page=1)
    API-->>Repository: 추가 여행 목록 반환
    ViewModel->>ViewModel: 기존 상태에 항목 병합
    Screen->>Screen: LazyColumn 업데이트
    
    alt 로드 실패
        API-->>Repository: 에러 응답
        ViewModel->>ViewModel: Error 상태 생성
        ViewModel->>ViewModel: ShowSnackBar 사이드 이펙트 발행
        Screen->>Snackbar: 에러 메시지 표시
        User->>Screen: onRetryClick 클릭
        ViewModel->>ViewModel: ClickRetry 처리 → loadMore 재시도
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR title clearly describes the two main changes: implementing paging for the popular travel list and adding an error view.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/NDGL-124/impl-popular-list-paging

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListViewModel.kt (1)

52-58: ⚠️ Potential issue | 🟠 Major

초기 템플릿 로드 실패가 성공 상태로 삼켜집니다.

getOrNull() 사용으로 실패가 null로 변환되고, Line 90에서 항상 Success를 emit하고 있습니다. 전체 요청 실패 시 에러 뷰 대신 빈 목록이 노출될 수 있습니다. 최소한 “전체 실패” 케이스는 PopularTravelListState.Error로 전환하는 분기가 필요합니다.

🔧 수정 예시
             val allResult = allTemplateDeferred.await()
+            val programResults = popularTemplateDeferred.awaitAll()
+
+            if (allResult == null && programResults.all { (_, result) -> result == null }) {
+                reduce { PopularTravelListState.Error }
+                return@launch
+            }
+
             val allTravels = buildList {
                 allResult?.content?.forEach { add(PopularTravelListItem.Travel(it.toTravelContent())) }
                 if (allResult?.hasNext == true) add(PopularTravelListItem.Loading(nextPage = 1))
             }.toImmutableList()

             val travelsByProgram = mutableMapOf<Long, ImmutableList<PopularTravelListItem>>()
-            popularTemplateDeferred.awaitAll().forEach { (program, result) ->
+            programResults.forEach { (program, result) ->
                 travelsByProgram[program.id] = buildList {
                     result?.content?.forEach { add(PopularTravelListItem.Travel(it.toTravelContent())) }
                     if (result?.hasNext == true) add(PopularTravelListItem.Loading(nextPage = 1))
                 }.toImmutableList()
             }

Also applies to: 75-94

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListViewModel.kt`
around lines 52 - 58, The current use of suspendRunCatching(...).getOrNull()
when calling travelTemplateRepository.getAllPopularTravelTemplates() and
getPopularTravelTemplates(...) swallows failures and causes the ViewModel to
always emit a Success state (e.g., in the flow that resolves
popularTemplateDeferred), exposing empty lists instead of an error; change the
logic in PopularTravelListViewModel to detect failures from suspendRunCatching
(inspect the Result instead of calling getOrNull()), and when the overall fetch
for all templates or any required aggregate request fails, emit
PopularTravelListState.Error (rather than Success with empty data); update the
code around popularTemplateDeferred and the aggregation/emit pathway to branch
on Result.isSuccess/exceptionOrNull()/getOrElse so that real errors propagate to
the Error state while successful results continue to emit Success.
🧹 Nitpick comments (4)
feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListContract.kt (2)

25-35: by lazy@Immutable data class 내에서 안정성 문제를 일으킬 수 있습니다.

lazy 델리게이트는 내부적으로 mutable한 상태를 가지므로 @Immutable 어노테이션과 의미적으로 충돌합니다. Compose 컴파일러가 이 클래스를 불안정(unstable)으로 판단하여 불필요한 리컴포지션이 발생할 수 있습니다.

♻️ 일반 함수로 변경하는 방안
-        val selectedProgramTravels: ImmutableList<PopularTravelListItem> by lazy {
-            val selectTab = travelProgramTabs.getOrElse(selectedTabIndex) { TravelProgramTab.All }
-
-            when (selectTab) {
-                TravelProgramTab.All -> allPopularTravels
-                is TravelProgramTab.Custom -> popularTravelsByProgram.getOrDefault(
-                    selectTab.programId,
-                    persistentListOf(),
-                )
-            }
-        }
+        fun getSelectedProgramTravels(): ImmutableList<PopularTravelListItem> {
+            val selectTab = travelProgramTabs.getOrElse(selectedTabIndex) { TravelProgramTab.All }
+
+            return when (selectTab) {
+                TravelProgramTab.All -> allPopularTravels
+                is TravelProgramTab.Custom -> popularTravelsByProgram.getOrDefault(
+                    selectTab.programId,
+                    persistentListOf(),
+                )
+            }
+        }

또는 호출 빈도가 높다면 ViewModel에서 미리 계산하여 Success 생성 시 별도 필드로 전달하는 방법도 고려해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListContract.kt`
around lines 25 - 35, The lazy delegate in the selectedProgramTravels property
inside PopularTravelListContract introduces mutable state and conflicts with
`@Immutable`; replace the by lazy usage with a plain computed val/getter (or have
the ViewModel compute and pass the value) so selectedProgramTravels is derived
deterministically from travelProgramTabs, selectedTabIndex, TravelProgramTab
(e.g., TravelProgramTab.All), popularTravelsByProgram and allPopularTravels;
specifically remove the by lazy delegate on selectedProgramTravels and implement
it as a direct when-expression (or supply it as a precomputed field) to preserve
immutability and avoid Compose instability.

66-70: ShowSnackBarType enum 확장성을 고려해 주세요.

현재 ERR_UNKNOWN만 정의되어 있는데, 향후 네트워크 에러, 타임아웃 등 구체적인 에러 타입이 추가될 가능성이 있다면 현재 구조로 충분합니다. 다만, 모든 에러를 하나의 타입으로 처리할 계획이라면 enum 없이 단순화할 수도 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListContract.kt`
around lines 66 - 70, The ShowSnackBar side effect currently defines enum Type
with a single value ERR_UNKNOWN which limits clarity or is unnecessary; update
PopularTravelListContract's ShowSnackBar to either expand Type with concrete
cases (e.g., NETWORK_ERROR, TIMEOUT, AUTH_ERROR) to explicitly represent
expected error categories, or simplify by replacing enum Type with a payload
type (e.g., a String message or an Error/Throwable reference) if you intend to
handle all errors generically; adjust usages of ShowSnackBar and any consumers
of PopularTravelListSideEffect to match the new Type shape.
feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListScreen.kt (1)

235-252: LaunchedEffect의 키를 nextPage로 변경하는 것을 권장합니다.

현재 LaunchedEffect(Unit)을 사용하면 컴포저블이 컴포지션에 처음 진입할 때만 onLoadMore가 호출됩니다. 만약 동일한 LoadingItem이 에러 후에도 컴포지션에 남아있다면, 재시도 로직이 트리거되지 않을 수 있습니다.

nextPage를 키로 사용하면 페이지 값이 변경될 때마다 effect가 재실행되어 더 예측 가능한 동작을 보장합니다.

🔧 제안된 수정
 `@Composable`
 private fun LoadingItem(
     nextPage: Int,
     onLoadMore: (Int) -> Unit,
 ) {
     Box(
         modifier = Modifier
             .fillMaxWidth()
             .padding(vertical = 16.dp),
         contentAlignment = Alignment.Center,
     ) {
         CircularProgressIndicator(color = NDGLTheme.colors.green500)
     }

-    LaunchedEffect(Unit) {
+    LaunchedEffect(nextPage) {
         onLoadMore(nextPage)
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListScreen.kt`
around lines 235 - 252, The LoadingItem composable currently uses
LaunchedEffect(Unit) so onLoadMore is only invoked on first composition; change
the LaunchedEffect key to nextPage so the effect re-runs whenever the page
changes (i.e., replace LaunchedEffect(Unit) with LaunchedEffect(nextPage) in the
LoadingItem function) ensuring onLoadMore(nextPage) is called on page updates
and retry scenarios.
core/ui/src/main/java/com/yapp/ndgl/core/ui/CommonErrorView.kt (1)

26-30: 공통 컴포넌트 내부의 고정 세로 패딩은 재사용성을 낮출 수 있습니다.

Line 29의 vertical = 165.dp는 화면별 레이아웃 제어를 어렵게 만들 수 있어, 세로 여백은 호출부에서 주입하는 방식이 더 유연합니다.

예시 리팩터링
+import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
@@
 fun CommonErrorView(
     modifier: Modifier = Modifier,
     title: String? = null,
     description: String? = null,
+    contentPadding: PaddingValues = PaddingValues(horizontal = 24.dp),
 ) {
     Column(
         modifier = modifier
             .fillMaxWidth()
-            .padding(horizontal = 24.dp, vertical = 165.dp),
+            .padding(contentPadding),
         verticalArrangement = Arrangement.spacedBy(16.dp),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/ui/src/main/java/com/yapp/ndgl/core/ui/CommonErrorView.kt` around lines
26 - 30, The Column in CommonErrorView.kt uses a hardcoded vertical padding
(.padding(horizontal = 24.dp, vertical = 165.dp)), which reduces reusability;
change the CommonErrorView composable signature to accept a verticalPadding: Dp
(with a sensible default) or a contentPadding: PaddingValues parameter, remove
the fixed vertical = 165.dp from the modifier and apply the injected padding
instead, and update callers to supply appropriate vertical spacing where needed
(search for CommonErrorView and the Column modifier to locate the code to
modify).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListViewModel.kt`:
- Around line 99-105: The isLoadingMore flag in loadMore is set to true before
launching but is not guaranteed to be reset if the early return occurs; wrap the
viewModelScope.launch body in a try/finally (or place the flag reset in a
finally) so isLoadingMore is always set to false after the coroutine completes
or errors, ensuring both the path where state.value is not
PopularTravelListState.Success and the normal completion paths (referencing
loadMore, isLoadingMore, viewModelScope.launch and
PopularTravelListState.Success) will clear the flag.
- Around line 133-147: The code unconditionally calls dropLast(1) on
allPopularTravels and current (in branches handling TravelProgramTab.All and
TravelProgramTab.Custom) which assumes the last element is always a Loading
marker; change both places to first inspect the last element and only drop it if
it is the Loading marker (otherwise keep all items), then append newItems and
convert toImmutableList(); update the logic that sets
popularTravelsByProgram[selectedTab.programId] and the allPopularTravels
assignment to use this conditional-last-item-removal behavior so real Travel
items are never accidentally removed.

---

Outside diff comments:
In
`@feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListViewModel.kt`:
- Around line 52-58: The current use of suspendRunCatching(...).getOrNull() when
calling travelTemplateRepository.getAllPopularTravelTemplates() and
getPopularTravelTemplates(...) swallows failures and causes the ViewModel to
always emit a Success state (e.g., in the flow that resolves
popularTemplateDeferred), exposing empty lists instead of an error; change the
logic in PopularTravelListViewModel to detect failures from suspendRunCatching
(inspect the Result instead of calling getOrNull()), and when the overall fetch
for all templates or any required aggregate request fails, emit
PopularTravelListState.Error (rather than Success with empty data); update the
code around popularTemplateDeferred and the aggregation/emit pathway to branch
on Result.isSuccess/exceptionOrNull()/getOrElse so that real errors propagate to
the Error state while successful results continue to emit Success.

---

Nitpick comments:
In `@core/ui/src/main/java/com/yapp/ndgl/core/ui/CommonErrorView.kt`:
- Around line 26-30: The Column in CommonErrorView.kt uses a hardcoded vertical
padding (.padding(horizontal = 24.dp, vertical = 165.dp)), which reduces
reusability; change the CommonErrorView composable signature to accept a
verticalPadding: Dp (with a sensible default) or a contentPadding: PaddingValues
parameter, remove the fixed vertical = 165.dp from the modifier and apply the
injected padding instead, and update callers to supply appropriate vertical
spacing where needed (search for CommonErrorView and the Column modifier to
locate the code to modify).

In
`@feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListContract.kt`:
- Around line 25-35: The lazy delegate in the selectedProgramTravels property
inside PopularTravelListContract introduces mutable state and conflicts with
`@Immutable`; replace the by lazy usage with a plain computed val/getter (or have
the ViewModel compute and pass the value) so selectedProgramTravels is derived
deterministically from travelProgramTabs, selectedTabIndex, TravelProgramTab
(e.g., TravelProgramTab.All), popularTravelsByProgram and allPopularTravels;
specifically remove the by lazy delegate on selectedProgramTravels and implement
it as a direct when-expression (or supply it as a precomputed field) to preserve
immutability and avoid Compose instability.
- Around line 66-70: The ShowSnackBar side effect currently defines enum Type
with a single value ERR_UNKNOWN which limits clarity or is unnecessary; update
PopularTravelListContract's ShowSnackBar to either expand Type with concrete
cases (e.g., NETWORK_ERROR, TIMEOUT, AUTH_ERROR) to explicitly represent
expected error categories, or simplify by replacing enum Type with a payload
type (e.g., a String message or an Error/Throwable reference) if you intend to
handle all errors generically; adjust usages of ShowSnackBar and any consumers
of PopularTravelListSideEffect to match the new Type shape.

In
`@feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListScreen.kt`:
- Around line 235-252: The LoadingItem composable currently uses
LaunchedEffect(Unit) so onLoadMore is only invoked on first composition; change
the LaunchedEffect key to nextPage so the effect re-runs whenever the page
changes (i.e., replace LaunchedEffect(Unit) with LaunchedEffect(nextPage) in the
LoadingItem function) ensuring onLoadMore(nextPage) is called on page updates
and retry scenarios.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 685d988 and 7751d9e.

📒 Files selected for processing (8)
  • core/ui/src/main/java/com/yapp/ndgl/core/ui/CommonErrorView.kt
  • core/ui/src/main/res/values/strings.xml
  • data/travel/src/main/java/com/yapp/ndgl/data/travel/repository/TravelTemplateRepository.kt
  • feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListContract.kt
  • feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListScreen.kt
  • feature/home/src/main/java/com/yapp/ndgl/feature/home/popular/PopularTravelListViewModel.kt
  • feature/home/src/main/java/com/yapp/ndgl/feature/home/search/TemplateSearchScreen.kt
  • feature/home/src/main/res/values/strings.xml
💤 Files with no reviewable changes (1)
  • feature/home/src/main/res/values/strings.xml

@jihee-dev jihee-dev force-pushed the feature/NDGL-124/impl-popular-list-paging branch 2 times, most recently from 14108d8 to 0800a06 Compare February 27, 2026 16:00
@jihee-dev jihee-dev force-pushed the feature/NDGL-124/impl-popular-list-paging branch from 0800a06 to 7420bfc Compare February 27, 2026 16:05
@jihee-dev jihee-dev merged commit f042d3f into develop Feb 27, 2026
4 checks passed
@jihee-dev jihee-dev deleted the feature/NDGL-124/impl-popular-list-paging branch February 27, 2026 16:09
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