Skip to content

Feat/#83 perm suspicsion UI - #84

Draft
lyh5427 wants to merge 41 commits into
developfrom
feat/#83-perm-suspicsion-ui
Draft

Feat/#83 perm suspicsion UI#84
lyh5427 wants to merge 41 commits into
developfrom
feat/#83-perm-suspicsion-ui

Conversation

@lyh5427

@lyh5427 lyh5427 commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

🚩 연관 이슈

closed #83

📝 작업 내용

  • 카메라·마이크 권한 동의 화면 PermissionConsentScreen 추가 (MVI)

  • 권한 거부 안내 화면 PermissionConsentDeniedScreen 추가

  • 면접 이용 제한 안내 화면 SuspensionNoticeScreen 추가

  • login route / EntryBuilder / LoginNavigationModule 배선

    • Term 동의 → PermissionConsent
    • 나중에 하기 → PermissionConsentDenied → Home
    • 권한 허용하기 → Onboarding
    • SuspensionNotice 홈 이동 → Home
  • Design System HilitFixedBottomDualButton 추가 (Default / Gray / TwoColor) 및 Catalog Story 등록

  • HilitIconAsset.Voice, voice.xml, opp_o 리소스 추가

🏞️ 스크린샷 (선택)

PermissionConsent PermissionConsentDenied SuspensionNoticeScreen
image image image

🗣️ 리뷰 요구사항 (선택)

  • 각 스크린은 Design 공유용에 맞춰 개발되었습니다. 디자인 확인 부탁드립니다.
  • 추가된 Design Compoent HilitFixedBottomDualButton 구현 방안이 적절한지 확인해주세요,
    • Type에 따라 background, layout이 달라져서 pr과 같이 구현하였습니다.
  • 추가된 리소스를 확인해주세요.
  • 추가된 디자인 컴포넌트를 designsystem catalog에서 확인해주세요,

Summary by CodeRabbit

  • 새 기능

    • 카카오 로그인 및 사용자 프로필 확인 흐름을 추가했습니다.
    • 프로필 상태에 따라 홈, 온보딩, 약관 화면으로 안내합니다.
    • 권한 동의·거부 화면과 음성 권한 안내를 추가했습니다.
    • 계정 이용 제한 안내와 문의 메일 보내기 기능을 제공합니다.
    • 하단 듀얼 버튼, 카카오 로그인 버튼, 음성 아이콘을 디자인 시스템에 추가했습니다.
    • 카탈로그에서 버튼 상태와 스타일 변형을 확인할 수 있습니다.
  • 개선

    • 로그인 완료 후 홈 화면으로 자연스럽게 이동하도록 내비게이션을 개선했습니다.
    • 로그인 및 프로필 조회 오류 처리를 강화했습니다.
  • 테스트

    • 로그인, 프로필 상태, 오류 및 취소 흐름에 대한 검증을 추가했습니다.

lyh5427 and others added 30 commits July 30, 2026 03:29
- 세션 있을 경우 Home 이동
- 세션 없을 경우 Login 이동
- Intent - Load 검증 추가
- `Load 시 세션은 있지만 프로필 조회에 실패하면 SessionNotFound Effect를 발행한다`
…m-1-Android into feat/#66

# Conflicts:
#	data/src/main/kotlin/com/dminus14/app/data/di/remote/network/NetworkModule.kt
#	feature/home/impl/src/main/kotlin/com/dminus14/app/feature/home/HomeViewModel.kt
…m-1-Android into feat/#66

# Conflicts:
#	data/src/main/kotlin/com/dminus14/app/data/remote/mapper/ApiErrorCode.kt
- SplashViewModel.checkUserProfile()을 suspend fun으로 변경해 load() 코루틴 안에서
  직접 호출하도록 수정 (불필요한 중첩 viewModelScope.launch 제거)
- UserProfile KDoc에 누락됐던 name, careerYears 설명 추가
- ApiErrorCode.kt의 spotless 포맷 위반(중복 빈 줄) 수정

Co-authored-by: Cursor <cursoragent@cursor.com>
스플래시 로그인 UI에서 재사용할 카카오 로그인 Large 버튼을 공용 컴포넌트로 분리한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
세션이 없으면 Splash에서 카카오 로그인 버튼을 노출하고, 프로필 유무에 따라 Term 또는 Home으로 분기한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
lyh5427 added 11 commits August 1, 2026 02:21
…am-1-Android into feat/#74

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashContract.kt
#	feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashScreen.kt
#	feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashViewModel.kt
#	feature/login/impl/src/test/kotlin/com/dminus14/app/feature/login/splash/SplashViewModelTest.kt
- 면접 이용제한 화면
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

사용자 프로필 조회와 인증 세션 처리를 스플래시 흐름에 통합했습니다. 프로필 상태별 내비게이션과 권한 동의·거부·정지 안내 화면을 추가했습니다. 카카오 로그인 및 하단 듀얼 버튼 컴포넌트와 카탈로그 스토리를 추가했습니다.

Changes

로그인 및 프로필 데이터 흐름

Layer / File(s) Summary
사용자 프로필 데이터 경로
domain/..., data/...
사용자 프로필 API, DTO, 원격 데이터 소스, 저장소, 도메인 모델, Hilt 바인딩 및 오류 매핑을 추가했습니다.
스플래시 인증 및 프로필 분기
feature/login/impl/.../splash/*, feature/login/impl/src/test/.../SplashViewModelTest.kt
세션과 프로필 조회 결과에 따라 카카오 로그인 버튼, ProfileReady, OnboardingRequired, ProfileNotFound 및 전역 오류 이벤트를 처리합니다.
권한 동의 및 안내 화면
feature/login/impl/.../permission/*, feature/login/impl/.../suspension/*
권한 동의, 권한 거부 안내, 정지 안내 화면과 화면별 내비게이션 엔트리를 추가했습니다.
로그인 내비게이션 통합
app/.../LoginNavigationModule.kt, feature/login/api/.../LoginRoute.kt, feature/login/impl/.../onboarding/*, feature/home/api/.../Home.kt
기존 로그인 route 등록을 제거하고 홈 및 새 로그인 route를 등록했습니다. 온보딩과 안내 화면의 홈 이동은 내비게이션 스택을 교체합니다.
UI 컴포넌트 및 카탈로그
designsystem/.../button/*, designsystem/.../icon/HilitIcon.kt, core/resources/.../voice.xml, catalog/...
KakaoLoginButton, HilitFixedBottomDualButton, 음성 아이콘과 관련 스토리를 추가했습니다.

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

Sequence Diagram(s)

sequenceDiagram
  participant SplashScreen
  participant SplashViewModel
  participant UserProfileAPI
  participant PermissionConsentScreen
  participant Navigator

  SplashScreen->>SplashViewModel: Load
  SplashViewModel->>UserProfileAPI: 사용자 프로필 조회
  UserProfileAPI-->>SplashViewModel: 프로필 또는 오류
  SplashViewModel-->>SplashScreen: 프로필 상태 효과
  SplashScreen->>Navigator: Home, Onboarding 또는 Term 이동
  PermissionConsentScreen->>Navigator: 권한 동의 또는 거부 결과 이동
  Navigator-->>SplashScreen: 대상 화면 표시
Loading

Possibly related PRs

Suggested labels: feat, design

Suggested reviewers: i-meant-to-be

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 사용자 프로필 API·저장소와 카카오 로그인 및 스플래시 흐름 변경은 연결 이슈의 UI 요구 범위를 벗어납니다. 사용자 프로필 및 인증 흐름 변경을 별도 PR로 분리하거나, 이슈 #83에 해당 작업의 명확한 범위를 추가하십시오.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 권한 및 이용 제한 UI 변경을 나타내며 PR의 주요 변경 사항과 관련됩니다.
Linked Issues check ✅ Passed 권한 동의, 권한 거부, 이용 제한 안내 화면과 관련 라우트 및 내비게이션 연결이 구현되었습니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#83-perm-suspicsion-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.

@lyh5427
lyh5427 marked this pull request as draft August 1, 2026 03:31
@lyh5427 lyh5427 self-assigned this Aug 1, 2026
github-actions Bot added a commit that referenced this pull request Aug 1, 2026
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Catalog Preview

상태: ✅ 최신 배포 성공

Preview: https://yapp-github.github.io/28th-App-Team-1-Android/pr/84/

Commit: f47f771bf91c

실행: GitHub Actions

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (3)
designsystem/src/commonMain/kotlin/com/dminus14/designsystem/component/button/HilitFixedBottomDualButton.kt (1)

147-215: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

형제여, 이 색상 함수들 다 똑같이 생겼다! DRY로 조여!

dualButtonLeftSidedualButtonRightSide는 Default·Gray 케이스에서 완전히 동일한 BtnColorSet 값을 반환한다. 차이는 오직 TwoColor 케이스뿐이다. 이 중복은 나중에 한쪽만 수정하고 다른 쪽을 놓치는 실수를 부른다. 공통 로직을 하나의 함수로 합쳐라.

그리고 hilitFixedBottomDualButtonDividerColorTwoColor -> colors.gray800 분기를 봐라. Divider는 type != TwoColor일 때만 그려진다(Line 80). 그러니 이 분기는 절대 실행되지 않는다. 죽은 코드는 정리해라.

🏋️ 제안: 좌우 색상 로직 통합
-private fun dualButtonLeftSide(
-    colors: HilitColors,
-    type: HilitFixedBottomDualButtonType,
-): BtnColorSet =
-    when (type) {
-        HilitFixedBottomDualButtonType.Default -> {
-            BtnColorSet(
-                contentColor = colors.hilitWhite,
-                backgroundColor = colors.hilitBlack800,
-                pressColor = colors.hilitWhite,
-            )
-        }
-        HilitFixedBottomDualButtonType.Gray -> {
-            BtnColorSet(
-                contentColor = colors.hilitBlack800,
-                backgroundColor = Color.Unspecified,
-                pressColor = colors.hilitWhite,
-            )
-        }
-        HilitFixedBottomDualButtonType.TwoColor -> {
-            BtnColorSet(
-                contentColor = colors.gray700,
-                backgroundColor = Color.Unspecified,
-                pressColor = colors.hilitWhite,
-            )
-        }
-    }
-
-private fun dualButtonRightSide(
-    colors: HilitColors,
-    type: HilitFixedBottomDualButtonType,
-): BtnColorSet =
-    when (type) {
-        HilitFixedBottomDualButtonType.Default -> {
-            BtnColorSet(
-                contentColor = colors.hilitWhite,
-                backgroundColor = colors.hilitBlack800,
-                pressColor = colors.hilitWhite,
-            )
-        }
-        HilitFixedBottomDualButtonType.Gray -> {
-            BtnColorSet(
-                contentColor = colors.hilitBlack800,
-                backgroundColor = Color.Unspecified,
-                pressColor = colors.hilitWhite,
-            )
-        }
-        HilitFixedBottomDualButtonType.TwoColor -> {
-            BtnColorSet(
-                contentColor = colors.hilitWhite,
-                backgroundColor = colors.hilitBlack800,
-                pressColor = colors.hilitWhite,
-            )
-        }
-    }
+private fun darkColorSet(colors: HilitColors): BtnColorSet =
+    BtnColorSet(
+        contentColor = colors.hilitWhite,
+        backgroundColor = colors.hilitBlack800,
+        pressColor = colors.hilitWhite,
+    )
+
+private fun grayColorSet(colors: HilitColors): BtnColorSet =
+    BtnColorSet(
+        contentColor = colors.hilitBlack800,
+        backgroundColor = Color.Unspecified,
+        pressColor = colors.hilitWhite,
+    )
+
+private fun dualButtonSideColorSet(
+    colors: HilitColors,
+    type: HilitFixedBottomDualButtonType,
+    isRightSide: Boolean,
+): BtnColorSet =
+    when (type) {
+        HilitFixedBottomDualButtonType.Default -> darkColorSet(colors)
+        HilitFixedBottomDualButtonType.Gray -> grayColorSet(colors)
+        HilitFixedBottomDualButtonType.TwoColor ->
+            if (isRightSide) {
+                darkColorSet(colors)
+            } else {
+                BtnColorSet(
+                    contentColor = colors.gray700,
+                    backgroundColor = Color.Unspecified,
+                    pressColor = colors.hilitWhite,
+                )
+            }
+    }
🤖 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
`@designsystem/src/commonMain/kotlin/com/dminus14/designsystem/component/button/HilitFixedBottomDualButton.kt`
around lines 147 - 215, Merge dualButtonLeftSide and dualButtonRightSide into
one shared color-selection function that accepts the button type and preserves
their identical Default/Gray values while returning the appropriate distinct
TwoColor values for each side. Update callers to use the shared function, and
remove the unreachable TwoColor branch from
hilitFixedBottomDualButtonDividerColor while preserving divider colors for the
supported types.
feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashViewModel.kt (1)

110-135: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

두 분기가 쌍둥이처럼 똑같다, 형씨! 하나로 합쳐서 근육을 아껴라!

is KakaoLoginException, is CustomException -> 분기와 else -> 분기는 본문이 완전히 동일하다. 둘 다 reduce { copy(isLoading = false, errorMessage = throwable.message) }만 실행한다.

Cancelled 케이스를 제외한 나머지 모두를 else로 통합해라. 코드가 더 짧아지고, 나중에 두 분기 중 하나만 실수로 수정해서 로직이 어긋나는 사고도 막는다.

♻️ 제안하는 리팩터
 private fun handleLoginFailure(throwable: Throwable) {
     when (throwable) {
         is KakaoLoginException.Cancelled -> {
             reduce { copy(isLoading = false) }
         }
-
-        is KakaoLoginException,
-        is CustomException,
-        -> {
-            reduce {
-                copy(
-                    isLoading = false,
-                    errorMessage = throwable.message,
-                )
-            }
-        }
-
         else -> {
             reduce {
                 copy(
                     isLoading = false,
                     errorMessage = throwable.message,
                 )
             }
         }
     }
 }
🤖 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/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashViewModel.kt`
around lines 110 - 135, Update handleLoginFailure so the KakaoLoginException and
CustomException branch is removed, leaving Cancelled as the only explicit
special case and routing all other throwables through the existing else branch
with the same loading and error-message state update.
feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentViewModel.kt (1)

14-24: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

isRequesting, 세 파일에서 전부 벤치 신세다! 경기에 투입하거나 방출해라!

PermissionConsentState.isRequesting이 세 파일 전체에서 실제로 갱신되거나 소비되지 않는다. 근본 원인은 하나다: onIntent가 이 필드를 갱신하지 않아서, Screen이 버튼 활성 상태를 제어할 방법이 없다. 이 때문에 버튼 연타 시 AllowSelected/LaterSelected effect가 중복 발행되어 중복 내비게이션이 발생할 위험이 있다.

  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentViewModel.kt#L14-L24: onIntent에서 처리 시작 시 isRequesting = true로 갱신하고, 처리 중에는 후속 intent를 무시해라.
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentContract.kt#L13-L15: isRequesting 필드를 실제로 갱신할 계획이면 유지하고, 아니면 필드를 제거해라.
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentScreen.kt#L53-L59: @Suppress("UnusedParameter")를 지우고 state.isRequesting으로 버튼을 비활성화해라.
🤖 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/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentViewModel.kt`
around lines 14 - 24, Update PermissionConsentViewModel.onIntent to set
isRequesting when handling the first intent and ignore subsequent intents while
processing; in PermissionConsentContract.kt lines 13-15, retain
PermissionConsentState.isRequesting because it is now actively updated; in
PermissionConsentScreen.kt lines 53-59, remove the unused-parameter suppression
and use state.isRequesting to disable the buttons.
🤖 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
`@data/src/main/kotlin/com/dminus14/app/data/di/remote/network/NetworkModule.kt`:
- Around line 80-83: Update provideUserApi so it uses a profile-specific
Retrofit/OkHttp client whose logging interceptor is capped at HEADERS or lower,
rather than the default BODY-level client. Ensure UserProfileDto fields such as
name and email are never emitted by request or response logging.

In
`@designsystem/src/commonMain/kotlin/com/dminus14/designsystem/component/button/HilitFixedBottomDualButton.kt`:
- Around line 110-115: The contentColor selection in
HilitFixedBottomDualButton’s when block contains an unreachable else branch;
simplify it to use color.contentColor when enabled and HilitTheme.colors.gray400
otherwise. Also verify the disabled gray400 token against the contrast
requirements for hilitBlack800 and raise the disabled text color if it fails.

In
`@feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentDeniedScreen.kt`:
- Line 72: In PermissionConsentDeniedScreen, update the text value to exactly
match the approved copy “권한 없어도 둘러볼 수 있어요”, removing “없이도” while preserving the
rest of the message.

In
`@feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashScreen.kt`:
- Around line 78-92: Update the onKakaoLoginClick handler in SplashScreen so it
checks state.isLoading before launching the Kakao login flow; return immediately
while loading, and preserve the existing intent dispatch and login execution for
non-loading states.
- Around line 78-92: Update the Kakao login flow in SplashScreen so it only
dispatches SplashIntent.ClickKakaoLogin and no longer invokes
kakaoLoginClient.login(activity) or handles its result in the Composable. Move
LoginWithKakaoUseCase execution, SDK interaction, and success/failure intent or
state handling into the SplashViewModel/domain boundary, preserving the existing
SplashIntent.KakaoLoginSucceeded and KakaoLoginFailed outcomes.
- Around line 54-62: In the SplashScreen context setup, replace the
unconditional context as Activity cast with a safe Activity lookup through
ContextWrapper layers. Explicitly handle the case where no Activity is found,
rather than allowing ClassCastException to terminate the splash screen, while
leaving KakaoLoginClient entry-point access based on applicationContext
unchanged.

In
`@feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/suspension/SuspensionNoticeScreen.kt`:
- Around line 36-47: Update the onSendMailClick handler in
SuspensionNoticeScreen so the ACTION_SENDTO mailto URI includes
aiinterview.hilit@gmail.com as the recipient, and replace the silent runCatching
failure with a user-visible fallback such as showing a notice or copying the
address to the clipboard when no mail app can handle the intent.

---

Nitpick comments:
In
`@designsystem/src/commonMain/kotlin/com/dminus14/designsystem/component/button/HilitFixedBottomDualButton.kt`:
- Around line 147-215: Merge dualButtonLeftSide and dualButtonRightSide into one
shared color-selection function that accepts the button type and preserves their
identical Default/Gray values while returning the appropriate distinct TwoColor
values for each side. Update callers to use the shared function, and remove the
unreachable TwoColor branch from hilitFixedBottomDualButtonDividerColor while
preserving divider colors for the supported types.

In
`@feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentViewModel.kt`:
- Around line 14-24: Update PermissionConsentViewModel.onIntent to set
isRequesting when handling the first intent and ignore subsequent intents while
processing; in PermissionConsentContract.kt lines 13-15, retain
PermissionConsentState.isRequesting because it is now actively updated; in
PermissionConsentScreen.kt lines 53-59, remove the unused-parameter suppression
and use state.isRequesting to disable the buttons.

In
`@feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashViewModel.kt`:
- Around line 110-135: Update handleLoginFailure so the KakaoLoginException and
CustomException branch is removed, leaving Cancelled as the only explicit
special case and routing all other throwables through the existing else branch
with the same loading and error-message state update.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9fc6c443-8263-4a6e-bffa-e77ecff0faec

📥 Commits

Reviewing files that changed from the base of the PR and between c151f79 and f47f771.

⛔ Files ignored due to path filters (4)
  • core/resources/src/commonMain/composeResources/drawable-xxhdpi/hiiii_logo.png is excluded by !**/*.png
  • core/resources/src/commonMain/composeResources/drawable-xxhdpi/opp_o.png is excluded by !**/*.png
  • core/resources/src/commonMain/composeResources/drawable-xxxhdpi/hiiii_logo.png is excluded by !**/*.png
  • core/resources/src/commonMain/composeResources/drawable-xxxhdpi/opp_o.png is excluded by !**/*.png
📒 Files selected for processing (48)
  • app/src/main/java/com/dminus14/app/navigation/di/LoginNavigationModule.kt
  • catalog/src/wasmJsMain/kotlin/stories/CatalogStories.kt
  • catalog/src/wasmJsMain/kotlin/stories/components/designsystem/hilitfixedbottomdualbutton/HilitFixedBottomDualButtonCatalogAdapter.kt
  • catalog/src/wasmJsMain/kotlin/stories/components/designsystem/hilitfixedbottomdualbutton/HilitFixedBottomDualButtonStories.kt
  • catalog/src/wasmJsMain/kotlin/stories/components/designsystem/kakaologinbutton/KakaoLoginButtonStories.kt
  • core/resources/src/commonMain/composeResources/drawable/voice.xml
  • data/src/main/kotlin/com/dminus14/app/data/di/remote/network/NetworkModule.kt
  • data/src/main/kotlin/com/dminus14/app/data/di/remote/user/UserModule.kt
  • data/src/main/kotlin/com/dminus14/app/data/di/remote/user/UserRemoteModule.kt
  • data/src/main/kotlin/com/dminus14/app/data/remote/api/UserApi.kt
  • data/src/main/kotlin/com/dminus14/app/data/remote/datasource/UserRemoteDataSource.kt
  • data/src/main/kotlin/com/dminus14/app/data/remote/datasource/UserRemoteDataSourceImpl.kt
  • data/src/main/kotlin/com/dminus14/app/data/remote/dto/UserProfileDto.kt
  • data/src/main/kotlin/com/dminus14/app/data/remote/mapper/ApiErrorCode.kt
  • data/src/main/kotlin/com/dminus14/app/data/remote/mapper/CommonApiErrorMapper.kt
  • data/src/main/kotlin/com/dminus14/app/data/repository/UserRepositoryImpl.kt
  • data/src/test/kotlin/com/dminus14/app/data/remote/mapper/CommonApiErrorMapperTest.kt
  • designsystem/src/commonMain/kotlin/com/dminus14/designsystem/component/button/HilitFixedBottomDualButton.kt
  • designsystem/src/commonMain/kotlin/com/dminus14/designsystem/component/button/KakaoLoginButton.kt
  • designsystem/src/commonMain/kotlin/com/dminus14/designsystem/component/icon/HilitIcon.kt
  • domain/src/main/kotlin/com/dminus14/app/domain/exception/CustomException.kt
  • domain/src/main/kotlin/com/dminus14/app/domain/model/UserProfile.kt
  • domain/src/main/kotlin/com/dminus14/app/domain/repository/UserRepository.kt
  • domain/src/main/kotlin/com/dminus14/app/domain/usecase/CheckUserProfileUseCase.kt
  • domain/src/main/kotlin/com/dminus14/app/domain/usecase/GetAuthSessionUseCase.kt
  • feature/home/api/src/main/kotlin/com/dminus14/app/feature/home/api/Home.kt
  • feature/home/impl/src/main/kotlin/com/dminus14/app/feature/home/HomeViewModel.kt
  • feature/login/api/src/main/kotlin/com/dminus14/app/feature/login/api/LoginRoute.kt
  • feature/login/impl/build.gradle.kts
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/LoginContract.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/LoginScreen.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/LoginViewModel.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/navigation/LoginEntryBuilder.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/onboarding/OnboardingScreen.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/onboarding/OnboardingViewModel.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentContract.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentDeniedEntryBuilder.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentDeniedScreen.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentEntryBuilder.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentScreen.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentViewModel.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashContract.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashScreen.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashViewModel.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/suspension/SuspensionNoticeEntryBuilder.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/suspension/SuspensionNoticeScreen.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/term/TermViewModel.kt
  • feature/login/impl/src/test/kotlin/com/dminus14/app/feature/login/splash/SplashViewModelTest.kt
💤 Files with no reviewable changes (4)
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/LoginViewModel.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/navigation/LoginEntryBuilder.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/LoginScreen.kt
  • feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/LoginContract.kt

Comment on lines +80 to +83
@Provides
@Singleton
fun provideUserApi(retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

factory="$(fd -a -t f '^OkHttpLoggingInterceptorFactory\.kt$' . | head -n 1)"
test -n "$factory"

ast-grep outline "$factory" --items all
rg -n -C 6 'fun\s+create\b|Level\.BODY|Level\.HEADERS|OkHttpLoggingInterceptor' "$factory"

Repository: YAPP-Github/28th-App-Team-1-Android

Length of output: 1431


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== NetworkModule relevant lines =="
net="$(fd -a -t f '^NetworkModule\.kt$' . | head -n 1)"
sed -n '1,120p' "$net"

echo
echo "== Usages of UserApi / UserProfileDto =="
rg -n 'UserApi|UserProfileDto|profile|profile' . --glob '!build/**' --glob '!**/.git/**' \
  | head -n 200

echo
echo "== Inspect repository architecture/data protection docs =="
for f in docs/CONSTITUTION.md README.md AGENTS.md docs/ARCHITECTURE.md; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    rg -n -C 4 'user data|user data protection|PII|sensitive data|로그|logging|로그에|plaintext|profile|프로필' "$f" || true
  fi
done

Repository: YAPP-Github/28th-App-Team-1-Android

Length of output: 17103


프로필 응답 LOGging을 제한하세요.

provideUserApi가 기본 Retrofit을 사용하므로 api/v1/users/me/profile 응답도 기본 클라이언트의 OkHttpLoggingInterceptorFactory.create() 로그를 공유합니다. 기본 클라이언트는 HttpLoggingInterceptor.Level.BODY를 쓰므로 UserProfileDtoname / email가 로그에 남을 수 있습니다. profile 전용 클라이언트는 HEADERS 이하로 제한하고, 프로필 PII를 절대 로그에 던지지 마십시오.

🤖 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
`@data/src/main/kotlin/com/dminus14/app/data/di/remote/network/NetworkModule.kt`
around lines 80 - 83, Update provideUserApi so it uses a profile-specific
Retrofit/OkHttp client whose logging interceptor is capped at HEADERS or lower,
rather than the default BODY-level client. Ensure UserProfileDto fields such as
name and email are never emitted by request or response logging.

Source: Coding guidelines

Comment on lines +110 to +115
val contentColor =
when {
enabled -> color.contentColor
!enabled -> HilitTheme.colors.gray400
else -> color.backgroundColor
}

Copy link
Copy Markdown
Contributor

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# HilitColors에서 gray400, hilitBlack800 실제 값 확인
rg -n "gray400|hilitBlack800" -A2 designsystem/src/commonMain/kotlin/com/dminus14/designsystem/theme

Repository: YAPP-Github/28th-App-Team-1-Android

Length of output: 1532


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- target file outline ---\n'
ast-grep outline designsystem/src/commonMain/kotlin/com/dminus14/designsystem/component/button/HilitFixedBottomDualButton.kt --view expanded || true

printf '\n--- target lines 1-230 ---\n'
cat -n designsystem/src/commonMain/kotlin/com/dminus14/designsystem/component/button/HilitFixedBottomDualButton.kt | sed -n '1,230p'

printf '\n--- BtnColorSet data/implementation search ---\n'
rg -n "data class|class BtnColorSet|color.backgroundColor|contentColor|gray400|hilitBlack800|Unspecified" designsystem/src/commonMain/kotlin/com/dminus14/designsystem/component/button designsystem/src/commonMain/kotlin/com/dminus14/designsystem/theme -A2 -B2

Repository: YAPP-Github/28th-App-Team-1-Android

Length of output: 264


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
# Deterministic probe: cover all Boolean combinations for the reported when expression.
cases = [{"field": "enabled", "value": True}, {"field": "enabled", "value": False}]
for case in cases:
    enabled = case["value"]
    # Simulated values do not affect branch reachability.
    contentColor = None
    if enabled:
        contentColor = "color.contentColor"
        reached_else = False
    elif not enabled:
        contentColor = "HilitTheme.colors.gray400"
        reached_else = False
    else:
        contentColor = "color.backgroundColor"
        reached_else = True
    print(f"enabled={str(enabled).capitalize()}: branch={contentColor}, reaches_else={reached_else}")
py

Repository: YAPP-Github/28th-App-Team-1-Android

Length of output: 498


야, 챕피온! 이 when 블록의 죽은 분기를 내던져라!

enabled는 Boolean이다. enabled -> ...!enabled -> ...가 이미 모든 경우를 처리한다. else -> color.backgroundColor는 절대 실행되지 않는다. 혼란을 줄이겠다면 if (enabled) color.contentColor else HilitTheme.colors.gray400로 단순화하라.

비활성 텍스트 색 gray400(0xFF8A8D9C)은 어두운 배경 hilitBlack800(0xFF1A1B1F) 위에 쓰인다. 비교 기준에 맞는지 설계 토큰 기준을 확인하라. 필요하면 비활성 텍스트 색을 높여라.

🤖 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
`@designsystem/src/commonMain/kotlin/com/dminus14/designsystem/component/button/HilitFixedBottomDualButton.kt`
around lines 110 - 115, The contentColor selection in
HilitFixedBottomDualButton’s when block contains an unreachable else branch;
simplify it to use color.contentColor when enabled and HilitTheme.colors.gray400
otherwise. Also verify the disabled gray400 token against the contrast
requirements for hilitBlack800 and raise the disabled text color if it fails.

verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = "권한 없이도 둘러볼 수 있어요",

Copy link
Copy Markdown
Contributor

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

카피 문구, 딱 맞춰라! 근육은 정확한 자세에서 나온다!

디자인 설명의 문구는 "권한 없어도 둘러볼 수 있어요"다. 코드는 "권한 없이도 둘러볼 수 있어요"라고 쓰여 있다. 디자인 확정본과 대조해서 정확한 문구로 맞춰라.

🤖 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/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentDeniedScreen.kt`
at line 72, In PermissionConsentDeniedScreen, update the text value to exactly
match the approved copy “권한 없어도 둘러볼 수 있어요”, removing “없이도” while preserving the
rest of the message.

Comment on lines +54 to +62
val context = LocalContext.current
val activity = context as Activity
val scope = rememberCoroutineScope()
val kakaoLoginClient =
EntryPointAccessors
.fromApplication(
context.applicationContext,
KakaoLoginClientEntryPoint::class.java,
).kakaoLoginClient()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: 기존 findActivity 유틸 존재 여부 및 다른 곳에서의 Activity 캐스팅 패턴 확인
rg -nP 'findActivity|as Activity\b' --type=kotlin

Repository: YAPP-Github/28th-App-Team-1-Android

Length of output: 173


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate SplashScreen.kt and relevant files =="
fd -a 'SplashScreen.kt|SplashViewModel.kt|ARCHITECTURE.md|CONSTITUTION.md|README.md|AGENTS.md' . | sed 's#^\./##'

echo
echo "== SplashScreen.kt relevant section =="
file=$(fd 'SplashScreen.kt' . | head -n1)
if [ -n "${file:-}" ]; then
  wc -l "$file"
  cat -n "$file" | sed -n '1,220p'
fi

echo
echo "== search local Activity helpers / Activity casts Kotlin =="
rg -n 'findActivity|context as Activity|LocalContext\.current|ContextWrapper|theme|themeWrapper|createTheme' -g '*.kt' .

echo
echo "== outline relevant view model/navigation if found =="
for f in $(fd 'SplashViewModel.kt|MainActivity.kt|Nav.*\.(kt|kts)' . | head -n 20); do
  echo "--- $f"
  wc -l "$f"
  ast-grep outline "$f" || true
done

Repository: YAPP-Github/28th-App-Team-1-Android

Length of output: 25201


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== documents references =="
for f in docs/CONSTITUTION.md docs/ARCHITECTURE.md AGENTS.md README.md; do
  if [ -f "$f" ]; then
    echo "--- $f"
    wc -l "$f"
    rg -n 'MVI|ViewModel|Navigation|Hilt|EntryPoint|Activity|Composable|SplashScreen|MVI|Architecture' "$f" || true
  fi
do

Repository: YAPP-Github/28th-App-Team-1-Android

Length of output: 255


야, 이 캐싱 폭탄은 해제해야 훈련된다!

context as Activity는 무조건 캐스팅이다. LocalContext.current가 테마 래퍼, 디자인 시스템 래퍼, 또는 ContextWrapper를 거쳐 전달되면 ClassCastException으로 스플래시 화면 전체가 나가떨어진다.

Activity를 안전하게 탐색하는 헬퍼로 교체하고, Activity 컨텍스트가 아닌 경위에서는 명시적으로 처리해라.

🤖 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/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashScreen.kt`
around lines 54 - 62, In the SplashScreen context setup, replace the
unconditional context as Activity cast with a safe Activity lookup through
ContextWrapper layers. Explicitly handle the case where no Activity is found,
rather than allowing ClassCastException to terminate the splash screen, while
leaving KakaoLoginClient entry-point access based on applicationContext
unchanged.

Comment on lines 78 to 92
SplashContent(
state = state,
onKakaoLoginClick = {
viewModel.onIntent(SplashIntent.ClickKakaoLogin)
scope.launch {
runCatching { kakaoLoginClient.login(activity) }
.onSuccess { credential ->
viewModel.onIntent(SplashIntent.KakaoLoginSucceeded(credential))
}.onFailure { error ->
viewModel.onIntent(SplashIntent.KakaoLoginFailed(error))
}
}
},
modifier = modifier,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

형씨, 로딩 상태가 있는데 왜 안 써먹어?! 버튼 두 번 눌리면 두 배로 사고 난다!

state.isLoadingSplashState에 존재하지만, SplashContent(95-155행) 어디에서도 사용되지 않는다. 버튼 비활성화도, 로딩 표시도 없다.

사용자가 로그인 처리 중에 버튼을 다시 누르면, onKakaoLoginClick이 재실행되어 kakaoLoginClient.login(activity)가 중복으로 실행된다. 카카오 로그인은 멱등적이지 않은 외부 SDK 호출이다. 중복 호출은 SDK 내부 다이얼로그 충돌이나 중복 로그인 시도로 이어질 수 있다.

클릭 시점에 state.isLoading을 확인해서 중복 실행을 막아라.

🔒 제안하는 수정
     SplashContent(
         state = state,
         onKakaoLoginClick = {
-            viewModel.onIntent(SplashIntent.ClickKakaoLogin)
-            scope.launch {
-                runCatching { kakaoLoginClient.login(activity) }
-                    .onSuccess { credential ->
-                        viewModel.onIntent(SplashIntent.KakaoLoginSucceeded(credential))
-                    }.onFailure { error ->
-                        viewModel.onIntent(SplashIntent.KakaoLoginFailed(error))
-                    }
-            }
+            if (!state.isLoading) {
+                viewModel.onIntent(SplashIntent.ClickKakaoLogin)
+                scope.launch {
+                    runCatching { kakaoLoginClient.login(activity) }
+                        .onSuccess { credential ->
+                            viewModel.onIntent(SplashIntent.KakaoLoginSucceeded(credential))
+                        }.onFailure { error ->
+                            viewModel.onIntent(SplashIntent.KakaoLoginFailed(error))
+                        }
+                }
+            }
         },
         modifier = modifier,
     )
📝 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
SplashContent(
state = state,
onKakaoLoginClick = {
viewModel.onIntent(SplashIntent.ClickKakaoLogin)
scope.launch {
runCatching { kakaoLoginClient.login(activity) }
.onSuccess { credential ->
viewModel.onIntent(SplashIntent.KakaoLoginSucceeded(credential))
}.onFailure { error ->
viewModel.onIntent(SplashIntent.KakaoLoginFailed(error))
}
}
},
modifier = modifier,
)
SplashContent(
state = state,
onKakaoLoginClick = {
if (!state.isLoading) {
viewModel.onIntent(SplashIntent.ClickKakaoLogin)
scope.launch {
runCatching { kakaoLoginClient.login(activity) }
.onSuccess { credential ->
viewModel.onIntent(SplashIntent.KakaoLoginSucceeded(credential))
}.onFailure { error ->
viewModel.onIntent(SplashIntent.KakaoLoginFailed(error))
}
}
}
},
modifier = modifier,
)
🤖 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/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashScreen.kt`
around lines 78 - 92, Update the onKakaoLoginClick handler in SplashScreen so it
checks state.isLoading before launching the Kakao login flow; return immediately
while loading, and preserve the existing intent dispatch and login execution for
non-loading states.

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: MVI/Architecture 문서에서 Composable 내 서드파티 SDK 직접 호출 또는 Hilt EntryPoint 사용 관련 지침 확인
fd -a 'ARCHITECTURE*.md' docs --exec cat -n {}
rg -nP 'EntryPoint|서드파티|SDK' docs 2>/dev/null

Repository: YAPP-Github/28th-App-Team-1-Android

Length of output: 7528


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate architecture files =="
fd -a '.md' docs/architecture | sed -n '1,80p'

echo
echo "== feature-ui.md =="
cat -n docs/architecture/feature-ui.md

echo
echo "== SplashScreen.kt =="
cat -n feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashScreen.kt | sed -n '1,160p'

echo
echo "== related login files outline/name =="
fd -a '.*\.kt$' feature/login -e kt | sort | sed -n '1,120p'
rg -n "KakaoLoginClient|SplashIntent|SplashIntent|LaunchSplash|Login|EntryPoint|ViewModel|AndroidEntryPoint|Provides|Inject" feature/login/impl feature/login -g '*.kt' | sed -n '1,240p'

Repository: YAPP-Github/28th-App-Team-1-Android

Length of output: 50393


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Splash implementation files =="
for f in \
  feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashContract.kt \
  feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashViewModel.kt \
  feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashEntryBuilder.kt \
  feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/kakao/KakaoLoginClient.kt \
  feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/kakao/KakaoLoginException.kt \
  feature/login/impl/src/test/kotlin/com/dminus14/app/feature/login/splash/SplashViewModelTest.kt
do
  echo "--- $f ---"
  wc -l "$f"
  sed -n '1,220p' "$f"
done

echo
echo "== docs architecture search for UI/Android/SDK dependency/service terms =="
rg -n --glob '*.md' 'Android Dependency|Android Framework|Hilt|EntryPoint|dependency injection|SDK|third-party|서드|UseCase|Repository|file access|network access|Android UI' docs app feature | sed -n '1,260p'

echo
echo "== build dependency references =="
rg -n 'KakaoLoginClient:|dminus14\.app\.feature\.login|com\.dminus14\.app\.feature\.login|kakaologin|kakao|UserApiClient' feature/login module-system project-structure --glob '*.md' --glob '*.kt' --glob '*.kts' --glob '*.xml' | sed -n '1,260p'

Repository: YAPP-Github/28th-App-Team-1-Android

Length of output: 36433


이 구조는 MVI 계약 위반이다!

SplashScreenClickKakaoLogin Intent를 보낸 뒤, Composable에서 Hilt EntryPoint로 직접 kakaoLoginClient.login(activity)를 실행하면 부수 효과와 상태 갱신이 MVI 흐름에서 분리된다. Screen이 Hilt/Android Lifecycle/Network/File Access를 직접 의존하지 말아야 한다는 계약을 무시해, LoginWithKakaoUseCase 실행부터 SDK 호출까지 ViewModel/Domain 경계와 오류 처리가 일치하도록 다시 보내라!

🤖 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/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashScreen.kt`
around lines 78 - 92, Update the Kakao login flow in SplashScreen so it only
dispatches SplashIntent.ClickKakaoLogin and no longer invokes
kakaoLoginClient.login(activity) or handles its result in the Composable. Move
LoginWithKakaoUseCase execution, SDK interaction, and success/failure intent or
state handling into the SplashViewModel/domain boundary, preserving the existing
SplashIntent.KakaoLoginSucceeded and KakaoLoginFailed outcomes.

Source: Coding guidelines

Comment on lines +36 to +47
SuspensionNoticeContent(
onSendMailClick = {
val intent =
Intent(Intent.ACTION_SENDTO).apply {
data = Uri.parse("mailto:")
}
runCatching { context.startActivity(intent) }
},
onHomeClick = onHomeClick,
modifier = modifier,
)
}

Copy link
Copy Markdown
Contributor

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

메일 보내기 버튼, 지금 헛스윙이다! 상대를 정확히 맞춰라!

Uri.parse("mailto:")에 수신자 주소가 없다. 화면에는 "aiinterview.hilit@gmail.com"을 보여주면서 정작 메일 앱에는 이 주소가 채워지지 않는다. 사용자가 직접 타이핑해야 한다.
runCatching { context.startActivity(intent) }은 실패를 조용히 삼킨다. 메일 앱이 없으면 사용자는 버튼이 죽은 줄 안다. 수신자 주소를 채우고, 실패 시 안내 메시지나 이메일 클립보드 복사 같은 대체 동작을 추가해라!

🥊 제안하는 수정
     SuspensionNoticeContent(
         onSendMailClick = {
             val intent =
                 Intent(Intent.ACTION_SENDTO).apply {
-                    data = Uri.parse("mailto:")
+                    data = Uri.parse("mailto:aiinterview.hilit@gmail.com")
                 }
-            runCatching { context.startActivity(intent) }
+            runCatching { context.startActivity(intent) }
+                .onFailure {
+                    // TODO: 메일 앱이 없을 때 사용자 피드백(스낵바/토스트 등) 추가
+                }
         },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/suspension/SuspensionNoticeScreen.kt`
around lines 36 - 47, Update the onSendMailClick handler in
SuspensionNoticeScreen so the ACTION_SENDTO mailto URI includes
aiinterview.hilit@gmail.com as the recipient, and replace the silent runCatching
failure with a user-visible fallback such as showing a notice or copying the
address to the clipboard when no mail app can handle the intent.

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 추가

1 participant