Feat/#83 perm suspicsion UI - #84
Conversation
- 세션 있을 경우 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>
…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
- 면접 이용제한 화면
📝 WalkthroughWalkthrough사용자 프로필 조회와 인증 세션 처리를 스플래시 흐름에 통합했습니다. 프로필 상태별 내비게이션과 권한 동의·거부·정지 안내 화면을 추가했습니다. 카카오 로그인 및 하단 듀얼 버튼 컴포넌트와 카탈로그 스토리를 추가했습니다. Changes로그인 및 프로필 데이터 흐름
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: 대상 화면 표시
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Catalog Preview상태: ✅ 최신 배포 성공 Preview: https://yapp-github.github.io/28th-App-Team-1-Android/pr/84/ Commit: 실행: GitHub Actions |
There was a problem hiding this comment.
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로 조여!
dualButtonLeftSide와dualButtonRightSide는 Default·Gray 케이스에서 완전히 동일한BtnColorSet값을 반환한다. 차이는 오직 TwoColor 케이스뿐이다. 이 중복은 나중에 한쪽만 수정하고 다른 쪽을 놓치는 실수를 부른다. 공통 로직을 하나의 함수로 합쳐라.그리고
hilitFixedBottomDualButtonDividerColor의TwoColor -> 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/LaterSelectedeffect가 중복 발행되어 중복 내비게이션이 발생할 위험이 있다.
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
⛔ Files ignored due to path filters (4)
core/resources/src/commonMain/composeResources/drawable-xxhdpi/hiiii_logo.pngis excluded by!**/*.pngcore/resources/src/commonMain/composeResources/drawable-xxhdpi/opp_o.pngis excluded by!**/*.pngcore/resources/src/commonMain/composeResources/drawable-xxxhdpi/hiiii_logo.pngis excluded by!**/*.pngcore/resources/src/commonMain/composeResources/drawable-xxxhdpi/opp_o.pngis excluded by!**/*.png
📒 Files selected for processing (48)
app/src/main/java/com/dminus14/app/navigation/di/LoginNavigationModule.ktcatalog/src/wasmJsMain/kotlin/stories/CatalogStories.ktcatalog/src/wasmJsMain/kotlin/stories/components/designsystem/hilitfixedbottomdualbutton/HilitFixedBottomDualButtonCatalogAdapter.ktcatalog/src/wasmJsMain/kotlin/stories/components/designsystem/hilitfixedbottomdualbutton/HilitFixedBottomDualButtonStories.ktcatalog/src/wasmJsMain/kotlin/stories/components/designsystem/kakaologinbutton/KakaoLoginButtonStories.ktcore/resources/src/commonMain/composeResources/drawable/voice.xmldata/src/main/kotlin/com/dminus14/app/data/di/remote/network/NetworkModule.ktdata/src/main/kotlin/com/dminus14/app/data/di/remote/user/UserModule.ktdata/src/main/kotlin/com/dminus14/app/data/di/remote/user/UserRemoteModule.ktdata/src/main/kotlin/com/dminus14/app/data/remote/api/UserApi.ktdata/src/main/kotlin/com/dminus14/app/data/remote/datasource/UserRemoteDataSource.ktdata/src/main/kotlin/com/dminus14/app/data/remote/datasource/UserRemoteDataSourceImpl.ktdata/src/main/kotlin/com/dminus14/app/data/remote/dto/UserProfileDto.ktdata/src/main/kotlin/com/dminus14/app/data/remote/mapper/ApiErrorCode.ktdata/src/main/kotlin/com/dminus14/app/data/remote/mapper/CommonApiErrorMapper.ktdata/src/main/kotlin/com/dminus14/app/data/repository/UserRepositoryImpl.ktdata/src/test/kotlin/com/dminus14/app/data/remote/mapper/CommonApiErrorMapperTest.ktdesignsystem/src/commonMain/kotlin/com/dminus14/designsystem/component/button/HilitFixedBottomDualButton.ktdesignsystem/src/commonMain/kotlin/com/dminus14/designsystem/component/button/KakaoLoginButton.ktdesignsystem/src/commonMain/kotlin/com/dminus14/designsystem/component/icon/HilitIcon.ktdomain/src/main/kotlin/com/dminus14/app/domain/exception/CustomException.ktdomain/src/main/kotlin/com/dminus14/app/domain/model/UserProfile.ktdomain/src/main/kotlin/com/dminus14/app/domain/repository/UserRepository.ktdomain/src/main/kotlin/com/dminus14/app/domain/usecase/CheckUserProfileUseCase.ktdomain/src/main/kotlin/com/dminus14/app/domain/usecase/GetAuthSessionUseCase.ktfeature/home/api/src/main/kotlin/com/dminus14/app/feature/home/api/Home.ktfeature/home/impl/src/main/kotlin/com/dminus14/app/feature/home/HomeViewModel.ktfeature/login/api/src/main/kotlin/com/dminus14/app/feature/login/api/LoginRoute.ktfeature/login/impl/build.gradle.ktsfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/LoginContract.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/LoginScreen.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/LoginViewModel.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/navigation/LoginEntryBuilder.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/onboarding/OnboardingScreen.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/onboarding/OnboardingViewModel.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentContract.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentDeniedEntryBuilder.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentDeniedScreen.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentEntryBuilder.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentScreen.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/permission/PermissionConsentViewModel.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashContract.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashScreen.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/splash/SplashViewModel.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/suspension/SuspensionNoticeEntryBuilder.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/suspension/SuspensionNoticeScreen.ktfeature/login/impl/src/main/kotlin/com/dminus14/app/feature/login/term/TermViewModel.ktfeature/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
| @Provides | ||
| @Singleton | ||
| fun provideUserApi(retrofit: Retrofit): UserApi = retrofit.create(UserApi::class.java) | ||
|
|
There was a problem hiding this comment.
🔒 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
doneRepository: YAPP-Github/28th-App-Team-1-Android
Length of output: 17103
프로필 응답 LOGging을 제한하세요.
provideUserApi가 기본 Retrofit을 사용하므로 api/v1/users/me/profile 응답도 기본 클라이언트의 OkHttpLoggingInterceptorFactory.create() 로그를 공유합니다. 기본 클라이언트는 HttpLoggingInterceptor.Level.BODY를 쓰므로 UserProfileDto의 name / 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
| val contentColor = | ||
| when { | ||
| enabled -> color.contentColor | ||
| !enabled -> HilitTheme.colors.gray400 | ||
| else -> color.backgroundColor | ||
| } |
There was a problem hiding this comment.
🎯 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/themeRepository: 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 -B2Repository: 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}")
pyRepository: 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 = "권한 없이도 둘러볼 수 있어요", |
There was a problem hiding this comment.
🎯 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.
| val context = LocalContext.current | ||
| val activity = context as Activity | ||
| val scope = rememberCoroutineScope() | ||
| val kakaoLoginClient = | ||
| EntryPointAccessors | ||
| .fromApplication( | ||
| context.applicationContext, | ||
| KakaoLoginClientEntryPoint::class.java, | ||
| ).kakaoLoginClient() |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: 기존 findActivity 유틸 존재 여부 및 다른 곳에서의 Activity 캐스팅 패턴 확인
rg -nP 'findActivity|as Activity\b' --type=kotlinRepository: 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
doneRepository: 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
doRepository: 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.
| 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, | ||
| ) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
형씨, 로딩 상태가 있는데 왜 안 써먹어?! 버튼 두 번 눌리면 두 배로 사고 난다!
state.isLoading은 SplashState에 존재하지만, 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.
| 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/nullRepository: 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 계약 위반이다!
SplashScreen이 ClickKakaoLogin 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
| SuspensionNoticeContent( | ||
| onSendMailClick = { | ||
| val intent = | ||
| Intent(Intent.ACTION_SENDTO).apply { | ||
| data = Uri.parse("mailto:") | ||
| } | ||
| runCatching { context.startActivity(intent) } | ||
| }, | ||
| onHomeClick = onHomeClick, | ||
| modifier = modifier, | ||
| ) | ||
| } |
There was a problem hiding this comment.
🎯 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.
🚩 연관 이슈
closed #83
📝 작업 내용
카메라·마이크 권한 동의 화면
PermissionConsentScreen추가 (MVI)권한 거부 안내 화면
PermissionConsentDeniedScreen추가면접 이용 제한 안내 화면
SuspensionNoticeScreen추가login route / EntryBuilder /
LoginNavigationModule배선Design System
HilitFixedBottomDualButton추가 (Default / Gray / TwoColor) 및 Catalog Story 등록HilitIconAsset.Voice,voice.xml,opp_o리소스 추가🏞️ 스크린샷 (선택)
🗣️ 리뷰 요구사항 (선택)
HilitFixedBottomDualButton구현 방안이 적절한지 확인해주세요,background,layout이 달라져서 pr과 같이 구현하였습니다.Summary by CodeRabbit
새 기능
개선
테스트