Skip to content

[Feature/#186] : 큐레이션 기능 구현#193

Merged
sohyun127 merged 15 commits intodevelopfrom
feature/#186-curation
Oct 25, 2025
Merged

[Feature/#186] : 큐레이션 기능 구현#193
sohyun127 merged 15 commits intodevelopfrom
feature/#186-curation

Conversation

@sohyun127
Copy link
Copy Markdown
Collaborator

@sohyun127 sohyun127 commented Oct 24, 2025

✅ 𝗖𝗵𝗲𝗰𝗸-𝗟𝗶𝘀𝘁

  • merge할 브랜치의 위치를 확인해 주세요(main❌/develop⭕)
  • 리뷰가 필요한 경우 리뷰어를 지정해 주세요
  • P1 단계의 리뷰는 필수로 반영합니다.
  • Approve된 PR은 assigner가 머지하고, 수정 요청이 온 경우 수정 후 다시 push를 합니다.

📌 𝗜𝘀𝘀𝘂𝗲𝘀

📎𝗪𝗼𝗿𝗸 𝗗𝗲𝘀𝗰𝗿𝗶𝗽𝘁𝗶𝗼𝗻

  • 큐레이션 UI 구현
  • 큐레이션 API 연동
  • 소식, 큐레이션 레드닷 구현(정책: 큐레이션, 공지사항 레드닷이 모두 없어질 때 소식 레드닷이 사라져야 함.)

📷 𝗦𝗰𝗿𝗲𝗲𝗻

엠티뷰 컨텐츠
Screenshot_20251024_132042 image
KakaoTalk_20251024_224024006.mp4

💬 𝗧𝗼 𝗥𝗲𝘃𝗶𝗲𝘄𝗲𝗿𝘀

Summary by CodeRabbit

  • 새로운 기능

    • 큐레이션 탭 추가 및 큐레이션 목록 페이징 표시
    • 큐레이션 항목 클릭 시 외부 링크 열기
    • 서버·로컬 큐레이션 비교로 빨간 점(알림 배지) 표시 및 동기화 저장
  • UI

    • 큐레이션 전용 화면(Compose 기반) 및 항목 UI 추가
    • 큐레이션 탭명·빈 상태 문구·아이콘 리소스 추가
  • 기타

    • 로컬 환경에서 큐레이션 ID 저장/관리 기능 추가

@sohyun127 sohyun127 requested a review from chanubc October 24, 2025 13:47
@sohyun127 sohyun127 self-assigned this Oct 24, 2025
@sohyun127 sohyun127 added ⭐ [FEAT] 새로운 기능 구현 🐱 소현 리나 ☕ [COMPOSE] 컴포즈 ui labels Oct 24, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 24, 2025

Walkthrough

뉴스 탭을 큐레이션으로 전환하는 변경으로, 네트워크 엔드포인트·DTO·모델 추가, 매퍼와 저장소/데이터소스 변경, 로컬 Preference 키 교체, 페이징 기반 Compose UI(큐레이션 화면) 및 관련 ViewModel/인텐트/사이드이펙트가 추가되었습니다.

Changes

Cohort / File(s) Summary
Model & DTO
core/model/src/main/java/com/teamwable/model/news/CurationModel.kt, core/network/src/main/java/com/teamwable/network/dto/response/news/ResponseCurationInfoDto.kt, core/network/src/main/java/com/teamwable/network/dto/response/news/ResponseNewsCurationNumberDto.kt
새로운 CurationModel 및 큐레이션 관련 DTO들 추가.
Mapper & Network
core/data/src/main/java/com/teamwable/data/mapper/toModel/ResponseCurationDtoMapper.kt, core/network/src/main/java/com/teamwable/network/datasource/NewsService.kt
ResponseCurationInfoDto.toCuration() 매퍼 추가. getCurationInfo()getCurationNumber() API 메서드 추가.
Repository Interfaces
core/data/src/main/java/com/teamwable/data/repository/NewsRepository.kt, core/data/src/main/java/com/teamwable/data/repository/UserInfoRepository.kt
NewsRepository에 큐레이션 API 추가. UserInfoRepository에서 newsNumber 관련 API 삭제 후 curationId 관련 API로 대체.
Repository Implementations
core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultNewsRepository.kt, core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultUserInfoRepository.kt
큐레이션 페이징 구현(Pager + GenericPagingSource → toCuration 매핑) 및 curationId 저장/조회 구현 추가.
Datastore / Preferences
core/datastore/src/main/java/com/teamwable/datastore/datasource/DefaultWablePreferenceDatasource.kt, core/datastore/src/main/java/com/teamwable/datastore/datasource/WablePreferencesDataSource.kt
newsNumber 제거, curationId: Long 키/flow 추가. notice/news명칭 일부 정렬 및 업데이트 메서드 교체.
Main 화면
feature/main/src/main/java/com/teamwable/main/MainViewModel.kt, feature/main/src/main/java/com/teamwable/main/MainActivity.kt
MainViewModel 추가로 빨간점(red-dot) 상태 계산(서버 notice/curation vs 로컬) 및 MainActivity에서 관찰·표시 로직 추가.
News 기본 ViewModel/Fragment
feature/news/src/main/java/com/teamwable/news/NewsViewModel.kt, feature/news/src/main/java/com/teamwable/news/NewsFragment.kt
curationId 상태/메서드 추가, 기존 newsNumber → noticeNumber로 일부 명칭 변경, 빨간점 로직·저장 호출을 큐레이션 중심으로 변경.
탭/어댑터/네비게이션
feature/news/src/main/java/com/teamwable/news/NewsTabType.kt, feature/news/src/main/java/com/teamwable/news/NewsViewPagerAdapter.kt, feature/news/src/main/java/com/teamwable/news/news/NewsNewsFragment.kt
NEWS 탭 항목을 CURATION으로 교체하고 관련 프래그먼트/네비게이션 인자 업데이트.
Curation 기능 — ViewModel / Intent / SideEffect
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationViewModel.kt, feature/news/src/main/java/com/teamwable/news/curation/NewsCurationIntent.kt, feature/news/src/main/java/com/teamwable/news/curation/NewsCurationSideEffect.kt
페이징 흐름 제공하는 NewsCurationViewModel 추가, ClickLink/PullToRefresh 인텐트 및 Navigation/UI 사이드이펙트 정의.
Curation 기능 — Compose UI
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationRoute.kt, feature/news/src/main/java/com/teamwable/news/curation/NewsCurationItem.kt, feature/news/src/main/java/com/teamwable/news/curation/NewsCurationFragment.kt
Compose 기반 큐레이션 화면(리스트, 아이템, 클릭/리프레시 처리) 및 Fragment 호환 레이아웃 추가.
리소스 & 빌드
feature/news/src/main/res/layout/fragment_news_curation.xml, feature/news/src/main/res/drawable/ic_curation_link.xml, feature/news/src/main/res/values/strings.xml, core/network/build.gradle.kts, gradle/libs.versions.toml
레이아웃·아이콘·문자열 리소스 추가, core/network에 annotation-experimental 의존성 추가 및 libs 버전 항목 추가.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as Curation UI (Compose)
    participant VM as NewsCurationViewModel
    participant Repo as NewsRepository
    participant Service as NewsService
    participant Local as UserInfoRepository (Preferences)

    User->>UI: 큐레이션 화면 진입
    UI->>VM: ViewModel 사용(페이징 구독)
    VM->>Repo: getCurationInfo() 요청 (Paging Flow)
    Repo->>Service: GET /api/v1/curation(cursor)
    Service-->>Repo: ResponseCurationInfoDto 리스트
    Repo-->>VM: CurationModel 페이징 스트림
    VM-->>UI: 페이징 항목 노출
    UI->>UI: 리스트 렌더링

    User->>UI: 아이템 클릭
    UI->>VM: ClickLink(url) 인텐트
    VM-->>UI: Navigation.ToUrl(side-effect)
    UI->>User: URL 오픈

    User->>UI: Pull-to-Refresh
    UI->>VM: PullToRefresh 인텐트
    VM->>Repo: 페이징 재요청
    Repo->>Service: getCurationInfo()
    Service-->>Repo: 업데이트된 데이터
    VM-->>UI: UI.Refresh(side-effect)
    UI->>UI: 리스트 새로고침
Loading
sequenceDiagram
    participant Fragment as NewsFragment / MainActivity
    participant MainVM as MainViewModel
    participant NewsRepo as NewsRepository
    participant UserRepo as UserInfoRepository
    participant Service as NewsService

    Fragment->>MainVM: redDotUiState 구독
    MainVM->>NewsRepo: getCurationNumber()
    NewsRepo->>Service: GET /api/v1/curation/number
    Service-->>NewsRepo: ResponseNewsCurationNumberDto
    NewsRepo-->>MainVM: Result<Long>
    MainVM->>UserRepo: getCurationNumber() (local flow)
    MainVM->>UserRepo: getNoticeNumber() (local flow)
    MainVM->>MainVM: 서버 vs 로컬 비교 (notice 증가 또는 curation 변경)
    MainVM-->>Fragment: UiState.Success(isRedDot)
    Fragment->>Fragment: 빨간점 표시/숨김
Loading

🎯 Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

  • 주의할 파일/영역:
    • 페이징 파이프라인: DefaultNewsRepository → GenericPagingSource 매핑(toCuration) 검증
    • Preference 타입 변경: newsNumber(int) → curationId(long)로 인한 마이그레이션/타입 불일치 가능성
    • MainViewModel/NewsViewModel의 red-dot 계산 로직(동시성/예외 처리)
    • Compose 페이징 UI의 null/로드 상태 처리 및 이미지 로딩 실패 처리

🏷️ Suggested labels

💟 [UI]

👥 Suggested Reviewers

  • chanubc

🐰 Poem

🐇 토끼가 남긴 짧은 축하 시
뉴스는 떠나고, 큐레이션이 왔네,
페이지를 넘기니 링크들이 춤추네 ✨
빨간 점 하나로 소식이 속삭이고,
토끼는 깡총, 새 화면에 기뻐하네 🥕
모두의 손끝에 더 가벼운 소식이 닿기를.

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 "[Feature/#186] : 큐레이션 기능 구현"은 changeset의 주요 변경사항을 명확하게 나타내고 있습니다. 새로운 큐레이션 화면 구현, 큐레이션 API 연동, 레드닷 기능 추가 등 핵심 기능들이 제목에 잘 요약되어 있으며, 이슈 번호 참조도 포함되어 있어 추적이 용이합니다. 제목은 간결하고 읽기 쉬우며 명확합니다.
Linked Issues Check ✅ Passed 연결된 이슈 #186의 요구사항인 "뉴스 → 큐레이션 화면 구현"이 changeset에서 완전히 충족되고 있습니다. NewsCurationFragment, NewsCurationRoute, NewsCurationViewModel 등의 새로운 큐레이션 화면 컴포넌트들이 구현되었고, NewsTabType.NEWS에서 CURATION으로의 변경을 통해 네비게이션이 반영되었습니다. 또한 ResponseCurationInfoDto, CurationModel 등 필요한 데이터 모델과 API 연동도 완성되어 있습니다.
Out of Scope Changes Check ✅ Passed 모든 변경사항이 이슈 #186의 범위 내에 있습니다. 큐레이션 화면 관련 UI 컴포넌트, API 연동, 데이터 모델 변경이 모두 이슈의 요구사항과 관련있으며, PR 설명에서 명시한 "레드닷 구현"도 포함되어 있습니다. NewsNumber에서 NoticeNumber로의 변경 및 CurationId 추가는 큐레이션 기능 구현을 위한 필수적인 데이터 아키텍처 변경입니다.
Description Check ✅ Passed PR 설명이 template의 모든 주요 섹션을 포함하고 있습니다. 체크리스트, 이슈 참조(closed #186), 상세한 작업 설명(큐레이션 UI 구현, API 연동, 레드닷 구현 정책), 스크린샷이 모두 포함되어 있습니다. To Reviewers 섹션은 비어있지만, 필수가 아닌 선택적 섹션으로 보이며 전체적으로 changeset을 이해하기에 충분한 정보가 제공되어 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#186-curation

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: 6

🧹 Nitpick comments (15)
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationSideEffect.kt (2)

10-12: data object 사용은 Kotlin 1.9+ 전제 — 버전 확인 필요

프로젝트 Kotlin이 1.9 미만이면 컴파일 오류입니다. 호환성 우선이면 아래처럼 교체를 고려하세요.

-        data object Refresh : UI
+        object Refresh : UI

13-13: UI 계층에 Throwable 전달 지양

UI로 예외 객체를 넘기면 구현 누수·로깅/직렬화 이슈가 생깁니다. 사용자용 메시지(@stringres) 또는 도메인 에러 타입만 전달하세요.

-        data class ShowSnackBar(val throwable: Throwable?) : UI
+        data class ShowSnackBar(@androidx.annotation.StringRes val messageRes: Int) : UI
core/datastore/src/main/java/com/teamwable/datastore/datasource/DefaultWablePreferenceDatasource.kt (2)

82-92: Preferences 키 변경에 따른 마이그레이션 필요: newsNumber → noticeNumber

기존 설치에서 newsNumber만 저장되어 있으면 신규 noticeNumber로 이관되지 않아 레드닷 정책이 어긋날 수 있습니다. 최초 실행 시 1회 마이그레이션을 권장합니다.

다음 유틸을 추가해 앱 시작 시 호출해 주세요(예: Repository/Initializer):

private val OldNewsNumber = androidx.datastore.preferences.core.intPreferencesKey("newsNumber")

suspend fun migrateLegacyNewsNumberToNoticeNumberIfNeeded(
    dataStore: androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences>
) {
    dataStore.edit { prefs ->
        val old = prefs[OldNewsNumber]
        val new = prefs[PreferencesKeys.NoticeNumber]
        if (old != null && new == null) {
            prefs[PreferencesKeys.NoticeNumber] = old
            prefs.remove(OldNewsNumber)
        }
    }
}

또한 -1 센티널 사용이 전역적으로 동일한 의미(“미설정/없음”)인지 호출부에서 확인 부탁드립니다.


148-152: 키 초기화 방식 확인

curationId를 초기화할 때 -1 기록 vs 키 제거 중 어떤 정책인지 합의가 필요합니다. 키 제거가 더 명확하다면 preferences.remove(PreferencesKeys.CurationId)를 사용하는 별도 clear API를 두는 방안을 고려해 주세요.

feature/news/src/main/res/drawable/ic_curation_link.xml (1)

6-22: 하드코딩된 색상 리소스화 및 테마 대응 제안

#2D2D2D, #0FE5D0를 색상 리소스로 분리해 다크/라이트 모드 및 브랜드 변경에 유연하게 대응하세요. 필요 시 벡터 루트에 android:tint 적용을 고려해 단색 아이콘 테마 연동도 가능하게 할 수 있습니다.

feature/news/src/main/res/layout/fragment_news_curation.xml (1)

7-14: ComposeView CompositionStrategy 설정 요청 + 레이아웃 간소화 제안

  • 누수 방지를 위해 Fragment에서 DisposeOnViewTreeLifecycleDestroyed 설정을 확인해 주세요.
  • 이 화면이 Compose 전용이면 ConstraintLayout 래핑 없이 ComposeView를 match_parent로 사용하는 것도 간단합니다.
// in NewsCurationFragment.onViewCreated(...)
composeView.setViewCompositionStrategy(
    androidx.compose.ui.platform.ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
composeView.setContent { /* NewsCurationRoute(...) */ }
feature/news/src/main/java/com/teamwable/news/NewsTabType.kt (1)

5-10: 현재 코드는 라벨 매핑이 정상이지만, 열거형 설계 개선이 권장됩니다

검증 결과:

  1. NEWS 상수 잔존: 없음 (이미 제거됨) ✓
  2. 라벨 매핑: NewsFragment의 TabLayoutMediator에서 각 탭(MATCH, RANK, CURATION, NOTICE)을 명시적으로 when 분기로 처리하므로 기본값 문제 발생 안 함 ✓
  3. String 리소스: tv_news_tab_match, tv_news_tab_rank, tv_news_tab_curation, tv_news_tab_notice 모두 정의됨 ✓

다만 현재 enum의 title 파라미터는 정의되어 있으면서 실제로는 사용되지 않고, NewsViewPagerAdapterNewsFragment에서 ordinal 기반 when 분기로 처리하고 있습니다. 원본 제안처럼 enum 항목별로 title을 명시적으로 지정하도록 리팩토링하면, ordinal에 의존하지 않으므로 유지보수성이 향상됩니다.

feature/news/src/main/java/com/teamwable/news/curation/NewsCurationViewModel.kt (1)

18-22: Paging 에러 처리 개선: catch 위치/전달값 정정 및 UI 연동 권장

  • Paging3는 예외를 LoadState(Error)로 노출합니다. 이 Flow의 catch는 기대만큼 호출되지 않을 수 있어 UI가 빈 화면이 될 수 있습니다.
  • it.cause 대신 it 자체를 전달하세요. cause가 null인 경우가 많아 정보가 소실됩니다.

빠른 수정:

-        .catch {
-            postSideEffect(NewsCurationSideEffect.UI.ShowSnackBar(it.cause))
-        }
+        .catch { throwable ->
+            postSideEffect(NewsCurationSideEffect.UI.ShowSnackBar(throwable))
+        }

추가 권장: 에러 처리는 Route의 WablePagingScreen(errorContent = …)로 노출하고, 여기 catch는 제거해 이중처리를 피하세요. Route 파일 코멘트 참고.

feature/news/src/main/java/com/teamwable/news/curation/NewsCurationItem.kt (4)

96-121: 로딩/실패 플레이스홀더가 영역을 꽉 채우지 않을 수 있음

GlideImage 슬롯 내 컴포저블은 별도 크기 지시가 없으면 축소될 수 있습니다. 리스트 스켈레톤이 들쭉날쭉해 보여요. matchParentSize()로 고정하세요.

-            loading = {
-                Box(
-                    modifier = Modifier
-                        .background(WableTheme.colors.gray200),
-                )
-            },
+            loading = {
+                Box(
+                    modifier = Modifier
+                        .matchParentSize()
+                        .background(WableTheme.colors.gray200),
+                )
+            },
             failure = {
-                Image(
-                    painter = painterResource(id = R.drawable.img_view_it_empty),
-                    contentScale = ContentScale.Crop,
-                    contentDescription = null,
-                )
+                Image(
+                    painter = painterResource(id = R.drawable.img_view_it_empty),
+                    contentScale = ContentScale.Crop,
+                    contentDescription = null,
+                    modifier = Modifier.matchParentSize(),
+                )
             },

Also applies to: 106-111


123-141: 배경/외곽선 모서리 값 불일치

배경은 topEnd+bottomEnd, 보더는 bottomStart+bottomEnd로 달라 이음새가 보일 수 있습니다. 하단 블록에는 하단 모서리만 라운드가 자연스럽습니다.

-                .background(
-                    color = WableTheme.colors.gray100,
-                    shape = RoundedCornerShape(
-                        topEnd = 8.dp,
-                        bottomEnd = 8.dp,
-                    ),
-                )
+                .background(
+                    color = WableTheme.colors.gray100,
+                    shape = RoundedCornerShape(
+                        bottomStart = 8.dp,
+                        bottomEnd = 8.dp,
+                    ),
+                )
                 .border(
                     width = 1.dp,
                     color = WableTheme.colors.gray200,
-                    shape = RoundedCornerShape(
-                        bottomStart = 8.dp,
-                        bottomEnd = 8.dp,
-                    ),
+                    shape = RoundedCornerShape(
+                        bottomStart = 8.dp,
+                        bottomEnd = 8.dp,
+                    ),
                 )

91-98: 접근성 보완: 클릭 가능 영역에 역할 부여

noRippleClickable 사용 시 시각적 피드백/역할 노출이 약합니다. 스크린리더용 역할을 추가하세요.

-import com.teamwable.designsystem.extension.modifier.noRippleClickable
+import com.teamwable.designsystem.extension.modifier.noRippleClickable
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics

 ...
     Column(
         modifier = Modifier
             .fillMaxWidth()
-            .noRippleClickable { onItemClick(data.curationLink) },
+            .noRippleClickable { onItemClick(data.curationLink) }
+            .semantics { role = Role.Button },
     ) {

[release_note_optional]

Also applies to: 167-173


70-73: 하드코딩 문자열 리소스화

"와블 큐레이터"는 strings.xml로 이동해 i18n/테마 적용을 용이하게 하세요.

-import androidx.compose.material3.Text
+import androidx.compose.material3.Text
+import androidx.compose.ui.res.stringResource
 ...
-        Text(
-            text = "와블 큐레이터",
+        Text(
+            text = stringResource(id = com.teamwable.news.R.string.curation_author_label),
             style = WableTheme.typography.body03,
         )

(리소스 추가: feature/news/src/main/res/values/strings.xmlcuration_author_label.)

feature/main/src/main/java/com/teamwable/main/MainViewModel.kt (1)

27-41: 중복 수집/메모리 누수 가능성 및 실패 시 오탑재 방지

  • validateNewsRedDot()를 여러 번 호출하면 combine 수집이 중복됩니다. Job 관리 또는 stateIn으로 단일 스트림화가 안전합니다.
  • 서버 실패 시 serverCurationId == -1L이면 항상 local과 달라 레드닷이 켜질 수 있습니다. 실패는 무시하거나 안전 기본값을 사용하세요.
+    private var redDotJob: Job? = null
@@
-    fun validateNewsRedDot() {
-        viewModelScope.launch {
-            val serverNoticeNumber = newsRepository.getNumber().getOrNull()?.get("notice") ?: -1
-            val serverCurationId = newsRepository.getCurationNumber().getOrNull() ?: -1
+    fun validateNewsRedDot() {
+        redDotJob?.cancel()
+        redDotJob = viewModelScope.launch {
+            val serverNoticeNumber = newsRepository.getNumber().getOrNull()?.get("notice") ?: -1
+            val serverCurationId: Long? = newsRepository.getCurationNumber().getOrNull()
@@
-            ) { localNoticeNumber, localCurationId ->
-                serverNoticeNumber > localNoticeNumber || serverCurationId != localCurationId
-            }.collect { isRedDot ->
+            ) { localNoticeNumber, localCurationId ->
+                val hasNewNotice = serverNoticeNumber > localNoticeNumber
+                val hasNewCuration = serverCurationId?.let { it != localCurationId } ?: false
+                hasNewNotice || hasNewCuration
+            }
+            .distinctUntilChanged()
+            .collect { isRedDot ->
                 _redDotUiState.value = UiState.Success(isRedDot)
             }
         }
     }
  • 정책 확인: 서버 오류 시 레드닷을 끄는 것이 맞는지(현재 제안은 실패 무시).
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationRoute.kt (1)

52-62: 초기 로드 실패시 사용자 피드백 부재: errorContent 연결

WablePagingScreenerrorContent가 비어 있어 첫 로드 실패 시 빈 화면 위험이 있습니다. 스낵바 콜백에 위임하세요.

     WablePagingScreen(
         lazyPagingItems = curations,
         onRefresh = { viewModel.onIntent(NewsCurationIntent.PullToRefresh) },
+        errorContent = { throwable -> onShowSnackBar(throwable) },
         content = {
             CurationScreen(
                 curations,
                 listState,
                 onItemClick = { viewModel.onIntent(NewsCurationIntent.ClickLink(it)) },
             )
         },
     )
feature/news/src/main/java/com/teamwable/news/NewsViewModel.kt (1)

84-90: 에러 메시지 가독성

UiState.Failure(it.message.toString())는 null 메시지를 "null"로 표기해 디버깅에 불리할 수 있습니다. 기본 메시지 보강을 권장합니다.

-                .onFailure { _curationIdUiState.value = UiState.Failure(it.message.toString()) }
+                .onFailure { _curationIdUiState.value = UiState.Failure(it.message ?: "Unknown error") }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 132dde1 and fee7665.

📒 Files selected for processing (27)
  • core/data/src/main/java/com/teamwable/data/mapper/toModel/ResponseCurationDtoMapper.kt (1 hunks)
  • core/data/src/main/java/com/teamwable/data/repository/NewsRepository.kt (2 hunks)
  • core/data/src/main/java/com/teamwable/data/repository/UserInfoRepository.kt (2 hunks)
  • core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultNewsRepository.kt (2 hunks)
  • core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultUserInfoRepository.kt (2 hunks)
  • core/datastore/src/main/java/com/teamwable/datastore/datasource/DefaultWablePreferenceDatasource.kt (4 hunks)
  • core/datastore/src/main/java/com/teamwable/datastore/datasource/WablePreferencesDataSource.kt (2 hunks)
  • core/model/src/main/java/com/teamwable/model/news/CurationModel.kt (1 hunks)
  • core/network/src/main/java/com/teamwable/network/datasource/NewsService.kt (2 hunks)
  • core/network/src/main/java/com/teamwable/network/dto/response/news/ResponseCurationInfoDto.kt (1 hunks)
  • core/network/src/main/java/com/teamwable/network/dto/response/news/ResponseNewsCurationNumberDto.kt (1 hunks)
  • feature/main/src/main/java/com/teamwable/main/MainActivity.kt (6 hunks)
  • feature/main/src/main/java/com/teamwable/main/MainViewModel.kt (1 hunks)
  • feature/news/src/main/java/com/teamwable/news/NewsFragment.kt (5 hunks)
  • feature/news/src/main/java/com/teamwable/news/NewsTabType.kt (1 hunks)
  • feature/news/src/main/java/com/teamwable/news/NewsViewModel.kt (2 hunks)
  • feature/news/src/main/java/com/teamwable/news/NewsViewPagerAdapter.kt (1 hunks)
  • feature/news/src/main/java/com/teamwable/news/curation/NewsCurationFragment.kt (1 hunks)
  • feature/news/src/main/java/com/teamwable/news/curation/NewsCurationIntent.kt (1 hunks)
  • feature/news/src/main/java/com/teamwable/news/curation/NewsCurationItem.kt (1 hunks)
  • feature/news/src/main/java/com/teamwable/news/curation/NewsCurationRoute.kt (1 hunks)
  • feature/news/src/main/java/com/teamwable/news/curation/NewsCurationSideEffect.kt (1 hunks)
  • feature/news/src/main/java/com/teamwable/news/curation/NewsCurationViewModel.kt (1 hunks)
  • feature/news/src/main/java/com/teamwable/news/news/NewsNewsFragment.kt (1 hunks)
  • feature/news/src/main/res/drawable/ic_curation_link.xml (1 hunks)
  • feature/news/src/main/res/layout/fragment_news_curation.xml (1 hunks)
  • feature/news/src/main/res/values/strings.xml (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationViewModel.kt (1)
core/common/src/main/java/com/teamwable/common/base/BaseViewModel.kt (1)
  • postSideEffect (41-47)
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationItem.kt (3)
core/designsystem/src/main/java/com/teamwable/designsystem/extension/composable/ImageExt.kt (1)
  • toImageVector (8-9)
feature/news/src/main/java/com/teamwable/news/news/component/NewsTextComponent.kt (1)
  • WableNewsTimeText (13-29)
core/designsystem/src/main/java/com/teamwable/designsystem/theme/Theme.kt (1)
  • WableTheme (54-65)
feature/main/src/main/java/com/teamwable/main/MainActivity.kt (1)
core/ui/src/main/java/com/teamwable/ui/extensions/ContextExt.kt (1)
  • colorOf (45-47)
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationRoute.kt (3)
core/designsystem/src/main/java/com/teamwable/designsystem/component/screen/PagingScreen.kt (1)
  • WablePagingScreen (72-120)
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationItem.kt (1)
  • NewsCurationItem (38-52)
core/designsystem/src/main/java/com/teamwable/designsystem/component/paging/WablePagingSpinner.kt (1)
  • WablePagingSpinner (30-60)
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationFragment.kt (3)
core/designsystem/src/main/java/com/teamwable/designsystem/theme/Theme.kt (1)
  • WableTheme (54-65)
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationRoute.kt (1)
  • NewsCurationRoute (30-63)
core/ui/src/main/java/com/teamwable/ui/extensions/FragmentExt.kt (1)
  • openUri (49-54)
feature/news/src/main/java/com/teamwable/news/NewsFragment.kt (3)
core/ui/src/main/java/com/teamwable/ui/extensions/FragmentExt.kt (1)
  • stringOf (31-33)
core/ui/src/main/java/com/teamwable/ui/extensions/ContextExt.kt (1)
  • stringOf (41-43)
core/common/src/main/java/com/teamwable/common/util/AmplitudeUtil.kt (1)
  • trackEvent (21-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: PR Checker
🔇 Additional comments (17)
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationIntent.kt (1)

1-3: 패키지 구조와 임포트가 적절합니다.

패키지 네이밍과 BaseIntent 임포트가 올바르게 구성되어 있습니다.

feature/news/src/main/java/com/teamwable/news/curation/NewsCurationSideEffect.kt (1)

7-7: 외부 URL 내비게이션은 스킴/호스트 검증 필요

onOpenUrl 처리부에서 http/https만 허용하고, 기타 스킴 차단 및 CustomTabs 사용을 확인해 주세요. 안전하지 않은 인텐트 실행을 예방합니다.

feature/news/src/main/res/values/strings.xml (1)

47-49: LGTM!

큐레이션 기능을 위한 문자열 리소스가 적절하게 추가되었습니다. 네이밍 컨벤션도 기존 패턴을 잘 따르고 있습니다.

core/data/src/main/java/com/teamwable/data/mapper/toModel/ResponseCurationDtoMapper.kt (1)

6-13: LGTM!

DTO를 도메인 모델로 변환하는 매퍼 로직이 명확하고 적절합니다. nullable 필드에 대한 Elvis 연산자 처리도 올바르게 구현되어 있습니다.

feature/news/src/main/java/com/teamwable/news/NewsViewPagerAdapter.kt (1)

13-21: LGTM!

뉴스 탭을 큐레이션 탭으로 교체하는 로직이 올바르게 구현되었습니다. NewsTabType.CURATIONNewsCurationFragment의 사용이 일관성 있게 적용되었습니다.

core/data/src/main/java/com/teamwable/data/repository/NewsRepository.kt (1)

23-25: LGTM!

큐레이션 데이터 접근을 위한 새로운 API 메서드가 기존 패턴을 잘 따르고 있습니다. Flow<PagingData<T>>Result<T> 사용이 일관성 있게 적용되었습니다.

core/network/src/main/java/com/teamwable/network/dto/response/news/ResponseCurationInfoDto.kt (1)

9-15: DTO 구조는 적절합니다.

필드 매핑과 타입 선택이 올바르게 구현되어 있습니다. nullable 필드 처리도 적절합니다.

feature/news/src/main/java/com/teamwable/news/curation/NewsCurationFragment.kt (1)

14-28: LGTM!

Compose를 통합한 프래그먼트 구현이 표준 패턴을 잘 따르고 있습니다. 생명주기 관리와 콜백 처리가 적절하게 구현되었습니다.

core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultUserInfoRepository.kt (1)

38-39: API 변경사항이 완전히 구현되었습니다 - 검증 완료

검증 결과, 기존 메서드 getNewsNumber()/saveNewsNumber() 에 대한 사용처가 발견되지 않았습니다. 새로운 메서드 getCurationNumber()/saveCurationId() 로의 전환이 전체 코드베이스에서 완전히 완료되었습니다:

  • NewRepository: getCurationNumber() 호출 (line 86)
  • NewsViewModel: saveCurationId() 구현 및 사용 (line 94-97)
  • NewsFragment: saveCurationId() 호출 (line 104)
  • MainViewModel: 두 메서드 모두 올바르게 사용 중 (line 30, 34)

(NewsViewModel의 private getNewsNumber() 함수는 newsRepository.getNumber()를 호출하는 별개의 내부 함수이므로 무관합니다)

core/model/src/main/java/com/teamwable/model/news/CurationModel.kt (1)

1-9: LGTM! 깔끔한 데이터 모델입니다.

큐레이션 기능을 위한 데이터 모델이 명확하게 정의되어 있습니다. 모든 필드가 적절한 타입으로 선언되었으며, 네이밍도 일관성 있게 작성되었습니다.

core/data/src/main/java/com/teamwable/data/repository/UserInfoRepository.kt (1)

24-25: LGTM! 인터페이스 변경이 적절합니다.

newsNumber에서 curationId로의 전환이 명확하게 이루어졌으며, 타입도 Long으로 일관성 있게 변경되었습니다. 다른 메서드들과의 네이밍 컨벤션도 잘 지켜지고 있습니다.

Also applies to: 44-45

feature/news/src/main/java/com/teamwable/news/NewsFragment.kt (2)

51-56: LGTM! Lifecycle-aware Flow 수집이 적절하게 구현되었습니다.

flowWithLifecycle을 사용하여 생명주기를 고려한 큐레이션 ID 상태 관찰이 올바르게 구현되었습니다. Lifecycle.State.STARTED를 사용한 것도 적절합니다.


101-104: Analytics 이벤트 태그명 확인이 필요합니다.

큐레이션 탭 클릭 시 CLICK_NEWS 이벤트를 트래킹하고 있습니다. 큐레이션 전용 이벤트 태그(예: CLICK_CURATION)를 사용하는 것이 더 명확하지 않을까요? 아니면 기획 의도상 NEWS와 동일하게 처리하는 것이 맞는지 확인해주세요.

core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultNewsRepository.kt (1)

76-91: LGTM! 리포지토리 구현이 기존 패턴을 잘 따르고 있습니다.

큐레이션 정보 조회 및 큐레이션 번호 조회 메서드가 기존 코드의 패턴을 일관성 있게 따르고 있습니다:

  • Paging 설정(pageSize=15, prefetchDistance=1)이 다른 메서드들과 동일
  • GenericPagingSource를 활용한 커서 기반 페이징 구현
  • 적절한 에러 핸들링 적용
  • 매핑 함수(toCuration()) 사용으로 계층 분리 유지
core/datastore/src/main/java/com/teamwable/datastore/datasource/WablePreferencesDataSource.kt (1)

15-15: LGTM! DataSource 인터페이스 변경이 깔끔합니다.

newsNumber에서 curationId로의 전환이 명확하며, 다른 속성 및 메서드들과 일관된 패턴을 따르고 있습니다. Long 타입 사용도 적절합니다.

Also applies to: 35-35

feature/main/src/main/java/com/teamwable/main/MainActivity.kt (1)

218-233: LGTM! 메인 화면 레드닷 구현이 적절합니다.

뉴스 레드닷 관찰 및 표시 로직이 잘 구현되었습니다:

  • flowWithLifecycle을 사용한 생명주기 인식 Flow 수집
  • Lifecycle.State.STARTED 상태 사용으로 불필요한 업데이트 방지
  • 배지 표시/숨김 로직이 명확하게 분리됨
  • UI 스타일링(offset, color)이 일관성 있게 적용됨

PR 목표에 명시된 "큐레이션 및 공지사항 레드닷이 모두 없어질 때 소식 레드닷이 사라짐" 정책이 ViewModel에서 처리되고 있다고 이해됩니다.

core/network/src/main/java/com/teamwable/network/datasource/NewsService.kt (1)

38-44: LGTM! API 엔드포인트 추가가 적절합니다.

새로운 큐레이션 관련 API 엔드포인트들이 기존 패턴을 일관성 있게 따르고 있습니다:

  • getCurationInfo(): 커서 기반 페이징을 위한 파라미터와 기본값 설정이 다른 정보 조회 메서드들과 동일
  • getCurationNumber(): 간단한 번호 조회 엔드포인트로 명확한 책임 분리
  • Retrofit 어노테이션과 응답 타입 사용이 올바름

Comment on lines +7 to +11
@OptIn(InternalSerializationApi::class)
@Serializable
data class ResponseNewsCurationNumberDto(
@SerialName("curationId") val curationId: Long,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

@OptIn(InternalSerializationApi::class) 사용이 필요한지 확인해주세요.

단순한 데이터 클래스에 @OptIn(InternalSerializationApi::class)를 사용하고 있습니다. Internal API는 경고 없이 변경될 수 있어 안정성에 문제가 될 수 있습니다.

이 어노테이션을 제거하고도 정상적으로 직렬화가 동작하는지 확인해주세요. 일반적으로 @Serializable@SerialName만으로 충분합니다.


🏁 Script executed:

#!/bin/bash
# Description: Check if other DTOs in the project use @OptIn(InternalSerializationApi::class)

# Search for other DTO files to see the pattern
rg -n "@OptIn\(InternalSerializationApi" --type kotlin -C 3

Length of output: 1846


추가 검증을 위해 프로젝트의 다른 DTO 파일들을 확인하여 이 어노테이션 사용 패턴을 비교하겠습니다.

@OptIn(InternalSerializationApi::class) 제거 필요

이 어노테이션으로 표시된 API는 효과적으로 내부이며, kotlinx.serialization 외부에서 사용되어서는 안 되며, 호환성이 보장되지 않습니다. 단순한 데이터 클래스에서는 @serializable과 @SerialName 어노테이션만으로 충분합니다. 프로젝트의 다른 DTO 파일들을 확인하고 이 어노테이션 없이도 정상적으로 작동하는지 검증한 후 제거하시기 바랍니다.

@Serializable
data class ResponseNewsCurationNumberDto(
    @SerialName("curationId") val curationId: Long,
)
🤖 Prompt for AI Agents
In
core/network/src/main/java/com/teamwable/network/dto/response/news/ResponseNewsCurationNumberDto.kt
around lines 7 to 11, remove the unnecessary
@OptIn(InternalSerializationApi::class) annotation because this DTO only needs
@Serializable and @SerialName; update the file to leave @Serializable and
@SerialName on the data class, verify other DTOs follow the same pattern, and
run a build/tests to ensure serialization still works after removal.

Comment on lines +5 to +9
sealed interface NewsCurationIntent : BaseIntent {
data class ClickLink(val url: String) : NewsCurationIntent

data object PullToRefresh : NewsCurationIntent
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Intent 구조가 잘 설계되었습니다.

sealed interface 사용과 data class/data object 활용이 적절합니다. MVI 패턴에 맞는 깔끔한 구현입니다.

ClickLink의 url 파라미터가 ViewModel이나 유스케이스 레이어에서 적절히 검증되는지 확인해주세요 (악성 URL, 딥링크 인젝션 등 방지).

다음 스크립트로 URL 처리 로직을 확인할 수 있습니다:


🏁 Script executed:

#!/bin/bash
# Description: NewsCurationIntent.ClickLink 처리 로직 확인

# ClickLink intent가 어떻게 처리되는지 확인
ast-grep --pattern $'when ($$$) {
  $$$
  is NewsCurationIntent.ClickLink -> $$$
  $$$
}'

# URL 검증 관련 코드 확인
rg -nP --type=kt 'ClickLink|url.*valid|URLUtil|Patterns\.WEB_URL' -C3

Length of output: 26



🏁 Script executed:

#!/bin/bash
# Search for ClickLink usage and URL handling without type restrictions
echo "=== ClickLink intent handling ==="
rg -n 'ClickLink' -C5

echo -e "\n=== URL validation patterns ==="
rg -n 'url.*valid|validateUrl|isValidUrl|URLUtil|openUrl' -C3

echo -e "\n=== Intent handler/processor files ==="
find . -type f -name '*ViewModel*' -o -name '*Processor*' -o -name '*Handler*' | grep -i news | head -20

Length of output: 10123


🏁 Script executed:

#!/bin/bash
# Check ripgrep supported types for Kotlin
rg --type-list | grep -i kotlin

Length of output: 19


🏁 Script executed:

#!/bin/bash
# Search for NewsCurationIntent usage in ViewModels or handlers
rg -n 'NewsCurationIntent' -C3 --max-count=20

Length of output: 4177



🏁 Script executed:

#!/bin/bash
# Get full onLinkClick implementation from NewsCurationViewModel
echo "=== onLinkClick implementation ==="
rg -n 'private fun onLinkClick' -A 20 feature/news/src/main/java/com/teamwable/news/curation/NewsCurationViewModel.kt

echo -e "\n=== Check for URL utilities or navigation handlers ==="
rg -n 'openUrl|startActivity|Intent|navigateTo' -C2 feature/news/src/main/java/com/teamwable/news/curation/NewsCurationViewModel.kt

Length of output: 828



🏁 Script executed:

#!/bin/bash
# Find where NewsCurationSideEffect.Navigation.ToUrl is handled
echo "=== NewsCurationSideEffect definition ==="
find . -name '*NewsCurationSideEffect*' -o -name '*SideEffect*' | grep -i news

echo -e "\n=== ToUrl side effect handling ==="
rg -n 'ToUrl|Navigation\.ToUrl' -C5 feature/news/src/main/java/com/teamwable/news/curation/

echo -e "\n=== Check how side effects are consumed in Route/Screen ==="
rg -n 'sideEffect|LaunchedEffect|collectAsState' -C3 feature/news/src/main/java/com/teamwable/news/curation/NewsCurationRoute.kt

Length of output: 5116



🏁 Script executed:

#!/bin/bash
# Find onOpenUrl implementation and parameters
echo "=== onOpenUrl parameter and implementation ==="
rg -n 'onOpenUrl' -B5 -A10 feature/news/src/main/java/com/teamwable/news/curation/

echo -e "\n=== Check the full NewsCurationRoute composable signature ==="
rg -n '@Composable.*NewsCurationRoute|fun NewsCurationRoute' -A30 feature/news/src/main/java/com/teamwable/news/curation/NewsCurationRoute.kt | head -50

Length of output: 6359



🏁 Script executed:

#!/bin/bash
# Find openUri implementation
echo "=== openUri implementation ==="
rg -n 'fun openUri|private fun openUri' -A15 feature/news/src/main/java/com/teamwable/news/curation/NewsCurationFragment.kt

echo -e "\n=== Check base class or extension for openUri ==="
rg -n 'openUri' -B5 -A15 feature/news/src/main/java/com/teamwable/news/ | head -80

Length of output: 8961



🏁 Script executed:

#!/bin/bash
# Find openUri extension function definition
echo "=== Find openUri extension in ui module ==="
find . -path '*/ui/extensions*' -name '*.kt' -o -name '*Extension*.kt' | grep -i ui

echo -e "\n=== Search for openUri function definition ==="
rg -n 'fun.*openUri|\.openUri' --type kotlin -B2 -A15 | head -100

Length of output: 12746


URL 검증 로직이 누락되었습니다. 보안을 위해 추가가 필요합니다.

sealed interface 구조는 우수하지만, 검증 결과 URL이 별도의 검증 없이 직접 처리되고 있습니다. NewsCurationViewModel.onLinkClick()NewsCurationSideEffect.Navigation.ToUrlFragment.openUri() 흐름에서 악성 URL이나 딥링크 인젝션 방지 로직이 없습니다. openUri 확장 함수는 startActivity() 실패만 처리할 뿐 URL 유효성 검증은 수행하지 않습니다.

  • ViewModel 레이어에서 URL 형식(프로토콜, 도메인 등) 검증 추가
  • 외부 출처 URL에 대한 화이트리스트 또는 보안 정책 적용 검토
🤖 Prompt for AI Agents
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationIntent.kt
around lines 5-9: the intent currently carries raw URLs without validation;
update the ViewModel (where onLinkClick produces
NewsCurationSideEffect.Navigation.ToUrl) to validate and sanitize the URL before
emitting navigation: parse the URL with URI/URL, ensure allowed schemes
(http/https), reject unsafe schemes (intent, javascript, data, file), verify
host is present and optionally check against a domain whitelist or apply a
security policy, normalize/encode the URL to remove unsafe characters, and if
validation fails emit an error side effect instead of navigation; include
minimal logging for rejected URLs and unit tests covering valid/invalid schemes
and whitelist behavior.

Copy link
Copy Markdown
Member

@chanubc chanubc left a comment

Choose a reason for hiding this comment

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

깔끔하네요! 고생많으셨습니다~

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: 3

Caution

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

⚠️ Outside diff range comments (1)
core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultNewsRepository.kt (1)

68-75: 중복 네트워크 호출 제거(성능·일관성 이슈)

getNumber()에서 동일 API를 2회 호출합니다. 한 번만 호출하고 응답을 재사용하세요.

적용 diff:

     override suspend fun getNumber(): Result<Map<String, Int>> {
         return runCatching {
-            mapOf(
-                "news" to newsService.getNumber().data.newsNumber,
-                "notice" to newsService.getNumber().data.noticeNumber,
-            )
+            val number = newsService.getNumber().data
+            mapOf(
+                "news" to number.newsNumber,
+                "notice" to number.noticeNumber,
+            )
         }.onFailure { return it.handleThrowable() }
     }
♻️ Duplicate comments (3)
feature/main/src/main/java/com/teamwable/main/MainViewModel.kt (1)

30-30: 과거 리뷰 코멘트: 타입 안전성 이슈는 이미 해결되었습니다.

과거 리뷰에서 -1 대신 -1L을 사용하라는 제안이 있었으나, 현재 코드는 이미 -1L을 사용하여 Long 타입 안전성을 보장하고 있습니다.

core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultNewsRepository.kt (1)

88-92: runHandledCatching 적용 확인 — 좋습니다

이전 리뷰 제안대로 에러 매핑 일관화가 되었습니다.

feature/news/src/main/java/com/teamwable/news/curation/NewsCurationRoute.kt (1)

87-97: 로딩 스피너 contentType 수정 반영 완료

함수 전달 → 값 전달로 교정된 것 확인. LGTM.

🧹 Nitpick comments (4)
core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultNewsRepository.kt (1)

77-86: Pager 설정은 적절. placeholders 비활성화 고려(선택)

네트워크 무한 스크롤이라면 enablePlaceholders = false로 null 아이템 가드 제거 가능.

-        return Pager(PagingConfig(pageSize = 15, prefetchDistance = 1)) {
+        return Pager(PagingConfig(pageSize = 15, prefetchDistance = 1, enablePlaceholders = false)) {
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationRoute.kt (3)

77-85: items에 contentType 지정으로 재조합 최적화(선택)

contentType을 지정하면 Compose가 아이템 타입별 재사용을 더 잘합니다.

         items(
             count = curations.itemCount,
             key = curations.itemKey { it.curationId },
+            contentType = curations.itemContentType { ContentType.Curation.name },
         ) { index ->

40-49: flowWithLifecycle 패턴 OK. repeatOnLifecycle 대안(선택)

현재도 문제없지만, 다음처럼 repeatOnLifecycle을 쓰면 라이프사이클 범위를 더 명시적으로 관리할 수 있습니다.

-    LaunchedEffect(lifecycleOwner) {
-        viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle)
-            .collectLatest { sideEffect ->
+    LaunchedEffect(Unit) {
+        lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+            viewModel.sideEffect.collect { sideEffect ->
                 when (sideEffect) {
                     is NewsCurationSideEffect.Navigation.ToUrl -> onOpenUrl(sideEffect.url)
                     is NewsCurationSideEffect.UI.ShowSnackBar -> onShowSnackBar(sideEffect.throwable)
                     NewsCurationSideEffect.UI.Refresh -> curations.refresh()
                 }
-            }
+            }
-    }
+        }
+    }

추가 import:

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle

29-31: @OptIn 제거 가능성 점검(선택)

실제 실험적 API 사용이 디자인시스템 내부에 캡슐화됐다면 이 파일의 @OptIn(ExperimentalMaterial3Api::class)는 불필요할 수 있습니다. 컴파일 확인 후 제거 고려해 주세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fee7665 and f7a355c.

📒 Files selected for processing (6)
  • core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultNewsRepository.kt (2 hunks)
  • core/network/build.gradle.kts (1 hunks)
  • core/network/src/main/java/com/teamwable/network/dto/response/news/ResponseCurationInfoDto.kt (1 hunks)
  • feature/main/src/main/java/com/teamwable/main/MainViewModel.kt (1 hunks)
  • feature/news/src/main/java/com/teamwable/news/curation/NewsCurationRoute.kt (1 hunks)
  • gradle/libs.versions.toml (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • gradle/libs.versions.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/network/src/main/java/com/teamwable/network/dto/response/news/ResponseCurationInfoDto.kt
🧰 Additional context used
🧬 Code graph analysis (2)
core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultNewsRepository.kt (1)
core/data/src/main/java/com/teamwable/data/util/runSuspendCatching.kt (1)
  • runHandledCatching (19-24)
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationRoute.kt (3)
core/designsystem/src/main/java/com/teamwable/designsystem/component/screen/PagingScreen.kt (1)
  • WablePagingScreen (72-120)
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationItem.kt (1)
  • NewsCurationItem (38-52)
core/designsystem/src/main/java/com/teamwable/designsystem/component/paging/WablePagingSpinner.kt (1)
  • WablePagingSpinner (30-60)
🔇 Additional comments (1)
core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultNewsRepository.kt (1)

7-7: 새 의존 import 정합성 OK

toCuration, runHandledCatching, CurationModel import 적절합니다.

Also applies to: 13-15

implementation(project(":core:model"))
implementation(project(":core:datastore"))
implementation(libs.paging)
implementation(libs.androidx.annotation.experimental)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

annotation-experimental 의존성, 실제 사용 여부 점검(네트워크 모듈엔 불필요 가능성 큼)

이 모듈에서 androidx.annotation.Experimental 계열을 쓰지 않으면 제거하거나 실제 사용하는 UI/feature 모듈로 이동하는 게 좋습니다. 번들 최소화와 그래들 그래프 단순화에 도움이 됩니다.

다음 스크립트로 core/network 내 사용 여부를 빠르게 확인해 주세요:


🏁 Script executed:

#!/bin/bash
rg -nP '\bandroidx\.annotation\.Experimental\b|@Experimental\b' core/network || echo "No usages in core/network"

Length of output: 25


추가 검증을 위해 전체 코드베이스에서 @Experimental 사용 여부를 확인하겠습니다.


---

🏁 Script executed:

```shell
#!/bin/bash
rg -nP '@Experimental\b' --type=kt --type=java || echo "No @Experimental usages found in codebase"

Length of output: 68


core/network 모듈에서 androidx.annotation.experimental 의존성 제거 필요

검증 결과, @Experimental 어노테이션이 네트워크 모듈은 물론 전체 코드베이스에서 사용되지 않습니다. 이 의존성은 완전히 불필요하므로 삭제하시기 바랍니다.

    implementation(libs.androidx.annotation.experimental)  // 제거 필요

번들 크기 최소화와 Gradle 의존성 그래프 정리를 위해 이 라인을 삭제하세요.

🤖 Prompt for AI Agents
In core/network/build.gradle.kts around line 36, remove the unnecessary
dependency declaration for androidx.annotation.experimental; the network module
(and the whole codebase) does not use @Experimental annotations, so delete the
line "implementation(libs.androidx.annotation.experimental)" from the
dependencies block to reduce bundle size and simplify the Gradle dependency
graph.

@Team-Wable Team-Wable deleted a comment from coderabbitai bot Oct 25, 2025
@Team-Wable Team-Wable deleted a comment from coderabbitai bot Oct 25, 2025
@sohyun127 sohyun127 merged commit 855580b into develop Oct 25, 2025
2 checks passed
@sohyun127 sohyun127 deleted the feature/#186-curation branch October 25, 2025 09:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

☕ [COMPOSE] 컴포즈 ui ⭐ [FEAT] 새로운 기능 구현 🐱 소현 리나

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] : 큐레이션 화면 구현

2 participants