Skip to content

Feature/#76 landing - #78

Merged
lluke0 merged 5 commits into
developfrom
feature/#76-landing
Mar 16, 2026
Merged

Feature/#76 landing#78
lluke0 merged 5 commits into
developfrom
feature/#76-landing

Conversation

@lluke0

@lluke0 lluke0 commented Mar 14, 2026

Copy link
Copy Markdown
Member

📌 PR 요약

🌱 작업한 내용

🌱 PR 포인트

📸 스크린샷

스크린샷
파일첨부바람

📮 관련 이슈

  • Resolved: #이슈번호

RCA 룰을 사용하여 코드 리뷰를 해주세요

R (Request Changes) : 적극적으로 반영을 고려해주세요
C (Comment) : 웬만하면 반영해주세요
A (Approve) : 반영해도 좋고, 넘어가도 좋습니다. 사소한 의견입니다.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 시작 시 스플래시 스크린 추가
    • 랜딩 화면 및 카카오/애플 소셜 로그인 화면 추가
    • 토큰 자동 갱신(세션 유지) 기능 추가
  • 개선사항

    • 디버그 시 HTTP 요청/응답 로깅 추가
    • 로그인 관련 아이콘·리소스 추가
  • 변경

    • 웹뷰 가이드에서 플랫폼별 로그인 버튼 제거 (UI 단순화)

lluke0 and others added 4 commits March 14, 2026 15:43
- Android SplashScreen API(core-splashscreen) 추가, startDestination 결정 전까지 Splash 유지
- App.kt startDestination을 nullable state로 변경하여 NavHost 조건부 렌더링
- Token refresh API 호출 (POST api/auth/refresh) 및 응답 모델 추가
- CheckAuthStateUseCase 구현: 토큰 존재 확인 → refresh → 에러 타입별 분기 처리
- ApiException 시 토큰 삭제, 네트워크 에러 시 토큰 유지 정책 적용

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Landing 화면 MVI 구현 (Contract, ViewModel, Screen)
- 카카오 로그인 연동 및 애플 로그인 준비 중 토스트 (iOS only)
- CheckAuthStateUseCase 기반 startDestination 동적 결정
- Landing → Home 네비게이션 (popUpTo inclusive)
- WebViewGuideScreen 로그인 버튼 제거
- Refresh API 엔드포인트 경로 수정 (api/ 중복 제거)
- HttpClient request body pretty-print 로깅 추가

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CchBasicButton을 아이콘 포함 SocialLoginButton으로 교체하고,
카카오(노란 배경 + 말풍선 로고) / 애플(검정 배경 + 애플 로고)
표준 디자인 가이드라인에 맞게 구현

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lluke0
lluke0 requested review from BEEEAM-J and kimmandoo March 14, 2026 07:43
@lluke0 lluke0 self-assigned this Mar 14, 2026
@coderabbitai

coderabbitai Bot commented Mar 14, 2026

Copy link
Copy Markdown

Walkthrough

랜딩 화면의 소셜 로그인 UI와 관련 뷰모델/네비게이션을 추가하고, 앱 시작 시 스플래시 화면을 유지하며 인증 상태(리프레시 토큰을 이용한 갱신)를 확인해 시작 목적지(startDestination)를 결정하는 인증·네비게이션 흐름과 토큰 새로고침 API를 도입했습니다.

Changes

Cohort / File(s) Summary
Configuration & Dependencies
\.mcp.json, gradle/libs.versions.toml, composeApp/build.gradle.kts
MCP 서버 설정 파일 추가 및 AndroidX SplashScreen 라이브러리 버전/종속성 추가
Android Activity / Splash
composeApp/src/androidMain/kotlin/com/chukchukhaksa/mobile/MainActivity.kt
SplashScreen 설치 및 앱 준비 상태에 따른 유지(keep-on-screen) 로직 추가
App entry & Navigation
composeApp/src/commonMain/kotlin/.../App.kt, composeApp/src/commonMain/kotlin/.../MainNavigator.kt
App composable에 onReady 콜백 추가, 인증 기반 startDestination 결정 로직 도입; Landing→Home 네비게이션 유틸 추가
Landing UI & MVVM
composeApp/src/commonMain/kotlin/.../presentation/landing/*
랜딩 화면 컴포저블, 상태/사이드이펙트 계약, LandingViewModel 및 네비게이션 그래프 추가(소셜 로그인 버튼 포함)
Auth domain & use-cases
composeApp/src/commonMain/kotlin/.../domain/auth/*
RefreshTokenResult 모델과 CheckAuthStateUseCase 추가; AuthRepository 인터페이스에 refreshToken() 선언 추가
Auth data layer
composeApp/src/commonMain/kotlin/.../data/auth/*, composeApp/src/commonMain/kotlin/.../remote/auth/*
RemoteAuthDataSource에 refreshToken 함수 추가 및 RemoteAuthDataSourceImpl에 refresh API/매퍼 구현; RefreshRequest/RefreshResponse 모델 추가; AuthRepositoryImpl에 refreshToken 구현 추가
DI / ViewModel wiring
composeApp/src/commonMain/kotlin/.../di/DomainModules.kt, .../PresentationModules.kt
CheckAuthStateUseCase와 LandingViewModel의 DI 등록 추가
Resources
composeApp/src/commonMain/composeResources/drawable/ic_apple_logo.xml, .../ic_kakao_logo.xml
Apple과 Kakao 로고 벡터 드로어블 추가
HTTP client / Debug logging
composeApp/src/commonMain/kotlin/.../remote/di/HttpClientModule.kt
디버그 모드에서 요청 바디를 pretty-print 형식으로 로깅하는 HttpSend 인터셉터 추가
UI simplification
composeApp/src/commonMain/kotlin/.../presentation/timetable/.../WebViewGuideScreen.kt
웹뷰 가이드에서 플랫폼별 로그인 버튼 제거 및 관련 로직 정리

Sequence Diagram(s)

sequenceDiagram
    participant App as App Composable
    participant CheckAuth as CheckAuthStateUseCase
    participant AuthRepo as AuthRepository
    participant LocalAuth as LocalAuthDataSource
    participant RemoteAuth as RemoteAuthDataSource

    App->>CheckAuth: invoke()
    CheckAuth->>LocalAuth: getRefreshToken()
    alt token exists
        LocalAuth-->>CheckAuth: refreshTokenValue
        CheckAuth->>AuthRepo: refreshToken()
        AuthRepo->>LocalAuth: getRefreshToken()
        LocalAuth-->>AuthRepo: token
        AuthRepo->>RemoteAuth: refreshToken(token)
        alt success
            RemoteAuth-->>AuthRepo: RefreshTokenResult
            AuthRepo->>LocalAuth: saveTokens(access, refresh)
            AuthRepo-->>CheckAuth: success
            CheckAuth-->>App: Result.success(true)
        else api error
            RemoteAuth-->>AuthRepo: ApiException
            AuthRepo->>LocalAuth: clearTokens()
            AuthRepo-->>CheckAuth: error
            CheckAuth-->>App: Result.success(false)
        end
    else no token
        LocalAuth-->>CheckAuth: null
        CheckAuth-->>App: Result.success(false)
    end
    App->>App: setStartDestination()
    App->>App: onReady()
Loading
sequenceDiagram
    participant Landing as LandingRoute (Composable)
    participant VM as LandingViewModel
    participant Store as MviStore
    participant Kakao as KakaoLoginUseCase
    participant Nav as MainNavigator

    Landing->>VM: onKakaoLogin()
    VM->>Store: setLoading(true)
    VM->>Kakao: invoke()
    Kakao-->>VM: Result
    alt success
        VM->>Store: emit(NavigateHome)
        Store-->>Landing: NavigateHome effect
        Landing->>Nav: navigateFromLandingToHome()
    else failure
        VM->>Store: emit(HandleException)
        Store-->>Landing: HandleException effect
        Landing->>Landing: handleException()
    end
    VM->>Store: setLoading(false)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Feature/#70 kakao login server #71: 인증 관련 원격 데이터소스 및 HTTP 클라이언트 변경과 연관되어 토큰 갱신·인증 상태 검사 코드와 직접적인 코드 레벨 연관성이 있음.

Suggested reviewers

  • kimmandoo
  • BEEEAM-J

Poem

🐰 반짝이는 스플래시 아래서,
랜딩은 웃고 버튼은 반짝,
토큰은 살짝 새로 고쳐지고,
길은 홈으로 바삐 열리네,
당근 한 입, 배포 축하해요 🥕

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning PR 설명이 템플릿의 빈 자리만 포함하고 있으며, '작업한 내용', 'PR 포인트', '관련 이슈' 등 모든 필수 섹션이 실제 내용 없이 비어있습니다. PR 설명을 채워주세요. '작업한 내용'에 랜딩 화면 추가, 소셜 로그인 통합, 스플래시 스크린 구현 등을 명시하고, 관련 이슈 번호(#76)를 입력하며, 스크린샷을 첨부해주세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 3.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive 제목이 주요 변경사항을 명확히 설명하지 못하고 있습니다. 랜딩 화면 기능이 추가되었지만, '랜딩' 또는 'Feature/#76'은 변경사항의 구체적인 내용을 전달하지 못합니다. 더 구체적인 제목으로 변경해주세요. 예: 'Add landing screen with social login integration' 또는 '랜딩 화면 추가 및 소셜 로그인 통합' 같이 주요 변경사항을 명확히 하는 제목을 사용하세요.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#76-landing
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Tip

CodeRabbit can generate a title for your PR based on the changes with custom instructions.

Set the reviews.auto_title_instructions setting to generate a title for your PR based on the changes in the PR with custom instructions.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.mcp.json:
- Around line 1-8: 파일 `.mcp.json`에 하드코딩된 개발용 MCP 서버 설정(mcpServers ->
"figma-desktop" -> "url": "http://127.0.0.1:3845/mcp")이 커밋되어 있어 팀별 로컬 설정 차이를 유발할
수 있으니, 이 파일을 버전관리에서 제외하거나 설정을 템플릿화하세요; 결정 방식으로는 1) 로컬 전용이라면 `.mcp.json`을
`.gitignore`에 추가하고 이미 커밋된 경우 원격에서 제거(예: git rm --cached) 후 커밋, 2) 공용 기본값이 필요하면
`.mcp.json.example` 같은 템플릿 파일로 교체하고 실제 비포맷 파일은 gitignore에 추가, 또는 3) 환경변수/프로필을
사용하도록 코드(참조 대상: mcpServers / "figma-desktop" / "url")를 변경하여 파일을 안전하게 처리하고
README에 사용 방법을 문서화하세요.

In `@composeApp/src/androidMain/kotlin/com/chukchukhaksa/mobile/MainActivity.kt`:
- Around line 16-26: Add a hard timeout when waiting for auth to avoid the
splash screen staying forever: in App.kt where CheckAuthStateUseCase is invoked
(inside the LaunchedEffect that sets startDestination), wrap the call with
withTimeoutOrNull (e.g., 5000L) and treat a null result as false so you always
set startDestination and call onReady; also ensure the Activity's splash logic
(installSplashScreen / isReady) is set to true when App signals readiness so the
splash is dismissed even on timeout.

In `@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/App.kt`:
- Around line 55-56: koinInject을 Composable 본문에서 직접 호출하면 리컴포지션마다 새 인스턴스가 생성될 수
있으니, CheckAuthStateUseCase 인스턴스 생성 위치를 변경하세요: Composable 내부에서 사용할 경우 val
checkAuthStateUseCase by remember {
mutableStateOf(koinInject<CheckAuthStateUseCase>()) }처럼 remember로 한 번만 생성하거나 더
권장되는 방식으로 MainViewModel에 CheckAuthStateUseCase를 주입하고 Composable에서는 viewModel()을
통해 MainViewModel의 공개된 메서드/프로퍼티를 사용해 LaunchedEffect(Unit)에서 인증 상태를 확인하도록 수정하세요;
관련 심볼: koinInject, CheckAuthStateUseCase, LaunchedEffect(Unit), MainViewModel,
startDestination.
- Around line 64-73: Replace the two LaunchedEffect blocks with a single
LaunchedEffect that calls checkAuthStateUseCase(), assigns startDestination
(using TimetableRoute.route or LandingRoute.route), and then invokes onReady()
if startDestination is non-null; locate the code around LaunchedEffect,
checkAuthStateUseCase(), startDestination, and onReady() and perform the merge
so the auth check and the onReady() call happen sequentially in one effect.

In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/data/auth/repository/AuthRepositoryImpl.kt`:
- Around line 17-23: The refreshToken() implementation duplicates token-persist
logic — replace the two calls
localAuthDataSource.saveAccessToken(result.accessToken) and
localAuthDataSource.saveRefreshToken(result.refreshToken) with a single call to
the existing unified method localAuthDataSource.saveTokens(result.accessToken,
result.refreshToken) (or saveTokens(result) if that API exists), updating
AuthRepositoryImpl.refreshToken to pass the access and refresh token to
saveTokens so all token persistence goes through the single saveTokens path.

In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/domain/auth/usecase/CheckAuthStateUseCase.kt`:
- Around line 11-26: The inner try-catch inside CheckAuthStateUseCase.invoke
duplicates CancellationException handling already provided by
runCatchingIgnoreCancelled; remove the explicit catch (e: CancellationException)
branch and simplify the inner block to only handle ApiException and other
Exceptions (or replace the inner try-catch with a runCatching that maps
ApiException to clearing tokens and false, and other exceptions to false),
ensuring you still call authRepository.refreshToken() and on ApiException call
authRepository.clearTokens() before returning false; update the method body in
CheckAuthStateUseCase.invoke accordingly.

In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/landing/LandingScreen.kt`:
- Around line 163-204: Add a Compose preview for the new SocialLoginButton so
designers and reviewers can visually validate it under preview/designsystem;
create a `@Preview` composable (in preview/designsystem package under androidMain)
that calls SocialLoginButton with representative parameters (sample text, a
drawable icon resource, containerColor/contentColor, and enabled true —
optionally add a second preview with enabled=false) and apply the app theme
(CchTheme) so typography and colors match; reference the SocialLoginButton
function from LandingScreen.kt when implementing the preview.
- Around line 90-119: The five repeated Image(...) calls should be replaced by
iterating over a list of drawable resource IDs to avoid duplication; create a
list like listOf(Res.drawable.img_landing_1, ..., Res.drawable.img_landing_5)
and forEach { id -> Image(painter = painterResource(id), contentDescription =
null, modifier = imageModifier, contentScale = ContentScale.FillWidth) } so the
repeated use of Image, painterResource, imageModifier and ContentScale.FillWidth
is consolidated and adding/removing images only requires editing the list.

In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/landing/LandingViewModel.kt`:
- Around line 16-31: The isLoading guard in onKakaoLogin is not atomic: the
current check (mviStore.uiState.value.isLoading) and subsequent setState are
separated, allowing races and duplicate launches; fix by performing an atomic
check-and-set before launching the coroutine — e.g., add a single-operation
helper on mviStore such as trySetLoading(desired: Boolean): Boolean (or use a
Mutex around the check+set) and call if (!mviStore.trySetLoading(true)) return,
then launch viewModelScope.launch and ensure you set trySetLoading(false) (or
call setState) in finally; reference symbols: onKakaoLogin, mviStore, isLoading,
viewModelScope.launch.

In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/landing/navigation/LandingNavigation.kt`:
- Around line 6-10: The identifier LandingRoute is duplicated between the import
(com.chukchukhaksa.mobile.presentation.landing.LandingRoute), the local object
LandingRoute, and the Composable call; rename the navigation object to
LandingNavRoute (or use an import alias) and update all references — change the
local object name from LandingRoute to LandingNavRoute and replace occurrences
in the Composable invocation and any imports that refer to the local symbol
(e.g., update usages that currently reference LandingRoute to LandingNavRoute or
alias the imported LandingRoute as ImportedLandingRoute) so the external route
type and the local nav object are unambiguous.

In `@gradle/libs.versions.toml`:
- Line 40: Update the androidx-splashscreen entry in libs.versions.toml from
"1.0.1" to "1.2.0": locate the dependency key androidx-splashscreen and change
its value to "1.2.0", then sync the Gradle project (re-import or run a build) to
ensure the updated dependency resolves and tests/build succeed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a413c457-79df-44cc-9f58-bf85b901f01a

📥 Commits

Reviewing files that changed from the base of the PR and between e78d846 and 746d1a0.

📒 Files selected for processing (29)
  • .mcp.json
  • composeApp/build.gradle.kts
  • composeApp/src/androidMain/kotlin/com/chukchukhaksa/mobile/MainActivity.kt
  • composeApp/src/commonMain/composeResources/drawable/ic_apple_logo.xml
  • composeApp/src/commonMain/composeResources/drawable/ic_kakao_logo.xml
  • composeApp/src/commonMain/composeResources/drawable/img_landing_1.webp
  • composeApp/src/commonMain/composeResources/drawable/img_landing_2.webp
  • composeApp/src/commonMain/composeResources/drawable/img_landing_3.webp
  • composeApp/src/commonMain/composeResources/drawable/img_landing_4.webp
  • composeApp/src/commonMain/composeResources/drawable/img_landing_5.webp
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/App.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/MainNavigator.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/data/auth/datasource/RemoteAuthDataSource.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/data/auth/repository/AuthRepositoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/di/DomainModules.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/di/PresentationModules.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/domain/auth/model/RefreshTokenResult.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/domain/auth/repository/AuthRepository.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/domain/auth/usecase/CheckAuthStateUseCase.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/landing/LandingContract.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/landing/LandingScreen.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/landing/LandingViewModel.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/landing/navigation/LandingNavigation.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/timetable/timetable/component/WebViewGuideScreen.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/auth/RemoteAuthDataSourceImpl.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/auth/model/RefreshRequest.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/auth/model/RefreshResponse.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/remote/di/HttpClientModule.kt
  • gradle/libs.versions.toml
💤 Files with no reviewable changes (1)
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/timetable/timetable/component/WebViewGuideScreen.kt

Comment thread .mcp.json
Comment on lines +55 to +56
val checkAuthStateUseCase: CheckAuthStateUseCase = koinInject()
var startDestination by remember { mutableStateOf<String?>(null) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

koinInject를 Composable 본문에서 직접 호출하면 리컴포지션마다 새 인스턴스가 생성될 수 있습니다.

CheckAuthStateUseCaseLaunchedEffect(Unit)에서 한 번만 사용되지만, koinInject가 리컴포지션될 때마다 호출됩니다. remember로 감싸거나 MainViewModel에 의존성을 주입하는 것이 더 적절합니다.

♻️ 권장 수정안
-            val checkAuthStateUseCase: CheckAuthStateUseCase = koinInject()
+            val checkAuthStateUseCase: CheckAuthStateUseCase = remember { koinInject() }
             var startDestination by remember { mutableStateOf<String?>(null) }

또는 MainViewModelCheckAuthStateUseCase를 주입하고 ViewModel에서 인증 상태를 확인하는 것이 더 좋은 아키텍처입니다.

📝 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
val checkAuthStateUseCase: CheckAuthStateUseCase = koinInject()
var startDestination by remember { mutableStateOf<String?>(null) }
val checkAuthStateUseCase: CheckAuthStateUseCase = remember { koinInject() }
var startDestination by remember { mutableStateOf<String?>(null) }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/App.kt` around
lines 55 - 56, koinInject을 Composable 본문에서 직접 호출하면 리컴포지션마다 새 인스턴스가 생성될 수 있으니,
CheckAuthStateUseCase 인스턴스 생성 위치를 변경하세요: Composable 내부에서 사용할 경우 val
checkAuthStateUseCase by remember {
mutableStateOf(koinInject<CheckAuthStateUseCase>()) }처럼 remember로 한 번만 생성하거나 더
권장되는 방식으로 MainViewModel에 CheckAuthStateUseCase를 주입하고 Composable에서는 viewModel()을
통해 MainViewModel의 공개된 메서드/프로퍼티를 사용해 LaunchedEffect(Unit)에서 인증 상태를 확인하도록 수정하세요;
관련 심볼: koinInject, CheckAuthStateUseCase, LaunchedEffect(Unit), MainViewModel,
startDestination.

Comment thread composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/App.kt
Comment on lines +163 to +204
private fun SocialLoginButton(
modifier: Modifier = Modifier,
text: String,
iconRes: DrawableResource,
containerColor: Color,
contentColor: Color,
enabled: Boolean,
onClick: () -> Unit,
) {
val clickableModifier = if (enabled) {
Modifier.cchClickable { onClick() }
} else {
Modifier
}

Box(
modifier = modifier
.wrapContentHeight()
.clip(RoundedCornerShape(10.dp))
.then(clickableModifier)
.background(containerColor)
.padding(18.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(iconRes),
contentDescription = null,
modifier = Modifier.size(32.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = text,
color = contentColor,
style = CchTheme.typography.bodyMdStrong,
)
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

신규 SocialLoginButton 컴포넌트 프리뷰를 추가해 주세요.

재사용 컴포넌트가 추가되어 디자인/상태 확인을 위해 preview/designsystem 프리뷰가 있으면 회귀 확인이 쉬워집니다.

Based on learnings: "Applies to composeApp/src/androidMain/kotlin/com/chukchukhaksa/mobile/preview/designsystem/**/*.kt : Preview components in preview/designsystem/ package when creating new UI components".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/landing/LandingScreen.kt`
around lines 163 - 204, Add a Compose preview for the new SocialLoginButton so
designers and reviewers can visually validate it under preview/designsystem;
create a `@Preview` composable (in preview/designsystem package under androidMain)
that calls SocialLoginButton with representative parameters (sample text, a
drawable icon resource, containerColor/contentColor, and enabled true —
optionally add a second preview with enabled=false) and apply the app theme
(CchTheme) so typography and colors match; reference the SocialLoginButton
function from LandingScreen.kt when implementing the preview.

Comment on lines +16 to +31
fun onKakaoLogin(context: Any? = null) {
if (mviStore.uiState.value.isLoading) return

viewModelScope.launch {
mviStore.setState { copy(isLoading = true) }

kakaoLoginUseCase(context)
.onSuccess {
mviStore.postSideEffect(LandingSideEffect.NavigateHome)
}
.onFailure { throwable ->
mviStore.postSideEffect(LandingSideEffect.HandleException(throwable))
}

mviStore.setState { copy(isLoading = false) }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

isLoading 가드가 원자적이지 않아 중복 로그인 요청이 발생할 수 있습니다.

Line 17에서 체크하고 Line 20에서 나중에 상태를 바꾸기 때문에, 빠른 연속 탭 시 코루틴이 여러 개 실행될 수 있습니다.

🐛 제안 diff
     fun onKakaoLogin(context: Any? = null) {
         if (mviStore.uiState.value.isLoading) return
+        mviStore.setState { copy(isLoading = true) }
 
         viewModelScope.launch {
-            mviStore.setState { copy(isLoading = true) }
-
-            kakaoLoginUseCase(context)
-                .onSuccess {
-                    mviStore.postSideEffect(LandingSideEffect.NavigateHome)
-                }
-                .onFailure { throwable ->
-                    mviStore.postSideEffect(LandingSideEffect.HandleException(throwable))
-                }
-
-            mviStore.setState { copy(isLoading = false) }
+            try {
+                kakaoLoginUseCase(context)
+                    .onSuccess {
+                        mviStore.postSideEffect(LandingSideEffect.NavigateHome)
+                    }
+                    .onFailure { throwable ->
+                        mviStore.postSideEffect(LandingSideEffect.HandleException(throwable))
+                    }
+            } finally {
+                mviStore.setState { copy(isLoading = false) }
+            }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/landing/LandingViewModel.kt`
around lines 16 - 31, The isLoading guard in onKakaoLogin is not atomic: the
current check (mviStore.uiState.value.isLoading) and subsequent setState are
separated, allowing races and duplicate launches; fix by performing an atomic
check-and-set before launching the coroutine — e.g., add a single-operation
helper on mviStore such as trySetLoading(desired: Boolean): Boolean (or use a
Mutex around the check+set) and call if (!mviStore.trySetLoading(true)) return,
then launch viewModelScope.launch and ensure you set trySetLoading(false) (or
call setState) in finally; reference symbols: onKakaoLogin, mviStore, isLoading,
viewModelScope.launch.

Comment thread gradle/libs.versions.toml
- App.kt: 인증 확인 후 onReady() 호출하는 LaunchedEffect 통합
- AuthRepositoryImpl: refreshToken()에서 saveTokens() 재사용으로 중복 제거
- LandingScreen: 반복되는 Image 호출을 forEach 루프로 축약
- LandingViewModel: isLoading 상태를 launch 전에 설정하고 try-finally로 안전하게 해제

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/landing/LandingScreen.kt`:
- Around line 156-168: 현재 LandingScreen에서 enabled가 false일 때 클릭만 비활성화되고 시각적 피드백이
없어 사용자가 상태를 인지하지 못합니다; LandingScreen.kt의 clickableModifier/enabled 정의와 Box 호출부를
수정해 비활성화 시 배경이나 전체 컴포저블에 투명도 또는 색상 변화를 적용하도록 변경하세요 (예: background에
containerColor.copy(alpha = if (enabled) 1f else 0.5f) 또는 Box에 .alpha(if
(enabled) 1f else 0.5f)을 추가), clickableModifier와 충돌하지 않도록
.then(clickableModifier) 순서는 유지하세요.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 17dbe659-350e-422a-9e94-0f72fbf4761a

📥 Commits

Reviewing files that changed from the base of the PR and between 746d1a0 and 85d4baa.

📒 Files selected for processing (4)
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/App.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/data/auth/repository/AuthRepositoryImpl.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/landing/LandingScreen.kt
  • composeApp/src/commonMain/kotlin/com/chukchukhaksa/mobile/presentation/landing/LandingViewModel.kt

@BEEEAM-J BEEEAM-J left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

리뷰 완료 했습니다~

@lluke0
lluke0 merged commit 65f4761 into develop Mar 16, 2026
1 of 2 checks passed
@lluke0
lluke0 deleted the feature/#76-landing branch March 16, 2026 13:07
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.

2 participants