Conversation
Walkthrough뉴스 탭을 큐레이션으로 전환하는 변경으로, 네트워크 엔드포인트·DTO·모델 추가, 매퍼와 저장소/데이터소스 변경, 로컬 Preference 키 교체, 페이징 기반 Compose UI(큐레이션 화면) 및 관련 ViewModel/인텐트/사이드이펙트가 추가되었습니다. Changes
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: 리스트 새로고침
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: 빨간점 표시/숨김
🎯 Estimated code review effort🎯 3 (Moderate) | ⏱️ ~35 minutes
🏷️ Suggested labels
👥 Suggested Reviewers
🐰 Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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) : UIcore/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: 현재 코드는 라벨 매핑이 정상이지만, 열거형 설계 개선이 권장됩니다검증 결과:
- NEWS 상수 잔존: 없음 (이미 제거됨) ✓
- 라벨 매핑: NewsFragment의 TabLayoutMediator에서 각 탭(MATCH, RANK, CURATION, NOTICE)을 명시적으로 when 분기로 처리하므로 기본값 문제 발생 안 함 ✓
- String 리소스: tv_news_tab_match, tv_news_tab_rank, tv_news_tab_curation, tv_news_tab_notice 모두 정의됨 ✓
다만 현재 enum의
title파라미터는 정의되어 있으면서 실제로는 사용되지 않고,NewsViewPagerAdapter와NewsFragment에서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.xml에curation_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 연결
WablePagingScreen의errorContent가 비어 있어 첫 로드 실패 시 빈 화면 위험이 있습니다. 스낵바 콜백에 위임하세요.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
📒 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.CURATION과NewsCurationFragment의 사용이 일관성 있게 적용되었습니다.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 어노테이션과 응답 타입 사용이 올바름
core/network/src/main/java/com/teamwable/network/dto/response/news/ResponseCurationInfoDto.kt
Outdated
Show resolved
Hide resolved
| @OptIn(InternalSerializationApi::class) | ||
| @Serializable | ||
| data class ResponseNewsCurationNumberDto( | ||
| @SerialName("curationId") val curationId: Long, | ||
| ) |
There was a problem hiding this comment.
🛠️ 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 3Length 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.
| sealed interface NewsCurationIntent : BaseIntent { | ||
| data class ClickLink(val url: String) : NewsCurationIntent | ||
|
|
||
| data object PullToRefresh : NewsCurationIntent | ||
| } |
There was a problem hiding this comment.
🧩 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' -C3Length 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 -20Length of output: 10123
🏁 Script executed:
#!/bin/bash
# Check ripgrep supported types for Kotlin
rg --type-list | grep -i kotlinLength of output: 19
🏁 Script executed:
#!/bin/bash
# Search for NewsCurationIntent usage in ViewModels or handlers
rg -n 'NewsCurationIntent' -C3 --max-count=20Length 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.ktLength 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.ktLength 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 -50Length 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 -80Length 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 -100Length of output: 12746
URL 검증 로직이 누락되었습니다. 보안을 위해 추가가 필요합니다.
sealed interface 구조는 우수하지만, 검증 결과 URL이 별도의 검증 없이 직접 처리되고 있습니다. NewsCurationViewModel.onLinkClick() → NewsCurationSideEffect.Navigation.ToUrl → Fragment.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.
feature/news/src/main/java/com/teamwable/news/curation/NewsCurationRoute.kt
Show resolved
Hide resolved
core/data/src/main/java/com/teamwable/data/repositoryimpl/DefaultNewsRepository.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
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
📒 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,CurationModelimport 적절합니다.Also applies to: 13-15
| implementation(project(":core:model")) | ||
| implementation(project(":core:datastore")) | ||
| implementation(libs.paging) | ||
| implementation(libs.androidx.annotation.experimental) |
There was a problem hiding this comment.
🧩 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.
✅ 𝗖𝗵𝗲𝗰𝗸-𝗟𝗶𝘀𝘁
📌 𝗜𝘀𝘀𝘂𝗲𝘀
📎𝗪𝗼𝗿𝗸 𝗗𝗲𝘀𝗰𝗿𝗶𝗽𝘁𝗶𝗼𝗻
📷 𝗦𝗰𝗿𝗲𝗲𝗻
KakaoTalk_20251024_224024006.mp4
💬 𝗧𝗼 𝗥𝗲𝘃𝗶𝗲𝘄𝗲𝗿𝘀
Summary by CodeRabbit
새로운 기능
UI
기타