Skip to content

feat: 홈 화면 UI 구현#23

Merged
JiwonLee42 merged 19 commits into
developfrom
feat/#7-home-ui
Jul 18, 2026
Merged

feat: 홈 화면 UI 구현#23
JiwonLee42 merged 19 commits into
developfrom
feat/#7-home-ui

Conversation

@JiwonLee42

@JiwonLee42 JiwonLee42 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

📄 작업 내용 요약

  • 홈 UI 구현: 홈 헤더, 통화 예약 카드, 알림/통화 기록 탭 UI를 구현했습니다.
  • Home API DTO, Domain Model, Repository 계약 및 구현체를 추가했습니다.
  • API 연결 전이므로 MockData 사용하도록 RepositoryImpl에서 처리했습니다.
  • 네트워크 모델을 Domain Model로, Domain Model을 UI State로 변환하는 Mapper를 구성했습니다.
  • 통화 기록 날짜와 예약 시간을 표시하는 공통 시간 포맷 유틸을 추가했습니다.
2026-07-17.10.24.13.mov

📎 Issue 번호


✅ 작업 목록

  • 기능 구현
  • 코드 리뷰 반영
  • 테스트 코드 작성
  • 문서 업데이트

📝 기타 참고사항

Summary by CodeRabbit

  • 새로운 기능
    • 홈 화면에 관계 요약, 통화 약속, 알림, 통화 기록 섹션을 추가했습니다.
    • 통화 기록에서 발신·수신·부재중 상태와 AI 요약을 확인하고, 아이콘/상세 진입을 제공합니다.
    • 알림 유형별 카드, 읽지 않음 표시, 부재중 전화 재통화 버튼을 제공합니다.
    • 예약 시간과 통화 기록을 한국어 날짜·시간 및 “n분 전/시간/일” 형식으로 표시합니다.
  • UI/레이아웃 개선
    • 하단 바 및 화면 인셋 처리를 Scaffold 기반으로 정리해 레이아웃 안정성을 높였습니다.

@JiwonLee42 JiwonLee42 self-assigned this Jul 17, 2026
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

홈 화면의 API·도메인 데이터 계약, 저장소와 ViewModel 흐름, 예약·통화 기록·알림 Compose UI, 시간·이미지 표시 유틸리티, 디자인 리소스와 앱 레이아웃이 추가되었습니다.

Changes

홈 화면 기능

Layer / File(s) Summary
홈 데이터 계약과 시간 유틸리티
core/common/..., core/domain/..., core/network/...
홈 API DTO와 Retrofit 인터페이스, 예약·통화 기록·요약 도메인 모델, HomeRepository, HomeOverview 및 시간 포맷 유틸리티가 추가되었습니다.
저장소 구현과 DTO 매핑
core/data/..., core/data/build.gradle.kts
공통 모듈 의존성, Hilt 저장소 바인딩, DTO-to-domain 매핑과 고정 홈 데이터 반환이 추가되었습니다.
홈 상태와 화면 데이터 변환
feature/home/impl/.../viewmodel/*, .../mapper/*, .../mock/*
홈 상태·Intent·SideEffect, UI 모델, ViewModel 로딩 처리, 도메인-to-UI 매핑과 모킹 데이터가 추가되었습니다.
홈 화면 조립 및 앱 레이아웃
feature/home/impl/.../screen/*, .../component/header/*, .../tab/*, app/.../AppScreen.kt
HomeScreen이 ViewModel 상태를 구독하고 홈 콘텐츠를 조립하며, 앱 루트가 Scaffold와 안전 영역 인셋을 사용하도록 변경되었습니다.
알림·통화 기록 UI와 디자인 리소스
feature/home/impl/.../component/history/*, core/designsystem/...
알림·통화 기록 카드, 읽지 않음 표시, 프로필 이미지와 홈 아이콘 리소스가 추가되었습니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HomeScreen
  participant HomeViewModel
  participant HomeRepository
  participant HomeScreenContent
  HomeScreen->>HomeViewModel: HomeState 구독
  HomeViewModel->>HomeRepository: 홈 요약·예약·통화 기록 조회
  HomeRepository-->>HomeViewModel: Result 도메인 데이터 반환
  HomeViewModel->>HomeScreenContent: UI 상태와 알림 Flow 전달
  HomeScreenContent-->>HomeScreen: 헤더·예약·히스토리 UI 렌더링
Loading

Possibly related PRs

Suggested labels: 🎨 UI/Style

Suggested reviewers: wacaw9, codebidoof

🚥 Pre-merge checks | ✅ 5 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive 핵심 변경은 홈 화면 구현 범위에 맞지만, .idea 설정 변경은 요구사항과 직접 관련이 없습니다. 비기능성 .idea 변경을 제거하거나 별도 커밋으로 분리해 범위를 명확히 해주세요.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Conventional Commits 형식을 따르며 홈 화면 UI 구현이라는 변경 내용을 정확히 요약합니다.
Linked Issues check ✅ Passed #7의 홈화면 UI와 지난 알림·최근 기록 탭바 구현 요구를 모두 충족합니다.
Ui 변경 시 스크린샷 첨부 확인 ✅ Passed HomeScreen.kt 등 Compose UI 파일이 변경됐고 PR 설명에 GitHub user-attachments 링크가 있어 스크린샷/녹화 첨부가 확인됩니다.
모듈 의존성 방향 검증 ✅ Passed feature/home/impl은 core:domain/common/designsystem만 의존했고 core/data/타 feature impl import는 없었으며, core/domain에도 android/androidx import가 없었습니다.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#7-home-ui

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

@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요! GitHub Pull Request 리뷰 요청해주셔서 감사합니다. 시니어 Android 개발자의 관점에서 PR diff를 꼼꼼히 검토했습니다. 전반적으로 코드 구조가 깔끔하고, java.time API, Hilt, Coil, Compose LazyColumn key 등 최신 기술 스택을 잘 활용하고 있는 점이 인상 깊습니다.

다만, 몇 가지 개선 사항과 잠재적 문제점을 발견하여 아래와 같이 상세한 피드백을 드립니다.


1. core/common/src/main/java/kr/co/call/core/common/util/TimeUtil.kt

  • 하드코딩된 문자열 (Critical)

    • toTimeAgoText: "분 전", "시간 전", "일 전"
    • toReservationTimeText: "오늘", "내일", "새벽", "아침", "낮", "저녁", "밤", "님과", "전화가 예정되어 있어요", "시", "분"
    • 개선 필요: 사용자에게 노출되는 모든 문자열은 strings.xml 리소스 파일로 이동하여 관리해야 합니다. 이는 다국어 지원(i18n) 및 유지보수를 위해 필수적입니다. @StringRes 애노테이션을 사용하여 리소스 ID를 전달받는 방식으로 변경해주세요.
  • 매직 넘버 (Minor)

    • toTimeAgoText60, 24: 시간 계산에 사용되는 숫자들은 문맥상 명확하지만, MINUTES_IN_HOUR, HOURS_IN_DAY와 같은 const val 상수로 정의하면 가독성이 더욱 향상될 수 있습니다.

2. core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt

  • Mock API 처리 (Major)
    • provideHomeApi()에 mock 구현이 포함되어 있습니다. 주석으로 "실제 HomeApi 연결 전 사용하는 Mock 데이터"라고 명시되어 있어 현재로서는 이해하지만, 실제 프로덕션 코드에서는 반드시 실제 Retrofit 서비스 구현으로 교체되어야 합니다.
    • 개선 필요: // TODO: Replace with actual Retrofit service before production release.와 같은 명확한 TODO 주석을 추가하고, 실제 서비스 구현 시 이 object를 제거해주세요. 이상적으로는 mock 구현을 별도의 FakeHomeApi 클래스로 분리하고, 테스트 환경에서만 주입되도록 Hilt 모듈을 분리하는 것이 좋습니다.

3. core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt

  • Enum valueOf 사용 시 null 안전성 및 예외 처리 (Major)
    • CallSender.valueOf(sender), CallType.valueOf(callType), CallStatus.valueOf(status), HomeModel.ReservationStatus.valueOf(status)와 같이 Enum.valueOf(string)를 직접 사용하는 것은 위험합니다. 네트워크 응답으로 받은 문자열이 해당 Enum 클래스에 정의되지 않은 값일 경우 IllegalArgumentException이 발생하여 앱이 크래시될 수 있습니다.
    • 개선 필요:
      • when 식을 사용하여 매핑하고, 알 수 없는 값에 대한 else 분기에서 기본값 또는 안전한 UNKNOWN Enum 값을 반환하도록 처리하거나, 예외를 던져 상위 레이어에서 처리하도록 변경하는 것이 좋습니다.
      • 예시:
        internal fun CallHistoryDto.toDomain(): CallHistory {
            // ...
            val senderEnum = when (sender) {
                "USER" -> CallSender.USER
                "AI" -> CallSender.AI
                else -> CallSender.UNKNOWN // 또는 적절한 예외 처리
            }
            // ...
        }
      • 또는 각 Enum 클래스에 companion objectfromString(value: String) 팩토리 메서드를 추가하여 안전하게 변환하는 방법을 고려할 수 있습니다.

4. core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt

  • 네트워크 응답 isSuccess 필드 처리 (Major)

    • 현재 runCatching { homeApi.getReservations().result.toDomain() } 와 같이 result 필드를 직접 접근하고 있습니다. 하지만 ReservationListResponseDto, CallHistoryListResponseDto, HomeSummaryResponseDto DTO에는 isSuccess: Boolean, code: String, message: String 필드가 있습니다. 실제 API 통신 시 isSuccessfalse이고 resultnull인 경우가 발생할 수 있습니다.
    • 개선 필요: 네트워크 응답의 isSuccess 필드를 확인하고, false일 경우 Result.failure()를 반환하거나 특정 예외(예: ApiException)를 던져 비즈니스 로직 단에서 에러를 명확하게 처리해야 합니다.
      override suspend fun getReservations(): Result<HomeModel.Reservations> =
          runCatching {
              val response = homeApi.getReservations()
              if (!response.isSuccess) {
                  throw ApiException(response.code, response.message) // 커스텀 예외 정의 필요
              }
              response.result.toDomain()
          }
    • ApiException과 같은 커스텀 예외를 정의하여 에러 코드와 메시지를 포함시키는 것이 좋습니다.
  • Hilt DI 생성자 주입 원칙 (Minor)

    • HomeRepositoryImpl의 생성자에 @Inject 어노테이션이 사용되었으므로, RepositoryModule에서 provideHomeRepository 함수는 homeRepositoryImpl: HomeRepositoryImpl 파라미터에 @Inject를 붙여도 되고, 생략해도 Hilt가 알아서 주입합니다. 현재 방식도 동작에는 문제가 없으나, 명시적으로 homeRepositoryImpl: HomeRepositoryImpl 대신 HomeRepositoryImpl()을 바로 반환하는 것이 더 흔하게 사용되는 패턴입니다.
    • @Provides
      @Singleton
      fun provideHomeRepository(
          homeApi: HomeApi // HomeRepositoryImpl에 필요한 의존성을 여기서 주입
      ): HomeRepository = HomeRepositoryImpl(homeApi)
    • 하지만 현재처럼 provideHomeRepository(homeRepositoryImpl: HomeRepositoryImpl) 형태로 하는 것은 Hilt가 이미 HomeRepositoryImpl의 인스턴스를 생성할 수 있도록 설정되어 있음을 의미하며, 이 방식도 유효합니다. 이 경우 RepositoryModuleHomeRepositoryImpl의 존재를 알고 있어야 합니다.

5. core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt

  • CallHistory 중복 정의 (Major)
    • kr.co.call.domain.model.home.CallHistoryHomeModel.CallHistory가 동일한 구조로 중복 정의되어 있습니다.
    • 개선 필요: HomeModel 내부의 CallHistory 정의를 제거하고, HomeModelcallHistories 속성을 List<CallHistory> (즉, kr.co.call.domain.model.home.CallHistory)로 변경하여 기존 도메인 모델을 재사용해야 합니다. 이는 코드 중복을 피하고 단일 책임 원칙을 유지하는 데 중요합니다.

6. core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImageWithIcon.kt

  • contentDescription 개선 (Minor)
    • Image (콜 배지)의 contentDescriptionnull로 설정되어 있습니다. 이 아이콘이 순전히 장식적인 요소라면 null도 허용되지만, "프로필 이미지 위에 전화 가능 표시"와 같이 더 구체적인 설명을 추가하면 시각 장애인 사용자에게 더 나은 사용자 경험을 제공할 수 있습니다.
    • 개선 필요: 이 아이콘이 어떤 의미를 가지는지 (예: "통화 가능", "예약된 통화") 명확히 하고, 그에 맞는 contentDescription을 제공하는 것을 고려해보세요.

7. feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt

  • 하드코딩된 문자열 (Critical)

    • "메인 연인 교체하기"
    • "님, 반가워요! 👋🏻"
    • "오늘은 어떤 이야기를\n나누게 될까요?"
    • 개선 필요: TimeUtil.kt와 동일하게 strings.xml 리소스로 이동해야 합니다.
  • 매직 넘버 (Minor)

    • 레이아웃에 사용된 dp 값(예: 312.dp, 265.dp, 93.dp, 24.dp, 53.dp 등)이 많습니다. 고정된 디자인이라면 괜찮지만, 반응형 UI를 위해 fillMax*, weight, padding 등을 더 적극적으로 사용하여 유연성을 높이는 방안을 고려해볼 수 있습니다. 현재는 특정 레이아웃에 고정된 비율을 따르는 것으로 보이므로, 의도된 것이라면 괜찮습니다.

8. feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt

  • 하드코딩된 문자열 (Critical)

    • "약속", "건" (예: "약속 1건")
    • "님과", "전화가 예정되어 있어요" (예: "민준님과 오늘 아침 9시 전화가 예정되어 있어요")
    • "시간 변경"
    • 개선 필요: strings.xml 리소스로 이동해야 합니다. 특히 "약속 ${count}건"과 같이 동적으로 숫자가 들어가는 문자열은 %d 포맷팅을 사용해야 합니다. (예: <string name="reservation_count">약속 %d건</string>)
  • LocalMinimumInteractiveComponentSize 사용 (Major - Accessibility)

    • "시간 변경" 버튼에 LocalMinimumInteractiveComponentSize provides 0.dp를 사용하여 버튼의 최소 터치 영역을 0으로 설정했습니다. 이는 Jetpack Compose의 Material Design 컴포넌트가 기본적으로 제공하는 최소 터치 영역(48dp)보다 작게 버튼을 만들 수 있게 하지만, 접근성(Accessibility) 측면에서 문제가 될 수 있습니다.
    • 개선 필요:
      • 이 버튼이 중요한 인터랙티브 요소라면, 최소 터치 영역을 48dp 이상으로 유지하는 것이 좋습니다.
      • 만약 디자인 상 더 작은 버튼이 필요하다면, 해당 버튼 주변에 더 넓은 clickable 영역을 제공하거나, 버튼 자체를 누르기 쉬운 다른 컴포넌트로 대체하는 방법을 고려해야 합니다. 현재로서는 27dp 높이의 버튼이 독립적인 터치 타겟 역할을 하는 것으로 보입니다.

9. feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt

  • 하드코딩된 문자열 (Critical)

    • RelationshipSummaryItemlabel ("관계", "통화", "연속 통화")
    • withSuffixOrDash 확장 함수의 suffix ("일째", "회", "일")
    • 개선 필요: strings.xml 리소스로 이동해야 합니다.
  • 0일 경우 "-" 표시 (withSuffixOrDash) (Minor)

    • 0일 때 "-"를 표시하는 로직은 이해되지만, 비즈니스/UX 요구사항에 따라 "첫 통화", "첫 인연" 등 좀 더 의미 있는 텍스트로 대체할 수도 있습니다. 현재 방식도 유효합니다.

10. feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt

  • 하드코딩된 문자열 (Critical)

    • "최근 20건의 기록만 표시됩니다."
    • "통화 기록 보기" (contentDescription)
    • 개선 필요: strings.xml 리소스로 이동해야 합니다.
  • CallTypeIconcontentDescription (Minor)

    • CallTypeIcon 내부의 Image에는 contentDescription이 명시되어 있지 않습니다. 이 아이콘이 통화의 방향(송신, 수신)이나 상태(부재중)를 시각적으로 나타내는 중요한 요소이므로, 해당 타입에 맞는 contentDescription을 제공하는 것이 좋습니다.
    • 개선 필요: type 값에 따라 "보낸 통화", "받은 통화", "부재중 통화" 등으로 contentDescription을 동적으로 설정하는 것을 고려해보세요.
  • 프리뷰 누락 (Minor)

    • CallHistoryList에 대한 @Preview Composable이 없습니다. LazyColumn과 내부 아이템들이 잘 렌더링되는지 확인하기 위해 프리뷰를 추가하는 것이 좋습니다.

ViewModel (Implied by UI States)

제공된 diff에 ViewModel 코드가 없어 상세한 리뷰는 어렵지만, UI 상태를 나타내는 HomeSummaryState, HomeReservationState, CallHistoryState, HomeCallHistoryItemStatedata class 사용은 Jetpack Compose의 UI 상태 관리 원칙에 잘 부합합니다. 특히, startedAtText와 같이 UI에서 바로 사용할 수 있도록 가공된 데이터를 HomeCallHistoryItemState에 포함시킨 것은 훌륭합니다.


전반적인 좋은 점

  • java.time API 사용: DateCalendar 대신 java.time 패키지를 사용하여 현대적인 날짜/시간 처리를 하는 점이 아주 좋습니다.
  • Hilt DI: 생성자 주입 원칙을 준수하고 있습니다.
  • Coil Compose: 이미지 로딩 라이브러리를 잘 적용했습니다.
  • Design System 활용: CallTheme.colors와 같은 디자인 시스템을 활용하여 일관된 UI를 구축하려는 노력이 보입니다.
  • Compose 컴포넌트 분리: HomeHeader, HomeReservationCard, RelationshipSummaryCard, CallHistoryList 등 컴포넌트가 역할에 따라 잘 분리되어 재사용성과 유지보수성이 높습니다.
  • State Hoisting: Composable 함수들이 UI 상태를 파라미터로 받아 상위 Composable 또는 ViewModel에 상태를 위임하는 State Hoisting 원칙을 잘 준수하고 있습니다.
  • LazyColumn key 사용: LazyColumn에서 key 파라미터를 사용하여 리컴포지션 효율성을 높이고 목록 상태를 안정적으로 유지하는 것은 매우 좋은 구현입니다.
  • Repository Result 래핑: runCatching을 통해 Result<T>를 반환하여 에러 처리를 명시적으로 하는 패턴은 바람직합니다. (API-level 에러 처리 보강 필요)
  • Mapper 분리: DTO와 도메인 모델 간의 매핑 로직이 mapper 패키지에 잘 분리되어 있습니다.

결론

전반적으로 잘 작성된 코드입니다. 특히 KotlinJetpack Compose의 현대적인 기능을 잘 활용하고 있어 팀의 기술 수준이 높다고 판단됩니다. 하지만 위에 언급된 하드코딩된 문자열DTO-Enum 매핑의 안전성, Repository의 API 응답 isSuccess 처리, 그리고 HomeModel.CallHistory 중복 문제는 반드시 수정되어야 할 중요한 사항입니다.

이 피드백이 코드 품질 향상에 도움이 되기를 바랍니다. 궁금한 점이 있다면 언제든지 질문해주세요.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (1)
feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt (1)

19-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

toCallHistoryType()else 브랜치를 명시적으로 작성하세요.

elseRECEIVE_MISSED로 매핑되는데, 이는 sender == CallSender.AI && status == CallStatus.MISSED 케이스를 암시적으로 처리합니다. 향후 CallStatus에 새 값(예: CANCELLED)이 추가되면 의도치 않게 RECEIVE_MISSED로 분류됩니다.

♻️ 제안하는 수정
 private fun CallHistory.toCallHistoryType(): CallHistoryType =
     when {
         sender == CallSender.USER && status == CallStatus.COMPLETED ->
             CallHistoryType.SEND

         sender == CallSender.AI && status == CallStatus.COMPLETED ->
             CallHistoryType.RECEIVED

         sender == CallSender.USER && status == CallStatus.MISSED ->
             CallHistoryType.SEND_MISSED

-        else ->
+        sender == CallSender.AI && status == CallStatus.MISSED ->
             CallHistoryType.RECEIVE_MISSED
+
+        else ->
+            CallHistoryType.RECEIVE_MISSED // 또는 예외 처리
     }
🤖 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/mapper/CallHistoryUiMapper.kt`
around lines 19 - 32, Update CallHistory.toCallHistoryType() to explicitly match
the AI sender with CallStatus.MISSED and return CallHistoryType.RECEIVE_MISSED.
Remove the catch-all else branch so future CallStatus values cannot be
implicitly classified as missed calls.
🤖 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 `@core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt`:
- Around line 35-48: Update the mock reservation returned by getReservations in
RepositoryModule so scheduledAt is in the future relative to the current
mock/test date, or use the project’s test Clock to define that reference time.
Keep the reservation status and response structure unchanged while ensuring
HomeReservationCard cannot render this fixture as a past appointment.

In
`@core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt`:
- Around line 14-27: Update getReservations, getCallHistories, and getSummary to
validate each API wrapper’s isSuccess before mapping result data. For
unsuccessful responses, convert the failure using the response code and message
into the repository’s domain-level error result, while preserving the existing
mappings for successful responses and DataSource exception conversion.

In `@core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt`:
- Line 8: Remove the nested HomeModel.CallHistory declaration and ensure
HomeModel.callHistories resolves to the top-level
kr.co.call.domain.model.home.CallHistory type. Keep the existing
CallHistoryMapper and CallMockData return types unchanged so HomeModel
construction accepts their results.

In
`@feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt`:
- Around line 49-52: Update HomeHeader to accept callbacks for the main-lover
replacement and phone actions, then invoke them from the corresponding header
buttons instead of no-ops. Pass these callbacks from the parent screen and
connect them to the appropriate ViewModel intent or side-effect handling,
preserving the existing UI behavior.

In
`@feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt`:
- Around line 202-209: Update the CompositionLocalProvider surrounding the “시간
변경” Button in HomeReservationCard so LocalMinimumInteractiveComponentSize no
longer overrides the minimum to 0.dp. Preserve the button’s 27.dp visual height
while restoring the standard minimum 48.dp touch target.

In `@feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt`:
- Around line 68-104: HomeScreenContent currently leaves the time-change,
record-view, and callback actions unconnected. In
feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt:68-104, add
these callbacks to the screen contract and pass them through HomeReservationCard
and HomeHistorySection; in
feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt:43-46
and
feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt:40-69,
remove the no-op default callbacks; in
feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt:16-39,
accept and forward the history and call callbacks; in
feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt:49-87,
replace the hardcoded empty onCallClick with the propagated external callback.

In `@feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt`:
- Around line 18-19: HomeViewModel이 직접 수행하는 세 Repository 호출과 HomeModel 조합을
core:domain의 GetHomeUseCase로 이동하세요. GetHomeUseCase가 필요한 Repository를 호출해
HomeModel을 반환하도록 구성하고, HomeViewModel은 이를 주입받아 호출한 뒤 UI 상태 변환만 담당하게 하세요.

In
`@feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeReservationState.kt`:
- Line 1: Update the UI state data-class defaults in HomeReservationState,
HomeSummaryState, and HomeState to neutral values: use null or an empty string
for reservation name fields, 0 for summary counters, false for
hasUnreadNotification, and null for scheduledAt. Keep mock data in dedicated
mock-data layers such as HomeMockData.kt rather than state defaults.
- Around line 10-12: HomeReservationState의 scheduledAt과 firstName 기본값에 하드코딩된
mock 데이터를 제거하고 각각 null 등 중성 기본값으로 변경하세요. 실제 mock 데이터는 HomeMockData.kt 같은 별도 mock
레이어에서 제공하며, API 로딩 전이나 빈 응답에서는 가짜 값이 표시되지 않도록 하세요.

In
`@feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt`:
- Line 12: Update the hasUnreadNotification property in HomeState so its default
value is false; preserve updating it with the actual notification state after
data loading.

In
`@feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeSummaryState.kt`:
- Around line 5-8: HomeSummaryState의 firstName, relationshipDays,
totalCallCount, callStreakDays 기본값에서 하드코딩된 mock 데이터를 제거하고 각각 "" 또는 0의 중성값을
사용하세요. mock 데이터가 필요하면 State 외부의 별도 레이어에서 제공하도록 유지하세요.

---

Nitpick comments:
In
`@feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt`:
- Around line 19-32: Update CallHistory.toCallHistoryType() to explicitly match
the AI sender with CallStatus.MISSED and return CallHistoryType.RECEIVE_MISSED.
Remove the catch-all else branch so future CallStatus values cannot be
implicitly classified as missed calls.
🪄 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: e1a7c511-1914-4a64-ba81-d500c6c298e8

📥 Commits

Reviewing files that changed from the base of the PR and between 148b3eb and 4224e4a.

⛔ Files ignored due to path filters (5)
  • core/designsystem/src/main/res/drawable/img_call_logo.png is excluded by !**/*.png
  • core/designsystem/src/main/res/drawable/img_home_call_missed.png is excluded by !**/*.png
  • core/designsystem/src/main/res/drawable/img_home_call_received.png is excluded by !**/*.png
  • core/designsystem/src/main/res/drawable/img_home_call_send.png is excluded by !**/*.png
  • core/designsystem/src/main/res/drawable/img_profile_url_default.png is excluded by !**/*.png
📒 Files selected for processing (53)
  • core/common/src/main/java/kr/co/call/core/common/util/TimeUtil.kt
  • core/data/build.gradle.kts
  • core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt
  • core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt
  • core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt
  • core/designsystem/build.gradle.kts
  • core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImageWithIcon.kt
  • core/designsystem/src/main/java/kr/co/call/designsystem/component/gitkeep
  • core/designsystem/src/main/res/drawable/ic_home_alarm.xml
  • core/designsystem/src/main/res/drawable/ic_home_call.xml
  • core/designsystem/src/main/res/drawable/ic_home_call_btn.xml
  • core/designsystem/src/main/res/drawable/ic_home_record.xml
  • core/domain/src/main/java/kr/co/call/domain/model/home/.gitkeep
  • core/domain/src/main/java/kr/co/call/domain/model/home/CallHistory.kt
  • core/domain/src/main/java/kr/co/call/domain/model/home/CallSender.kt
  • core/domain/src/main/java/kr/co/call/domain/model/home/CallStatus.kt
  • core/domain/src/main/java/kr/co/call/domain/model/home/CallType.kt
  • core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt
  • core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt
  • core/network/src/main/java/kr/co/call/network/api/HomeApi.kt
  • core/network/src/main/java/kr/co/call/network/dto/CallHistoryListResponseDto.kt
  • core/network/src/main/java/kr/co/call/network/dto/HomeSummaryResponseDto.kt
  • core/network/src/main/java/kr/co/call/network/dto/ReservationListResponseDto.kt
  • feature/home/impl/build.gradle.kts
  • feature/home/impl/src/main/java/kr/co/call/impl/component/.gitkeep
  • feature/home/impl/src/main/java/kr/co/call/impl/component/UnreadIndicator.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/entry/.gitkeep
  • feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/mock/CallMockData.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/screen/.gitkeep
  • feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/tab/HomeHistoryTab.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/tab/HomeHistoryTabBar.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/.gitkeep
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/CallHistoryType.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeIntent.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeSideEffect.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/NotificationType.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/CallHistoryState.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeCallHistoryItemState.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeNotificationState.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeReservationState.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeSummaryState.kt
💤 Files with no reviewable changes (1)
  • core/designsystem/src/main/java/kr/co/call/designsystem/component/gitkeep

Comment thread core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt Outdated
Comment on lines +14 to +27
override suspend fun getReservations(): Result<HomeModel.Reservations> =
runCatching {
homeApi.getReservations().result.toDomain()
}

override suspend fun getCallHistories(): Result<List<CallHistory>> =
runCatching {
homeApi.getCallHistories(size = CALL_HISTORY_SIZE).result.content.map { it.toDomain() }
}

override suspend fun getSummary(characterId: Long): Result<HomeModel.Summary> =
runCatching {
homeApi.getSummary(characterId).result.toDomain(characterId)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

업무 실패 응답을 성공 데이터로 매핑하지 마세요.

세 메서드 모두 wrapper의 isSuccess를 무시하고 result를 바로 변환합니다. HTTP 200이지만 업무적으로 실패한 응답은 성공으로 처리되어 잘못된 홈 상태를 렌더링할 수 있습니다. 매핑 전에 isSuccess를 검증하고, 실패 시 codemessage를 포함한 실패 결과로 변환하세요.

As per path instructions, “DataSource(Remote/Local) 예외를 Repository에서 도메인 레벨 에러로 적절히 변환”해야 합니다.

🤖 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 14 - 27, Update getReservations, getCallHistories, and getSummary
to validate each API wrapper’s isSuccess before mapping result data. For
unsuccessful responses, convert the failure using the response code and message
into the repository’s domain-level error result, while preserving the existing
mappings for successful responses and DataSource exception conversion.

Source: Path instructions

Comment thread core/domain/src/main/java/kr/co/call/domain/model/home/HomeModel.kt Outdated
Comment on lines +49 to +52
internal fun HomeHeader(
summaryState: HomeSummaryState,
hasUnreadNotification: Boolean,
modifier: Modifier = Modifier,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

헤더 액션을 상위 계층으로 전달하세요.

메인 연인 교체와 전화하기 버튼이 모두 no-op이라 사용자가 탭해도 아무 동작이 없습니다. 콜백을 HomeHeader 파라미터로 받고, 부모 화면에서 ViewModel intent/side effect로 연결하세요.

수정 예시
 internal fun HomeHeader(
     summaryState: HomeSummaryState,
     hasUnreadNotification: Boolean,
+    onMainPartnerChangeClick: () -> Unit,
+    onCallClick: () -> Unit,
     modifier: Modifier = Modifier,
 ) {
     // ...
     Button(
-        onClick = {},
+        onClick = onMainPartnerChangeClick,
     ) {
         // ...
     }

     FilledIconButton(
-        onClick = {},
+        onClick = onCallClick,
     ) {
         // ...
     }
 }

Also applies to: 77-78, 162-163

🤖 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 49 - 52, Update HomeHeader to accept callbacks for the main-lover
replacement and phone actions, then invoke them from the corresponding header
buttons instead of no-ops. Pass these callbacks from the parent screen and
connect them to the appropriate ViewModel intent or side-effect handling,
preserving the existing UI behavior.

Comment thread feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt Outdated
@@ -0,0 +1,13 @@
package kr.co.call.impl.viewmodel.state

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

UI State 기본값에 Mock 데이터가 하드코딩된 공통 문제. 세 곳 모두 동일한 근본 원인: State data class의 기본값이 중성값("", 0, false, null)이 아닌 mock 데이터로 설정되어 있어, API 로딩 전이나 빈 응답 시 UI에 가짜 데이터가 표시됩니다. Mock 데이터는 HomeMockData.kt 등 별도 레이어에서만 제공해야 합니다.

  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeReservationState.kt#L10-12: scheduledAt 기본값을 null로, firstName 기본값을 null 또는 ""로 변경
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeSummaryState.kt#L5-8: firstName"", relationshipDays/totalCallCount/callStreakDays0으로 변경
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt#L12: hasUnreadNotification 기본값을 false로 변경
🤖 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/state/HomeReservationState.kt`
at line 1, Update the UI state data-class defaults in HomeReservationState,
HomeSummaryState, and HomeState to neutral values: use null or an empty string
for reservation name fields, 0 for summary counters, false for
hasUnreadNotification, and null for scheduledAt. Keep mock data in dedicated
mock-data layers such as HomeMockData.kt rather than state defaults.

Comment on lines +10 to +12
val profileImageUrl: String? = "",
val scheduledAt: LocalDateTime? = LocalDateTime.of(2026, 6, 30, 21, 0),
val firstName: String? = "민준",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Mock 데이터가 State 기본값에 하드코딩되어 있습니다.

scheduledAtfirstName의 기본값이 실제 데이터가 아닌 mock 값으로 설정되어 있습니다. API 로딩 전이나 빈 응답 시 UI에 가짜 데이터가 표시됩니다. Mock 데이터는 HomeMockData.kt 등 별도 레이어에서 제공하고, State 기본값은 중성값(null, "")을 사용해야 합니다.

🤖 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/state/HomeReservationState.kt`
around lines 10 - 12, HomeReservationState의 scheduledAt과 firstName 기본값에 하드코딩된
mock 데이터를 제거하고 각각 null 등 중성 기본값으로 변경하세요. 실제 mock 데이터는 HomeMockData.kt 같은 별도 mock
레이어에서 제공하며, API 로딩 전이나 빈 응답에서는 가짜 값이 표시되지 않도록 하세요.

val summary: HomeSummaryState = HomeSummaryState(),
val reservation: HomeReservationState = HomeReservationState(),
val callHistory: CallHistoryState = CallHistoryState(),
val hasUnreadNotification: Boolean = true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

hasUnreadNotification 기본값이 true로 하드코딩되어 있습니다.

실제 알림 상태와 무관하게 항상 읽지 않음 표시가 렌더링됩니다. 기본값은 false여야 하며, 실제 값은 데이터 로드 후 반영되어야 합니다.

🤖 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/state/HomeState.kt`
at line 12, Update the hasUnreadNotification property in HomeState so its
default value is false; preserve updating it with the actual notification state
after data loading.

Comment on lines +5 to +8
val firstName: String = "수현",
val relationshipDays: Int = 30,
val totalCallCount: Int = 24,
val callStreakDays: Int = 12,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Mock 데이터가 State 기본값에 하드코딩되어 있습니다.

firstName, relationshipDays, totalCallCount, callStreakDays의 기본값이 mock 값입니다. API 로딩 전이나 빈 응답 시 가짜 데이터가 표시됩니다. 중성 기본값("", 0)을 사용하고 mock 데이터는 별도 레이어에서 제공하세요.

🤖 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/state/HomeSummaryState.kt`
around lines 5 - 8, HomeSummaryState의 firstName, relationshipDays,
totalCallCount, callStreakDays 기본값에서 하드코딩된 mock 데이터를 제거하고 각각 "" 또는 0의 중성값을
사용하세요. mock 데이터가 필요하면 State 외부의 별도 레이어에서 제공하도록 유지하세요.

@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요, 시니어 안드로이드 개발자로서 PR 리뷰를 시작하겠습니다.

이번 PR은 홈 화면의 주요 컴포넌트들을 구성하고 데이터 레이어를 연결하는 중요한 작업으로 보입니다. 전반적으로 클린 아키텍처 원칙을 잘 따르고 있으며, Jetpack Compose와 Hilt 사용에 대한 이해도가 높은 것으로 평가됩니다.

하지만 몇 가지 개선이 필요한 부분과, 아직 PR에 포함되지 않아 검토가 불가능한 중요한 영역(특히 ViewModel)에 대한 코멘트가 있습니다.


전반적인 피드백 (General Observations)

  • 아키텍처: corefeature 모듈 분리가 잘 되어 있고, domain, data, network 레이어의 역할 구분이 명확합니다. Repository 인터페이스와 구현체, Mapper 등 클린 아키텍처 패턴을 잘 따르고 있습니다.
  • DI: Hilt를 사용하여 Repository를 주입하는 방식이 올바르게 적용되었습니다.
  • Kotlin: data classval을 사용하여 불변성을 유지하고, enum class를 활용하여 도메인 상태를 명확히 표현한 점이 좋습니다.
  • Jetpack Compose: LazyColumn에서 key를 사용하는 등 Recomposition 최적화에 신경 쓴 점이 인상 깊습니다. AsyncImage와 같은 Jetpack Compose 라이브러리를 적절히 활용하고 있습니다.
  • PLACEHOLDER DATA: HomeRepositoryImpl의 더미 데이터는 개발 초기에는 유용하지만, 실제 네트워크 연동 시 교체가 필요하며, 이 과정에서 적절한 에러 핸들링이 추가되어야 합니다.
  • ViewModel 부재: 현재 PR에는 UI 컴포넌트와 데이터 레이어만 포함되어 있고, ViewModel 코드가 보이지 않습니다. ViewModel은 UI 상태 관리, 비즈니스 로직, 데이터 로딩 상태(로딩/성공/에러)를 담당하는 핵심 계층이므로, ViewModel이 추가될 때 해당 규칙(규칙 4)에 따라 추가적인 검토가 필요합니다. 특히, 로딩/에러 상태를 LoadStatus로 관리하는 부분(규칙 2)도 ViewModel에서 정의된 UI State를 통해 이루어져야 합니다.

세부 코드 리뷰 (Detailed Code Review)

1. core/common/src/main/java/kr/co/call/core/common/util/TimeUtil.kt

  • Hardcoded Strings (P1): toCallHistoryDateText, toTimeAgoText, toHomeDateText, toReservationTimeText 함수 내의 날짜/시간 포맷 문자열("M월 d일 a h시 m분", "M월 d일 EEEE", "HH:mm") 및 "분 전", "시간 전", "일 전", "오늘", "내일", "새벽", "아침", "낮", "저녁", "밤", "시", "분" 등 모든 사용자에게 노출되는 문자열은 strings.xml로 이동하여 i18n(국제화)을 지원하고 코드 유지보수성을 높이는 것이 좋습니다. 현재 Locale.KOREAN을 명시하고 있어 국제화의 첫걸음은 떼었지만, 문자열 자체는 하드코딩되어 있습니다.
  • LocalDateTime.parse 예외 처리 (P2): parseLocalDateTime는 입력 value가 유효하지 않은 LocalDateTime 형식일 경우 DateTimeParseException을 발생시킬 수 있습니다. 현재 CallHistoryMapper에서 이를 직접 사용하고 있는데, 만약 네트워크 응답 등 외부로부터 오는 데이터라면 파싱 실패 가능성을 염두에 두고 runCatching 블록으로 감싸거나 LocalDateTime? 또는 Result<LocalDateTime>를 반환하도록 변경하여 호출 측에서 오류를 명시적으로 처리하도록 하는 것이 더 안전합니다. 지금은 'API 응답은 항상 유효한 시간 문자열을 반환한다'고 가정하고 넘어갈 수 있지만, 실제 프로덕션 코드에서는 고려해볼 만합니다.
  • toTimeAgoText: coerceAtLeast(0)을 사용하여 음수 방지하는 로직은 좋습니다.

2. core/data/build.gradle.kts

  • implementation(project(":core:common")): TimeUtil 사용을 위한 의존성 추가는 적절합니다.

3. core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt

  • Hilt DI (규칙 1): abstract class RepositoryModule, @Binds, @Singleton, abstract fun bindHomeRepository(homeRepositoryImpl: HomeRepositoryImpl): HomeRepository 모두 Hilt의 생성자 주입 원칙과 바인딩 모듈 패턴을 올바르게 준수하고 있습니다. 매우 좋습니다.

4. core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt

  • enum.valueOf() Robustness (P0): CallSender.valueOf(sender), CallType.valueOf(callType), CallStatus.valueOf(status), HomeModel.ReservationStatus.valueOf(status)와 같이 문자열을 enum으로 변환하는 valueOf() 메서드는 해당 문자열이 enum 상수에 존재하지 않을 경우 IllegalArgumentException을 발생시킵니다. 이는 네트워크 응답의 데이터가 예상과 다를 경우 앱 크래시로 이어질 수 있는 위험이 있습니다.
    • 개선 방안: runCatching { Enum.valueOf(...) }.getOrNull()을 사용하거나, Enum.values().firstOrNull { it.name == inputString }와 같이 안전하게 변환하고 실패 시 null 또는 기본값을 반환하도록 구현하는 것이 좋습니다. 또는, 커스텀 역직렬화 로직을 도입하는 것도 방법입니다.
  • 네트워크 응답과 도메인 모델 매핑 분리 (규칙 3): DTO를 도메인 모델로 매핑하는 역할에 충실하며, 잘 분리되어 있습니다.

5. core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt

  • Retrofit 에러 핸들링 & Result 래핑 (규칙 3 - P0 for future): 현재는 하드코딩된 Result.success 데이터를 반환하고 있습니다. 실제 HomeApi와 연동 시에는 try-catch 블록을 사용하여 네트워크 요청 실패(IOException), 서버 응답 에러(HttpException), 데이터 파싱 에러 등을 적절히 처리하고, 그 결과를 Result<T>로 래핑하여 반환해야 합니다. HomeApi 인터페이스는 Result를 반환하지 않으므로, 이 계층에서 Result 래핑 로직이 구현되어야 합니다.
  • 하드코딩된 문자열/매직 넘버 (규칙 1 - P2): CALL_HISTORY_SIZE = 20private companion object로 분리하여 관리가 용이합니다. 하지만 LocalDateTime.of(...)에 사용된 연/월/일/시/분 등의 값, characterName, aiSummary 등은 임시 데이터임을 감안하더라도 실제 API 연동 시 모두 대체되어야 합니다.

6. core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImageWithIcon.kt

  • Jetpack Compose & Recomposition (규칙 2):
    • ProfileImageWithIcon은 상태를 직접 갖지 않고 파라미터로 받아오는 Stateless Composable입니다. Recomposition 관점에서 효율적입니다.
    • AsyncImage 사용: URL 로딩, 플레이스홀더, 에러/폴백 이미지 처리를 위해 coil.compose.AsyncImage를 사용한 것은 적절합니다. model = profileImageUrl?.takeIf { it.isNotBlank() }는 URL의 null 및 blank 상태를 잘 처리하여 defaultProfile이 로드되도록 합니다.
    • contentDescription: 이미지에 대한 contentDescription을 제공하여 접근성을 높인 점은 좋습니다. (단, img_call_logocontentDescription = null로 두어 시각 장애인에게 읽히지 않도록 처리한 것은 UI의 장식 요소일 경우 적절합니다.)
  • 하드코딩된 매직 넘버 (규칙 1, 2 - P1): 49.dp, 51.dp, 25.dp (RoundedCornerShape), 16.dp (Badge Size) 등은 디자인 시스템의 일부로 간주될 수 있지만, 가능하면 CallTheme.dimens와 같이 테마에서 중앙 관리되는 값으로 추출하는 것이 전체적인 디자인 일관성과 유지보수성 측면에서 더 좋습니다.

7. core/domain/src/main/java/kr/co/call/domain/model/home/*

  • data class & 불필요한 var 사용 지양 (규칙 1): 모든 도메인 모델 (CallHistory, HomeModel, Summary, Reservations, Reservation)이 data class로 선언되었고, 필드들이 모두 val로 선언되어 불변성을 유지하고 있습니다. 매우 좋은 구현입니다.
  • UI 상태 data class 표현 (규칙 2): 도메인 모델 자체는 UI 상태가 아니지만, UI 상태를 data class로 표현하는 원칙을 HomeModel 내부 클래스에서 잘 보여주고 있습니다.

8. feature/home/impl/build.gradle.kts

  • implementation(libs.androidx.paging.compose): 페이징 라이브러리 추가는 NotificationListCallHistoryList에서 페이징 데이터를 다룰 계획임을 시사하며, 이는 대규모 리스트 처리 시 성능 및 사용자 경험에 긍정적입니다.

9. feature/home/impl/src/main/java/kr/co/call/impl/component/UnreadIndicator.kt

  • Jetpack Compose: 6.dp 매직 넘버 외에는 특별한 이슈 없이 간결하게 잘 구현되었습니다.

10. feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt

  • Jetpack Compose & Recomposition (규칙 2):
    • summaryState, hasUnreadNotification을 파라미터로 받아 UI를 구성하는 Stateless Composable입니다.
    • buildAnnotatedString을 사용하여 텍스트의 부분적인 스타일을 적용한 점이 좋습니다.
  • 하드코딩된 문자열/매직 넘버 (규칙 1, 2 - P1): HomeHeader 컴포넌트에는 레이아웃(height, padding, size 등)을 위한 많은 .dp 값들과 사용자에게 노출되는 문자열("메인 연인 교체하기", "님, 반가워요! 👋🏻", "오늘은 어떤 이야기를\n나누게 될까요?", "알림", "전화하기")이 하드코딩되어 있습니다. 이들은 strings.xml 및 디자인 토큰(CallTheme.dimens 또는 CallTheme.sizes 등)으로 추출하여 관리하는 것이 좋습니다.

11. feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt

  • Jetpack Compose & Recomposition (규칙 2):
    • reservationState, onTimeChangeClick을 받아 처리하는 Stateless Composable입니다.
    • TimeUtil.toHomeDateText(LocalDate.now()): LocalDate.now() 호출은 Compose 런타임이 아닌 일반적인 Kotlin 코드로 인식되어 실행됩니다. Composable 함수 내부에서 now()와 같이 Side-Effect가 있는 함수를 직접 호출하는 것은 매 recomposition마다 새로운 LocalDate 객체를 생성하게 됩니다. 큰 성능 저하를 일으키진 않지만, ViewModel 등 상위에서 LocalDate.now()를 계산하여 HomeReservationCard의 파라미터로 넘겨주는 것이 더 명확한 책임 분리 및 테스트 용이성을 확보할 수 있습니다. (P2)
  • 하드코딩된 문자열/매직 넘버 (규칙 1, 2 - P1): HomeHeader와 마찬가지로 많은 .dp 값과 "약속 ${reservationCount}건", "시간 변경" 등의 문자열이 하드코딩되어 있습니다. strings.xml 및 디자인 토큰으로 추출하는 것을 권장합니다.
  • LocalMinimumInteractiveComponentSize provides 0.dp: "시간 변경" 버튼의 최소 상호작용 크기를 0.dp로 설정했습니다. 이는 기본 머티리얼 디자인 가이드라인 (48dp 최소 터치 영역)을 우회하는 것으로, 접근성(특히 손가락이 굵거나 정교한 조작이 어려운 사용자)을 저해할 수 있습니다. UI상 필요한 경우, 버튼의 실제 보이는 크기가 충분히 크게 (예: 48dp 이상) 보장되는지 확인해야 합니다. (P2)
  • Null 안전성 (규칙 1): reservationState.scheduledAt?.let { ... }firstName != null && scheduledAt != null과 같이 nullable 데이터를 안전하게 처리하는 로직은 좋습니다.

12. feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt

  • Jetpack Compose & Recomposition (규칙 2): summaryState를 받아 UI를 구성하는 Stateless Composable입니다. 효율적으로 보입니다.
  • 하드코딩된 문자열/매직 넘버 (규칙 1, 2 - P1): 역시 .dp 값과 "관계", "통화", "연속 통화", "일째", "회", "일", "-" 등의 문자열을 strings.xml 및 디자인 토큰으로 추출하는 것이 좋습니다.
  • Int.withSuffixOrDash: 0일 경우 "-"를 표시하는 확장 함수는 재사용성과 가독성 측면에서 좋은 아이디어입니다.

13. feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt

  • Jetpack Compose & Recomposition (규칙 2):
    • LazyColumn: 스크롤 가능한 리스트에 LazyColumn을 사용한 것은 적절합니다.
    • key = { history -> history.callId }: items 블록에서 key 파라미터를 사용한 것은 매우 좋습니다. 이는 LazyColumn의 성능을 최적화하고, 리스트 아이템의 변경 사항을 효율적으로 추적하여 불필요한 recomposition을 줄이는 핵심적인 방법입니다.
  • 하드코딩된 문자열/매직 넘버 (규칙 1, 2 - P1): HomeHeader와 마찬가지로 많은 .dp 값과 "최근 20건의 기록만 표시됩니다.", "통화 기록 보기" 등의 문자열이 하드코딩되어 있습니다. strings.xml 및 디자인 토큰으로 추출하는 것을 권장합니다.
  • TextOverflow.Ellipsis: 긴 텍스트를 한 줄로 표시하고 오버플로우 시 말줄임표를 사용하는 것은 좋은 UI/UX 고려입니다.
  • UI 상태를 data class로 표현 (규칙 2): CallHistoryStatehistoriesHomeCallHistoryItemState 리스트를 사용하는 방식은 UI 상태를 data class로 표현하는 좋은 예시입니다.
  • CallHistoryType.isMissed: Enum에 확장 프로퍼티를 추가하여 상태를 확인하는 방식은 코드를 더 읽기 쉽게 만듭니다.

14. feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt

  • Jetpack Compose:
    • selectedTab, onTabClick: 탭 선택 상태와 이벤트 핸들링을 상위 컴포넌트에서 받아 처리하는 State Hoisting 원칙을 잘 따르고 있습니다.
    • notifications: Flow<PagingData<HomeNotificationState>>: Paging 데이터 스트림을 직접 Composable로 전달하는 방식은 ViewModel에서 collectAsLazyPagingItems를 통해 데이터를 처리할 것임을 예상하게 합니다. 이는 repeatOnLifecycle을 내부적으로 처리하여 Flow 컬렉션을 생명주기에 안전하게 관리하므로, 올바른 접근 방식입니다.

리뷰 요약 및 주요 개선 권고 (Summary & Key Recommendations)

P0 (Critical / 즉시 수정 필요)

  1. Repository 에러 핸들링 (규칙 3): HomeRepositoryImpl에서 실제 네트워크 요청을 수행할 때, try-catch 블록을 사용하여 네트워크 오류, HTTP 에러 등을 명시적으로 처리하고 Result<T>로 결과를 래핑해야 합니다. (현재 더미 데이터이므로 실제 구현 시 적용)
  2. enum.valueOf()의 안전성 확보 (규칙 1): CallHistoryMapper.kt에서 DTO 문자열을 enum으로 변환할 때 valueOf() 대신 runCatching 또는 안전한 매핑 로직을 사용하여 존재하지 않는 enum 값으로 인한 크래시를 방지해야 합니다.

P1 (High Priority / 반영 권고)

  1. ViewModel 구현 및 UI 상태 관리 (규칙 2, 4): 현재 PR에 포함되지 않은 HomeViewModel이 구현될 때, UI 상태(HomeUiState 등)를 정의하고, 데이터 로딩 상태(로딩, 성공, 에러)를 LoadStatus(sealed class/interface)로 명확하게 표현하여 UI 컴포넌트에 전달해야 합니다. HomeSummaryState, HomeReservationState, CallHistoryState 등은 LoadStatus 내부에 래핑되어야 합니다. 또한, ViewModel의 비즈니스 로직 분리, Orbit 패턴 준수 (선택 시), Side Effect 관리, Blocking 작업 처리 등 규칙 4의 모든 항목을 검토해야 합니다.
  2. 하드코딩된 문자열 외부화 (규칙 1): TimeUtil.kt, HomeHeader.kt, HomeReservationCard.kt, RelationshipSummaryCard.kt, CallHistoryList.kt 내 모든 사용자에게 노출되는 문자열을 strings.xml로 이동하여 국제화 및 유지보수성을 확보해야 합니다.
  3. 하드코딩된 디자인 토큰 외부화 (규칙 1, 2): core:designsystemfeature:home:impl 모듈 내 Composable에서 사용된 많은 매직 넘버(.dp 값)들을 CallTheme.dimens와 같은 디자인 토큰으로 추출하여 디자인 일관성과 유지보수성을 높이는 것이 좋습니다.

P2 (Medium Priority / 고려 사항)

  1. TimeUtil.parseLocalDateTime 예외 처리: TimeUtilparseLocalDateTime 함수가 외부 입력(String)을 받을 경우, 파싱 실패에 대한 예외 처리 전략을 수립하는 것을 고려해 보세요.
  2. LocalDate.now() 호출 위치: HomeReservationCard에서 LocalDate.now()를 직접 호출하는 대신, ViewModel 또는 상위 컴포넌트에서 오늘 날짜를 주입받아 사용하는 것을 고려해볼 수 있습니다. (성능상 큰 문제는 아니지만, 책임 분리 및 테스트 용이성 측면)
  3. LocalMinimumInteractiveComponentSize 접근성 검토: HomeReservationCard에서 "시간 변경" 버튼에 LocalMinimumInteractiveComponentSize provides 0.dp를 사용한 경우, 실제 버튼의 터치 영역이 접근성 가이드라인(48dp 이상)을 만족하는지 수동으로 확인해야 합니다.

전반적으로 코드 품질이 높으며, 클린 아키텍처와 Jetpack Compose 활용에 대한 좋은 기반을 마련하고 있습니다. 위에 언급된 P0, P1 항목들을 우선적으로 개선해 주시면 더 견고하고 유지보수하기 좋은 코드가 될 것입니다. 특히 ViewModel이 추가될 때 다시 한번 자세한 리뷰를 진행하는 것이 중요합니다.

수고 많으셨습니다!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt (1)

150-156: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

HorizontalDivider 너비가 380dp로 고정되어 있습니다.

화면 폭이 380dp보다 좁은 기기에서는 divider가 오버플로우되고, 넓은 기기에서는 분할선이 끝까지 닿지 않습니다. fillMaxWidth()를 사용하여 부모 너비에 맞추는 것이 안전합니다.

♻️ 제안 수정
         HorizontalDivider(
             modifier = Modifier
                 .align(Alignment.BottomStart)
-                .width(380.dp),
+                .fillMaxWidth(),
             thickness = 1.dp,
             color = CallTheme.colors.gray100,
         )
🤖 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/CallHistoryList.kt`
around lines 150 - 156, Update the HorizontalDivider modifier in the
CallHistoryList layout to use fillMaxWidth() instead of the fixed width(380.dp),
allowing the divider to match its parent width on all screen sizes while
preserving its bottom-start alignment.
🤖 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 `@core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt`:
- Around line 48-56: Update ReservationDto.toDomain to parse scheduledAt through
TimeUtil.parseLocalDateTime instead of LocalDateTime.parse, matching
CallHistoryDto.toDomain and supporting the API’s non-ISO date formats.

In `@core/domain/src/main/java/kr/co/call/domain/model/home/CallReservation.kt`:
- Around line 14-16: Update ReservationStatus and the parsing flow in
CallHistoryMapper so unknown API status values do not reach a direct
ReservationStatus.valueOf(status) call. Add and use a safe fromString-style
parser that returns SCHEDULED when no enum entry matches, while preserving
normal parsing for known values.

---

Nitpick comments:
In
`@feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt`:
- Around line 150-156: Update the HorizontalDivider modifier in the
CallHistoryList layout to use fillMaxWidth() instead of the fixed width(380.dp),
allowing the divider to match its parent width on all screen sizes while
preserving its bottom-start alignment.
🪄 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: 22ace0f7-8f85-45e5-8aa0-ee8047299c16

📥 Commits

Reviewing files that changed from the base of the PR and between 1ba4013 and 5ce2bb1.

📒 Files selected for processing (23)
  • core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt
  • core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt
  • core/domain/src/main/java/kr/co/call/domain/model/home/CallHistory.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/CallReservations.kt
  • core/domain/src/main/java/kr/co/call/domain/model/home/HomeSummary.kt
  • core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/header/RelationshipSummaryCard.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/mapper/CallHistoryUiMapper.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/mapper/HomeUiMapper.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/mock/CallMockData.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeMockData.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CallHistoryIconType.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CallHistoryUiModel.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeReservationUiModel.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeSummaryUiModel.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt
🚧 Files skipped from review as they are similar to previous changes (5)
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/state/HomeState.kt
  • core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeHeader.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt

Comment on lines +48 to +56
private fun ReservationDto.toDomain(): CallReservation =
CallReservation(
id = callReservationId,
characterId = characterId,
firstName = firstName,
imageUrl = imageUrl,
scheduledAt = LocalDateTime.parse(scheduledAt),
status = ReservationStatus.valueOf(status),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

날짜 파싱 방식이 CallHistoryDto와 일치하지 않습니다.

CallHistoryDto.toDomain()TimeUtil.parseLocalDateTime(startedAt)를 사용하지만, ReservationDto.toDomain()LocalDateTime.parse(scheduledAt)를 직접 호출합니다. API가 ISO-8601 외 포맷(예: "2026-06-30 21:00:00")을 반환하면 예약 매핑에서 DateTimeParseException이 발생합니다. TimeUtil.parseLocalDateTime을 일관되게 사용하세요.

🔧 제안 수정
-        scheduledAt = LocalDateTime.parse(scheduledAt),
+        scheduledAt = TimeUtil.parseLocalDateTime(scheduledAt),
📝 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.

Suggested change
private fun ReservationDto.toDomain(): CallReservation =
CallReservation(
id = callReservationId,
characterId = characterId,
firstName = firstName,
imageUrl = imageUrl,
scheduledAt = LocalDateTime.parse(scheduledAt),
status = ReservationStatus.valueOf(status),
)
private fun ReservationDto.toDomain(): CallReservation =
CallReservation(
id = callReservationId,
characterId = characterId,
firstName = firstName,
imageUrl = imageUrl,
scheduledAt = TimeUtil.parseLocalDateTime(scheduledAt),
status = ReservationStatus.valueOf(status),
)
🤖 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/CallHistoryMapper.kt` around
lines 48 - 56, Update ReservationDto.toDomain to parse scheduledAt through
TimeUtil.parseLocalDateTime instead of LocalDateTime.parse, matching
CallHistoryDto.toDomain and supporting the API’s non-ISO date formats.

Comment thread core/domain/src/main/java/kr/co/call/domain/model/home/CallReservation.kt Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImage.kt (1)

28-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Boolean 매개변수명을 Kotlin 컨벤션에 맞추세요.

showCallBadge는 Boolean인데 is/has/should 접두사를 사용하지 않습니다. isCallBadgeVisible 또는 shouldShowCallBadge로 변경하고 호출부도 함께 갱신하세요.

As per path instructions, Boolean 변수/함수는 is/has/should 접두사를 사용해야 합니다.

🤖 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/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImage.kt`
around lines 28 - 31, Rename the Boolean parameter showCallBadge in
ProfileImageWithIcon to use an is/has/should prefix, such as isCallBadgeVisible
or shouldShowCallBadge, and update every call site and internal reference
accordingly.

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
`@feature/home/impl/src/main/java/kr/co/call/impl/component/CharacterChangeDialog.kt`:
- Around line 6-11: CharacterChangeDialog is currently an empty composable, so
implement its character-option list, selection state, and close/confirm event
handling so the character-change flow is usable. Expose callbacks and state
through the CharacterChangeDialog API as needed, and render the dialog UI using
the provided modifier. If the flow cannot be implemented yet, remove or restrict
the callable API instead of leaving an empty public composable.

In
`@feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt`:
- Around line 52-56: The CallHistoryList and NotificationList click callbacks
are not consistently connected to real parent actions. In
feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt
lines 52-56, make onRecordClick required and connect it to the parent’s
detail-navigation or event-handling flow; in
feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt
lines 64-74, replace the empty onCallClick lambda with a callback that forwards
the notification identifier to the parent re-call action.

---

Nitpick comments:
In
`@core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImage.kt`:
- Around line 28-31: Rename the Boolean parameter showCallBadge in
ProfileImageWithIcon to use an is/has/should prefix, such as isCallBadgeVisible
or shouldShowCallBadge, and update every call site and internal reference
accordingly.
🪄 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: ca84cb1b-4dbc-40a6-a95c-9bc77e5e7d4a

📥 Commits

Reviewing files that changed from the base of the PR and between 5ce2bb1 and 82794d2.

📒 Files selected for processing (12)
  • core/designsystem/build.gradle.kts
  • core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImage.kt
  • core/domain/src/main/java/kr/co/call/domain/model/home/HomeOverview.kt
  • feature/home/impl/build.gradle.kts
  • feature/home/impl/src/main/java/kr/co/call/impl/component/CharacterChangeDialog.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/mock/CharacterMockData.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/CharacterOptionUiModel.kt
💤 Files with no reviewable changes (2)
  • feature/home/impl/build.gradle.kts
  • feature/home/impl/src/main/java/kr/co/call/impl/component/history/HomeHistorySection.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • core/designsystem/build.gradle.kts
  • feature/home/impl/src/main/java/kr/co/call/impl/component/header/HomeReservationCard.kt
  • feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeScreen.kt

Comment on lines +6 to +11
@Composable
fun CharacterChangeDialog(
modifier: Modifier = Modifier,
){

} No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

다이얼로그 본문과 이벤트를 구현하세요.

현재 CharacterChangeDialog는 호출되어도 아무 UI도 그리지 않아 캐릭터 변경 흐름이 동작하지 않습니다. 옵션 목록, 선택 상태, 닫기·확정 이벤트를 구현하거나, 구현 전이라면 호출 가능한 API로 노출하지 않아야 합니다.

🧰 Tools
🪛 detekt (1.23.8)

[warning] 9-11: This empty block of code can be removed.

(detekt.empty-blocks.EmptyFunctionBlock)

🤖 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/CharacterChangeDialog.kt`
around lines 6 - 11, CharacterChangeDialog is currently an empty composable, so
implement its character-option list, selection state, and close/confirm event
handling so the character-change flow is usable. Expose callbacks and state
through the CharacterChangeDialog API as needed, and render the dialog UI using
the provided modifier. If the flow cannot be implemented yet, remove or restrict
the callable API instead of leaving an empty public composable.

Source: Linters/SAST tools

Comment on lines +52 to +56
histories.forEach { history ->
CallHistoryCard(
callHistory = history,
onRecordClick = { onRecordClick(history.callId) },
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

사용자 액션 콜백을 실제 이벤트에 연결하세요.

현재 기록 보기와 재통화 UI가 모두 클릭 가능하게 보이지만 상위 호출부에서 콜백을 전달하지 않거나 빈 람다를 사용해 실제 동작이 없습니다.

  • feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt#L52-L56: onRecordClick을 필수 콜백으로 만들고 실제 상세 화면 이동 또는 이벤트 처리까지 연결하세요.
  • feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt#L64-L74: onCallClick = {}를 제거하고 알림 식별자를 상위 재통화 이벤트로 전달하세요.
📍 Affects 2 files
  • feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt#L52-L56 (this comment)
  • feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt#L64-L74
🤖 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/CallHistoryList.kt`
around lines 52 - 56, The CallHistoryList and NotificationList click callbacks
are not consistently connected to real parent actions. In
feature/home/impl/src/main/java/kr/co/call/impl/component/history/CallHistoryList.kt
lines 52-56, make onRecordClick required and connect it to the parent’s
detail-navigation or event-handling flow; in
feature/home/impl/src/main/java/kr/co/call/impl/component/history/NotificationList.kt
lines 64-74, replace the empty onCallClick lambda with a callback that forwards
the notification identifier to the parent re-call action.

# Conflicts:
#	core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt
@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요, 시니어 안드로이드 개발자입니다. 새로운 Home Feature PR 잘 봤습니다. 전체적으로 클린 아키텍처 원칙을 잘 지키려 노력했고, Compose 컴포넌트 분리도 깔끔하게 진행하신 점이 인상 깊습니다. 몇 가지 개선 사항과 피드백을 공유 드립니다.


🔍 코드 리뷰

1. Kotlin & General

  • core/common/src/main/java/kr/co/call/core/common/util/TimeUtil.kt (New File)

    • TimeUtil 클래스가 추가되어 날짜 및 시간 포맷팅 로직을 한 곳에 모아 관리하는 점이 좋습니다. 재사용성과 유지보수성이 향상될 것입니다.
    • DateTimeFormatter.ofPattern에 사용된 패턴 문자열("M월 d일 a h시 m분", "M월 d일 EEEE")은 하드코딩된 문자열로 볼 수 있습니다. 현재는 한국어 로케일 고정이라 문제가 없지만, 다국어 지원을 고려한다면 strings.xml에 정의하고 가져다 쓰는 것을 고려해볼 수 있습니다. 다만, TimeUtil의 목적 자체가 특정 포맷팅을 제공하는 것이므로, 이 정도는 허용 가능하다고 판단됩니다.
  • core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt (New File)

    • 에러 핸들링 개선 (error() 사용 지양): toIsOutgoing()toIsMissed() 함수에서 서버 응답(sender, status)이 예상 범위를 벗어날 경우 error()를 호출하여 런타임에 앱이 크래시되도록 되어 있습니다. 프로덕션 환경에서는 예상치 못한 서버 응답으로 인해 앱이 크래시되는 것은 치명적입니다.
      • 개선 제안:
        1. Repository 레이어에서 Result 래핑: Mapper에서 예외를 발생시키기보다, Repository에서 네트워크 응답 전체를 Result로 래핑하고, Mapper 단계에서 실패 처리 로직을 포함하는 것이 더 견고합니다. (예: Result.failure(UnknownCallSenderException()))
        2. 안전한 기본값 또는 null 반환: 도메인 모델에 isOutgoing: Boolean? 또는 status: CallStatus (enum에 UNKNOWN 추가)와 같이 안전하게 처리할 수 있는 타입을 사용하고, Mapper에서 알 수 없는 값일 경우 null을 반환하거나 UNKNOWN 상태로 매핑하는 것을 고려해보세요.
        3. 로그 기록: 최소한 Timber.e("Unsupported call sender: $this") 와 같이 로그를 남겨 디버깅에 도움이 되도록 하는 것이 좋습니다.
  • core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt (New File)

    • 현재는 더미 데이터를 반환하고 있지만, Result.success로 감싸서 반환하는 패턴은 좋습니다. 실제 네트워크 호출 시 try-catch를 통해 Result.success 또는 Result.failure를 적절히 반환하도록 구현해야 합니다.
    • private companion object { const val CALL_HISTORY_SIZE = 20 } 와 같이 상수 정의를 잘 해주셨습니다.
  • Hilt DI: @Binds@Singleton

    • RepositoryModule에서 HomeRepository 바인딩 시 abstract fun bindHomeRepository(homeRepositoryImpl: HomeRepositoryImpl): HomeRepository 와 같이 @Binds를 사용하고, @Singleton 스코프를 명시하여 싱글톤 인스턴스를 보장하는 것은 올바른 Hilt 사용법입니다.
  • Data Class (val 사용):

    • 모든 data class에서 프로퍼티를 val로 선언하여 불변성을 유지하고 있는 점 아주 좋습니다.
  • gradle.xml, misc.xml

    • 파일 끝에 \ No newline at end of file 경고가 있는데, 이는 파일 마지막에 개행 문자가 없음을 의미합니다. 보통 IDE나 린트 도구에서 경고를 띄울 수 있으므로, 파일 마지막에 빈 줄을 하나 추가하여 모든 파일이 개행 문자로 끝나도록 하는 것이 컨벤션에 맞습니다. (이는 아주 사소한 부분이지만, 자동화된 도구들이 체크하는 항목입니다.)

2. Jetpack Compose

  • app/src/main/java/kr/co/call/callfromai/AppScreen.kt

    • Box -> Scaffold 변경: Box 대신 Scaffold를 사용하여 앱의 기본 레이아웃(TopBar, BottomBar, Snackbar, FloatingActionButton 등)을 쉽게 구성할 수 있도록 변경한 점 매우 좋습니다. Scaffold가 제공하는 padding 값을 NavDisplay에 전달하여 시스템 UI (BottomBar)와 콘텐츠가 겹치지 않도록 처리한 것도 올바른 방법입니다.
    • WindowInsets 사용: contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal)를 사용하여 시스템 바 인셋을 정확히 처리한 점도 모범적입니다.
  • core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImage.kt (New File)

    • 재사용성 높은 컴포넌트: ProfileImageWithIcon 컴포넌트를 디자인 시스템 모듈에 추가하여 프로필 이미지와 통화 배지를 함께 표시하는 기능을 재사용 가능하게 만든 점 좋습니다.
    • Coil 이미지 로딩: AsyncImage를 사용하여 이미지 로딩과 캐싱을 효율적으로 처리하고, placeholder, error, fallback을 통해 로딩 및 에러 상태에 대한 UI를 명확하게 제공하는 것도 좋습니다.
    • profileImageUrl?.takeIf { it.isNotBlank() }: null 또는 빈 문자열일 경우 안전하게 처리하는 로직 좋습니다.
  • Composable 함수의 Recomposition:

    • 제공된 Compose 코드만으로는 불필요한 recomposition 유발 여부를 명확히 판단하기 어렵지만, 컴포넌트들이 val을 사용하는 data class 형태의 UI 모델을 파라미터로 받고 있어 Stable 또는 Immutable 객체를 전달하는 원칙을 따르고 있을 것으로 예상됩니다.
    • CallHistoryListforEach vs LazyColumn:
      • CallHistoryList에서 histories.forEach { ... }를 사용하여 통화 기록을 렌더링하고 있습니다. PR 설명에 "최근 20건의 기록만 표시됩니다."라고 명시되어 있어 현재로서는 큰 성능 문제가 없을 수 있습니다.
      • 하지만 feature/home/impl/build.gradle.ktsandroidx.paging.compose 의존성이 추가된 것을 보아, 향후 Paging 라이브러리를 사용하거나 목록이 확장될 가능성이 있습니다. 이때는 LazyColumn을 사용하여 스크롤 성능을 최적화하는 것이 필수적입니다.
      • 개선 제안: 지금 당장 변경하지 않더라도, 20건을 초과하는 목록을 다룰 때는 LazyColumn으로 리팩터링할 계획을 염두에 두시거나, 미리 LazyColumn으로 구현하고 items(histories)를 사용하는 것을 고려해 보세요. key 파라미터를 사용하여 각 항목의 고유성을 보장하는 것도 중요합니다 (callId 사용).
  • State hoisting:

    • HomeHeader, HomeReservationCard, CallHistoryList 등 주요 컴포넌트들이 UiModel과 이벤트 람다 (onRecordClick, onTimeChangeClick)를 파라미터로 받고 있어 State Hoisting 원칙을 잘 따르고 있습니다. UI 로직과 상태 관리를 분리하려는 노력이 돋보입니다.
  • UI 상태를 data class로 표현:

    • HomeSummaryUiModel, HomeReservationUiModel, CallHistoryUiModel 등 UI 레이어에서 사용할 모델을 별도의 data class로 정의하고 있는 점 좋습니다. 이는 ViewModel에서 UI 상태를 관리하는 데 매우 효과적입니다. CallHistoryIconType과 같은 enum class를 사용하여 UI의 다양한 상태를 표현하는 것도 좋은 패턴입니다.
  • 하드코딩된 문자열 (Compose UI):

    • HomeHeader, HomeReservationCard, RelationshipSummaryCard, CallHistoryList 등 여러 Composable에서 사용자에게 보여지는 텍스트(예: "메인 연인 교체하기", "님, 반가워요! 👋🏻", "시간 변경", "관계", "통화 기록 보기")가 직접 하드코딩되어 있습니다.
    • 개선 제안: 모든 사용자 대상 문자열은 app/src/main/res/values/strings.xml에 정의하고 stringResource(id = R.string.my_string) 함수를 사용하여 가져오도록 변경해주세요. 이는 다국어 지원, 재사용성, 그리고 코드 가독성 측면에서 필수적인 좋은 관례입니다.
  • 매직 넘버 (Compose UI):

    • 패딩, 사이즈, 오프셋 등 dp 단위의 매직 넘버가 많이 사용되고 있습니다. (예: height(312.dp), padding(top = 7.dp, end = 3.dp), size(width = 93.dp, height = 24.dp)).
    • 개선 제안: 반복적으로 사용되거나 의미 있는 값을 가지는 dp 값들은 Dimens.kt와 같은 별도의 파일에 상수로 정의하거나, Material Theme의 spacing, size 시스템을 확장하여 사용하는 것을 고려해볼 수 있습니다. 이는 일관된 디자인을 유지하고 추후 디자인 시스템 변경에 유연하게 대응하는 데 도움이 됩니다.

3. Repository/DataSource 레이어

  • 네트워크 응답과 도메인 모델 매핑 분리:

    • core/network/dto에 DTO를 정의하고, core/data/mapper/CallHistoryMapper.kt에서 toDomain() 확장 함수를 통해 도메인 모델로 매핑하는 구조는 클린 아키텍처 원칙을 잘 따르고 있습니다. 각 레이어의 책임이 명확하게 분리되어 있습니다.
  • Retrofit 에러 핸들링:

    • HomeApi 인터페이스는 suspend 함수를 정의하여 Retrofit이 코루틴과 함께 작동하도록 준비되어 있습니다. 이는 좋은 시작입니다.
    • 앞서 언급했듯이 HomeRepositoryImpl의 더미 데이터가 아닌 실제 HomeApi를 사용할 경우, 네트워크 호출 시 발생할 수 있는 IOException (네트워크 연결 문제), HttpException (HTTP 오류 코드) 등의 예외를 try-catch 블록으로 안전하게 처리하고, 이 결과를 Result.success 또는 Result.failure로 래핑하여 상위 레이어(Use Case 또는 ViewModel)로 전달해야 합니다. 현재 PR에는 이 로직이 없으므로, 실제 구현 시 반드시 반영해야 할 부분입니다.

4. ViewModel (리뷰 불가)

  • 제공된 diff에는 ViewModel 코드가 포함되어 있지 않아 리뷰할 수 없습니다.

5. FCM/SSE/실시간 통신 (해당 없음)

  • 해당 PR에는 FCM/SSE/실시간 통신 관련 코드가 포함되어 있지 않아 리뷰할 수 없습니다.

✅ 총평 및 다음 단계 제안

새로운 Home 기능의 도입은 전체적인 아키텍처 원칙을 잘 지키며 진행되었고, 특히 ScaffoldWindowInsets를 활용한 AppScreen 리팩토링 및 ProfileImageWithIcon과 같은 재사용 가능한 Compose 컴포넌트 개발은 아주 좋은 접근 방식입니다.

몇 가지 주요 개선 사항은 다음과 같습니다:

  1. 하드코딩된 문자열을 strings.xml로 이동하여 다국어 지원 및 유지보수성을 확보해 주세요.
  2. CallHistoryMappererror() 호출 대신 더 견고한 에러 핸들링 전략을 적용하여 앱 크래시를 방지해 주세요.
  3. HomeRepositoryImpl실제 네트워크 에러 핸들링 (try-catch, Result 래핑) 로직을 구현할 때 이를 반영해 주세요.
  4. CallHistoryListforEach 대신 LazyColumn 사용을 고려하여 목록 성능을 최적화할 준비를 해주세요.
  5. gradle.xml, misc.xml 파일 마지막에 개행 문자를 추가해 주세요.

이 피드백들이 PR을 더 완벽하게 만드는 데 도움이 되기를 바랍니다. 궁금한 점이 있으면 언제든지 질문해 주세요. 계속해서 좋은 코드 기대하겠습니다!

HomeViewModel이 HomeRepository를 직접 호출하지 않고 HomeUseCase를 경유하도록 변경.
develop에서 병합된 characterId/ReservationStatus 필드는 사용하지 않아 제거.

@KateteDeveloper KateteDeveloper left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

지나가는 나그네 입니다....
KDOC 도입을 적극적으로 도입해보면 더욱 컨벤션에 도움이 될 것 같아요 😊
너무 잘하는데요 LGTM👍

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale

@KateteDeveloper KateteDeveloper Jul 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

지나가는 안드 나그네 입니다...!
이건 core common에서 공통적으로 사용하는 유틸이기에 KDOC로 주석 달면 편하게 볼 수 있을 것 같아요:)

https://velog.io/@dudgns0507/Kotlin-KDoc%EC%9C%BC%EB%A1%9C-%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%93%9C-%EB%AC%B8%EC%84%9C%ED%99%94%ED%95%98%EA%B8%B0-feat.-Dokka

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@codebidoof 잠깐만 지나가겠습니다....

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ㄱㅊ아영

@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요! 시니어 Android 개발자입니다. 이번 PR에서 Home 기능을 새롭게 추가하신 것으로 보입니다. 전반적으로 최신 Android 개발 트렌드와 모범 사례를 잘 적용하려고 노력한 점이 인상 깊습니다. 특히 Jetpack Compose, Orbit MVI, 클린 아키텍처 원칙을 잘 따르고 있습니다. 몇 가지 개선점에 대한 피드백을 드리겠습니다.


PR 요약

새로운 Home 기능 개발을 위해 여러 모듈에 걸쳐 코드와 리소스를 추가했습니다.

  • core:common: TimeUtil 유틸리티 클래스 추가.
  • core:data: HomeRepository 및 관련 DI 설정, DTO-Domain 매핑 로직 추가. 현재는 Mock 데이터 사용.
  • core:designsystem: 프로필 이미지 컴포넌트 (ProfileImageWithIcon) 및 Home 관련 아이콘/이미지 리소스 추가.
  • core:domain: Home 관련 도메인 모델 (CallHistory, CallReservation, HomeSummary, HomeOverview) 및 HomeRepository 인터페이스, HomeUseCase 추가.
  • core:network: HomeApi 인터페이스 및 관련 DTO 모델 추가.
  • feature:home:impl: HomeScreen, HomeViewModel 및 UI 컴포넌트 (Header, ReservationCard, CallHistoryList 등) 구현. Orbit MVI 패턴과 LoadStatus를 사용하여 UI 상태를 관리.
  • app: AppScreenScaffold를 도입하여 BottomBar 및 WindowInsets 처리 개선.
  • .idea: feature/main 모듈 관련 설정 제거.

전반적인 피드백

이 PR은 Home 기능을 성공적으로 통합하고 있으며, 특히 Jetpack Compose의 UI 상태 관리 (LoadStatus, UI Model), Orbit MVI 패턴, java.time API 활용, 그리고 DTO와 Domain 모델 간의 명확한 분리에 있어 모범적인 사례를 보여줍니다. 아키텍처 설계와 구현 모두 높은 수준입니다.

주요 개선점은 다음과 같습니다:

  1. 하드코딩된 문자열: 사용자에게 노출되는 대부분의 텍스트가 Compose 코드 내에 하드코딩되어 있습니다. 국제화(i18n) 및 유지보수를 위해 String Resource로 분리해야 합니다.
  2. 데이터 레이어의 오류 처리: DTO 매핑 시 error() 사용 및 HomeUseCasegetOrThrow() 사용은 프로덕션 환경에서 앱 크래시로 이어질 수 있으므로, 더 견고한 오류 처리 전략 (예: Result 반환, 커스텀 예외)이 필요합니다.
  3. UI 컴포넌트의 유연성/재사용성: 일부 디자인 시스템 컴포넌트의 하드코딩된 크기가 특정 디자인에 종속적일 수 있습니다.

세부 코드 리뷰

1. Kotlin 코드

  • core/common/util/TimeUtil.kt
    • object로 선언하여 유틸리티 클래스로 사용한 점이 적절합니다.
    • java.time 패키지를 사용하여 최신 Date/Time API를 활용한 점이 매우 좋습니다.
    • DateTimeFormatterLocale.KOREAN을 명시적으로 사용한 점도 훌륭합니다.
    • toTimeAgoTextcoerceAtLeast(0) 사용은 음수 값 방지를 위한 좋은 방어 코드입니다.
  • core/data/di/RepositoryModule.kt, UseCaseModule.kt
    • Hilt DI 컨벤션을 잘 준수하고 있습니다. HomeRepository@Binds로, HomeUseCase@Provides로 바인딩한 점이 올바릅니다. @Singleton 스코프 지정도 적절합니다.
  • core/data/mapper/CallHistoryMapper.kt
    • Outstanding (네트워크 응답과 도메인 모델 매핑 분리): DTO를 도메인 모델로 매핑하는 toDomain() 확장 함수들을 별도로 구현하여 네트워크 레이어와 도메인 레이어의 명확한 분리를 보여줍니다. 매우 좋은 접근 방식입니다.
    • toIsOutgoing(), toIsMissed()와 같은 확장 함수로 로직을 캡슐화한 점도 깔끔합니다.
    • Concern (오류 처리): toIsOutgoing(), toIsMissed() 함수에서 else -> error("지원하지 않는 ...") 구문을 사용하고 있습니다. 이는 런타임에 예기치 않은 서버 응답이 올 경우 앱을 강제 종료시키는 심각한 문제로 이어집니다.
      • Recommendation: error() 대신 Result.failure()를 반환하거나, 커스텀 예외를 던져 상위 레이어에서 처리하도록 유도해야 합니다. 예를 들어, Result<Boolean>을 반환하고 매핑 실패 시 Result.failure(IllegalArgumentException("Unsupported sender: $this"))와 같이 처리할 수 있습니다.
  • core/data/repositoryImpl/HomeRepositoryImpl.kt
    • 현재 Mock 데이터를 사용하고 있지만, Result.success()로 결과를 래핑하여 Repository 인터페이스의 반환 타입(Result<T>)을 준수한 점이 좋습니다.
    • Note (Retrofit 에러 핸들링): 실제 HomeApi와 연동될 때 try-catch 블록을 사용하여 HttpException, IOException 등을 처리하고 Result.failure()를 반환하는 로직이 추가되어야 합니다.
    • CALL_HISTORY_SIZE 상수를 사용한 점이 좋습니다.
  • core/domain/model/home/*.kt
    • data class에 모든 속성을 val로 선언하여 불변(immutable) 도메인 모델을 구현한 점이 매우 좋습니다. 불필요한 var 사용이 없습니다.
    • imageUrl: String?와 같이 null 안전성을 잘 고려했습니다.
  • core/domain/usecase/home/HomeUseCase.kt
    • operator fun invoke() 패턴을 사용하여 UseCase를 명확하게 정의한 점이 좋습니다.
    • Concern (오류 처리): getOrThrow()를 사용하여 HomeRepositoryResult 값을 언래핑하고 있습니다. 이는 CallHistoryMapper.kterror()와 마찬가지로, Repository에서 오류가 발생하면 UseCase 레벨에서 예외를 발생시켜 앱이 크래시될 수 있습니다.
      • Recommendation: HomeUseCaseResult<HomeOverview>를 반환하도록 변경하여 UseCase를 호출하는 ViewModel에서 모든 오류를 통합적으로 처리할 수 있도록 하는 것이 더 견고한 접근 방식입니다. 이렇게 하면 UI에서 로딩, 성공, 특정 오류(예: 네트워크 오류, 데이터 파싱 오류)를 구분하여 사용자에게 더 상세한 피드백을 제공할 수 있습니다.
  • feature/home/impl/viewmodel/HomeViewModel.kt
    • Outstanding (UI 상태와 비즈니스 로직 분리, Orbit ContainerHost 패턴, intent 내부에서 상태 변경만 수행, postSideEffect 적절성): Orbit MVI ContainerHost 패턴을 완벽하게 준수하고 있습니다. intent 블록에서 사용자 이벤트를 처리하고, reduce에서만 UI 상태(HomeUiState)를 변경하며, postSideEffect를 통해 토스트 메시지 같은 일회성 UI 효과를 전달하는 방식이 매우 모범적입니다.
    • viewModelScope 내에서 비동기 작업을 runCatching으로 래핑하여 오류를 안전하게 처리하고 UI 상태(LoadStatus.Error) 및 postSideEffect로 전달하는 로직이 훌륭합니다.
    • homeUseCase()homeRepository.changeReservationTime() 호출 시 직접적인 blocking 작업을 수행하지 않고 suspend 함수에 위임한 점도 좋습니다.
  • feature/home/impl/viewmodel/model/HomeUiState.kt
    • Outstanding (UI 상태를 data class로 표현, 로딩/에러 상태를 LoadStatus로 관리): data class를 사용하여 UI 상태를 명확하게 정의하고 있으며, LoadStatus sealed class를 통해 로딩, 성공, 오류 상태를 일관되게 관리하는 점은 Jetpack Compose 앱 개발의 핵심 모범 사례입니다. 매우 잘하셨습니다.
    • fromDomain 확장 함수를 사용하여 도메인 모델을 UI 모델로 매핑하는 로직이 깔끔하게 분리되어 있습니다.
    • HomeSideEffect sealed interface도 일회성 이벤트를 잘 표현합니다.
    • Minor (하드코딩된 문자열): HomeOverviewUiModel.fromDomain에서 "${totalCount}건"과 같이 '건'이라는 단위가 하드코딩되어 있습니다. 이는 strings.xml로 분리하여 다국어 지원을 용이하게 할 수 있습니다.

2. Jetpack Compose 코드

  • app/src/main/java/kr/co/call/callfromai/AppScreen.kt
    • Outstanding (Scaffold 및 WindowInsets 사용): Scaffold 컴포넌트를 사용하여 MainBottomBarbottomBar 슬롯으로 이동시키고, contentWindowInsetspadding modifier를 NavDisplay에 올바르게 적용하여 시스템 UI 및 BottomBar의 높이를 고려한 레이아웃 처리를 개선한 점이 훌륭합니다. Jetpack Compose에서 BottomBar를 처리하는 가장 모범적인 방법입니다.
  • core/designsystem/component/ProfileImage.kt
    • Coil AsyncImage를 사용하여 이미지 로딩을 처리하고 placeholder, error, fallback 리소스를 지정하여 다양한 로딩 상태에 대응한 점이 좋습니다.
    • contentDescription을 지정하여 접근성(Accessibility)을 고려한 점도 긍정적입니다.
    • showCallBadgezIndex를 활용한 레이어링도 깔끔합니다.
    • Note (디자인 시스템 유연성): 49.dp, 51.dp 등 이미지 크기나 25.dp RoundedCornerShape 같은 값들이 하드코딩되어 있습니다. 특정 디자인에 고정된 컴포넌트라면 괜찮지만, 만약 다양한 크기로 재사용될 가능성이 있다면 파라미터로 받거나 디자인 시스템 내의 Dimensions 객체 등을 통해 관리하는 것을 고려해볼 수 있습니다.
  • feature/home/impl/component/UnreadIndicator.kt
    • 간단한 상태 없는 Composable로 잘 구현되었습니다.
  • feature/home/impl/component/header/HomeHeader.kt
    • HomeSummaryUiModel을 사용하여 UI 상태를 전달받는 것이 적절합니다.
    • buildAnnotatedString을 사용하여 텍스트에 스타일을 부분적으로 적용한 점이 좋습니다.
    • Concern (하드코딩된 문자열/매직 넘버): '메인 연인 교체하기', '님, 반가워요! 👋🏻', '오늘은 어떤 이야기를 나누게 될까요?', '알림', '전화하기' 등 대부분의 사용자에게 노출되는 텍스트가 Compose 코드 내에 하드코딩되어 있습니다.
      • Recommendation: 이 모든 문자열을 strings.xml 리소스 파일로 이동시키고 stringResource() 함수를 사용하여 참조해야 합니다. 이는 국제화 및 유지보수를 위해 필수적입니다.
    • Note (디자인 시스템 유연성): 312.dp, 265.dp와 같은 컴포넌트의 고정 높이, 패딩 값 등 많은 UI 관련 숫자들이 하드코딩되어 있습니다. 디자인 시스템의 일관성을 유지하고 추후 변경 시 용이성을 위해 designsystem 모듈 내의 Dimens 객체 등을 통해 관리하는 것을 고려해볼 수 있습니다.
  • feature/home/impl/component/header/HomeReservationCard.kt
    • HomeReservationUiModel로 UI 상태를 전달받아 조건부 렌더링을 수행하는 점이 좋습니다.
    • TimeUtil을 사용하여 날짜/시간을 포맷팅하는 로직이 적절합니다.
    • Concern (하드코딩된 문자열): '님과 [시간] 전화가 예정되어 있어요', '시간 변경', '약속 1건' 등의 텍스트가 하드코딩되어 있습니다. HomeHeader와 마찬가지로 String Resource로 분리해야 합니다.
    • LocalMinimumInteractiveComponentSize를 import 했지만 Button에 명시적으로 적용하지는 않은 것으로 보입니다. contentPadding = PaddingValues(0.dp)를 사용했는데, Material Design의 최소 터치 타겟 가이드라인(48x48dp)을 준수하도록 유의해야 합니다. 패딩이 0이어도 내부적으로 최소 터치 타겟은 유지될 수 있지만, 디자인 팀과 논의하여 의도된 동작인지 확인하는 것이 좋습니다.
  • feature/home/impl/component/header/RelationshipSummaryCard.kt
    • HomeSummaryUiModel을 사용하여 UI 상태를 전달받는 점이 좋습니다.
    • Concern (하드코딩된 문자열): '관계', '통화', '연속 통화' 등의 텍스트가 하드코딩되어 있습니다. String Resource로 분리해야 합니다.
  • feature/home/impl/component/history/CallHistoryList.kt
    • Concern (불필요한 recomposition 유발 여부 / 성능): Column 내에서 histories.forEach { ... CallHistoryCard(...) } 형태로 리스트를 렌더링하고 있습니다. CallHistoryList 컴포넌트의 상단에 "최근 20건의 기록만 표시됩니다."라는 문구가 있지만, 20개 정도의 고정된 리스트라도 Compose의 리스트 렌더링 최적화를 위해서는 LazyColumn을 사용하는 것이 권장됩니다. 특히 feature:home:impl/build.gradle.ktspaging.compose 의존성이 추가된 것을 보면, 향후 페이징 처리가 필요한 목록이 될 가능성이 있으므로 LazyColumn으로 미리 전환하는 것이 좋습니다.
      • Recommendation: LazyColumn을 사용하여 items(histories) { history -> ... } 형태로 변경해주세요.
    • Concern (하드코딩된 문자열): "최근 20건의 기록만 표시됩니다." 텍스트는 String Resource로 분리해야 합니다.
  • feature/home/impl/component/history/CallHistoryCard.kt
    • CallHistoryUiModel을 사용하여 UI 상태를 전달받는 점이 좋습니다.
    • when 문을 사용하여 CallHistoryIconType에 따라 적절한 아이콘을 표시하는 로직이 깔끔합니다.
    • maxLinesoverflow를 사용하여 텍스트가 너무 길어질 경우의 UI 처리를 고려한 점이 좋습니다.
    • Concern (하드코딩된 문자열): '통화 요약' 텍스트는 String Resource로 분리해야 합니다.
    • contentDescriptionnull로 설정한 부분이 있는데, 아이콘이 순수하게 장식적인 요소라면 괜찮지만, 정보 전달의 역할을 한다면 stringResource 등을 통해 적절한 설명을 제공해야 합니다.
  • feature/home/impl/view/HomeScreen.kt
    • Outstanding (State Hoisting, Coroutine 사용 시 Flow collect 시점, 로딩/에러 상태를 LoadStatus로 관리): viewModel.container.stateFlow.collectAsStateWithLifecycle()을 사용하여 StateFlow를 생명주기(lifecycle)에 안전하게 collect하고 있는 점이 매우 훌륭합니다. 이는 Compose에서 Flow를 사용하는 가장 좋은 방법입니다.
    • LoadStatus sealed class를 사용하여 UI 상태를 Loading, Success, Error로 명확하게 구분하고, when 문으로 각 상태에 맞는 UI를 렌더링하는 패턴이 모범적입니다.
    • LazyColumn을 사용하여 스크롤 가능한 내용을 효율적으로 렌더링하고 ScaffoldcontentPadding을 적절히 적용한 점도 좋습니다.
    • pullRefresh modifier가 적용되어 있습니다. isRefreshingonRefreshHomeViewModel의 데이터 로딩 로직과 연동하여 Pull-to-Refresh 기능을 완성해야 합니다.
    • Concern (Error UI 개선): LoadStatus.Error 상태일 때 단순히 Text("Error: ${it.message}")를 표시하는 것은 사용자 경험 측면에서 부족합니다.
      • Recommendation: 사용자 친화적인 오류 메시지, "재시도" 버튼, 또는 특정 오류 유형에 따른 다른 안내 (예: "네트워크 연결 확인") 등을 제공하는 UI를 구현하는 것이 좋습니다.

3. Repository/DataSource 레이어

  • Retrofit 에러 핸들링: HomeRepositoryImpl은 Mock 데이터를 사용하고 있으므로, 현재 Retrofit 에러 핸들링은 없습니다. 실제 API 연동 시 HomeApi 호출 시 발생할 수 있는 HttpException (HTTP 오류 코드) 및 IOException (네트워크 연결 문제) 등을 try-catch 블록으로 캡처하고 Result.failure()를 반환하는 로직을 추가해야 합니다.
  • 네트워크 응답과 도메인 모델 매핑 분리: CallHistoryMapper.kt에서 toDomain() 확장 함수들을 통해 DTO와 도메인 모델의 매핑을 명확하게 분리한 점은 매우 훌륭합니다.
  • 캐싱 전략: 현재 PR에서는 캐싱 전략이 보이지 않습니다. 만약 필요하다면 로컬 DB 또는 메모리 캐싱을 고려해야 합니다. 초기 단계에서는 불필요할 수 있지만, 요구사항에 따라 추후 고려 대상입니다.

4. ViewModel

  • 위의 Kotlin 코드 리뷰(HomeViewModel.kt)에서 자세히 다루었듯이, Orbit MVI ContainerHost 패턴 준수, UI 상태와 비즈니스 로직 분리, intent 내부에서 상태 변경(reduce)만 수행, postSideEffect 남용 지양, blocking 작업 회피의 모든 규칙을 훌륭하게 준수하고 있습니다. HomeViewModel은 이 PR의 모범적인 부분 중 하나입니다.

5. FCM/SSE/실시간 통신 관련 코드

  • 이 PR에는 FCM/SSE/실시간 통신 관련 코드가 포함되어 있지 않습니다.

정리 및 제안

  1. 하드코딩된 문자열 제거 (Critical): HomeHeader, HomeReservationCard, RelationshipSummaryCard, CallHistoryList, CallHistoryCard, HomeUiState 등 사용자에게 노출되는 모든 문자열을 strings.xml로 이동시켜 stringResource()를 사용하도록 수정해주세요.
  2. 데이터 레이어 오류 처리 개선 (High):
    • CallHistoryMapper.kt에서 error() 호출 대신 Result.failure() 반환 등의 방식으로 변경하여 런타임 크래시를 방지해주세요.
    • HomeUseCase.ktgetOrThrow() 대신 Result<HomeOverview>를 반환하도록 변경하여 ViewModel에서 통합적인 오류 처리가 가능하도록 해주세요.
  3. HomeRepositoryImpl 실제 API 연동 시 에러 핸들링 (High): 현재 Mock 데이터를 사용하는 부분을 실제 HomeApi 호출로 변경할 때 try-catchResult.failure()를 포함한 견고한 에러 핸들링 로직을 구현해야 합니다.
  4. CallHistoryList LazyColumn으로 변경 (Medium): forEach 대신 LazyColumn을 사용하여 리스트 렌더링 성능을 최적화하고, 향후 Paging 도입 시 유연하게 대응할 수 있도록 변경하는 것을 권장합니다.
  5. HomeApi size = 20 매직 넘버 상수화 (Low): getCallHistoriessize 쿼리 파라미터 기본값 20을 상수로 정의하여 의미를 명확히 해주세요.
  6. HomeReservationCard의 최소 터치 타겟 확인 (Low): contentPadding = PaddingValues(0.dp) 사용과 관련하여 최소 터치 타겟 가이드라인 준수 여부를 확인해주세요.
  7. Error UI 개선 (Medium): HomeScreen에서 LoadStatus.Error 시 표시되는 UI를 사용자 친화적으로 개선하는 것을 고려해주세요.

위 제안사항들을 반영하면 더욱 안정적이고 유지보수하기 쉬운 애플리케이션이 될 것입니다. 전반적으로 훌륭한 PR입니다. 계속해서 좋은 코드 기대합니다!

@codebidoof codebidoof left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

고생했어

import kr.co.call.domain.repository.HomeRepository

class HomeUseCase(
private val homeRepository: HomeRepository,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

이거 힐트로 주입시키는 게 좋을듯 모듈도 만들어놨으니

* - 전화 버튼과 관계 요약 카드 포함
*/
@Composable
internal fun HomeHeader(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

internal은 빼도될듯 짜피 피쳐모듈 서로모름

* - 약속이 있으면 상대 정보와 시간 변경 버튼 표시
*/
@Composable
internal fun HomeReservationCard(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

이것도

Comment on lines +17 to +19
val isLoading: Boolean
get() = loadStatus == LoadStatus.Loading
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

이거 따로 한 이유 있음? 필드로 같이 넣는 게 나을듯?

val title: String,
val content: String,
val isRead: Boolean,
val createdAt: LocalDateTime,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

화면에 보여질 상태라면 이 단계에서는 그냥 String인 게 나을 듯?

HomeNotificationState/NotificationType을 core:domain의 HomeNotification으로 이동하고,
UI 모델(HomeNotificationUiModel)과 매퍼를 추가해 시간 포맷팅을 매퍼로 옮김.
알림/통화기록 리스트를 HomeScreen의 단일 LazyColumn으로 통합.
ProfileImageWithIcon 함수명을 ProfileImage로 변경.
@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요, PR 올려주셔서 감사합니다. 새로운 홈 화면 기능 도입과 관련된 PR이네요. 전반적으로 코드 구조와 Compose 원칙을 잘 따르려 노력한 점이 인상 깊습니다. 몇 가지 개선 사항과 코멘트를 드리니 참고하여 반영해주시면 좋을 것 같습니다.


📝 전반적인 피드백

새로운 Home 모듈을 잘 분리하여 도입했고, core 모듈에 공통 로직(TimeUtil), 도메인 모델, 리포지토리 인터페이스, 네트워크 DTO 및 API 정의를 깔끔하게 분리한 점이 좋습니다. 또한 Jetpack Compose UI 코드에서도 상태 호이스팅과 UI 모델 분리를 잘 적용하고 있습니다.

다만, 몇 가지 개선이 필요한 부분이 있습니다. 특히 UI 상태 관리(로딩/에러)와 하드코딩된 문자열 처리, 그리고 API 에러 핸들링 전략에 대한 명확성이 필요합니다.


🔍 파일별 상세 리뷰

1. app/src/main/java/kr/co/call/callfromai/AppScreen.kt

  • Jetpack Compose (Scaffold 사용): BoxScaffold로 변경하고 bottomBar 슬롯에 MainBottomBar를 배치한 것은 Jetpack Compose의 권장 방식에 잘 맞습니다. contentWindowInsets를 사용하여 Safe Drawing 영역을 올바르게 처리하는 것도 좋습니다.
  • LocalBottomBarPadding을 사용하여 하단 바의 패딩을 전달하는 방식도 유연성을 확보하는 좋은 접근입니다.

2. core/common/src/main/java/kr/co/call/core/common/util/TimeUtil.kt (신규)

  • Kotlin (Date/Time Utility): java.time 패키지를 사용하여 날짜/시간 처리를 하는 것은 모던하고 좋습니다.
  • parseLocalDateTime(value: String): 현재 LocalDateTime.parse(value)를 사용하고 있는데, 백엔드에서 내려오는 날짜/시간 문자열 형식이 ISO 8601 표준을 따르는지 확인이 필요합니다. 만약 다른 특정 형식을 사용한다면, DateTimeFormatter.ofPattern("...")을 명시적으로 사용해주는 것이 안정적입니다. scheduledAt: String 필드가 네트워크 DTO에 있는 것을 보아, 이 부분의 안정성을 확보하는 것이 중요합니다.
  • 하드코딩된 문자열/매직 넘버:
    • 날짜 포맷 문자열("M월 d일 a h시 m분", "M월 d일 EEEE", "HH:mm")은 유틸리티의 핵심 기능이므로 이대로 두어도 무방합니다.
    • toReservationTimeText 내의 "오늘", "내일", "새벽", "아침", "낮", "저녁", "밤"과 같은 문자열은 지역화(Localization)가 필요한 경우 String Resource로 분리하는 것을 고려해볼 수 있습니다. 현재는 core:common 모듈에서 사용되는 유틸리티이므로, 공용 모듈의 성격을 고려하여 판단하면 되겠습니다. 시간대 매직 넘버(0..5, 6..11 등)는 시간대 구분이라는 컨텍스트에서 이해 가능하므로 이대로 두어도 괜찮습니다.

3. core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt

  • Hilt DI: HomeRepository 바인딩을 추가하고 @Singleton 스코프를 지정한 것은 올바른 Hilt 사용입니다.

4. core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt (신규)

  • Kotlin (Mapper): DTO를 도메인 모델로 매핑하는 책임을 분리한 것은 좋은 구조입니다.
  • sender.toIsOutgoing()status.toIsMissed() 확장 함수 내에서 알 수 없는 값이 들어왔을 때 error() 함수를 호출하고 있는데, 이는 앱 크래시로 이어질 수 있습니다.
    • 개선 방안: 네트워크 응답으로부터 받은 값을 도메인 모델로 변환하는 과정에서 유효하지 않은 데이터가 발견되면, 단순히 크래시를 발생시키기보다 다음 중 하나를 고려하는 것이 좋습니다.
      1. Result 래핑을 통해 에러를 상위 계층으로 전파하고, UI에서 사용자에게 적절한 메시지를 표시하거나 기본값으로 처리합니다.
      2. 특정 비즈니스 요구사항에 따라 기본값을 반환하거나, null을 반환하여 이후 처리 로직에서 유연하게 대응하도록 합니다.
      3. 만약 비정상적인 데이터가 절대 발생해서는 안 되는 상황이라면, IllegalStateException 등 특정 예외를 throw하여 문제 발생 시점에 명확히 인지하고 디버깅할 수 있도록 할 수 있습니다. 하지만 이 경우에도 최상위에서 예외를 catch하여 앱 전체가 죽는 것을 방지해야 합니다.
    • LocalDateTime.parse(scheduledAt)에 대해서도 TimeUtil.kt에서 언급한 것과 동일하게 DateTimeFormatter 명시를 고려해야 합니다.

5. core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt (신규)

  • Repository/DataSource: HomeRepository 인터페이스를 구현하고 있습니다. 현재는 하드코딩된 더미 데이터를 반환하고 있는데, 이는 초기 개발 단계에서 좋은 접근입니다.
  • Retrofit 에러 핸들링 (try-catch, Result 래핑): 실제 네트워크 호출이 연동될 때, 이 Repository 계층에서 Retrofit 호출을 try-catch로 감싸고 Result.success() 또는 Result.failure()로 래핑하여 반환하는 로직이 추가되어야 합니다. 이는 core/domainResult<T> 반환 계약을 준수하는 핵심 부분입니다.
  • CALL_HISTORY_SIZEprivate companion object로 둔 것은 좋은 상수 관리 방식입니다.

6. core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImage.kt (신규)

  • Jetpack Compose: AsyncImage (Coil)를 사용하여 이미지를 로드하고 placeholder, error, fallback을 모두 기본 이미지로 설정하여 안정적인 이미지 로딩을 구현한 점이 좋습니다.
  • model = profileImageUrl?.takeIf { it.isNotBlank() }와 같이 URL의 유효성을 체크하는 것도 좋은 관행입니다.
  • zIndex를 사용하여 배지 이미지를 올바르게 배치한 것도 훌륭합니다.
  • Preview도 잘 작성되어 있습니다.

7. core/domain/build.gradle.kts

  • javax.inject:javax.inject:1 추가: Hilt 생성자 주입을 위해 필요한 의존성으로, 올바르게 추가되었습니다.

8. core/domain/src/main/java/kr/co/call/domain/model/home/*.kt (신규)

  • Kotlin (Data Class): 모든 도메인 모델이 data class로 정의되었고, 모든 필드가 val로 선언되어 불변성을 유지하고 있습니다. LocalDateTime 사용도 적절합니다. null 안전성도 잘 지켜지고 있습니다. 매우 좋은 구현입니다.
  • NotificationType enum에 title 속성을 추가하여 활용성을 높인 것도 좋습니다.

9. core/domain/src/main/java/kr/co/call/domain/repository/HomeRepository.kt (신규)

  • 인터페이스로 Repository 계약을 명확히 정의하고, 모든 함수가 suspendResult<T>를 반환하도록 한 것은 좋은 설계입니다.

10. core/domain/src/main/java/kr/co/call/domain/usecase/home/HomeUseCase.kt (신규)

  • Kotlin (Use Case): HomeRepository를 생성자 주입받는 것은 Hilt 원칙에 부합합니다. operator fun invoke() 패턴도 좋습니다.
  • homeRepository.getSummary().getOrThrow(): 이 부분은 매우 위험합니다. 네트워크 호출 중 하나라도 실패하면 getOrThrow() 때문에 앱이 크래시될 수 있습니다.
    • 개선 방안: Use Case는 Repository에서 발생한 실패를 ViewModel로 Result 형태로 전달하거나, 특정 에러 도메인 모델로 래핑하여 반환하는 것이 더 안전합니다.
    • 예를 들어, Result<HomeOverview>를 반환하여 호출자(ViewModel)가 성공/실패를 명확하게 처리하도록 해야 합니다. HomeOverview가 여러 하위 데이터로 구성되어 있으므로, 전체가 실패했을 때의 동작 방식을 명확히 정의하는 것이 중요합니다.
    • 만약 HomeOverview의 각 컴포넌트(summary, reservations, callHistories)가 개별적으로 로딩/에러 상태를 가질 수 있다면, HomeOverview 자체의 데이터 클래스에 각 컴포넌트별 ResultLoadStatus를 포함시키는 것도 한 방법입니다.

11. core/network/src/main/java/kr/co/call/network/dto/*.kt (신규)

  • Kotlin (DTOs): 모든 DTO가 data classval로 정의되어 좋습니다.
  • scheduledAt: String: 이 필드의 형식에 대해 TimeUtilMapper에서 언급한 DateTimeFormatter 명시 여부를 다시 한번 확인해주세요.

12. feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeRoute.kt (신규)

  • Jetpack Compose: hiltViewModel()을 사용하여 ViewModel을 주입받고, viewModel.container.stateFlow.collectAsStateWithLifecycle()을 사용하여 UI 상태를 수집하는 것은 매우 훌륭합니다. Lifecycle-aware 방식으로 Flow를 수집하여 리소스 낭비를 방지합니다.
  • LaunchedEffect(Unit)를 사용하여 화면 진입 시 초기 데이터(fetchHomeOverview())를 가져오는 것도 적절한 사이드 이펙트 처리입니다.

13. feature/home/impl/src/main/java/kr/co/call/impl/screen/HomeContent.kt (신규)

  • Jetpack Compose (UI 상태 관리): 현재 uiState.loading: BooleanuiState.error: String?으로 로딩/에러 상태를 관리하고 있습니다.
    • 개선 방안 (리뷰 규칙 준수): "로딩/에러 상태를 Boolean 대신 LoadStatus로 관리하는지" 규칙에 따라 sealed class LoadStatus<out T> (또는 enum class)를 도입하여 Loading, Success(data: T), Error(message: String) 등으로 상태를 명확히 표현하는 것을 권장합니다.
    • HomeState 내에 LoadStatus<HomeOverview>와 같이 전체 화면의 상태를 관리하거나, 각 개별 데이터(summary, reservations, callHistories)마다 LoadStatus를 적용하는 것을 고려해주세요. 이렇게 하면 UI가 더 명확하게 각 상태에 따라 다른 컴포넌트를 렌더링할 수 있습니다.
    • 현재 uiState.homeSummary?.let { ... }처럼 null check를 하고 있는데, LoadStatus를 사용하면 when(uiState.summaryStatus) { is Success -> ... }와 같이 더 구조화된 방식으로 처리할 수 있습니다.

14. feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeState.kt (신규)

  • Jetpack Compose (UI 상태 모델): data class HomeState로 UI 상태를 표현한 것은 좋습니다.
  • 개선 방안 (리뷰 규칙 준수): loading: Boolean = falseerror: String? = null 대신 위에서 언급한 LoadStatus 패턴을 적용하여 UI 상태의 표현력을 높여주세요.

15. feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeUiModel.kt (신규)

  • Jetpack Compose (UI Model): UI에 직접적으로 필요한 데이터만 담은 UI 모델(HomeSummaryUiModel, HomeReservationUiModel, HomeCallHistoryUiModel)을 정의한 것은 좋은 추상화입니다. 모든 필드가 val인 것도 좋습니다.
  • HomeReservationUiModel.Empty와 같은 비어있는 상태를 위한 싱글톤 객체를 제공하는 것도 좋은 패턴입니다.
  • CallHistoryType enum을 사용한 것도 좋습니다.

16. feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/HomeViewModel.kt (신규)

  • ViewModel (Orbit ContainerHost): Orbit의 ContainerHost 패턴을 사용하여 intent { reduce { ... } } 구조를 잘 따른 점이 좋습니다. HomeUseCase를 생성자 주입받은 것도 올바릅니다.
  • fetchHomeOverview() intent:
    • viewModelScope.launch { ... }: 코루틴 사용과 스코프 지정은 적절합니다.
    • reduce { state.copy(loading = true) }: 로딩 상태 업데이트는 reduce 블록 내에서 올바르게 이루어집니다.
    • runCatching { homeUseCase() }: Result를 사용하여 에러를 안전하게 캡처하는 것은 좋지만, HomeUseCase 내에서 getOrThrow()를 사용하는 것은 여전히 문제입니다. HomeUseCaseResult<HomeOverview>를 반환하도록 변경한 후, 여기서 onSuccessonFailure를 처리하는 것이 더 적절합니다.
    • 개선 방안 (리뷰 규칙 준수): HomeState에서 LoadStatus를 사용하도록 변경하면, 이 ViewModel에서도 reduce { state.copy(overviewStatus = LoadStatus.Loading) }, reduce { state.copy(overviewStatus = LoadStatus.Success(overview.mapToUiState())) }, reduce { state.copy(overviewStatus = LoadStatus.Error(throwable.message ?: "Unknown error")) } 와 같은 방식으로 상태를 관리해야 합니다.
  • mapToUiState 함수 내:
    • callHistory.startedAt.toCallHistoryDateText(LocalDateTime.now()) 부분에서 LocalDateTime.now() 인자는 불필요합니다. TimeUtil.toCallHistoryDateText(callHistory.startedAt)으로 충분합니다. 해당 유틸리티 함수는 주어진 LocalDateTime을 포맷팅하는 역할을 합니다. 만약 toTimeAgoText의 의도였다면 해당 함수를 사용해야 합니다. UI 모델에서 "M월 d일 a h시 m분" 형식이 필요한 것으로 보이니, toCallHistoryDateText 사용이 맞을 것 같습니다.

17. Jetpack Compose UI Components (HomeHeader, HomeReservationCard, RelationshipSummaryCard, CallHistoryList, CallHistoryItem)

  • State hoisting: 모든 컴포넌트가 필요한 데이터를 매개변수로 받고 이벤트를 콜백으로 전달하는 State Hoisting 원칙을 잘 따르고 있습니다. 매우 좋은 구현입니다.
  • 불필요한 recomposition: rememberkey 사용이 필요한 복잡한 리스트나 상태 변경 로직은 없어 보이며, LazyColumn을 사용한 리스트 처리도 효율적입니다.
  • 하드코딩된 문자열/매직 넘버:
    • UI에 노출되는 대부분의 텍스트("메인 연인 교체하기", "님, 반가워요! 👋🏻", "오늘은 어떤 이야기를\n나누게 될까요?", "시간 변경", "관계", "통화", "연속 통화", "통화 내역", "부재중", "발신", "수신")는 string resources로 분리해야 합니다. 지역화 및 유지보수 측면에서 필수적입니다.
    • 각종 dp 단위의 크기(예: height(312.dp), width = 93.dp), sp 단위의 폰트 크기(10.sp) 등 매직 넘버가 다수 사용되고 있습니다. 디자인 시스템에 정의된 테마 값(spacing, text styles)을 사용하거나, Dp 또는 Sp 상수로 정의하여 재활용성을 높이는 것을 고려해주세요.
  • HomeReservationCard의 "시간 변경" 버튼: contentPadding = PaddingValues(0.dp)는 Material 3의 최소 터치 영역(48.dp) 가이드라인을 위반할 수 있습니다. 기본 패딩을 사용하거나, 명시적으로 충분한 패딩을 제공하여 접근성을 확보하는 것이 좋습니다.

🚀 추가 제안

  1. 공통 에러 처리 컴포넌트: HomeContent에서 if (uiState.error != null)일 때 Text(uiState.error!!) 대신, 공통 에러 메시지를 표시하는 Reusable Composable을 만들어서 사용하는 것을 고려해보세요.
  2. 문자열 리소스 (Strings.xml): 하드코딩된 모든 사용자 대상 문자열을 strings.xml로 이동시켜주세요.
  3. Hilt @EntryPoint 또는 @Provides: 현재 HomeRepositoryImpl이 더미 데이터를 반환하고 있는데, 실제 HomeApi와 연동될 때 Retrofit 인스턴스 등을 주입하기 위한 NetworkModule 혹은 DataSourceModule (만약 분리한다면) 구성이 필요합니다.

전반적으로 좋은 PR이며, 제안된 피드백을 반영하면 더욱 견고하고 유지보수하기 쉬운 코드가 될 것입니다. 수고 많으셨습니다!

@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요! PR을 잘 리뷰해 보았습니다. 새로운 홈 기능이 추가되었고, 구조적으로 잘 분리하려는 노력이 엿보입니다. 특히 java.time API 사용 및 도메인 모델의 불변성(val 사용)은 아주 좋은 접근입니다.

몇 가지 개선할 점과 논의해 볼 부분이 있어 코멘트 남깁니다.


1. Kotlin 코드 및 공통 유틸리티 (TimeUtil, Mapper)

  • core/common/src/main/java/kr/co/call/core/common/util/TimeUtil.kt

    • 하드코딩된 문자열 (Critical): toTimeAgoText, toReservationTimeText 함수 내의 "분 전", "시간 전", "일 전", "오늘", "내일", "새벽", "아침", "낮", "저녁", "밤"과 같은 모든 사용자에게 표시될 문자열은 strings.xml로 외부화해야 합니다. 이는 다국어 지원뿐만 아니라 유지보수 측면에서도 매우 중요합니다.
    • 날짜/시간 포맷 패턴 하드코딩: DateTimeFormatter.ofPattern에 사용된 패턴 문자열(예: "M월 d일 a h시 m분") 또한 strings.xml로 관리하거나, Locale.KOREAN에 맞는 DateTimeFormatter.ofLocalizedDate() 등과 같은 로케일 지원 메서드를 활용하는 것을 고려해볼 수 있습니다. 현재는 단일 로케일이라 큰 문제는 아니지만, 확장성을 위해서는 좋은 선택입니다.
    • parseLocalDateTime의 에러 핸들링: LocalDateTime.parse(value)는 입력값이 유효하지 않을 경우 DateTimeParseException을 발생시킬 수 있습니다. 현재는 백엔드에서 항상 유효한 값을 보낸다고 가정하는 것 같지만, 조금 더 견고하게 만들려면 Result<LocalDateTime>를 반환하거나 try-catch 블록으로 예외를 처리하여 예상치 못한 에러에 대비하는 것이 좋습니다.
  • core/data/src/main/java/kr/co/call/data/mapper/CallHistoryMapper.kt

    • error() 함수 사용 (Critical): toIsOutgoing(), toIsMissed() 확장 함수에서 error()를 사용하는 것은 프로덕션 환경에서 앱 크래시를 유발할 수 있어 위험합니다. 백엔드에서 예상치 못한 sender 또는 status 문자열이 올 경우를 대비하여 다음 방법 중 하나를 고려해야 합니다.
      • Result 타입을 반환하여 호출 측에서 에러를 명시적으로 처리하도록 합니다.
      • 로그를 남기고 기본값(예: false 또는 null)을 반환하여 앱이 중단되지 않도록 합니다.
      • 백엔드와의 계약을 명확히 하고, DTO 레벨에서 해당 필드에 대한 유효성 검사를 추가하거나, 서버 측에서 유효하지 않은 값을 보내지 않도록 하는 것도 근본적인 해결책이 될 수 있습니다.
    • ReservationDto.toDomain()LocalDateTime.parse(scheduledAt)도 위 TimeUtil.parseLocalDateTime와 동일하게 에러 핸들링을 고려해야 합니다.
  • core/domain/src/main/java/kr/co/call/domain/usecase/home/HomeUseCase.kt

    • getOrThrow() 사용 (Major): homeRepository.getSummary().getOrThrow()와 같이 getOrThrow()를 사용하는 것은 Repository에서 Result.failure가 발생할 경우 UseCase에서 예외를 던지게 됩니다. 이는 UI 계층에서 이 예외를 try-catch로 명시적으로 처리해야 함을 의미하며, 그렇지 않으면 앱이 비정상 종료될 수 있습니다.
    • 권장 사항: UseCase 또한 Result<HomeOverview>를 반환하여, ViewModel에서 로딩/성공/에러 상태를 보다 명시적으로 관리하고 UI에 반영할 수 있도록 하는 것이 일반적인 패턴입니다. 이렇게 하면 ViewModel은 isSuccess, isFailure 등을 확인하여 적절한 UI 상태(예: 에러 메시지 표시)로 전환할 수 있습니다.

2. Jetpack Compose 코드

이번 PR의 Compose 코드 전반에 걸쳐 공통적으로 발견되는 중요한 개선점들이 있습니다.

  • 하드코딩된 크기 및 패딩 (Critical): ProfileImage, UnreadIndicator, HomeHeader, HomeReservationCard, RelationshipSummaryCard, CallHistoryList, CallHistoryItem 등 거의 모든 Composable에서 width, height, size, padding, offset, shape 등에 직접적인 dp 값을 사용하고 있습니다 (예: 25.dp, 50.dp, 140.dp, 217.dp, 6.dp 등).

    • 권장 사항: 이러한 값들은 디자인 시스템의 일부로 관리되어야 합니다. CallTheme.dimens와 같은 사용자 정의 Dimens 객체를 만들거나, Material Design의 Spacing 또는 Shape 토큰을 활용하여 일관성을 유지하고, 유지보수를 용이하게 해야 합니다.
  • 하드코딩된 문자열 (Critical): 위 TimeUtil과 마찬가지로 모든 사용자에게 표시될 텍스트(예: "메인 연인 교체하기", "프로필 이미지", "알림", "전화하기", "관계", "통화", "연속 통화", "오늘 오후 9시", "시간 변경" 등)는 반드시 stringResource(R.string.xxx)를 사용하여 strings.xml 파일로 외부화해야 합니다.

  • app/src/main/java/kr/co/call/callfromai/AppScreen.kt

    • Scaffold를 사용하여 bottomBar를 처리하는 것은 매우 좋은 개선입니다. contentWindowInsetspadding Modifier를 사용하여 시스템 인셋을 올바르게 처리하는 방식 또한 모범 사례입니다.
  • core/designsystem/src/main/java/kr/co/call/designsystem/component/ProfileImage.kt

    • Coil AsyncImagemodel?.takeIf { it.isNotBlank() }, placeholder, error, fallback을 모두 활용하여 견고하게 이미지를 로드하는 점이 좋습니다.
    • contentDescription = "프로필 이미지"는 하드코딩된 문자열이므로 stringResource로 외부화해야 합니다.
  • feature/home/impl/src/main/java/kr/co/call/impl/viewmodel/model/HomeUiModel.kt

    • UI 상태 관리 (LoadStatus) (Major): HomeUiModel에서 isLoading: BooleanerrorMessage: String?을 사용하고 있는데, 리뷰 규칙에 따라 로딩/에러 상태를 Boolean 대신 LoadStatus (또는 UiState와 같은 Sealed Class)로 관리하는 것이 더 좋습니다.
      sealed class LoadStatus<out T> {
          object Loading : LoadStatus<Nothing>()
          data class Success<out T>(val data: T) : LoadStatus<T>()
          data class Error(val throwable: Throwable) : LoadStatus<Nothing>()
      }
      
      // HomeUiModel
      data class HomeUiModel(
          val overview: LoadStatus<HomeOverviewUiModel> = LoadStatus.Loading, // Use HomeOverviewUiModel for actual data
          // ... other UI specific states not covered by overview
      )
      
      data class HomeOverviewUiModel(
          val summary: HomeSummaryUiModel,
          val reservations: List<HomeReservationUiModel>,
          val callHistories: List<CallHistoryUiModel>,
      )
      이렇게 하면 UI에서 현재 상태를 when 문으로 명확하게 처리하고, 각 상태에 맞는 UI(로딩 스피너, 에러 메시지, 실제 데이터)를 표시할 수 있습니다.
    • HomeSummaryUiModel, HomeReservationUiModel, CallHistoryUiModel 등 모든 UI 모델이 data class이고 모든 필드가 val인 것은 매우 좋은 구현입니다. UI 레이어에 필요한 데이터만 가공하여 제공하므로 recomposition 효율성 및 코드 가독성이 좋습니다.
  • Composables의 LocalDateTime.now()/LocalDate.now() 사용: HomeReservationCardTimeUtil.toHomeDateText(LocalDate.now())CallHistoryItemTimeUtil.toTimeAgoText(..., LocalDateTime.now())는 Composables가 현재 시간을 직접 가져오도록 합니다. 이는 프리뷰나 테스트 시 현재 시간을 고정하기 어렵게 만들 수 있습니다.

    • 권장 사항: 현재 LocalDate.now()LocalDateTime.now() 값을 ViewModel에서 계산하여 UI 모델로 넘겨주거나, 적어도 Composables의 파라미터로 주입하는 것을 고려해볼 수 있습니다. 이렇게 하면 Composable은 단순히 받은 데이터를 표시하는 역할에만 집중할 수 있습니다.

3. Repository/DataSource 레이어

  • core/data/src/main/java/kr/co/call/data/repositoryImpl/HomeRepositoryImpl.kt
    • 현재는 하드코딩된 Mock 데이터를 반환하고 있어, Retrofit 에러 핸들링은 이루어지지 않고 있습니다.
    • 미래 작업 (Critical): 실제 HomeApi와 연동 시, 각 suspend 함수 (예: getReservations, getCallHistories, getSummary) 내부에서 Retrofit 호출을 try-catch 블록으로 감싸고, 네트워크 통신 실패, 서버 에러 등에 대한 예외를 Result.failure(exception) 형태로 반환해야 합니다. Retrofit 자체의 Call 객체를 래핑하는 별도의 유틸리티 함수를 만들어 사용하면 코드 중복을 줄일 수 있습니다. (예: safeApiCall 함수).
    • 네트워크 응답 (DTO)과 도메인 모델 매핑은 CallHistoryMapper.kt를 통해 잘 분리될 것으로 기대됩니다. 현재 Mock 데이터는 이미 도메인 모델 형태이므로, 실제 API 연동 시 DTO를 받아서 매퍼를 통해 도메인 모델로 변환하는 로직이 추가되어야 합니다.
    • 캐싱 전략은 현재 이 RepositoryImpl에서 구현되어 있지 않습니다. 만약 이 데이터들이 자주 접근되거나 오프라인 지원이 필요하다면, 로컬 DB(Room)나 메모리 캐싱 전략을 고려해야 합니다.

4. ViewModel (간접 추론)

이 PR에서는 ViewModel 코드가 직접적으로 보이지 않지만, UI 모델(HomeUiModel)과 UseCase(HomeUseCase)를 통해 ViewModel의 역할을 유추할 수 있습니다.

  • UI 상태와 비즈니스 로직 분리: UI 모델(HomeUiModel)이 UI에 필요한 상태를 잘 캡슐화하고 있고, HomeUseCase가 비즈니스 로직(데이터 조합)을 담당하므로, 이 원칙은 잘 지켜지고 있을 것으로 예상됩니다.
  • LoadStatus 패턴 준수 여부: 위에서 언급했듯이, ViewModel에서 UI 모델을 업데이트할 때 LoadStatus 패턴을 적용하여 로딩/성공/에러 상태를 명시적으로 관리해야 합니다.
  • intent 내부에서 상태 변경(reduce)만 수행하는지: Orbit MVI 패턴을 사용한다면, intent 블록 내에서는 UseCase 호출과 결과에 따른 reduce만 수행하고, 복잡한 비즈니스 로직은 UseCase로 위임하는 것이 좋습니다.
  • postSideEffect 남용 여부: ViewModel에서 단발성 이벤트(예: 스낵바 표시, 화면 이동)는 postSideEffect를 통해 처리하는 것이 좋지만, 지속적인 UI 상태 변경은 reduce를 통해 상태에 반영해야 합니다. (ViewModel 코드가 없어서 직접 확인은 불가합니다.)

정리 및 제안

  1. 하드코딩된 문자열/매직 넘버 제거: Compose UI의 모든 사용자 가시 문자열은 strings.xml로, 모든 하드코딩된 dp 값은 디자인 시스템 테마 또는 명명된 상수로 관리해야 합니다. 이것이 가장 시급하게 개선해야 할 부분입니다.
  2. 안전한 에러 핸들링: TimeUtilparseLocalDateTimeCallHistoryMappererror() 호출, HomeUseCasegetOrThrow() 부분을 Result 타입 반환 및 try-catch로 전환하여 앱의 안정성을 높여야 합니다.
  3. ViewModel LoadStatus 적용: HomeUiModel의 로딩/에러 상태를 LoadStatus sealed class로 리팩토링하여 보다 명확하고 견고한 UI 상태 관리를 구현합니다.
  4. 실제 API 연동 시 Repository 강화: Mock Repository를 실제 HomeApi와 연동할 때, Result 래핑을 포함한 try-catch 기반의 Retrofit 에러 핸들링을 반드시 추가해야 합니다.
  5. Composables의 시간 의존성 분리: LocalDate.now()/LocalDateTime.now() 호출을 ViewModel 레이어로 옮기거나, 파라미터로 주입하여 Composables의 테스트 용이성과 순수성을 높이는 것을 고려합니다.

이러한 개선 사항들을 적용한다면 더욱 견고하고 유지보수하기 쉬운 코드가 될 것입니다. 수고 많으셨습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 홈화면 UI 구현

3 participants