[Feat] 통화 기록 UI 및 음성파일 재생 기능 구현 & 홈 화면 팝업창 구현#36
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthrough홈 화면이 저장소·상태 기반 흐름과 다이얼로그를 지원하도록 확장되었고, 통화 기록 도메인·mock 데이터·전사 매핑·재생 화면이 추가되었습니다. 홈에서 Changes홈 및 통화 기록 기능
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 4❌ Failed checks (4 warnings)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
Gemini AI 코드리뷰안녕하세요! 시니어 Android 개발자로서 GitHub Pull Request를 리뷰해 드리겠습니다. 전반적으로 코드 품질이 좋고, 새로운 기능 통합 및 기존 코드의 리팩토링이 잘 이루어진 것 같습니다. 특히 Compose Paging3 통합 및 Repository 레이어의 아래에 파일별로 상세한 피드백을 남기겠습니다. AppScreen.kt
RepositoryModule.kt
CallHistoryMapper.kt
CallTranscriptMapper.kt (신규)
HomeMapper.kt (신규)
ReservationMapper.kt (신규)
CallRecordMockRepository.kt (신규)
CallRecordRepositoryImpl.kt (신규)
HomeMockRepository.kt (신규)
HomeRepositoryImpl.kt
core/designsystem/build.gradle.kts
core/designsystem/src/main/res/drawable/*.xml (신규)
CharacterChangeUnavailableException.kt (신규)
도메인 모델 리팩토링 (
|
There was a problem hiding this comment.
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 (2)
feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt (1)
82-93: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
characterName이 null일 때 다시 전화하기 버튼이 무반응입니다.
characterName?.let(onCallClick)은 null이면 콜백을 호출하지 않지만, 아래 버튼은 여전히 활성 상태로 렌더링됩니다. 버튼을characterName != null일 때만 활성화하거나, 표시 이름이 아닌 안정적인characterId를 전달하는 계약으로 변경해 주세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt` around lines 82 - 93, NotificationCard 호출부의 onCallClick 처리에서 characterName이 null일 때도 버튼이 활성화되는 문제를 수정하세요. characterName이 존재할 때만 버튼이 활성화되도록 NotificationCard에 해당 상태를 전달하거나, 안정적인 characterId를 콜백 계약으로 사용하도록 관련 onCallClick 흐름을 변경하세요.feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt (1)
64-84: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
systemBarsPadding()를 실제 콘텐츠가 배치되는 컨테이너에 적용하세요.현재
systemBarsPadding()이 내용이 없는 배경Box에만 있어 상태바 인셋이 콘텐츠 레이아웃에 반영되지 않습니다. 상단 우측 버튼, 헤더 텍스트, 전화 버튼이 바깥Box기준align+고정 오프셋에 배치되어 있어 상태바와 겹칠 수 있으므로, 콘텐츠가 배치되는 바깥Box에statusBarsPadding()을 적용하거나 각 콘텐츠 컨테이너/최상위 ContentBox에 인셋을 전달해야 합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt` around lines 64 - 84, Apply status-bar insets to the outer content container that positions the header’s top-right button, header text, and call button, rather than only the empty background Box. Update the relevant parent Box or ContentBox around the Row and other header content to use statusBarsPadding(), while preserving the background sizing and existing child alignment.
🧹 Nitpick comments (10)
feature/home/impl/src/main/java/kr/co/call/impl/component/dialog/CallConnectDialog.kt (1)
12-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winModifier를 필수 선행 파라미터로 변경하세요.
이 프로젝트의 경로 지침에 따라
modifier를 파라미터 앞쪽에 배치하고 기본값 없이 받도록 맞춰 주세요. Preview와 모든 호출부에서도 명시적으로 전달해야 합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@feature/home/impl/src/main/java/kr/co/call/impl/component/dialog/CallConnectDialog.kt` around lines 12 - 16, CallConnectDialog의 modifier를 기본값 없는 필수 파라미터로 변경하고, 프로젝트 규칙에 맞게 기존 파라미터보다 앞에 배치하세요. Preview와 모든 CallConnectDialog 호출부에서 modifier를 명시적으로 전달하도록 업데이트하세요.Source: Path instructions
gradle/libs.versions.toml (1)
50-50: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winCompose BOM과 개별 라이브러리 버전을 혼용하지 마세요.
composeBom = "2026.02.01"을 사용하면서ui-graphics만1.11.4로 고정하면 BOM이 관리하는 Compose 버전을 우회합니다. 공식 문서도 BOM 사용 시 Compose 라이브러리 의존성에는 개별 버전을 지정하지 않는 방식을 안내합니다. (developer.android.com)기존
androidx-compose-ui-graphicsalias를 재사용하고uiGraphics버전과version.ref를 제거하거나, 전체 Compose BOM을 해당 버전에 맞게 함께 갱신해 주세요.Also applies to: 123-125
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@gradle/libs.versions.toml` at line 50, Compose BOM을 사용하는 구성에서 uiGraphics의 개별 버전 고정을 제거하세요. gradle/libs.versions.toml의 composeBom과 androidx-compose-ui-graphics alias를 기준으로 해당 alias를 재사용하고, uiGraphics 버전 항목과 version.ref 연결을 삭제해 BOM이 Compose 버전을 관리하도록 수정하세요.Source: MCP tools
core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeMockRepository.kt (2)
15-15: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
characters는 재할당되지 않으므로val로 선언 가능.파일 전체에서
characters를 다시 할당하는 코드가 없습니다.var대신val로 선언해 의도를 명확히 할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeMockRepository.kt` at line 15, In HomeMockRepository, change the characters property declaration from var to val, since it is never reassigned and should be immutable.
85-109: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win
reservationsmutable 상태에 대한 동시성 보호 부재.
@Singleton스코프인HomeMockRepository의reservations필드를 동기화 없이 read → copy → write 하고 있습니다. 여러 코루틴에서 동시에changeReservationTime을 호출하면 갱신이 손실될 수 있습니다. Mock 구현이라 당장 영향은 제한적이지만,Mutex나synchronized블록으로 보호하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeMockRepository.kt` around lines 85 - 109, Protect the read–modify–write sequence in HomeMockRepository.changeReservationTime with a Mutex or synchronized block. Ensure both reservation existence checking and the subsequent reservations copy/update occur atomically so concurrent calls cannot overwrite each other’s changes.core/data/src/main/java/kr/co/call/data/mapper/CallTranscriptMapper.kt (1)
16-21: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win알 수 없는
speaker값에 대한 예외 처리 검토.서버가
"사용자","USER","AI"외의 값을 반환하면error(...)로 즉시 예외가 발생합니다. 향후 이 매퍼가 실제 API 응답 처리에 연동될 때, 호출부에서runCatching으로 감싸지 않으면 예기치 못한 크래시로 이어질 수 있습니다. 알 수 없는 값에 대해 로깅 후 폴백(예:AI또는 별도UNKNOWN)을 반환하는 방식을 고려해보세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/data/src/main/java/kr/co/call/data/mapper/CallTranscriptMapper.kt` around lines 16 - 21, Update String.toDomainSpeaker so unknown speaker values do not throw via error(...); instead, log the unsupported value and return the project’s established fallback speaker, preferably a dedicated UNKNOWN value or AI if no UNKNOWN exists. Preserve the existing mappings for "사용자", "USER", and "AI".core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt (1)
50-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
unsupportedApi()헬퍼가 두 Repository 구현체에 중복 정의됨.
HomeRepositoryImpl과CallRecordRepositoryImpl이 동일한 구조의private fun <T> unsupportedApi(): Result<T>헬퍼를 각각 정의하고 있습니다. "서버 API 미연동" 실패를 나타내는 공통 패턴이므로 하나의 공용 확장 함수(예:core/data패키지의 공용Result<T>확장 또는 공유 유틸 객체)로 추출해 중복을 제거하는 것을 권장합니다.
core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt#L50-L53: 이 헬퍼를 공용 유틸로 추출하고 메시지를 파라미터화하세요.core/data/src/main/java/kr/co/call/data/repositoryImpl/CallRecordRepositoryImpl.kt#L17-L20: 동일한 공용 유틸을 재사용하도록 로컬 헬퍼를 제거하세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt` around lines 50 - 53, Extract the duplicated unsupportedApi() helpers from HomeRepositoryImpl (core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt:50-53) and CallRecordRepositoryImpl (core/data/src/main/java/kr/co/call/data/repositoryImpl/CallRecordRepositoryImpl.kt:17-20) into one shared core/data utility or Result<T> extension, parameterizing the failure message. Remove both local helpers and update each repository to reuse the shared function while preserving the existing failure behavior.core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt (1)
26-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMock Repository 임시 바인딩에 추적용 TODO 권장.
bindHomeRepository가HomeMockRepository로,bindCallRecordRepository가CallRecordMockRepository로 연결되어 있습니다. PR 설명상 의도된 임시 조치이나, 실제 API(HomeRepositoryImpl,CallRecordRepositoryImpl)로 되돌리는 작업이 누락되기 쉬운 지점입니다. 관련 이슈 번호를 포함한// TODO:주석을 남겨 추적하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt` around lines 26 - 43, Add tracking TODO comments with the relevant issue number above bindHomeRepository and bindCallRecordRepository in RepositoryModule, documenting that the mock bindings must later be restored to HomeRepositoryImpl and CallRecordRepositoryImpl. Keep the existing bindings unchanged.feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt (1)
62-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
notifications가 상태(ViewModel)가 아닌 화면 내부 mock에서 생성됩니다.
state는viewModel.collectAsState()로 구독하는 반면notifications는createHomeMockData().notificationFlow()로 composable 안에서 직접 만들어져 ViewModel/State 흐름을 우회합니다. 임시 mock 연결로 보이지만, 다른 상태와 데이터 소스가 이원화되어 있으니 추후 ViewModel(State/Flow) 경유로 통일하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt` around lines 62 - 64, Unify the notifications data source with the ViewModel state flow: update the HomeScreen composable to obtain notifications from the collected viewModel state, and remove the direct createHomeMockData().notificationFlow() creation from remember. Preserve the existing notification rendering while routing updates through the ViewModel alongside the other screen state.feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt (1)
338-352: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
loadHome의 독립적인 4개 조회를 병렬화하면 초기 로딩 지연을 줄일 수 있습니다.
getSummary/getReservations/getCallHistories/getCharacters는 서로 의존성이 없는데도 순차적으로getOrThrow()까지 대기하고 있어 총 지연이 네 호출의 합이 됩니다.coroutineScope+async로 병렬 실행하면 가장 느린 호출 기준으로 단축됩니다.♻️ 병렬 로딩 예시
- val summary = homeRepository.getSummary().getOrThrow() - val reservations = homeRepository.getReservations().getOrThrow() - val callHistories = homeRepository.getCallHistories().getOrThrow() - val characters = homeRepository.getCharacters().getOrThrow() + val (summary, reservations, callHistories, characters) = coroutineScope { + val summaryDeferred = async { homeRepository.getSummary().getOrThrow() } + val reservationsDeferred = async { homeRepository.getReservations().getOrThrow() } + val callHistoriesDeferred = async { homeRepository.getCallHistories().getOrThrow() } + val charactersDeferred = async { homeRepository.getCharacters().getOrThrow() } + listOf(summaryDeferred, reservationsDeferred, callHistoriesDeferred, charactersDeferred) + .awaitAll() + Quadruple( + summaryDeferred.getCompleted(), + reservationsDeferred.getCompleted(), + callHistoriesDeferred.getCompleted(), + charactersDeferred.getCompleted(), + ) + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt` around lines 338 - 352, Update loadHome so the independent getSummary, getReservations, getCallHistories, and getCharacters calls execute concurrently using coroutineScope with async, then await each result before the existing reduce mapping. Preserve the current error propagation through getOrThrow() and the resulting state contents.feature/home/impl/src/main/java/kr/co/call/impl/component/record/CallTranscriptList.kt (1)
51-53: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winLazyColumn 항목에 안정적인 key 미지정.
items(items = transcripts) { ... }에key가 없어 리스트 변경 시 비효율적이거나 잘못된 recomposition이 발생할 수 있습니다.CallTranscript에 고유 id가 있다면 이를 key로, 없다면 최소 index 기반 key를 지정하세요.이 코멘트는 path instruction
**/*Screen.kt(LazyColumn/LazyRow에 안정적인 key를 지정했는지)에 근거합니다.🛠️ 제안 수정 (예시, id가 없는 경우 index 사용)
- items(items = transcripts) { transcript -> + itemsIndexed(items = transcripts, key = { index, _ -> index }) { _, transcript -> CallTranscriptBubble(transcript = transcript) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@feature/home/impl/src/main/java/kr/co/call/impl/component/record/CallTranscriptList.kt` around lines 51 - 53, Update the LazyColumn items call containing CallTranscriptBubble to provide a stable key for each transcript, using CallTranscript’s unique identifier if available; otherwise use the item index as the fallback key. Keep the existing transcript rendering unchanged.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/main/java/kr/co/call/callfromai/AppScreen.kt`:
- Around line 106-113: Attach a screenshot or screen recording to the PR
demonstrating the home-to-call-record transition implemented in the homeEntry
navigation flow, including navigation via CallRecordNavKey and returning with
popBackStack.
In
`@feature/home/impl/src/main/java/kr/co/call/impl/component/record/CallRecordPlayer.kt`:
- Around line 103-121: CallRecordProgressBar의 pointerInput이 매 리컴포지션마다 변경되는
onProgressChange 람다를 key로 사용하지 않도록 수정하세요. rememberUpdatedState로 최신 콜백을 감싸고,
pointerInput은 Unit 같은 안정적인 key를 사용해 제스처 코루틴을 재시작하지 않으면서도 탭 처리 시 최신 콜백을 호출하도록
유지하세요.
---
Outside diff comments:
In
`@feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt`:
- Around line 64-84: Apply status-bar insets to the outer content container that
positions the header’s top-right button, header text, and call button, rather
than only the empty background Box. Update the relevant parent Box or ContentBox
around the Row and other header content to use statusBarsPadding(), while
preserving the background sizing and existing child alignment.
In
`@feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt`:
- Around line 82-93: NotificationCard 호출부의 onCallClick 처리에서 characterName이 null일
때도 버튼이 활성화되는 문제를 수정하세요. characterName이 존재할 때만 버튼이 활성화되도록 NotificationCard에 해당
상태를 전달하거나, 안정적인 characterId를 콜백 계약으로 사용하도록 관련 onCallClick 흐름을 변경하세요.
---
Nitpick comments:
In `@core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt`:
- Around line 26-43: Add tracking TODO comments with the relevant issue number
above bindHomeRepository and bindCallRecordRepository in RepositoryModule,
documenting that the mock bindings must later be restored to HomeRepositoryImpl
and CallRecordRepositoryImpl. Keep the existing bindings unchanged.
In `@core/data/src/main/java/kr/co/call/data/mapper/CallTranscriptMapper.kt`:
- Around line 16-21: Update String.toDomainSpeaker so unknown speaker values do
not throw via error(...); instead, log the unsupported value and return the
project’s established fallback speaker, preferably a dedicated UNKNOWN value or
AI if no UNKNOWN exists. Preserve the existing mappings for "사용자", "USER", and
"AI".
In
`@core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeMockRepository.kt`:
- Line 15: In HomeMockRepository, change the characters property declaration
from var to val, since it is never reassigned and should be immutable.
- Around line 85-109: Protect the read–modify–write sequence in
HomeMockRepository.changeReservationTime with a Mutex or synchronized block.
Ensure both reservation existence checking and the subsequent reservations
copy/update occur atomically so concurrent calls cannot overwrite each other’s
changes.
In
`@core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt`:
- Around line 50-53: Extract the duplicated unsupportedApi() helpers from
HomeRepositoryImpl
(core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt:50-53)
and CallRecordRepositoryImpl
(core/data/src/main/java/kr/co/call/data/repositoryImpl/CallRecordRepositoryImpl.kt:17-20)
into one shared core/data utility or Result<T> extension, parameterizing the
failure message. Remove both local helpers and update each repository to reuse
the shared function while preserving the existing failure behavior.
In
`@feature/home/impl/src/main/java/kr/co/call/impl/component/dialog/CallConnectDialog.kt`:
- Around line 12-16: CallConnectDialog의 modifier를 기본값 없는 필수 파라미터로 변경하고, 프로젝트 규칙에
맞게 기존 파라미터보다 앞에 배치하세요. Preview와 모든 CallConnectDialog 호출부에서 modifier를 명시적으로 전달하도록
업데이트하세요.
In
`@feature/home/impl/src/main/java/kr/co/call/impl/component/record/CallTranscriptList.kt`:
- Around line 51-53: Update the LazyColumn items call containing
CallTranscriptBubble to provide a stable key for each transcript, using
CallTranscript’s unique identifier if available; otherwise use the item index as
the fallback key. Keep the existing transcript rendering unchanged.
In `@feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt`:
- Around line 62-64: Unify the notifications data source with the ViewModel
state flow: update the HomeScreen composable to obtain notifications from the
collected viewModel state, and remove the direct
createHomeMockData().notificationFlow() creation from remember. Preserve the
existing notification rendering while routing updates through the ViewModel
alongside the other screen state.
In `@feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt`:
- Around line 338-352: Update loadHome so the independent getSummary,
getReservations, getCallHistories, and getCharacters calls execute concurrently
using coroutineScope with async, then await each result before the existing
reduce mapping. Preserve the current error propagation through getOrThrow() and
the resulting state contents.
In `@gradle/libs.versions.toml`:
- Line 50: Compose BOM을 사용하는 구성에서 uiGraphics의 개별 버전 고정을 제거하세요.
gradle/libs.versions.toml의 composeBom과 androidx-compose-ui-graphics alias를 기준으로
해당 alias를 재사용하고, uiGraphics 버전 항목과 version.ref 연결을 삭제해 BOM이 Compose 버전을 관리하도록
수정하세요.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 5635a074-5b6c-4cbf-8588-04465e837241
📒 Files selected for processing (65)
app/src/main/java/kr/co/call/callfromai/AppScreen.ktcore/data/src/main/java/kr/co/call/data/di/RepositoryModule.ktcore/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.ktcore/data/src/main/java/kr/co/call/data/mapper/CallTranscriptMapper.ktcore/data/src/main/java/kr/co/call/data/mapper/HomeMapper.ktcore/data/src/main/java/kr/co/call/data/mapper/ReservationMapper.ktcore/data/src/main/java/kr/co/call/data/repositoryImpl/CallRecordMockRepository.ktcore/data/src/main/java/kr/co/call/data/repositoryImpl/CallRecordRepositoryImpl.ktcore/data/src/main/java/kr/co/call/data/repositoryImpl/HomeMockRepository.ktcore/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.ktcore/designsystem/build.gradle.ktscore/designsystem/src/main/res/drawable/ic_home_call_pause.xmlcore/designsystem/src/main/res/drawable/ic_home_call_play.xmlcore/designsystem/src/main/res/drawable/ic_home_call_replay_5.xmlcore/designsystem/src/main/res/drawable/ic_home_call_replay_5_after.xmlcore/domain/src/main/java/kr/co/call/domain/exception/CharacterChangeUnavailableException.ktcore/domain/src/main/java/kr/co/call/domain/model/home/CallHistory.ktcore/domain/src/main/java/kr/co/call/domain/model/home/CallRecord.ktcore/domain/src/main/java/kr/co/call/domain/model/home/CallReservation.ktcore/domain/src/main/java/kr/co/call/domain/model/home/CallReservations.ktcore/domain/src/main/java/kr/co/call/domain/model/home/Home.ktcore/domain/src/main/java/kr/co/call/domain/model/home/HomeNotification.ktcore/domain/src/main/java/kr/co/call/domain/model/home/HomeOverview.ktcore/domain/src/main/java/kr/co/call/domain/model/home/HomeSummary.ktcore/domain/src/main/java/kr/co/call/domain/repository/CallRecordRepository.ktcore/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.ktcore/domain/src/main/java/kr/co/call/domain/usecase/home/HomeUseCase.ktcore/network/src/main/java/kr/co/call/network/dto/CallTranscriptResponseDto.ktfeature/home/api/src/main/java/kr/co/call/api/CallRecordRoute.ktfeature/home/impl/build.gradle.ktsfeature/home/impl/src/main/java/kr/co/call/impl/component/CharacterChangeDialog.ktfeature/home/impl/src/main/java/kr/co/call/impl/component/TimeChangeBottomSheet.ktfeature/home/impl/src/main/java/kr/co/call/impl/component/dialog/CallConnectDialog.ktfeature/home/impl/src/main/java/kr/co/call/impl/component/dialog/CharacterChangeConfirmDialog.ktfeature/home/impl/src/main/java/kr/co/call/impl/component/dialog/CharacterChangeUnavailableDialog.ktfeature/home/impl/src/main/java/kr/co/call/impl/component/dialog/NonMainCharacterCallDialog.ktfeature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.ktfeature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.ktfeature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.ktfeature/home/impl/src/main/java/kr/co/call/impl/component/record/CallRecordHeader.ktfeature/home/impl/src/main/java/kr/co/call/impl/component/record/CallRecordLoadingContent.ktfeature/home/impl/src/main/java/kr/co/call/impl/component/record/CallRecordPlayer.ktfeature/home/impl/src/main/java/kr/co/call/impl/component/record/CallRecordPlayerState.ktfeature/home/impl/src/main/java/kr/co/call/impl/component/record/CallTranscriptList.ktfeature/home/impl/src/main/java/kr/co/call/impl/entry/HomeEntryBuilder.ktfeature/home/impl/src/main/java/kr/co/call/impl/mapper/CallRecordUiMapper.ktfeature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.ktfeature/home/impl/src/main/java/kr/co/call/impl/mock/CharacterMockData.ktfeature/home/impl/src/main/java/kr/co/call/impl/screen/CallRecordScreen.ktfeature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.ktfeature/home/impl/src/main/java/kr/co/call/impl/util/DateTimeRounding.ktfeature/home/impl/src/main/java/kr/co/call/impl/viewmodel/CallRecordIntent.ktfeature/home/impl/src/main/java/kr/co/call/impl/viewmodel/CallRecordSideEffect.ktfeature/home/impl/src/main/java/kr/co/call/impl/viewmodel/CallRecordViewModel.ktfeature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeIntent.ktfeature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeSideEffect.ktfeature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.ktfeature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CallRecordUiModel.ktfeature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeReservationUiModel.ktfeature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/CallRecordState.ktfeature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeDialogState.ktfeature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.ktfeature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/TimeChangeState.ktfeature/home/impl/src/test/java/kr/co/call/impl/util/DateTimeRoundingTest.ktgradle/libs.versions.toml
💤 Files with no reviewable changes (10)
- core/domain/src/main/java/kr/co/call/domain/model/home/HomeNotification.kt
- feature/home/impl/src/main/java/kr/co/call/impl/component/CharacterChangeDialog.kt
- core/domain/src/main/java/kr/co/call/domain/model/home/HomeSummary.kt
- core/domain/src/main/java/kr/co/call/domain/model/home/CallReservations.kt
- core/domain/src/main/java/kr/co/call/domain/model/home/HomeOverview.kt
- core/domain/src/main/java/kr/co/call/domain/model/home/CallReservation.kt
- core/domain/src/main/java/kr/co/call/domain/model/home/CallHistory.kt
- feature/home/impl/src/main/java/kr/co/call/impl/mock/CharacterMockData.kt
- core/domain/src/main/java/kr/co/call/domain/usecase/home/HomeUseCase.kt
- core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt
| homeEntry( | ||
| navigateToCallRecord = { callId -> | ||
| appNavigator.navigate(CallRecordNavKey(callId = callId)) | ||
| }, | ||
| onCallRecordBack = { | ||
| appNavigator.popBackStack() | ||
| }, | ||
| ) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
홈→통화 기록 전환 화면 자료를 PR에 첨부해 주세요.
PR 설명에 이 변경 흐름을 확인할 스크린샷 또는 화면 녹화가 없습니다. As per coding guidelines, "*Screen.kt 변경 시 스크린샷 또는 화면 녹화가 첨부되었는지 확인하고 없으면 경고한다" 규칙에 따른 사항입니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/kr/co/call/callfromai/AppScreen.kt` around lines 106 - 113,
Attach a screenshot or screen recording to the PR demonstrating the
home-to-call-record transition implemented in the homeEntry navigation flow,
including navigation via CallRecordNavKey and returning with popBackStack.
Source: Coding guidelines
There was a problem hiding this comment.
@codebidoof 맞습니다, 이건 제가 코드 변경과 별개로 PR 설명의 화면 자료 첨부 여부를 체크하는 가이드라인 때문에 잡은 항목이에요. 이번 PR에서 HomeScreen.kt와 CallRecordScreen.kt가 변경됐는데, 설명에 홈 → 통화 기록 진입 및 뒤로가기 흐름을 보여주는 스크린샷/영상이 없어서 경고됐습니다.
번거롭게 해서 죄송합니다 🙇 PR 설명에 해당 화면 자료만 첨부하면 충분합니다.
🐰💦
| @Composable | ||
| private fun CallRecordProgressBar( | ||
| progress: Float, | ||
| onProgressChange: (Float) -> Unit, | ||
| modifier: Modifier = Modifier, | ||
| ) { | ||
| val inactiveColor = CallTheme.colors.gray100 | ||
| val activeColor = CallTheme.colors.mainVariant1 | ||
|
|
||
| Canvas( | ||
| modifier = modifier | ||
| .fillMaxWidth() | ||
| .height(12.dp) | ||
| .pointerInput(onProgressChange) { | ||
| detectTapGestures { offset -> | ||
| onProgressChange((offset.x / size.width).coerceIn(0f, 1f)) | ||
| } | ||
| }, | ||
| ) { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
pointerInput key로 불안정한 람다 사용 — 재생 중 탭 제스처 누락 위험.
onProgressChange는 state.durationMillis/onSeek을 캡처해 매 리컴포지션마다 새로 생성됩니다. 재생 중 250ms마다 상위(CallRecordScreen)에서 playerState가 갱신되며 이 람다도 계속 바뀌어 pointerInput 코루틴이 반복적으로 취소·재시작되고, 탭 도중 제스처가 씹힐 수 있습니다. rememberUpdatedState로 콜백을 감싸고 pointerInput은 Unit 등 안정적인 key로 고정하세요.
🛠️ 제안 수정
`@Composable`
private fun CallRecordProgressBar(
progress: Float,
onProgressChange: (Float) -> Unit,
modifier: Modifier = Modifier,
) {
val inactiveColor = CallTheme.colors.gray100
val activeColor = CallTheme.colors.mainVariant1
+ val currentOnProgressChange = rememberUpdatedState(onProgressChange)
Canvas(
modifier = modifier
.fillMaxWidth()
.height(12.dp)
- .pointerInput(onProgressChange) {
+ .pointerInput(Unit) {
detectTapGestures { offset ->
- onProgressChange((offset.x / size.width).coerceIn(0f, 1f))
+ currentOnProgressChange.value((offset.x / size.width).coerceIn(0f, 1f))
}
},
) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Composable | |
| private fun CallRecordProgressBar( | |
| progress: Float, | |
| onProgressChange: (Float) -> Unit, | |
| modifier: Modifier = Modifier, | |
| ) { | |
| val inactiveColor = CallTheme.colors.gray100 | |
| val activeColor = CallTheme.colors.mainVariant1 | |
| Canvas( | |
| modifier = modifier | |
| .fillMaxWidth() | |
| .height(12.dp) | |
| .pointerInput(onProgressChange) { | |
| detectTapGestures { offset -> | |
| onProgressChange((offset.x / size.width).coerceIn(0f, 1f)) | |
| } | |
| }, | |
| ) { | |
| `@Composable` | |
| private fun CallRecordProgressBar( | |
| progress: Float, | |
| onProgressChange: (Float) -> Unit, | |
| modifier: Modifier = Modifier, | |
| ) { | |
| val inactiveColor = CallTheme.colors.gray100 | |
| val activeColor = CallTheme.colors.mainVariant1 | |
| val currentOnProgressChange = rememberUpdatedState(onProgressChange) | |
| Canvas( | |
| modifier = modifier | |
| .fillMaxWidth() | |
| .height(12.dp) | |
| .pointerInput(Unit) { | |
| detectTapGestures { offset -> | |
| currentOnProgressChange.value((offset.x / size.width).coerceIn(0f, 1f)) | |
| } | |
| }, | |
| ) { |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@feature/home/impl/src/main/java/kr/co/call/impl/component/record/CallRecordPlayer.kt`
around lines 103 - 121, CallRecordProgressBar의 pointerInput이 매 리컴포지션마다 변경되는
onProgressChange 람다를 key로 사용하지 않도록 수정하세요. rememberUpdatedState로 최신 콜백을 감싸고,
pointerInput은 Unit 같은 안정적인 key를 사용해 제스처 코루틴을 재시작하지 않으면서도 탭 처리 시 최신 콜백을 호출하도록
유지하세요.
Gemini AI 코드리뷰Gemini API 응답을 읽지 못했습니다. {
"error": {
"code": 500,
"message": "An internal error has occurred. Please retry or report in https://developers.generativeai.google/guide/troubleshooting",
"status": "INTERNAL"
}
} |
| /** | ||
| * 서버 정책상 메인 캐릭터를 아직 교체할 수 없을 때 발생하는 예외 | ||
| */ | ||
| class CharacterChangeUnavailableException : | ||
| IllegalStateException("아직 메인 연인을 변경할 수 없습니다.") |
| data class CallTranscript( | ||
| val content: String, | ||
| val speaker: Speaker, | ||
| ) { | ||
| enum class Speaker { | ||
| USER, | ||
| AI, | ||
| } | ||
| } |
There was a problem hiding this comment.
나같으면 여기서 enum class 따로만들듯 개취입니당
| data class HomeSummary( | ||
| val firstName: String, | ||
| val relationshipDays: Int, | ||
| val totalCallCount: Int, | ||
| val callStreakDays: Int, | ||
| ) | ||
|
|
||
| data class HomeCharacter( | ||
| val id: Long, | ||
| val name: String, | ||
| val relationshipDays: Int, | ||
| val imageUrl: String?, | ||
| val isMain: Boolean, | ||
| ) | ||
|
|
||
| data class CallReservation( | ||
| val id: Long, | ||
| val characterId: Long, | ||
| val firstName: String, | ||
| val imageUrl: String?, | ||
| val scheduledAt: LocalDateTime, | ||
| ) | ||
|
|
||
| data class CallReservations( | ||
| val totalCount: Int, | ||
| val items: List<CallReservation>, | ||
| ) | ||
|
|
||
| data class CallHistory( | ||
| val callId: Long, | ||
| val characterName: String, | ||
| val aiSummary: String, | ||
| val startedAt: LocalDateTime, | ||
| val isOutgoing: Boolean, | ||
| val isMissed: Boolean, | ||
| ) | ||
|
|
||
| data class HomeNotification( | ||
| val notificationId: Long, | ||
| val type: NotificationType, | ||
| val title: String, | ||
| val content: String, | ||
| val isRead: Boolean, | ||
| val createdAt: LocalDateTime, | ||
| val characterName: String?, | ||
| val profileImageUrl: String?, | ||
| ) | ||
|
|
||
| enum class NotificationType( | ||
| val title: String, | ||
| ) { | ||
| MISSED_CALL(title = "부재중 전화"), | ||
| ANNIVERSARY(title = "기념일"), | ||
| CALL_RESERVATION(title = "통화 약속"), | ||
| } |
| enum class NotificationType( | ||
| val title: String, | ||
| ) { | ||
| MISSED_CALL(title = "부재중 전화"), | ||
| ANNIVERSARY(title = "기념일"), | ||
| CALL_RESERVATION(title = "통화 약속"), | ||
| } |
There was a problem hiding this comment.
여기서 정적 팩토리 메서드까지 정의해놓는 게 더 좋을 듯?
| @Composable | ||
| fun CallConnectDialog( | ||
| characterName: String, | ||
| onConnectClick: () -> Unit, | ||
| onDismissRequest: () -> Unit, | ||
| modifier: Modifier = Modifier, | ||
| ) { | ||
| TwoButtonPopup( | ||
| label = "통화 연결", | ||
| title = "${characterName}에게 바로\n통화를 연결할까요?", | ||
| positiveText = "연결", | ||
| negativeText = "취소", | ||
| onPositiveClick = onConnectClick, | ||
| onNegativeClick = onDismissRequest, | ||
| onDismissRequest = onDismissRequest, | ||
| modifier = modifier.width(321.dp), | ||
| ) | ||
| } | ||
|
|
There was a problem hiding this comment.
와 ㅁㅊ 공용컴포넌트를 이런 식으로 써먹을 수도 있구나..
| append("전화는 메인 연인과만 이용할 수 있어요.\n") | ||
| append("다른 캐릭터와 전화를 하고 싶다면\n") | ||
| append("상단에서 연인 교체하기를 클릭해주세요.") | ||
| }, |
There was a problem hiding this comment.
string.xml을 사용하면 더 좋을 듯?
| val iconResource = when (iconType) { | ||
| CallHistoryIconType.SENT -> | ||
| kr.co.call.designsystem.R.drawable.img_home_call_send | ||
| R.drawable.img_home_call_send | ||
|
|
||
| CallHistoryIconType.RECEIVED -> | ||
| kr.co.call.designsystem.R.drawable.img_home_call_received | ||
| R.drawable.img_home_call_received | ||
|
|
||
| CallHistoryIconType.MISSED -> | ||
| kr.co.call.designsystem.R.drawable.img_home_call_missed | ||
| R.drawable.img_home_call_missed | ||
| } | ||
| val iconSize = when (iconType) { | ||
| CallHistoryIconType.SENT -> DpSize(26.dp, 23.dp) |
There was a problem hiding this comment.
이거는 별도 매퍼 함수를 추출해서 하는게 더 나을 듯? util 패키지 같은 거 하나 파서 정의하는 식으로 개인적인 개발스타일입니당
| */ | ||
| @Composable | ||
| fun NotificationPagingContent( | ||
| notifications: Flow<PagingData<HomeNotification>>, |
There was a problem hiding this comment.
PagingData는 도메인 레이어에서만 알고 UI 레이어에서는 LazyPagingItems만 다루는 것이 계층 분리 측면에서 더 적절해서 이거 repository에서 Flow<PagingData>를 반환하고 ui에서는 LazyPagingItems 만 아는 게 더 이상적이야.
내 PR에 domain모듈에 paging:common 라이브러리(안드 의존성이 없어서 코틀린 모듈에서도 사용가능) 추가해놓은게 있는데 나중에 api 연동할 때 계층 분리도 같이 해 주면 될 것 같어
| entry<CallRecordNavKey> { key -> | ||
| CallRecordScreen( | ||
| callId = key.callId, | ||
| onBackClick = onCallRecordBack, | ||
| ) | ||
| } | ||
| } |
| try { | ||
| val reservations = homeRepository.changeReservationTime( | ||
| reservationId = reservationId, | ||
| scheduledAt = LocalDateTime.of( | ||
| timeChange.state.selectedDate, | ||
| timeChange.state.selectedTime, | ||
| ), | ||
| ).getOrThrow() | ||
|
|
||
| reduce { | ||
| state.copy( | ||
| reservation = reservations.toUiModel(), | ||
| dialogState = null, | ||
| ) | ||
| } | ||
| postSideEffect( | ||
| HomeSideEffect.ShowMessage( | ||
| message = "예약 시간을 변경했습니다.", | ||
| ), | ||
| ) | ||
| } catch (cancellationException: CancellationException) { | ||
| throw cancellationException | ||
| } catch (throwable: Throwable) { | ||
| postSideEffect( | ||
| HomeSideEffect.ShowMessage( | ||
| message = throwable.message ?: "예약 시간을 변경하지 못했습니다.", | ||
| ), | ||
| ) | ||
| } |
There was a problem hiding this comment.
에러 핸들링 책임은 레포지토리 구현체로 옮기는 게 좋아보임 뷰모델단부턴 성공/실패 만 알게
📄 작업 내용 요약
홈 화면
홈 화면 다이얼로그
통화 기록
📎 Issue 번호
✅ 작업 목록
📝 기타 참고사항
Summary by CodeRabbit
새로운 기능
개선 사항
버그 수정