Skip to content

feat: 마이페이지 - 고객지원 UI구현 (#17)#28

Merged
codebidoof merged 13 commits into
developfrom
feature/17-mypage-support-ui
Jul 25, 2026
Merged

feat: 마이페이지 - 고객지원 UI구현 (#17)#28
codebidoof merged 13 commits into
developfrom
feature/17-mypage-support-ui

Conversation

@WAcAW9

@WAcAW9 WAcAW9 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

📄 작업 내용 요약

  • 마이페이지 > 고객지원(이용약관(TermScreen), 자주하는 질문(FaqScreen)) 화면 UI구현
  • 마이페이지 > TermScreen, 마이페이지>FaqScreen 네비게이션 연결
  • 준비중 화면(CommingSoonScreen) 앱 로고 적용

📎 Issue 번호


✅ 작업 목록

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

📝 기타 참고사항

image image

Summary by CodeRabbit

  • 새로운 기능
    • 마이페이지에 FAQ 화면(캐릭터/통화·채팅/플랜·결제/계정·설정)과 질문-답변 펼침 UI를 추가했습니다.
    • 서비스 이용약관 및 개인정보 처리 방침 화면을 추가했습니다.
  • 개선
    • FAQ/약관 화면에서도 하단 탭 바가 함께 표시되도록 했습니다.
    • 마이페이지에서 FAQ/약관 이동과 뒤로가기 동작을 정리했습니다.
    • 설정 섹션의 제목을 공백일 때 숨기고, 화면 간격을 조정했습니다.

@WAcAW9 WAcAW9 linked an issue Jul 20, 2026 that may be closed by this pull request
2 tasks
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@WAcAW9, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 56 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c035b0cf-cd53-4ab1-872d-63b0a9213a40

📥 Commits

Reviewing files that changed from the base of the PR and between 6bd8bdc and 99d993d.

📒 Files selected for processing (6)
  • app/src/main/java/kr/co/call/callfromai/AppScreen.kt
  • core/data/src/main/java/kr/co/call/data/repositoryImpl/FaqRepositoryImpl.kt
  • core/domain/src/main/java/kr/co/call/domain/model/mypage/FaqItem.kt
  • core/domain/src/main/java/kr/co/call/domain/repository/FaqRepository.kt
  • feature/mypage/impl/src/main/java/kr/co/call/impl/screen/MyPageScreen.kt
  • feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/FaqViewModel.kt
📝 Walkthrough

Walkthrough

마이페이지에 FAQ와 약관 화면을 추가하고, FAQ 데이터를 도메인·데이터 계층에서 제공하도록 구성했습니다. 새로운 라우팅 키와 엔트리를 앱 네비게이션에 연결했으며, 관련 마이페이지 UI와 BottomBar 매핑도 변경했습니다.

Changes

마이페이지 고객지원

Layer / File(s) Summary
FAQ 도메인 및 데이터 계층
core/domain/..., core/data/...
FAQ 카테고리·문항 모델과 FaqRepository 계약을 추가하고, 4개 카테고리의 정적 FAQ 데이터 및 Hilt 바인딩을 구성했습니다.
FAQ 및 지원 화면 구현
feature/mypage/impl/...
FaqViewModel이 FAQ 조회 상태를 관리하고, FaqScreen이 카테고리 탭과 질문·답변 펼침 UI를 렌더링합니다. 약관 화면과 마이페이지 UI 간격 및 아이콘 변경도 포함됩니다.
마이페이지 네비게이션 연결
feature/mypage/api/..., feature/mypage/impl/entry/..., app/...
FaqNavKeyTermNavKey를 추가하고 화면 엔트리, 이동 콜백, 뒤로가기, BottomBar 표시 및 탭 매핑을 연결했습니다.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant 사용자
  participant MyPageScreen
  participant AppScreen
  participant FaqScreen
  사용자->>MyPageScreen: FAQ 선택
  MyPageScreen->>AppScreen: navigateToFaq()
  AppScreen->>FaqScreen: FaqNavKey 엔트리 표시
  FaqScreen->>FaqScreen: 카테고리 선택 및 질문 펼침
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 고객지원 범위와 무관한 ComingSoonScreen 앱 로고 변경이 포함되어 있습니다. ComingSoonScreen 로고 변경은 별도 PR로 분리하거나, 고객지원 관련 변경만 남기세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed FAQ/QnA 화면, 개인정보 처리방침 화면, 네비게이션 연결이 모두 구현되어 #17의 요구사항을 충족합니다.
Ui 변경 시 스크린샷 첨부 확인 ✅ Passed FAQ/약관 등 Compose 화면 파일이 변경됐고, PR 설명에 스크린샷 이미지 2장이 첨부돼 있습니다.
모듈 의존성 방향 검증 ✅ Passed feature/mypage/impl은 자체 impl/api와 core/domain 인터페이스만 사용하고, core/domain에는 android import가 없으며 core/data 구현 의존도 없습니다.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Conventional Commits 형식을 따르고 있으며, 마이페이지 고객지원 UI 구현이라는 변경 내용과도 직접 관련됩니다.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/17-mypage-support-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요! PR 잘 봤습니다. FAQ와 약관 화면 추가 및 마이페이지 내비게이션 연결 작업으로 보이네요. 전반적으로 Orbit MVI 패턴과 Jetpack Compose의 모범 사례들을 잘 적용하고 계셔서 인상 깊습니다. 특히 ViewModel에서 비즈니스 로직과 UI 상태를 분리하고, Compose에서 상태 호이스팅 및 불필요한 리컴포지션을 방지하려는 노력이 돋보입니다.

다만, 몇 가지 개선할 점이 있어 코멘트 남깁니다.


💡 General Feedback

가장 중요한 부분은 하드코딩된 문자열 관리입니다. 현재 FAQ 내용과 약관 내용, 그리고 UI에 표시되는 일부 텍스트들이 코드 내에 직접 입력되어 있습니다. 이는 다국어 지원, 유지보수, 그리고 텍스트 변경 시 앱 업데이트가 필요하다는 점에서 지양해야 합니다. FAQ와 약관 내용은 백엔드 API를 통해 가져오거나, 최소한 로컬 strings.xml 또는 별도의 JSON/Asset 파일로 분리하는 것이 좋습니다.

그 외에는 대체적으로 깔끔하고 좋은 코드라고 생각합니다.


🔎 Detailed Review

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

  • showBottomBar 로직: FaqNavKeyTermNavKey가 추가되면서 MyPage 탭과 함께 하단바를 표시하도록 한 점 좋습니다. MyPage 하위 화면에서도 하단바를 유지하는 UX를 고려하신 것 같습니다.
  • myPageEntry() 파라미터: navigateToFaq, navigateToTerms, onBack 람다를 myPageEntry에 전달하도록 변경한 것은 State Hoisting 원칙을 잘 지킨 훌륭한 패턴입니다. 내비게이션 로직을 상위 AppScreen에서 관리하고, MyPageEntryBuilder는 단순히 내비게이션 경로를 정의하는 역할만 하도록 분리했습니다.

app/src/main/java/kr/co/call/callfromai/util/MainTabExt.kt

  • toMainTab(): FaqNavKey, TermNavKeyMainTab.MYPAGE로 매핑한 것은 하단바 선택 상태를 일관되게 유지하는 데 좋습니다.

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

  • Hilt DI: FaqRepositoryImplFaqRepository에 바인딩하는 로직이 추가되었습니다. @Binds, @Singleton 어노테이션 사용 등 Hilt DI 생성자 주입 원칙을 잘 준수하고 있습니다.

core/data/src/main/java/kr/co/call/data/repositoryImpl/FaqRepositoryImpl.kt (New File)

  • 하드코딩된 문자열: 가장 중요한 개선점입니다. 현재 모든 FAQ 질문과 답변이 getFaqItems() 메서드 내에 하드코딩되어 있습니다.
    • 권장 사항: 이 데이터는 앱의 코드가 아닌, 백엔드 API를 통해 동적으로 가져오도록 구현하는 것이 가장 좋습니다.
    • 대안: 당장 API 연동이 어렵다면, strings.xml에 질문과 답변을 각각 정의하거나, JSON 파일을 assets에 두고 파싱하여 로드하는 방식으로 변경하는 것을 고려해주세요. 이렇게 하면 텍스트 변경 시 코드 수정 및 앱 업데이트 없이 유연하게 대응할 수 있습니다.
  • suspend 함수: getFaqItems()suspend 함수로 선언된 것은 좋습니다. 현재는 하드코딩된 데이터를 즉시 반환하지만, 향후 네트워크 통신이나 DB 접근 로직이 추가될 경우 비동기 처리에 용이합니다.

core/domain/src/main/java/kr/co/call/domain/model/mypage/FaqCategory.kt (New File)

  • Enum Class: FAQ 카테고리를 enum class로 정의한 점 좋습니다. 각 카테고리에 label을 부여하여 UI 표시와 도메인 로직을 분리한 것도 깔끔합니다.
  • 하드코딩된 문자열: label에 해당하는 문자열("캐릭터", "통화/채팅" 등)도 strings.xml로 분리하여 R.string.faq_category_character와 같이 참조하는 것이 다국어 지원을 위해 더 좋습니다.

core/domain/src/main/java/kr/co/call/domain/model/mypage/FaqItem.kt (New File)

  • Data Class: FaqItemdata class로 정의하고, 모든 필드를 val로 선언한 점 좋습니다. 불필요한 var 사용을 지양하여 불변성을 유지했습니다.

core/domain/src/main/java/kr/co/call/domain/repository/FaqRepository.kt (New File)

  • Interface: 레포지토리 인터페이스를 정의하여 데이터 소스 구현과 도메인 레이어를 분리한 점 좋습니다.

feature/mypage/api/src/main/java/kr/co/call/api/MyPageRoute.kt

  • FaqNavKey, TermNavKey: data object로 정의하여 내비게이션 키로 사용한 것은 좋습니다.

feature/mypage/impl/src/main/java/kr/co/call/impl/component/SettingSectionCard.kt

  • Null 안전성 개선: title: String? = ""title: String = ""로 변경하고, if (title != null) 대신 if (title.isNotBlank())를 사용한 점은 null 안전성을 높이고 더 명확한 조건을 제시하여 아주 훌륭합니다.

feature/mypage/impl/src/main/java/kr/co/call/impl/entry/MyPageEntryBuilder.kt

  • 내비게이션 구조: myPageEntry에 내비게이션 람다를 받아 MyPage의 하위 화면들을 구성한 점 좋습니다. FaqScreenTermScreenonBackClick을 전달하여 UI 컴포넌트가 직접 내비게이션 로직을 알 필요 없도록 State Hoisting을 잘 적용했습니다.

feature/mypage/impl/src/main/java/kr/co/call/impl/screen/ComingSoonScreen.kt

  • 접근성 및 아이콘 처리:
    • contentDescription = "앱 로고"를 추가하여 접근성을 개선한 점 좋습니다.
    • tint = Color.Unspecified를 명시하여 아이콘의 원본 색상을 유지하도록 한 점도 적절한 수정입니다. 기본적으로 Icon 컴포저블은 LocalContentColor를 따르기 때문에, 의도하지 않은 색상 변경을 막을 수 있습니다.
  • TODO 삭제: TODO 주석을 삭제하여 코드가 실제 구현을 반영하게 된 점 좋습니다.

feature/mypage/impl/src/main/java/kr/co/call/impl/screen/FaqScreen.kt (New File)

  • Compose Best Practices:
    • State Hoisting: FaqScreenContent를 별도의 @Composable 함수로 분리하고, 필요한 데이터와 이벤트를 파라미터로 받은 점이 상태 호이스팅 원칙을 잘 준수한 사례입니다.
    • remember & rememberSaveable: selectedCategoryremember, expandedQuestionsrememberSaveable(selectedCategory)와 함께 사용한 점은 불필요한 리컴포지션을 방지하고, selectedCategory 변경 시 펼쳐진 질문 상태를 초기화하며, 화면 회전 등 설정 변경에도 상태를 보존하는 좋은 구현입니다.
    • collectAsState: Orbit StateFlowcollectAsState()로 구독하는 것은 repeatOnLifecycle을 내부적으로 처리하므로, Flow/StateFlow collect 시점을 적절하게 관리하는 좋은 방법입니다.
    • UI 상태를 data class로 표현: FaqStateFaqItem 모두 데이터 클래스로 UI 상태를 잘 표현하고 있습니다.
    • LoadStatus 사용: 로딩/에러 상태를 Boolean 대신 LoadStatus (enum class 또는 sealed class)로 관리하여 UI 로직을 더 명확하고 확장성 있게 만든 점은 매우 훌륭합니다. "불러오는 중...", "등록된 질문이 없습니다." 메시지를 LoadStatus에 따라 보여주는 로직도 좋습니다.
    • AnimatedVisibility: QnA 아이템 확장/축소 시 애니메이션을 적용하여 사용자 경험을 향상시킨 점 좋습니다.
    • MutableInteractionSource: clickable 수정자에 remember { MutableInteractionSource() }를 사용한 점은 기본 리플 효과를 커스터마이징하거나 제거할 때 유용하며, 불필요한 객체 생성을 막는 좋은 습관입니다.
  • 하드코딩된 문자열: "자주 하는 질문", "Q", "A", "불러오는 중...", "등록된 질문이 없습니다." 등 사용자에게 노출되는 모든 텍스트는 strings.xml로 분리해야 합니다.

feature/mypage/impl/src/main/java/kr/co/call/impl/screen/MyPageScreen.kt

  • 내비게이션 람다 연결: navigateToFaqnavigateToTerms 람다를 받아 MyPageSideEffect에 따라 호출하도록 연결한 점 좋습니다. 이는 ViewModel과 Compose UI 간의 책임 분리를 명확히 하는 좋은 예시입니다.
  • UI 간격 조정: verticalArrangementSpacer를 조정한 것은 UI 디자인 변경으로 보입니다.
  • LoadStatus 사용: state.loadStatus == LoadStatus.Loading으로 로딩 상태를 표시하는 점도 좋습니다.

feature/mypage/impl/src/main/java/kr/co/call/impl/screen/TermScreen.kt (New File)

  • 하드코딩된 문자열: 가장 중요한 개선점입니다. 약관 내용 전체가 하드코딩되어 있습니다. FaqRepositoryImpl의 FAQ 내용과 마찬가지로, 이 내용은 반드시 strings.xml 또는 백엔드 API, 로컬 JSON/Asset 파일 등을 통해 관리되어야 합니다. 현재 "TODO: 추후 실제 내용으로 변경 + 하드코딩된 글자들 모두 string.xml으로 분리 or api 처리" 주석이 있는데, 이 PR에서 해당 작업을 진행하거나 후속 PR로 빠르게 진행하는 것이 중요합니다.
  • CommonTopAppBar: "약관/개인정보 처리 방침" 제목도 strings.xml로 분리해야 합니다.

feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/FaqSideEffect.kt (New File)

  • Orbit SideEffect: FaqSideEffectsealed interface로 정의하고 ShowError를 포함시킨 점 좋습니다. postSideEffect 남용을 방지하고, 단발성 이벤트를 처리하는 Orbit 패턴에 충실합니다.

feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/FaqState.kt (New File)

  • UI 상태 데이터 클래스: FaqStatedata class로 정의하고 LoadStatus를 사용하여 로딩/에러 상태를 Boolean 대신 관리하는 점은 매우 훌륭합니다. 모든 필드가 val로 선언된 것도 좋습니다.

feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/FaqViewModel.kt (New File)

  • Orbit MVI 구현:
    • ContainerHost 구현, container 초기화, initialState 설정 모두 Orbit ContainerHost 패턴을 잘 준수하고 있습니다.
    • Hilt DI: FaqRepository를 생성자 주입으로 받은 점 좋습니다.
    • loadFaqItems() intent:
      • intent 블록 내에서 reduce를 통해 상태(loadStatus, itemsByCategory)를 변경하는 것은 intent 내부에서 상태 변경(reduce)만 수행하도록 한 원칙을 잘 따릅니다.
      • runCatching을 사용하여 faqRepository.getFaqItems() 호출 시 발생할 수 있는 예외를 안전하게 처리하고, onSuccessonFailure로 결과에 따라 상태를 업데이트하는 방식이 매우 좋습니다.
      • 에러 발생 시 postSideEffect(FaqSideEffect.ShowError(message))를 통해 UI에 일회성 메시지를 전달하는 것도 postSideEffect 남용을 방지하고 적절하게 사용한 예시입니다.
      • faqRepository.getFaqItems()suspend 함수이므로 blocking 작업을 intent 내부에서 직접 수행하지 않고 비동기로 처리하도록 설계된 점도 좋습니다. (현재는 Mock 데이터지만, 실제 데이터가 들어와도 문제없도록 구조화됨)

✅ Summary & Action Items

반드시 개선해야 할 사항 (Critical):

  1. 하드코딩된 문자열 분리:
    • FaqRepositoryImpl.kt 내 모든 FAQ 질문/답변.
    • TermScreen.kt 내 약관 내용 전체.
    • FaqScreen.ktTermScreen.kt에서 CommonTopAppBar에 전달하는 제목 ("자주 하는 질문", "약관/개인정보 처리 방침").
    • FaqScreen.kt 내 "Q", "A", "불러오는 중...", "등록된 질문이 없습니다." 텍스트.
    • FaqCategory.ktlabel 문자열 (선택적이지만 권장).
    • 조치 방안: strings.xml로 분리하거나, FAQ 및 약관 내용은 백엔드 API를 통해 가져오도록 개선.

선택적으로 개선할 사항 (Optional/Good to have):

  1. Repository 캐싱 전략: FAQ 데이터가 동적인 데이터로 변경될 경우, 캐싱 전략(로컬 DB, 메모리 캐시 등)을 고려해볼 수 있습니다. (현재는 하드코딩이라 불필요)
  2. Magic Number: UI 컴포저블 내의 하드코딩된 dp 값들을 디자인 시스템의 Dimens 객체 등으로 통합하면 일관성을 높일 수 있습니다. (프로젝트 컨벤션에 따라)

전체적으로 기능 구현과 코드 품질 면에서 높은 수준을 보여주고 있습니다. 특히 MVI 패턴과 Compose의 권장사항들을 잘 적용하신 점이 아주 좋습니다. 하드코딩된 문자열 문제만 해결된다면 더욱 견고하고 유지보수하기 좋은 코드가 될 것입니다.

수고 많으셨습니다!

@WAcAW9 WAcAW9 self-assigned this Jul 20, 2026

@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

🤖 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/mypage/impl/src/main/java/kr/co/call/impl/screen/FaqScreen.kt`:
- Around line 109-123: Update the FAQ UI rendering around FaqViewModel and the
existing loadStatus/items branches so LoadStatus.Error is displayed as a
distinct error state instead of “등록된 질문이 없습니다.”, while preserving the
empty-state message only for successful empty results. Subscribe to
FaqSideEffect.ShowError using LaunchedEffect and collect, then surface the
failure to the user through the established error presentation mechanism.

In `@feature/mypage/impl/src/main/java/kr/co/call/impl/screen/TermScreen.kt`:
- Around line 42-67: Replace the hardcoded body text in TermScreen with the
approved 전화왔어 terms and privacy-policy content, removing all references to the
unrelated “화록” service, its features, and Kakao login. Keep the existing title
and rendering structure unless the approved source requires corresponding
content updates.
🪄 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: 401062b3-7a51-4063-a11e-ec296d9582c9

📥 Commits

Reviewing files that changed from the base of the PR and between 8f30c10 and 9859b2f.

📒 Files selected for processing (17)
  • app/src/main/java/kr/co/call/callfromai/AppScreen.kt
  • app/src/main/java/kr/co/call/callfromai/util/MainTabExt.kt
  • core/data/src/main/java/kr/co/call/data/di/RepositoryModule.kt
  • core/data/src/main/java/kr/co/call/data/repositoryImpl/FaqRepositoryImpl.kt
  • core/domain/src/main/java/kr/co/call/domain/model/mypage/FaqCategory.kt
  • core/domain/src/main/java/kr/co/call/domain/model/mypage/FaqItem.kt
  • core/domain/src/main/java/kr/co/call/domain/repository/FaqRepository.kt
  • feature/mypage/api/src/main/java/kr/co/call/api/MyPageRoute.kt
  • feature/mypage/impl/src/main/java/kr/co/call/impl/component/SettingSectionCard.kt
  • feature/mypage/impl/src/main/java/kr/co/call/impl/entry/MyPageEntryBuilder.kt
  • feature/mypage/impl/src/main/java/kr/co/call/impl/screen/ComingSoonScreen.kt
  • feature/mypage/impl/src/main/java/kr/co/call/impl/screen/FaqScreen.kt
  • feature/mypage/impl/src/main/java/kr/co/call/impl/screen/MyPageScreen.kt
  • feature/mypage/impl/src/main/java/kr/co/call/impl/screen/TermScreen.kt
  • feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/FaqSideEffect.kt
  • feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/FaqState.kt
  • feature/mypage/impl/src/main/java/kr/co/call/impl/viewmodel/FaqViewModel.kt

Comment on lines +109 to +123
when {
loadStatus == LoadStatus.Loading -> {
Text(
text = "불러오는 중...",
style = CallTheme.typography.bodyMedium,
color = CallTheme.colors.gray400,
)
}
items.isEmpty() -> {
Text(
text = "등록된 질문이 없습니다.",
style = CallTheme.typography.bodyMedium,
color = CallTheme.colors.gray400,
)
}

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 | 🟡 Minor | ⚡ Quick win

오류 상태를 빈 FAQ 상태로 표시하지 마세요.

FaqViewModel은 실패 시 LoadStatus.ErrorShowError를 발생시키지만, 여기서는 결국 “등록된 질문이 없습니다.”로 렌더링됩니다. 오류 상태를 별도 표시하고 FaqSideEffect.ShowError도 구독해 실제 실패를 사용자에게 알리세요.

As per path instructions, “sideEffect는 LaunchedEffect + collect로 구독”해야 합니다.

🤖 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/mypage/impl/src/main/java/kr/co/call/impl/screen/FaqScreen.kt` around
lines 109 - 123, Update the FAQ UI rendering around FaqViewModel and the
existing loadStatus/items branches so LoadStatus.Error is displayed as a
distinct error state instead of “등록된 질문이 없습니다.”, while preserving the
empty-state message only for successful empty results. Subscribe to
FaqSideEffect.ShowError using LaunchedEffect and collect, then surface the
failure to the user through the established error presentation mechanism.

Source: Path instructions

@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요, 시니어 Android 개발자입니다. PR 잘 봤습니다! FAQ 및 약관 화면 추가와 관련된 전반적인 구조 개선이 잘 진행된 것 같습니다. 특히 Jetpack Compose의 모범 사례와 Orbit MVI 패턴을 잘 적용하려는 노력이 돋보입니다. 몇 가지 중점적으로 살펴본 부분과 개선할 점을 말씀드리겠습니다.


✨ 전반적인 개선 사항 요약

새로운 FAQ 및 약관 화면을 추가하고, 기존 마이페이지 내비게이션에 통합하는 작업을 깔끔하게 처리했습니다. Compose 상태 관리 원칙(State Hoisting, rememberSaveable), ViewModel의 LoadStatus 활용, Orbit MVI 패턴 적용 등 모범 사례를 잘 따르고 있어 인상 깊습니다.

다만, 대부분의 사용자에게 노출되는 FAQ와 약관 내용이 현재는 하드코딩되어 있습니다. 이 부분은 실제 서비스 운영 시 유연한 대응을 위해 반드시 개선되어야 할 부분입니다.


1. Kotlin 코드 리뷰 (공통)

  • Coroutine 사용 (Dispatcher 명시 여부, viewModelScope/lifecycleScope 오남용)
    • FaqViewModelloadFaqItems()intent { ... } 블록 내에서 faqRepository.getFaqItems()를 호출합니다. Orbit intent는 기본적으로 Dispatchers.Default에서 실행되지만, faqRepository.getFaqItems()suspend 함수이므로 내부적으로 필요한 Dispatcher (예: Dispatchers.IO for network/DB)를 처리할 것으로 기대됩니다. 현재 FaqRepositoryImpl은 하드코딩된 데이터를 반환하므로 Dispatcher 문제는 없지만, 실제 API 연동 시 Repository에서 적절한 Dispatcher를 명시(withContext(Dispatchers.IO))하는 것이 좋습니다. ViewModel에서는 이에 대해 걱정할 필요가 없습니다.
    • viewModelScope/lifecycleScope 오남용은 보이지 않습니다. Orbit ContainerHost 패턴을 잘 따르고 있습니다.
  • Flow/StateFlow 사용 시 collect 시점 (repeatOnLifecycle 사용 여부)
    • FaqScreen에서 viewModel.collectAsState()를 사용하고 있어, Compose 생명주기에 맞춰 안전하게 Flow를 수집하고 있습니다. 이 접근 방식은 repeatOnLifecycle과 유사한 안전성을 제공하므로 적절합니다.
  • null 안전성 (!! 사용 지양)
    • SettingSectionCard.kt에서 title: String? = ""title: String = ""로 변경하고, if (title != null)if (title.isNotBlank())으로 개선한 점은 매우 좋습니다. 불필요한 nullability를 제거하고 더욱 견고한 코드를 만들었습니다.
    • 코드 전반에서 !! 사용은 발견되지 않았습니다.
  • data class에 불필요한 var 사용 여부
    • 새로 추가된 FaqItem, FaqState 모두 val만 사용하고 있어 올바른 data class 사용법을 따르고 있습니다.
  • Hilt DI 시 생성자 주입 원칙 준수 여부
    • FaqRepositoryImpl, FaqViewModel 모두 @Inject constructor()를 사용하여 생성자 주입 원칙을 잘 준수하고 있습니다.
    • RepositoryModule에서 @Binds를 사용하여 인터페이스와 구현체를 연결하는 방식도 올바릅니다.
  • 하드코딩된 문자열/매직 넘버
    • Actionable: FaqRepositoryImplTermScreen.kt에 있는 모든 FAQ 질문/답변 및 약관 내용은 현재 하드코딩되어 있습니다. 실제 서비스에서는 이 내용들을 원격 서버(API)에서 가져오거나, 최소한 strings.xml 리소스 파일로 분리하여 관리해야 합니다. 이는 다국어 지원 및 내용 변경 시 앱 업데이트 없이 유연하게 대응하기 위해 필수적입니다. 현재 TODO 주석이 달려있긴 하지만, 강조하여 개선을 요청드립니다.
    • Actionable: FaqScreen.kt의 "자주 하는 질문", "불러오는 중...", "등록된 질문이 없습니다.", "Q", "A", "네트워크 오류가 발생했습니다."와 TermScreen.kt의 "약관/개인정보 처리 방침", "전화왔어 서비스 이용약관" 등 사용자에게 직접 노출되는 모든 UI 문자열은 strings.xml로 분리해야 합니다.
    • Actionable: ComingSoonScreen.kt에서 사용된 modifier = Modifier.size(81.dp)와 같이 특정 컴포넌트에 고정된 dp 값은 디자인 시스템에 정의된 토큰을 사용하거나, Dp 상수로 정의하여 재사용성과 일관성을 높이는 것을 고려해볼 수 있습니다. (예: Dimens.icon_large 등) MyPageScreenverticalArrangement 값 변경(12.dp -> 20.dp)도 마찬가지입니다.

2. Jetpack Compose 코드

  • Composable 함수의 불필요한 recomposition 유발 여부 (remember, key 사용)
    • FaqScreen에서 selectedCategoryexpandedQuestions 상태를 rememberrememberSaveable(selectedCategory)를 사용하여 적절하게 관리하고 있습니다. 특히 expandedQuestionsselectedCategory를 key로 지정하여 카테고리 변경 시 질문 확장 상태를 초기화하는 방식은 매우 좋습니다.
    • clickable modifier 내부에서 remember { MutableInteractionSource() }를 사용하여 불필요한 객체 생성을 방지하고 있습니다. 전반적으로 recomposition을 최소화하려는 노력이 잘 보입니다.
  • State hoisting 원칙 준수
    • FaqScreenFaqScreenContent를 통해 UI 로직과 상태를 분리하고, 필요한 상태와 이벤트를 람다로 전달하는 State Hoisting 원칙을 잘 따르고 있습니다.
    • MyPageScreennavigateToFaq, navigateToTerms 람다를 통해 내비게이션 이벤트를 호이스팅하고 있습니다.
  • side effect (LaunchedEffect, DisposableEffect) 사용의 적절성
    • Orbit의 collectSideEffect를 사용하여 MyPageSideEffect.NavigateToFaq, MyPageSideEffect.NavigateToTerms 등 단발성 내비게이션 이벤트를 처리하고 있습니다. 이는 Compose의 LaunchedEffect와 유사한 용도로 적절하게 사용되었습니다. 새로운 FaqSideEffect.ShowError도 마찬가지입니다.
  • UI 상태를 data class로 표현했는지
    • FaqStatedata class로 정의되어 UI 상태를 명확하게 표현하고 있습니다. 좋습니다.
  • 로딩/에러 상태를 Boolean 대신 LoadStatus로 관리하는지
    • FaqState에서 LoadStatus (Idle, Loading, Error) enum 클래스를 사용하여 로딩 및 에러 상태를 체계적으로 관리하고 있습니다. FaqScreenContent에서도 when 문을 통해 각 상태에 따라 UI를 다르게 표시하는 방식이 매우 좋습니다.

3. Repository/DataSource 레이어

  • Retrofit 에러 핸들링 (try-catch, Result 래핑)
    • FaqRepositoryImpl은 현재 하드코딩된 데이터를 반환하므로 네트워크 에러 핸들링이 필요 없습니다.
    • 하지만 만약 실제 네트워크 통신을 하게 된다면, Repository 내부에서 try-catch 또는 Result 타입으로 네트워크 호출 결과를 래핑하여 ViewModel에 전달하는 것이 좋습니다. FaqViewModel에서는 runCatching을 사용하여 Repository 계층에서 발생할 수 있는 예외를 잘 처리하고 있어, Repository가 예외를 던지더라도 ViewModel이 크래시 되지 않도록 대비되어 있습니다.
  • 네트워크 응답과 도메인 모델 매핑 분리 여부
    • 현재 네트워크 호출이 없으므로 직접적인 DTO-도메인 모델 매핑은 없지만, FaqItem, FaqCategory와 같은 도메인 모델을 잘 정의해두었기 때문에 추후 네트워크 응답을 이 모델로 매핑하는 구조로 확장하기 용이합니다.
  • 캐싱 전략 (로컬 DB vs 메모리)
    • 하드코딩된 데이터이므로 캐싱 전략은 별도로 필요하지 않습니다.

4. ViewModel

  • UI 상태와 비즈니스 로직 분리
    • FaqViewModelFaqState를 통해 UI 상태를 관리하고, loadFaqItems()를 통해 FAQ 데이터를 불러오는 비즈니스 로직을 처리하여 분리가 잘 되어 있습니다.
  • Orbit ContainerHost 패턴 준수 여부
    • FaqViewModelContainerHost 인터페이스를 구현하고 container를 초기화하여 Orbit MVI 패턴을 잘 준수하고 있습니다.
  • intent 내부에서 상태 변경(reduce)만 수행하는지
    • loadFaqItems() intent 내에서 reduce { ... } 블록을 통해 상태 변경을 수행하고 있습니다. 올바른 Orbit 사용법입니다.
  • postSideEffect 남용 여부
    • FaqViewModelpostSideEffect(FaqSideEffect.ShowError(message))는 에러 메시지 표시와 같은 단발성 이벤트를 위한 것이므로 적절하게 사용되었습니다. 남용으로 보이지 않습니다.
  • blocking 작업을 intent 내부에서 직접 수행하지 않는지
    • faqRepository.getFaqItems()suspend 함수이므로, Orbit intent 내에서 호출해도 코루틴 스케줄링에 의해 blocking 되지 않습니다. 적절합니다.

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

  • 이 PR에는 FCM/SSE/실시간 통신 관련 코드가 포함되어 있지 않아 해당 규칙은 적용되지 않습니다.

기타 사항

  • No newline at end of file: 몇몇 새로 추가되거나 수정된 파일(RepositoryModule.kt, MyPageEntryBuilder.kt, FaqRepositoryImpl.kt, FaqCategory.kt, FaqItem.kt, FaqRepository.kt, MyPageRoute.kt, TermScreen.kt, FaqSideEffect.kt, FaqState.kt, FaqViewModel.kt)에 파일의 마지막에 빈 줄이 없습니다. 이는 일반적으로 코드 스타일 가이드에서 권장하지 않으며, 일부 도구에서 문제를 일으킬 수 있으니 각 파일 끝에 빈 줄을 추가하는 것이 좋습니다.
  • ComingSoonScreen.ktIcon에서 tint = Color.Unspecified를 사용한 것은 아이콘의 원본 색상을 유지하기 위한 것으로 보입니다. 의도된 것이라면 주석으로 간략하게 설명해주는 것도 좋습니다.

최종 의견

전반적으로 매우 좋은 PR입니다. 특히 Jetpack Compose와 Orbit MVI 패턴을 모범적으로 적용하고 있으며, UI 상태 관리도 견고하게 설계되었습니다. 위에서 언급한 하드코딩된 문자열 처리와 파일 끝 빈 줄 추가만 보완된다면 완벽할 것 같습니다.

계속해서 좋은 코드 기대하겠습니다!

@github-actions

Copy link
Copy Markdown

Gemini AI 코드리뷰

안녕하세요! GitHub PR 리뷰를 담당하는 시니어 Android 개발자입니다. 새로운 FAQ 및 약관 화면 추가 PR에 대해 리뷰를 진행하겠습니다. 전반적으로 MVI 패턴과 Jetpack Compose의 모범 사례를 잘 적용하려고 노력한 흔적이 보여서 좋습니다. 몇 가지 개선 사항과 잠재적 문제점에 대해 피드백 드리겠습니다.


PR 요약

이 PR은 앱에 FAQ (자주 묻는 질문) 및 약관 화면을 추가하는 기능입니다. 이를 위해 다음과 같은 변경 사항이 포함되었습니다:

  1. 내비게이션 구조 변경: AppScreenMainTabExtFaqNavKeyTermNavKey를 추가하고, MyPageEntryBuilder에서 FAQ 및 약관 화면으로 이동하는 경로를 정의했습니다. MyPageScreen은 이제 FAQ 및 약관 화면으로의 내비게이션 콜백을 받습니다.
  2. FAQ 기능 추가:
    • FaqRepository 인터페이스 및 FaqRepositoryImpl 구현체를 추가했습니다.
    • FaqCategory enum 클래스와 FaqItem data class를 정의했습니다.
    • FaqViewModel, FaqState, FaqSideEffect를 정의하여 Orbit MVI 패턴을 적용했습니다.
    • FaqScreen Composable을 추가하여 FAQ 질문 목록을 표시하고, 카테고리 필터링 및 질문-답변 확장/축소 기능을 구현했습니다.
  3. 약관 화면 추가: TermScreen Composable을 추가하여 약관 내용을 표시합니다. (현재는 하드코딩된 텍스트)
  4. DI 설정: Hilt RepositoryModuleFaqRepository 바인딩을 추가했습니다.
  5. 컴포넌트 개선: SettingSectionCardtitle 파라미터를 non-nullable String으로 변경하고 isNotBlank()로 체크하도록 개선했습니다.

코드 리뷰 상세

1. Kotlin 코드 리뷰

  • Coroutine 사용 시 Dispatcher 명시 여부, viewModelScope/lifecycleScope 오남용

    • FaqViewModel.kt: faqRepository.getFaqItems()suspend 함수이며, intent 블록 내에서 호출됩니다. Orbit intent는 적절한 Dispatcher (기본적으로 Dispatchers.Default 또는 Dispatchers.IO로 설정될 수 있음)에서 코루틴을 실행하므로 viewModelScopelifecycleScope 오남용은 보이지 않습니다.
    • 다만, FaqRepositoryImpl.ktgetFaqItems()는 현재 하드코딩된 데이터를 Result.success로 반환합니다. 만약 이 부분이 실제로 네트워크 호출이나 로컬 DB 접근과 같은 I/O 작업을 포함하게 된다면, FaqRepositoryImpl 내부에서 withContext(Dispatchers.IO)를 사용하여 명시적으로 I/O Dispatcher를 지정해주는 것이 좋습니다. 현재는 main-safe하여 문제가 없습니다.
  • Flow/StateFlow 사용 시 collect 시점 (repeatOnLifecycle 사용 여부)

    • FaqScreen.ktMyPageScreen.kt: viewModel.collectAsState()를 사용하여 StateFlow를 수집하고 있습니다. collectAsState는 Jetpack Compose에서 repeatOnLifecycle과 유사하게 컴포저블의 생명주기를 고려하여 수집을 시작/중단하므로 적절한 사용입니다.
  • null 안전성 (!! 사용 지양)

    • FaqScreen.kt: state.itemsByCategory[selectedCategory].orEmpty()와 같이 null 안전 연산자를 잘 활용하여 !! 사용을 지양하고 있습니다. 좋습니다.
    • SettingSectionCard.kt: title: String?title: String으로 변경하고 if (title != null) 대신 if (title.isNotBlank())를 사용한 것은 좋은 개선입니다. 불필요한 nullability를 제거하고 빈 문자열을 더 명확하게 처리합니다.
  • data class에 불필요한 var 사용 여부

    • FaqItem.kt (question, answer): 모두 val로 선언되어 불변성을 유지하고 있습니다. 좋습니다.
    • FaqState.kt (itemsByCategory, loadStatus): 모두 val로 선언되어 불변성을 유지하고 있습니다. 좋습니다.
  • Hilt DI 시 생성자 주입 원칙 준수 여부

    • FaqRepositoryImpl.kt: @Inject constructor() - 생성자 주입을 따르고 있습니다.
    • FaqViewModel.kt: @Inject constructor(private val faqRepository: FaqRepository) - 생성자 주입을 따르고 있습니다.
    • RepositoryModule.kt: @Binds @Singleton abstract fun bindFaqRepository(...) - 인터페이스에 대한 구현체 바인딩도 올바르게 설정되었습니다.
  • 하드코딩된 문자열/매직 넘버

    • 매우 중요한 개선점:
      • FaqRepositoryImpl.kt: 모든 FAQ 질문과 답변이 하드코딩되어 있습니다. 실제 서비스에서는 이러한 정적인 데이터라도 string.xml 리소스나 로컬 JSON 파일, 또는 서버 API를 통해 관리하는 것이 바람직합니다. 특히 내용이 방대하고 변경될 가능성이 있다면 서버 API를 통해 관리해야 합니다. 현재 방식은 유지보수 및 다국어 지원에 심각한 문제를 야기합니다.
      • TermScreen.kt: 약관 내용 전체가 하드코딩되어 있습니다. FaqRepositoryImpl과 동일하게 string.xml 리소스 또는 서버 API를 통해 관리해야 합니다. /**TODO: 추후 실제 내용으로 변경 + 하드코딩된 글자들 모두 string.xml으로 분리 or api 처리*/ 주석이 있지만, PR 제출 시점에는 반영되어야 할 내용입니다.
      • FaqScreen.ktTermScreen.kt: CommonTopAppBartitle ("자주 하는 질문", "약관/개인정보 처리 방침"), FaqScreen의 로딩/에러/빈 데이터 메시지 ("불러오는 중...", "등록된 질문이 없습니다.", "네트워크 오류가 발생했습니다.") 등 UI에 표시되는 텍스트들은 모두 string.xml 리소스로 분리해야 합니다.
    • 개선 권장:
      • UI 관련 dp 값들 (16.dp, 20.dp, 32.dp 등): 현재는 직접 dp 값을 사용하고 있는데, Design System에 정의된 Dimen 또는 Spacing 객체 (예: CallTheme.dimens.spacingMedium)를 사용하여 일관성을 유지하는 것이 좋습니다.

2. Jetpack Compose 코드 리뷰

  • Composable 함수의 불필요한 recomposition 유발 여부 (remember, key 사용)

    • FaqScreen.kt: selectedCategoryremember로, expandedQuestionsrememberSaveablekey를 사용하여 상태를 올바르게 관리하고 있습니다. rememberSaveable(selectedCategory)를 통해 카테고리가 변경될 때 펼쳐진 질문 목록이 초기화되도록 한 점이 좋습니다.
    • FaqCategoryTabsFaqQnaItem에서 clickable 모디파이어에 interactionSource = remember { MutableInteractionSource() }를 사용한 것도 올바른 방법입니다.
    • 전반적으로 불필요한 recomposition을 유발하는 코드는 보이지 않습니다.
  • State hoisting 원칙 준수

    • FaqScreen.kt: FaqScreen Composable이 FaqScreenContent를 호출하면서 모든 UI 관련 상태(selectedCategory, items, expandedQuestions, loadStatus)와 이벤트 콜백(onSelectCategory, onToggleQuestion, onBackClick)을 하위 Composable로 전달하는 State Hoisting 패턴을 잘 따르고 있습니다. FaqScreenContent는 대부분 stateless Composable로 작동하여 재사용성과 테스트 용이성이 높습니다.
    • MyPageScreen.kt: navigateToFaq, navigateToTerms 콜백을 MyPageScreen 함수 파라미터로 받은 후, 이를 MyPageScreenContent에 전달하는 방식도 State Hoisting에 부합합니다.
  • side effect (LaunchedEffect, DisposableEffect) 사용의 적절성

    • 이 PR에서는 LaunchedEffectDisposableEffect가 직접적으로 사용되지는 않았습니다. FaqViewModel에서는 Orbit의 postSideEffect를 통해 FaqSideEffect.ShowError와 같은 단발성 이벤트를 처리하고, 이는 ViewModel에서 Compose UI로 전달되는 side effect를 관리하는 좋은 방법입니다.
  • UI 상태를 data class로 표현했는지

    • FaqState.kt: data class FaqState(...)로 UI 상태를 명확하게 표현하고 있습니다. 좋습니다.
  • 로딩/에러 상태를 Boolean 대신 LoadStatus로 관리하는지

    • FaqState.kt: LoadStatus (LoadStatus.Idle, LoadStatus.Loading, LoadStatus.Error) enum 클래스를 사용하여 로딩/에러 상태를 관리하고 있습니다. FaqScreen.kt에서 이 loadStatus에 따라 적절한 UI를 표시하는 방식도 올바릅니다.

3. Repository/DataSource 레이어

  • Retrofit 에러 핸들링 (try-catch, Result 래핑)

    • FaqRepositoryImpl.kt: 현재는 Result.success만 반환하고 있어 실제 네트워크 또는 데이터베이스 접근이 없습니다. 따라서 Retrofit 에러 핸들링 로직은 포함되어 있지 않습니다.
    • 만약 추후 실제 데이터 소스가 추가된다면, try-catch 블록으로 예외를 잡고 Result.failure로 래핑하여 FaqViewModel로 전달해야 합니다. 현재 FaqRepository 인터페이스가 Result를 반환하도록 설계되어 있어 확장성은 좋습니다.
  • 네트워크 응답과 도메인 모델 매핑 분리 여부

    • 데이터가 하드코딩되어 있어 해당 로직은 아직 없습니다. 실제 네트워크 통신 시, Data Layer의 DataSource에서 네트워크 응답(DTO)을 Domain Layer의 모델로 매핑하는 책임을 분리하는 것이 중요합니다.
  • 캐싱 전략 (로컬 DB vs 메모리)

    • 현재는 정적 데이터이므로 캐싱 전략은 없습니다. 만약 데이터가 동적이고 자주 변하지 않는다면, FaqRepositoryImpl 또는 하위 DataSource에서 로컬 DB (Room)나 메모리 캐시를 고려할 수 있습니다.

4. ViewModel

  • UI 상태와 비즈니스 로직 분리

    • FaqState.kt로 UI 상태를, FaqViewModel.ktloadFaqItems() 함수로 비즈니스 로직 (FAQ 아이템 불러오기)을 분리했습니다. FaqScreen Composable은 이 상태를 받아 UI를 렌더링하는 역할만 수행하여 잘 분리되어 있습니다.
  • Orbit ContainerHost 패턴 준수 여부

    • FaqViewModelContainerHost 인터페이스를 구현하고 container, intent, reduce, postSideEffect를 사용하여 Orbit 패턴을 올바르게 준수하고 있습니다.
  • intent 내부에서 상태 변경(reduce)만 수행하는지

    • loadFaqItems() intent 함수 내에서 reduce { state.copy(...) }를 통해 상태를 변경하고, postSideEffect를 통해 단발성 이벤트를 발생시켜 역할을 잘 분리하고 있습니다. 좋습니다.
  • postSideEffect 남용 여부

    • FaqViewModel에서는 에러 발생 시 postSideEffect(FaqSideEffect.ShowError(message))만 사용하고 있습니다. 이는 사용자에게 에러 메시지를 표시하는 단발성 UI 이벤트를 전달하는 데 적절하며, 남용으로 보이지 않습니다.
  • blocking 작업을 intent 내부에서 직접 수행하지 않는지

    • faqRepository.getFaqItems()suspend 함수이므로 intent 내부에서 blocking 작업으로 간주되지 않습니다. 앞서 언급했듯이, FaqRepositoryImpl의 실제 구현에서 I/O 작업을 Dispatchers.IO에서 처리하는 것이 중요합니다. 현재는 하드코딩 데이터라 문제가 없습니다.

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

  • 해당 PR에는 FCM/SSE/실시간 통신 관련 코드가 포함되어 있지 않으므로 검토 대상이 아닙니다.

기타 개선 사항

  1. RepositoryModule.kt - 파일 마지막 개행: \ No newline at end of file로 표시된 것처럼, 파일 마지막에 개행이 없는 경우가 있습니다. 모든 Kotlin 파일은 마지막에 개행 문자가 있는 것이 일반적인 컨벤션이며 Git에서도 깔끔하게 처리됩니다.
  2. ComingSoonScreen.kt 아이콘: ic_mypage_ticket에서 ic_chat_manager로 아이콘이 변경되었습니다. "ComingSoonScreen"이라면 어떤 기능이 곧 출시될지 암시하는 아이콘을 사용하거나, 더 범용적인 "준비 중" 아이콘을 사용하는 것이 의미 전달에 더 좋을 것 같습니다. tint = Color.Unspecified는 이미지에 따라 잘 적용되었습니다.
  3. 내비게이션 키 관리: MyPageRoute.ktFaqNavKey, TermNavKey를 추가하여 MyPage 도메인 내의 내비게이션 경로를 한 곳에서 관리하는 것은 좋습니다.
  4. MyPageScreenContent UI 간격: verticalArrangement = Arrangement.spacedBy(12.dp)20.dp로 변경된 점은 UI 디자인 가이드라인에 따른 변경으로 보이며 좋습니다.
  5. FaqCategoryTabs 접근성: 시맨틱스를 추가하여 각 카테고리 탭이 어떤 역할을 하는지 스크린 리더 등에 알려주는 것이 좋습니다. 예를 들어 Modifier.semantics { role = Role.Tab } 같은 것을 고려할 수 있습니다.
  6. TermScreen 내용: 약관/개인정보 처리 방침 내용은 매우 중요하므로, 실제로 서비스에 필요한 정확한 내용으로 채워져야 하며, 내용이 변경될 때마다 업데이트가 필요하다는 점을 명심해야 합니다.

결론 및 다음 단계

전반적으로 이 PR은 Jetpack Compose와 Orbit MVI 패턴을 사용하여 FAQ 및 약관 화면을 잘 구현했습니다. 특히 Composable의 State Hoisting과 LoadStatus를 사용한 UI 상태 관리가 인상적입니다.

가장 중요한 개선점은 하드코딩된 문자열 (특히 FAQ 내용과 약관 내용)을 string.xml 리소스 또는 서버 API를 통해 관리하도록 변경하는 것입니다. 이는 다국어 지원, 유지보수성, 그리고 향후 내용 변경에 대한 유연성 측면에서 필수적입니다.

이 외의 지적사항들은 주로 컨벤션이나 향후 확장을 위한 조언이므로, 하드코딩된 문자열 문제만 해결된다면 이 PR은 merge 될 수 있을 것 같습니다.

수정 후 다시 리뷰를 요청해주세요. 감사합니다!

@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.

아마 faq 관련 부분은 ViewModel까지 안 가고 문자열만 xml로 관리해서 컴포저블 단에서만 처리해도 될 것 같긴 한데 추후에 확장성까지 고려한다면 지금 방식이 맞을 수 도 있겠당 어프루브 할 게여~!!

Comment on lines +6 to +8
interface FaqRepository {
suspend fun getFaqItems(): Result<Map<FaqCategory, List<FaqItem>>>
} No newline at end of file

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.

아마 이게 "자주 하는 질문" 쪽 같은데, 음 api 호출이 없다면 나같으면 UseCase 썼을 듯? 레포지토리는 약간 실제 저장소와 통하는 창구 느낌이라고 생각해서..근데 머 크게 상관은 없는 듯 해 헤헷

Comment on lines +12 to +31
fun EntryProviderScope<NavKey>.myPageEntry(
navigateToFaq: () -> Unit,
navigateToTerms: () -> Unit,
onBack: () -> Unit = {},
) {
entry<MyPageNavKey> {
MyPageScreen()
MyPageScreen(
navigateToFaq = navigateToFaq,
navigateToTerms = navigateToTerms,
)
}
}

entry<FaqNavKey> {
FaqScreen(onBackClick = onBack)
}

entry<TermNavKey> {
TermScreen(onBackClick = onBack)
}
} No newline at end of file

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.

이런 식으로 나의 초기세팅을 사용 해 주니 뿌듯하네..

Column(modifier = modifier
.fillMaxSize()
.background(CallTheme.colors.background)
.statusBarsPadding()) {

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 코드리뷰

안녕하세요! 시니어 안드로이드 개발자로서 이번 PR에 대한 코드 리뷰를 진행하겠습니다. 전반적으로 Jetpack Compose, Orbit MVI, Hilt 등 최신 기술 스택을 활용하여 깔끔하게 잘 구현된 PR이라고 생각합니다. 특히 Compose의 상태 호이스팅, rememberSaveable을 통한 상태 유지, LoadStatus를 활용한 UI 상태 관리 등이 모범적으로 적용되어 있습니다.

다만, 몇 가지 개선할 점과 고려해야 할 사항들이 있으니 아래 내용을 참고하여 반영해 주시면 더욱 완성도 높은 코드가 될 것 같습니다.


🔍 전체 PR 요약

이번 PR은 '자주 하는 질문(FAQ)' 및 '약관/개인정보 처리 방침(Terms)' 화면을 추가하는 내용입니다.

  • 내비게이션: FaqNavKey, TermNavKey 추가 및 AppScreen, MainTabExt, MyPageEntryBuilder에서 관련 내비게이션 로직 업데이트.
  • 데이터 계층: FaqRepository 인터페이스와 FaqRepositoryImpl (하드코딩된 데이터 포함) 구현 및 Hilt DI 설정.
  • 도메인 계층: FaqCategory enum, FaqItem data class 정의.
  • UI 계층: FaqScreen, TermScreen 컴포저블 신규 추가 및 MyPageScreen, SettingSectionCard, ComingSoonScreen 일부 수정.
  • ViewModel 계층: Orbit MVI 패턴을 적용한 FaqViewModel 및 관련 FaqState, FaqSideEffect 정의.

코드 리뷰 상세

1. AppScreen.kt

  • showBottomBar 로직 (UX 확인 필요)
    • 리뷰: FaqNavKeyTermNavKeyshowBottomBartrue인 조건에 추가되었습니다. 즉, FAQ와 약관 화면에서 하단 탭 바가 표시됩니다. 일반적으로 마이페이지에서 진입하는 FAQ나 약관과 같은 상세 화면에서는 하단 탭 바를 숨기는 것이 일반적인 사용자 경험입니다. 화면의 집중도를 높이고 불필요한 내비게이션 요소를 제거하는 것이 좋습니다.
    • 개선 권고: 기획/UX 팀과 협의하여 해당 화면에서 하단 탭 바가 정말 필요한지 확인하고, 필요 없다면 해당 조건에서 제거하는 것을 권장합니다.
  • myPageEntry 인자 전달 (Good)
    • 리뷰: myPageEntry 컴포저블에 navigateToFaq, navigateToTerms, onBack 람다를 전달하여 내비게이션 로직을 상위 AppScreen에서 관리하도록 상태 호이스팅을 적용한 점은 매우 좋습니다.

2. MainTabExt.kt

  • toMainTab() 로직 (Consistent)
    • 리뷰: FaqNavKeyTermNavKeyMyPageNavKey와 함께 MainTab.MYPAGE로 매핑되는 것은 AppScreen에서의 bottom bar 표시 로직과 일관성이 있어 보입니다.

3. RepositoryModule.kt

  • Hilt 바인딩 (Good)
    • 리뷰: FaqRepositoryImplFaqRepository 인터페이스에 @Binds로 바인딩하고 @Singleton 스코프를 지정한 것은 Hilt DI의 표준적이고 올바른 사용 방식입니다.

4. FaqRepositoryImpl.kt (신규 파일)

  • 하드코딩된 FAQ 데이터 (Critical)
    • 리뷰: 모든 FAQ 질문과 답변이 FaqRepositoryImpl 내부에 하드코딩되어 있습니다. 이는 향후 FAQ 내용 변경 시 앱 업데이트 없이는 수정할 수 없으며, 다국어 지원이 불가능하다는 치명적인 단점이 있습니다.
    • 개선 권고:
      • 장기적으로: FAQ 내용은 서버 API를 통해 동적으로 받아오도록 구현하는 것이 가장 좋습니다. 이를 통해 앱 업데이트 없이 내용을 변경하거나 관리할 수 있습니다.
      • 단기적으로: 최소한 string.xml 리소스 파일에 정의하여 관리하거나, 로컬 JSON 파일 등에서 읽어오도록 하여 유지보수성을 높이는 것을 고려해 볼 수 있습니다.
  • Coroutine Dispatcher (미래 대비)
    • 리뷰: 현재는 하드코딩된 데이터를 반환하므로 Dispatchers 명시가 불필요하지만, 만약 getFaqItems가 네트워크 요청이나 DB 접근과 같은 실제 I/O 작업을 수행하게 된다면 withContext(Dispatchers.IO)를 사용하여 스레드 블로킹을 방지해야 합니다. suspend 키워드는 이러한 가능성을 암시하므로, 실제 작업이 추가될 때 Dispatchers.IO 사용을 잊지 않도록 TODO 주석을 남기는 것도 좋은 방법입니다.
  • Retrofit 에러 핸들링 (N/A, 하지만 미래 대비)
    • 리뷰: 현재 네트워크 통신이 없으므로 Result.success로 직접 반환하는 것은 문제가 없지만, 향후 Retrofit이 도입될 경우 Result 래핑 및 예외 처리(try-catch) 로직이 추가되어야 합니다.

5. FaqCategory.kt, FaqItem.kt (신규 파일)

  • 도메인 모델 (Good)
    • 리뷰: enum class FaqCategory(val label: String)data class FaqItem(val question: String="", val answer: String="",)은 도메인 모델을 명확하고 불변적으로 정의한 좋은 예시입니다. 불필요한 var 사용 없이 val만 사용한 점, 기본값을 통해 null 안전성을 높인 점 모두 좋습니다.

6. MyPageRoute.kt

  • 내비게이션 키 정의 (Good)
    • 리뷰: @Serializable data object를 사용하여 내비게이션 키를 정의한 것은 올바른 방식입니다.

7. SettingSectionCard.kt

  • title 파라미터 개선 (Good)
    • 리뷰: title: String?title: String = ""으로 변경하고, if (title.isNotBlank())으로 조건을 수정한 것은 null 안전성을 높이고 더욱 명시적인 코드로 개선된 좋은 변경입니다.

8. MyPageEntryBuilder.kt

  • 내비게이션 그래프 추가 (Good)
    • 리뷰: FaqScreenTermScreen을 내비게이션 그래프에 추가하고 onBackClick 람다를 적절히 전달한 것은 Compose Navigation 및 상태 호이스팅 원칙에 부합합니다.

9. ComingSoonScreen.kt

  • contentDescriptiontint (Good)
    • 리뷰: IconcontentDescription을 추가하여 접근성을 개선하고, tint = Color.Unspecified를 명시하여 아이콘 본연의 색상을 유지하도록 한 점은 좋은 개선입니다.

10. FaqScreen.kt (신규 파일)

  • Composable Recomposition 최적화 (Excellent)
    • 리뷰: selectedCategoryremember를, expandedQuestionsrememberSaveable (그리고 selectedCategory를 key로 사용)을 적절히 사용하여 UI 상태를 관리하고, 컴포저블의 불필요한 리컴포지션을 방지하며 설정 변경 시 상태를 보존하는 점은 매우 훌륭합니다. FaqCategoryTabsFaqQnaItem을 stateless 컴포저블로 분리하여 재활용성과 테스트 용이성을 높인 점도 좋습니다.
  • State Hoisting (Excellent)
    • 리뷰: FaqScreenContentFaqScreen의 상태를 모두 인자로 받아 처리하는 stateless 컴포저블로 분리한 것은 Compose의 핵심 원칙을 잘 따른 모범적인 예시입니다.
  • UI 상태 및 LoadStatus (Excellent)
    • 리뷰: UI 상태를 data class FaqState로 표현하고, 로딩/에러 상태를 LoadStatus enum으로 명확하게 관리하는 것은 Jetpack Compose와 Orbit MVI 패턴에서 권장하는 좋은 방법입니다.
  • 하드코딩된 문자열 (Critical)
    • 리뷰: "불러오는 중...", "등록된 질문이 없습니다.", "Q", "A" 등 사용자에게 보여지는 텍스트들이 하드코딩되어 있습니다. FaqRepositoryImpl의 내용과 함께 가장 시급하게 개선해야 할 부분입니다.
    • 개선 권고: 이 텍스트들을 string.xml 리소스 파일로 분리하여 관리해야 합니다.
  • MutableInteractionSource 사용 (Minor)
    • 리뷰: clickable modifier에 remember { MutableInteractionSource() }를 명시적으로 사용하는 것은 일반적으로 커스텀한 상호작용 피드백이 필요할 때 사용됩니다. 단순한 클릭 이벤트에 기본 ripple 효과만 필요하다면 생략해도 무방합니다. (성능상 큰 차이는 없지만 코드 간결성 측면에서).
    • 개선 권고: 만약 기본 ripple 외에 특별한 상호작용 디자인이 없다면, Modifier.clickable(onClick = onToggle)과 같이 간결하게 사용하는 것을 고려해 보세요.
  • statusBarsPadding() (Good)
    • 리뷰: statusBarsPadding()을 사용하여 시스템 UI와의 겹침을 방지하는 것은 좋습니다.
  • Preview Composables (Excellent)
    • 리뷰: 기본, 펼침, 빈 카테고리, 에러 상태 등 다양한 시나리오에 대한 프리뷰를 제공하여 UI 개발 및 테스트를 용이하게 한 점은 매우 훌륭합니다.

11. TermScreen.kt (신규 파일)

  • 하드코딩된 약관 내용 (Critical)
    • 리뷰: 약관 내용 전체가 하드코딩되어 있습니다. /**TODO: 추후 실제 내용으로 변경 + 하드코딩된 글자들 모두 string.xml으로 분리 or api 처리*/ 주석이 달려있지만, 이는 매우 중요한 문제입니다. 법적 효력을 가지는 약관은 빈번하게 변경될 수 있으며, 앱 업데이트 없이 내용을 관리할 수 있어야 합니다.
    • 개선 권고: FAQ와 마찬가지로, 약관 내용은 서버 API를 통해 동적으로 받아오거나, 최소한 HTML/Markdown 파일 등으로 분리하여 앱 리소스 형태로 관리해야 합니다.
  • statusBarsPadding() (Good)
    • 리뷰: statusBarsPadding() 사용은 적절합니다.

12. FaqSideEffect.kt (신규 파일)

  • SideEffect 정의 (Good)
    • 리뷰: data class ShowError(val message: String) : FaqSideEffect와 같이 단발성 UI 이벤트(예: Toast, Snackbar)를 처리하기 위한 SideEffect를 정의한 것은 Orbit MVI 패턴에 부합합니다.

13. FaqState.kt (신규 파일)

  • UI 상태 정의 (Good)
    • 리뷰: data class FaqState(...)를 사용하여 UI의 현재 상태를 명확하게 기술한 것은 좋습니다. LoadStatus를 사용한 점도 훌륭합니다.

14. FaqViewModel.kt (신규 파일)

  • Hilt DI (Good)
    • 리뷰: @HiltViewModel@Inject constructor를 사용하여 FaqRepository를 주입받은 것은 Hilt DI의 원칙을 잘 따른 것입니다.
  • Orbit ContainerHost 패턴 (Good)
    • 리뷰: ContainerHost 인터페이스 구현 및 container 초기화를 통해 Orbit MVI 패턴을 충실히 따른 점은 좋습니다.
  • intent 내부 상태 변경 (reduce) (Good)
    • 리뷰: intent 블록 내에서 reduce 함수를 통해 UI 상태를 불변적으로 업데이트하는 방식은 MVI 패턴의 핵심을 잘 구현한 것입니다. 로딩 시작, 성공, 실패 각 시점에 맞는 상태 변경이 이루어지고 있습니다.
  • postSideEffect 사용 (Appropriate)
    • 리뷰: onFailureFaqSideEffect.ShowErrorpostSideEffect로 발행하여 에러 메시지를 사용자에게 보여주는 것은 단발성 이벤트 처리에 적합한 SideEffect 활용입니다. 남용되지 않고 적절하게 사용되었습니다.
  • Blocking 작업 방지 (미래 대비)
    • 리뷰: 현재 faqRepository.getFaqItems()는 하드코딩된 데이터를 반환하므로 blocking 작업이 아닙니다. intent 블록은 기본적으로 Dispatchers.Default에서 실행되므로, 만약 getFaqItems가 실제 네트워크/DB I/O를 수행하게 된다면, Repository 내부에서 withContext(Dispatchers.IO)를 사용하여 I/O 작업을 백그라운드 스레드로 분리해야 합니다. ViewModel은 Repository의 추상화된 suspend 함수를 호출하는 것이므로, Repository 계층에서 이 부분을 잘 처리해주면 됩니다. 현재 코드상으로는 ViewModel이 직접 blocking 작업을 수행하지 않으므로 문제없습니다.

최종 요약 및 개선 권고

이번 PR은 기술적으로 많은 부분에서 모범 사례를 따르고 있으며, Jetpack Compose와 MVI 패턴을 잘 적용하여 높은 품질의 코드를 보여줍니다.

가장 시급하게 개선해야 할 부분은 하드코딩된 문자열 문제입니다. 특히 FAQ 내용과 약관 내용은 서버에서 받아오거나, 최소한 string.xml로 외부화하여 관리하는 것이 필수적입니다. 이 부분을 우선적으로 처리하는 것을 강력히 권장합니다.

Must Have (출시 전 반드시 수정):

  1. 하드코딩된 문자열 외부화:
    • FaqRepositoryImpl.kt: 모든 FAQ 질문/답변을 서버 API로 동적 로딩하거나, string.xml 또는 로컬 JSON 파일로 관리하도록 변경.
    • FaqScreen.kt: "불러오는 중...", "등록된 질문이 없습니다.", "Q", "A" 등 모든 UI 텍스트를 string.xml로 분리.
    • TermScreen.kt: 약관 내용을 서버 API로 동적 로딩하거나, string.xml 또는 HTML/Markdown 파일로 관리하도록 변경.

Should Have (가급적 수정 권고):

  1. Bottom Bar UX 확인: FaqNavKey, TermNavKey 화면에서 하단 탭 바 표시 여부에 대해 기획/UX 팀과 확인 후 필요 없다면 제거.

Could Have (개선하면 좋음):

  1. FaqRepositoryImpl Dispatcher 명시 (TODO 추가): 향후 실제 I/O 작업 추가 시 Dispatchers.IO 사용을 잊지 않도록 TODO 주석 추가.
  2. FaqQnaItem "Q", "A" Text contentDescription 고려: 스크린 리더 사용자를 위해 접근성 개선 여부 고려.
  3. MutableInteractionSource 간소화: 특별한 커스텀 상호작용 피드백이 없다면 clickableMutableInteractionSource 생략 고려.

이러한 피드백을 반영하면 더욱 견고하고 유지보수하기 쉬운 코드가 될 것입니다. 수고하셨습니다!

@codebidoof
codebidoof merged commit 5ff1f70 into develop Jul 25, 2026
2 checks passed
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 구현 - 고객지원

2 participants