Skip to content

Comments

Feature/#77#77

Open
KateteDeveloper wants to merge 13 commits intodevelopfrom
feature/#77
Open

Feature/#77#77
KateteDeveloper wants to merge 13 commits intodevelopfrom
feature/#77

Conversation

@KateteDeveloper
Copy link
Contributor

@KateteDeveloper KateteDeveloper commented Feb 21, 2026

📝 설명

딥링크 기반 소셜 로그인 api 연동했습니다.

✔️ PR 유형

어떤 변경 사항이 있나요?

  • 새로운 기능 추가
  • 버그 수정
  • CSS 등 사용자 UI 디자인 변경
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 테스트 추가, 테스트 리팩토링
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

📎 관련 이슈 번호

해당 PR과 관련된 이슈 번호를 적어주세요.

Summary by CodeRabbit

  • 새로운 기능

    • 소셜 로그인 전용 딥링크 진입 및 전체 소셜 온보딩(약관 → 프로필 입력 → 환영) 추가
    • 소셜 온보딩용 닉네임·성별·직업·목적·관심사 입력 화면과 자동 처리 흐름 제공
    • 소셜 프로필 완성 API 연동 및 프로필 갱신 기능 추가
  • 개선

    • 시스템 바(상태/내비게이션 바)를 런타임에서 동적으로 제어하도록 변경
    • 로그인 시 브라우저 기반 인증(커스텀 탭) 지원 추가
    • 큐레이션 UX 개선: 새 애니메이션, 상세 오버레이, 로딩 스켈레톤/셔머 도입

@coderabbitai
Copy link

coderabbitai bot commented Feb 21, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

HTTPS/custom 스킴 딥링크 수신→파싱→SharedFlow 버스 전파, 소셜 온보딩 네비게이션·뷰모델·화면 대거 추가, SystemBarController/SystemBarMode로 런타임 시스템바 제어 도입, core 모델·리포지토리·API/DTO/매퍼 확장 및 curation UI 리팩터링 포함.

Changes

Cohort / File(s) Summary
Manifest & MainActivity
app/src/main/AndroidManifest.xml, app/src/main/java/com/example/linku_android/MainActivity.kt
Activity launchMode 변경(singleTask) 및 https/custom 딥링크 intent-filter 추가. onCreate/onNewIntent에서 딥링크 추출→버스 전파 및 SystemBarController 구현 연결.
딥링크 파싱·버스·데이터
app/src/main/java/.../deeplink/SocialDeepLinkHandler.kt, app/src/main/java/.../deeplink/SocialDeepLinkBus.kt, core/src/main/java/.../model/auth/SocialLoginData.kt, app/src/main/java/.../deeplink/SocialLoginData.kt
Intent→URI 파싱 유틸(extractSocialDeepLinkData) 추가, SharedFlow 기반 SocialDeepLinkBus 추가, core 데이터 클래스 SocialLoginData 추가(앱 문서 파일 별도 존재).
앱 통합(MainApp)
app/src/main/java/com/example/linku_android/MainApp.kt
SocialDeepLinkBus 구독하여 pending social token 전달 및 로그인/온보딩 네비게이션으로 라우팅, 로그인 성공 시 세션/화면 갱신 트리거.
System bar 제어 추가
core/src/main/java/com/example/core/model/SystemBarMode.kt, core/src/main/java/com/example/core/system/SystemBarController.kt, UI 파일들(Splash.kt, HomeScreen.kt, 로그인 관련 화면들)
SystemBarMode enum·SystemBarController 인터페이스 추가. 여러 화면에서 DesignSystemBars 제거 후 LocalContext 기반 setSystemBarMode(HIDDEN/VISIBLE) 적용(프리뷰 예외 처리 포함).
core auth 모델 확장
core/src/main/java/com/example/core/model/auth/*
SignUpForm, LoginState, SocialLoginEvent, EmailAuthState, Gender, Job, Interest, Purpose, SignUpState, NicknameCheckState 등 인증·온보딩 모델 추가/이관.
리포지토리/API/DTO/매퍼
core/.../repository/UserRepository.kt, data/.../api/UserApi.kt, data/.../dto/server/SocialProfileRequestDTO.kt, data/.../mapper/SocialProfileMapper.kt, data/.../implementation/repository/UserRepositoryImpl.kt
UserRepository에 sessionState 노출 및 refresh/complete/update 메서드 추가. UserApi 경로 변경 및 completeSocialProfile 엔드포인트 추가. DTO·매퍼 추가, Impl에서 enum 기반 매핑 적용 및 하드코드 맵 제거.
로그인 네비게이션 재구성
feature/login/src/main/java/.../navigation/LoginApp.kt, feature/login/src/main/java/.../navigation/NavEntryHelper.kt, feature/login/build.gradle.kts, feature/login/src/main/java/.../constants/ServerConfig.kt
auth_graph·social_auth_graph 도입, Nav helper(rememberAuthParentEntry 등) 추가, Chrome Custom Tabs 사용 의존성 추가, ServerConfig 상수 도입.
소셜 온보딩 화면군
feature/login/src/main/java/com/example/login/ui/screen/social/*
EmailInput, SocialEntry, SocialNickname, SocialGender, SocialJob, SocialPurpose, SocialInterest, WelcomeSocial 등 소셜 프로필 입력·검증·완료 화면 및 UI 유틸(InterestUI/PurposeUI 등) 대량 추가.
뷰모델 변경/추가
feature/login/src/main/java/.../viewmodel/*, feature/mypage/src/main/java/.../MyPageViewModel.kt
EmailAuth/SignUp 타입을 core로 이동. LoginViewModel에 handleSocialDeepLink·SocialLoginEvent 흐름 추가. 신규 SocialAuthViewModel 추가(닉네임 검증·completeSocialProfile). MyPageViewModel이 sessionStore 대신 userRepository.sessionState 사용.
Curation UI 리팩터링
feature/curation/src/main/java/...
네비게이션 재구성(ExperimentalSharedTransitionApi), CurationScreen 시그니처 축소, 페이저/카드/디테일 오버레이(CurationMainCardPager, CurationMonthDetailOverlay/Screen 등) 및 RadialGradientCircle·Shimmer·Skeleton·CurationCardItem 등 다수 추가. 기존 TopBar/Liked/Highlight 섹션 등 일부 파일 삭제.
소규모 유틸·빌드 설정
core/src/main/java/com/example/core/util/UrlUtils.kt, gradle/libs.versions.toml, feature/curation/build.gradle.kts
ensureHttpScheme 유틸 추가, 버전 카탈로그에 foundation-layout 추가, curation 의존성(accompanist/systemuicontroller, foundation-layout) 추가.

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자가 딥링크 클릭
    participant App as MainActivity/MainApp
    participant Bus as SocialDeepLinkBus
    participant Nav as LoginApp (NavHost)
    participant AuthVM as SocialAuthViewModel
    participant Repo as UserRepository
    participant API as Backend

    User->>App: 딥링크 인텐트 전달
    App->>App: extractSocialDeepLinkData(intent)
    App->>Bus: SocialDeepLinkBus.emit(SocialLoginData)
    Bus-->>Nav: 구독자(MainApp/LoginApp)로 데이터 흐름
    Nav->>Nav: social_entry 처리 (savedState에 socialToken 저장)
    alt status == ACTIVE
        Nav->>App: onLoginSuccess -> 홈으로 이동
    else status == TEMP
        Nav->>AuthVM: socialToken 전파
        User->>Nav: 프로필 입력(닉네임/성별/직업/목적/관심)
        Nav->>AuthVM: updateNickname/updateGender/...
        AuthVM->>Repo: completeSocialProfile(request DTO)
        Repo->>API: PATCH /api/users/social/complete
        API-->>Repo: 응답
        Repo-->>AuthVM: 성공/실패 반환
        AuthVM->>Nav: 성공 -> WelcomeSocialScreen -> onLoginSuccess -> 홈 이동
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • ugmin1030
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.55% 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/#77'은 이슈 번호만 표기하며, 변경사항의 핵심 내용(소셜 로그인 딥링크 연동, 캘린더 UI 추가 등)을 설명하지 않습니다. 제목을 더 구체적으로 수정해주세요. 예: '소셜 로그인 딥링크 연동 및 캘린더 UI 추가' 또는 '딥링크 기반 소셜 로그인 API 통합'
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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/#77

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (19)
feature/login/src/main/java/com/example/login/ui/screen/email/SignUpPasswordScreen.kt (1)

216-232: ⚠️ Potential issue | 🟡 Minor

SignUpPasswordScreenContent의 비밀번호 규칙 텍스트가 실제 유효성 검사 로직과 불일치합니다.

isPasswordComplex 검사는 숫자(it.isDigit())·영문자(it.isLetter())·특수문자(!it.isLetterOrDigit()) 세 가지를 모두 요구합니다. 그러나 SignUpPasswordScreenContent의 규칙 레이블(Line 224)은 "영문, 특수기호 조합" 으로 표시되어 숫자 요건이 빠져 있습니다. 반면 SignUpPasswordScreen(Line 101)에서는 "영문, 숫자, 특수기호 조합" 으로 올바르게 표시됩니다.

이 컴포저블이 네비게이션 그래프 또는 다른 경로로 진입점이 되는 경우, 사용자는 숫자를 포함하지 않아도 된다고 오해하게 됩니다.

🐛 규칙 텍스트 수정 제안
-            PasswordRuleItem(
-                text = "영문, 특수기호 조합",
-                satisfied = isPasswordComplex,
-                modifier = Modifier.weight(1f)
-            )
+            PasswordRuleItem(
+                text = "영문, 숫자, 특수기호 조합",
+                satisfied = isPasswordComplex,
+                modifier = Modifier.weight(1f)
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/SignUpPasswordScreen.kt`
around lines 216 - 232, SignUpPasswordScreenContent's displayed rule for the
complexity check is missing the numeric requirement; update the PasswordRuleItem
call inside SignUpPasswordScreenContent (the one paired with isPasswordComplex)
to change its text from "영문, 특수기호 조합" to "영문, 숫자, 특수기호 조합" so it matches the
isPasswordComplex validation and the label used in SignUpPasswordScreen.
feature/login/src/main/java/com/example/login/ui/screen/email/InterestContentScreen.kt (1)

208-210: ⚠️ Potential issue | 🟡 Minor

rememberdensity 키 누락 — 시스템 폰트 스케일 변경 시 초기 스크롤 위치 미반영

initialOffsetPx를 계산하는 remember 블록 내부에서 density를 사용하지만 키로 전달되지 않아, 사용자가 폰트 크기를 변경해도 재계산되지 않습니다.

🐛 제안 수정
-    val initialOffsetPx = remember { with(density) { initialOffsetDp.roundToPx() } }
+    val initialOffsetPx = remember(density) { with(density) { initialOffsetDp.roundToPx() } }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/InterestContentScreen.kt`
around lines 208 - 210, initialOffsetPx가 remember 블록에서 density를 사용하지만 키로 포함되어 있지
않아 시스템 폰트 스케일(밀도) 변경 시 재계산되지 않습니다; initialOffsetPx를 계산하는 remember 호출에 density를
키로 추가하거나 density, initialOffsetDp, leftGutter 등을 키로 전달하여 density 변경 시 값이 다시
계산되도록 변경하세요 (참조 심볼: initialOffsetPx, remember, density, initialOffsetDp,
leftGutter).
feature/login/src/main/java/com/example/login/ui/screen/email/SignUpNicknameScreen.kt (1)

90-104: ⚠️ Potential issue | 🟡 Minor

when 분기에서 Loading / Error 상태에 대한 별도 UI 피드백 없음

현재 NicknameCheckState.Duplicated만 명시적으로 처리하고, Loading이나 Error 같은 다른 상태는 모두 else 분기(규칙 안내 문구 표시)로 처리됩니다. 중복 검사 중인 Loading 상태나 오류 상태에서도 동일하게 "국문/영문 6자 이하" 메시지가 노출되어 사용자에게 혼란을 줄 수 있습니다.

💡 상태별 분기 처리 예시
 when (nicknameState) {
     is NicknameCheckState.Duplicated -> {
         WrongRuleItem(
             text = "이미 사용 중인 닉네임입니다.",
             modifier = Modifier.padding(start = (12.scaler))
         )
     }
+    is NicknameCheckState.Loading -> {
+        // 중복 검사 중 인디케이터 (예: CircularProgressIndicator 또는 안내 텍스트)
+    }
+    is NicknameCheckState.Error -> {
+        WrongRuleItem(
+            text = "닉네임 확인에 실패했습니다. 다시 시도해주세요.",
+            modifier = Modifier.padding(start = (12.scaler))
+        )
+    }
     else -> {
         PasswordRuleItem(
             text = "국문/영문 6자 이하",
             satisfied = isNicknameValid,
             modifier = Modifier.padding(start = (12.scaler))
         )
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/SignUpNicknameScreen.kt`
around lines 90 - 104, The when block handling nicknameState currently only
treats NicknameCheckState.Duplicated specially and uses PasswordRuleItem for all
other states, which hides Loading/Error feedback; update the when(nicknameState)
to explicitly handle NicknameCheckState.Loading (show a loading indicator or a
dedicated LoadingRuleItem) and NicknameCheckState.Error (show an error message
via WrongRuleItem or ErrorRuleItem), keep NicknameCheckState.Duplicated using
WrongRuleItem, and use PasswordRuleItem only for NicknameCheckState.Valid
(leveraging isNicknameValid) so each state (Loading, Error, Duplicated, Valid)
provides appropriate UI feedback.
feature/login/src/main/java/com/example/login/ui/screen/email/ResetPasswordScreen.kt (2)

46-46: ⚠️ Potential issue | 🟠 Major

테스트 이메일 주소가 초기값으로 하드코딩되어 있음 — 운영 환경에서 버그

"test@email.com"이 초기값으로 설정되어 있어, 화면 진입 즉시 isEmailValidtrue가 되고 "메일 보내기" 버튼이 활성화됩니다. 사용자가 이메일을 수정하지 않고 버튼을 누르면 test@email.com으로 비밀번호 재설정 이메일이 실제 발송될 수 있습니다.

🐛 수정 제안
-    var email by remember { mutableStateOf("test@email.com") }
+    var email by remember { mutableStateOf("") }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/ResetPasswordScreen.kt`
at line 46, The email field is initialized with a hardcoded test address causing
isEmailValid to be true on entry; change the initial value of the mutable state
backing the email (the var email by remember { mutableStateOf(...) } in
ResetPasswordScreen) to a safe default such as an empty string or a passed-in
parameter/default from ViewModel, and ensure any callers of ResetPasswordScreen
provide the proper initial email if needed so the "메일 보내기" button is disabled
until the user enters a real address.

110-110: ⚠️ Potential issue | 🔴 Critical

Map에 대한 !! 강제 언박싱 — NPE 크래시 위험

colorTheme.gray[600]!!은 맵에서 키 600을 조회한 뒤 강제로 non-null 처리합니다. 해당 키가 존재하지 않으면 런타임에 NullPointerException이 발생합니다.

🛡️ 수정 제안
-                color = colorTheme.gray[600]!!
+                color = colorTheme.gray[600] ?: Color.Gray
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/ResetPasswordScreen.kt`
at line 110, In ResetPasswordScreen.kt the expression colorTheme.gray[600]!!
force-unwraps a map lookup and can NPE; replace the forced unwrap on
colorTheme.gray[600]!! with safe handling — e.g. use the elvis operator to
supply a sensible fallback color (or explicitly requireNotNull with a clear
error message) so the UI won't crash if key 600 is missing; update the
assignment where color = colorTheme.gray[600]!! to use colorTheme.gray[600] ?:
<fallbackColor> or requireNotNull(colorTheme.gray[600]) { "Missing gray[600] in
colorTheme" } depending on desired behavior.
feature/login/src/main/java/com/example/login/ui/screen/email/SignUpScreen.kt (8)

57-57: ⚠️ Potential issue | 🔴 Critical

Spacer에 음수 높이값 사용 — 런타임 크래시 유발

Jetpack Compose는 음수 값을 지원하지 않으며, 음수 값을 전달하면 IllegalArgumentException이 발생합니다. 컴파일 에러는 발생하지 않지만 해당 코드가 실행될 때 런타임에서 크래시가 발생합니다.

시각적 간격 축소나 겹침 효과가 필요하다면 Modifier.offset 또는 상위 레이아웃의 padding/Arrangement 값을 조정하는 방식을 사용해야 합니다.

🔧 수정 예시 (오프셋으로 대체)
-            Spacer(modifier = Modifier.height((-8.scaler)))
+            Spacer(modifier = Modifier.offset(y = (-8).dp))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/SignUpScreen.kt`
at line 57, The Spacer is using a negative height (Spacer(modifier =
Modifier.height((-8.scaler)))) which will throw an IllegalArgumentException at
runtime; remove the negative height and replace the layout intent with a
non-negative spacing approach such as using Modifier.offset with a negative y
offset on the element that should overlap or adjusting the parent container's
padding/Arrangement to reduce space—update SignUpScreen.kt by removing the
negative-height Spacer and applying Modifier.offset(y = (-8).scaler) to the
target composable or changing the surrounding padding/Arrangement to achieve the
same visual result.

122-139: ⚠️ Potential issue | 🔴 Critical

하단 버튼에 클릭 핸들러 없음

BoxModifier.clickable 또는 onClick 콜백이 없어 버튼을 눌러도 아무 동작도 하지 않습니다. Button Composable로 교체하거나 Modifier.clickable { ... }을 추가하여 실제 동작을 연결해 주세요.

🐛 수정 예시 (Button으로 교체)
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
 
-        Box(
-            modifier = Modifier
-                .fillMaxWidth()
-                .height((48.scaler))
-                .background(
-                    brush = colorTheme.inactiveColor,
-                    shape = RoundedCornerShape(18.dp)
-                ),
-            contentAlignment = Alignment.Center
-        ) {
-            Text(
-                text = "임시 비밀번호 받기",
-                color = Color.White,
-                fontFamily = Paperlogy.font,
-                fontSize = 16.sp,
-                fontWeight = FontWeight.Bold
-            )
-        }
+        Button(
+            onClick = onSubmit,
+            modifier = Modifier
+                .fillMaxWidth()
+                .height((48.scaler)),
+            shape = RoundedCornerShape(18.dp),
+            colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
+        ) {
+            Text(
+                text = "임시 비밀번호 받기",
+                color = Color.White,
+                fontFamily = Paperlogy.font,
+                fontSize = 16.sp,
+                fontWeight = FontWeight.Bold
+            )
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/SignUpScreen.kt`
around lines 122 - 139, 하단의 Box(내용: Text "임시 비밀번호 받기")는 클릭 핸들러가 없어 동작하지 않으므로
SignUpScreen UI에서 해당 Box를 클릭 가능하게 바꿔주세요; 구체적으로 Box에 Modifier.clickable {
onTempPasswordClick() }를 추가하거나 Box를 Button으로 교체하고 Button(onClick = {
onTempPasswordClick() })로 감싸서 클릭 시 호출될 콜백(예: onTempPasswordClick 또는
requestTemporaryPassword)을 연결하도록 하고, 기존 Text 스타일은 유지하세요.

30-30: 🛠️ Refactor suggestion | 🟠 Major

파일명과 Composable 함수명이 일치하지 않음

파일명은 SignUpScreen.kt이지만 정의된 Composable은 PasswordResetScreen입니다. PR 목적("이메일 입력 화면 추가")에 비추어 봐도 어느 쪽 이름이 올바른지 명확하지 않습니다. 파일명과 함수명을 일치시키거나, 의도가 다른 화면이라면 별도 파일로 분리해 주세요.

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

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/SignUpScreen.kt`
at line 30, The Composable declared as PasswordResetScreen in SignUpScreen.kt
conflicts with the file name; either rename the composable to SignUpScreen
(update the `@Composable` function name and any internal references/usages) to
match the file and PR intent, or move PasswordResetScreen into a new file named
PasswordResetScreen.kt if it is a distinct screen; locate the function
PasswordResetScreen and apply the rename or relocation and update all call
sites/imports accordingly.

94-117: ⚠️ Potential issue | 🔴 Critical

이메일 입력 필드가 완전히 비기능 상태

value = ""onValueChange = {}로 고정되어 있어 사용자가 텍스트를 입력해도 아무런 반응이 없습니다. 상태 호이스팅 패턴을 적용하거나, 최소한 로컬 상태라도 연결해야 합니다.

🐛 수정 예시 (로컬 상태로 임시 연결)
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 
 `@Preview`(showBackground = true)
 `@Composable`
-fun PasswordResetScreen() {
+fun PasswordResetScreen(
+    email: String = "",
+    onEmailChange: (String) -> Unit = {}
+) {
     ...
                 OutlinedTextField(
-                    value = "",
-                    onValueChange = {},
+                    value = email,
+                    onValueChange = onEmailChange,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/SignUpScreen.kt`
around lines 94 - 117, The OutlinedTextField in SignUpScreen is non-interactive
because value is hardcoded to "" and onValueChange is empty; update the field to
use state (either hoist state to the parent composable or add a local state in
SignUpScreen via remember { mutableStateOf("") }) and wire that state into the
OutlinedTextField by setting value to the state variable and onValueChange to
update it; locate the OutlinedTextField usage in SignUpScreen.kt and replace the
static value/onValueChange with the state-backed pair so the email input becomes
editable.

94-117: ⚠️ Potential issue | 🔴 Critical

이메일 입력 필드가 완전히 비기능 상태

value = ""onValueChange = {}로 고정되어 있어 사용자가 텍스트를 입력해도 아무런 반응이 없습니다. 상태 호이스팅 패턴 또는 최소한 remember { mutableStateOf("") } 로컬 상태를 연결해야 합니다.

🐛 수정 예시 (파라미터를 통한 상태 호이스팅)
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue

 `@Preview`(showBackground = true)
 `@Composable`
-fun PasswordResetScreen() {
+fun PasswordResetScreen(
+    email: String = "",
+    onEmailChange: (String) -> Unit = {},
+    onSubmitClick: () -> Unit = {}
+) {
     ...
                 OutlinedTextField(
-                    value = "",
-                    onValueChange = {},
+                    value = email,
+                    onValueChange = onEmailChange,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/SignUpScreen.kt`
around lines 94 - 117, The OutlinedTextField is non-interactive because value is
hardcoded to "" and onValueChange is empty; update the SignUpScreen
implementation to connect a real state (either by adding a local remember {
mutableStateOf("") } like emailState and passing emailState.value / {
emailState.value = it } into the OutlinedTextField, or by hoisting state via
parameters such as email:String and onEmailChange:(String)->Unit) so that the
OutlinedTextField's value and onValueChange are wired to that state (refer to
the OutlinedTextField value/onValueChange usage in SignUpScreen).

30-30: 🛠️ Refactor suggestion | 🟠 Major

파일명과 Composable 함수명이 일치하지 않음

파일명은 SignUpScreen.kt이지만 정의된 Composable은 PasswordResetScreen입니다. PR 목적("이메일 입력 화면 추가")에 비추어 봐도 어느 쪽 이름이 올바른지 명확하지 않습니다. 파일명과 함수명을 일치시키거나, 의도가 다른 화면이라면 별도 파일로 분리해 주세요.

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

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/SignUpScreen.kt`
at line 30, 파일명 SignUpScreen.kt와 정의된 Composable 함수 PasswordResetScreen의 이름이
불일치합니다; 파일 목적이 "이메일 입력 화면 추가"라면 Composable 함수 이름을 SignUpScreen으로 변경하거나, 이
Composable이 실제로 비밀번호 재설정 화면이라면 PasswordResetScreen을 별도 파일로 분리해 주세요. 대상 심볼:
SignUpScreen.kt 파일 내 PasswordResetScreen() 함수를 찾아 함수명을 SignUpScreen으로 리네임하거나 파일을
분리하고 관련 호출(import/usage)도 함께 업데이트해 주세요.

122-139: ⚠️ Potential issue | 🔴 Critical

하단 버튼에 클릭 핸들러 없음

BoxModifier.clickable 또는 onClick 콜백이 없어 버튼을 눌러도 아무 동작도 하지 않습니다. Button Composable로 교체하거나 Modifier.clickable { ... }을 추가해 실제 동작을 연결해 주세요.

🐛 수정 예시 (Button Composable로 교체)
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults

-        Box(
-            modifier = Modifier
-                .fillMaxWidth()
-                .height((48.scaler))
-                .background(
-                    brush = colorTheme.inactiveColor,
-                    shape = RoundedCornerShape(18.dp)
-                ),
-            contentAlignment = Alignment.Center
-        ) {
-            Text(
-                text = "임시 비밀번호 받기",
-                color = Color.White,
-                fontFamily = Paperlogy.font,
-                fontSize = 16.sp,
-                fontWeight = FontWeight.Bold
-            )
-        }
+        Button(
+            onClick = onSubmitClick,
+            modifier = Modifier
+                .fillMaxWidth()
+                .height((48.scaler)),
+            shape = RoundedCornerShape(18.dp),
+            colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent)
+        ) {
+            Text(
+                text = "임시 비밀번호 받기",
+                color = Color.White,
+                fontFamily = Paperlogy.font,
+                fontSize = 16.sp,
+                fontWeight = FontWeight.Bold
+            )
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/SignUpScreen.kt`
around lines 122 - 139, The Box used for the "임시 비밀번호 받기" control has no click
handler, so make it interactive by either (a) replacing the Box with a Material
Button Composable (e.g., Button { /* call handler */ } with same styling) or (b)
add Modifier.clickable { onRequestTempPassword() } to the existing Box modifier;
update the SignUpScreen composable signature to accept a callback (e.g.,
onRequestTempPassword: () -> Unit) if one doesn't exist, and call that callback
from the Button's onClick or the Box's clickable lambda so the UI actually
performs the password-request action.

57-57: ⚠️ Potential issue | 🟡 Minor

Spacer에 음수 높이값 사용 — 의도한 동작이 실현되지 않음

Modifier.height((-8.scaler))는 Jetpack Compose에서 음수 Dp 값을 0으로 클램핑하기 때문에, Spacer는 실제로 높이 0인 상태로 렌더링됩니다. 이로 인해 시각적 간격 조정이나 겹침(overlap) 효과가 달성되지 않습니다.

레이아웃 간격을 줄이거나 겹침 효과를 원한다면 Modifier.offset(y = (-8).dp)를 사용하거나, 상위 레이아웃의 padding 또는 Arrangement 값을 조정하세요.

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

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/SignUpScreen.kt`
at line 57, The Spacer currently uses Modifier.height((-8.scaler)) which is
clamped to 0 in Compose and thus has no effect; in SignUpScreen replace that
negative-height Spacer usage with a vertical offset or adjust parent
padding/arrangement—e.g., locate the Spacer in SignUpScreen (the call using
Modifier.height((-8.scaler))) and change it to use Modifier.offset(y = (-8).dp)
or modify the surrounding layout's padding/Arrangement to achieve the intended
overlap/spacing.
feature/login/build.gradle.kts (1)

73-73: ⚠️ Potential issue | 🟠 Major

navigation-compose 중복 선언 — 버전 충돌

Line 73에 navigation-compose:2.5.3, Line 104에 navigation-compose:2.7.7이 중복 선언되어 있습니다. Gradle이 자동으로 높은 버전(2.7.7)으로 해소하지만, 의도치 않은 동작이 숨어있을 수 있습니다. 오래된 선언(Line 73)을 제거하세요.

🐛 제안 수정
-    implementation ("androidx.navigation:navigation-compose:2.5.3")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/login/build.gradle.kts` at line 73, Remove the duplicate older
dependency declaration for androidx.navigation:navigation-compose (version
2.5.3) so only the intended newer version (2.7.7) remains; locate the duplicate
dependency declaration string "androidx.navigation:navigation-compose" (the
entry with version 2.5.3) and delete that line from the build.gradle.kts to
avoid version ambiguity/conflicts.
feature/login/src/main/java/com/example/login/ui/screen/email/InterestPurposeScreen.kt (1)

82-82: ⚠️ Potential issue | 🟡 Minor

주석에 깨진 문자(인코딩 오류) 존재

// X초기 scrollX 앞의 문자가 인코딩 손상으로 보입니다. // X축 초기 스크롤 등으로 수정이 필요합니다.

✏️ 제안 수정
-    // X초기 scroll as before
+    // X축 초기 스크롤 설정
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/InterestPurposeScreen.kt`
at line 82, 해당 파일의 주석에 깨진 인코딩 문자(`// X� initial scroll as before`)가 포함되어 있으니
InterestPurposeScreen.kt 내 문제 있는 주석을 찾아 `// X축 초기 스크롤` 또는 더 자연스러운 한국어 설명으로 교체하고
불필요한 영어 잔여물("initial scroll as before")은 제거하거나 한국어로 통일하여 주석을 정리하세요; 관련 심볼로는
InterestPurposeScreen 클래스/파일을 확인하면 됩니다.
feature/login/src/main/java/com/example/login/ui/screen/email/SignUpGenderScreen.kt (1)

42-42: ⚠️ Potential issue | 🟡 Minor

실제 화면의 Box에 배경색 미설정

Preview(Line 120)에서는 background(colorTheme.white)를 명시하지만, 실제 SignUpGenderScreen의 외부 Box(Line 42)에는 배경이 없습니다. 부모 컴포저블이 흰 배경을 보장하지 않는 경우 시각적 불일치가 생길 수 있습니다.

✨ 제안 수정
-    Box(modifier = Modifier.fillMaxSize()) {
+    Box(modifier = Modifier.fillMaxSize().background(colorTheme.white)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/SignUpGenderScreen.kt`
at line 42, SignUpGenderScreen의 외부 Box에 배경색이 설정되어 있지 않아 Preview와 실제 화면이 달라질 수
있습니다; SignUpGenderScreen 컴포저블의 최상위 Box(현재 선언된 외부 Box)를 찾아
background(colorTheme.white) 또는 사용중인 테마의 배경 컬러로 명시적으로 설정하고, 필요하면
Modifier.fillMaxSize()와 함께 적용하여 부모가 흰 배경을 보장하지 않더라도 일관된 배경을 유지하도록 수정하세요.
app/src/main/java/com/example/linku_android/Splash.kt (1)

75-104: ⚠️ Potential issue | 🟡 Minor

프로덕션 코드에 println 사용 금지

Android에서 println은 Logcat 필터·레벨 제어가 불가능합니다. 특히 Line 99의 오류 출력은 Log.e로 교체하고, 나머지는 Log.d로 변환한 뒤 릴리스 빌드에서는 제거해야 합니다.

🛠️ 수정 예시
-            println(" Splash 시작됨")
+            Log.d("Splash", "Splash 시작됨")
 
-            println(" Glow Phase 진입")
+            Log.d("Splash", "Glow Phase 진입")
 
-                println("⚠️ Splash hydration failed: $e")
+                Log.e("Splash", "Splash hydration failed", e)
 
-        println("Splash onResult 호출")
+        Log.d("Splash", "Splash onResult 호출")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/example/linku_android/Splash.kt` around lines 75 - 104,
Replace all println calls in Splash.kt with Android logging: change non-error
prints (e.g., the " Splash 시작됨", " Glow Phase 진입", "Splash onResult 호출" calls
around rotationAnim.animateTo and isGlowPhase) to Log.d(TAG, ...), and change
the hydration failure print inside onFailure { e -> println("⚠️ Splash hydration
failed: $e") } to Log.e(TAG, "Splash hydration failed", e). Wrap the debug logs
in a BuildConfig.DEBUG guard (or use if (BuildConfig.DEBUG) { Log.d(...) }) so
they are omitted in release builds, add a private TAG constant in the Splash
composable/class, and import android.util.Log; ensure no println remains.
core/src/main/java/com/example/core/repository/UserRepository.kt (1)

49-75: ⚠️ Potential issue | 🟡 Minor

updateUserInfo는 인터페이스에서 제거하거나 internal으로 변경하세요.

updateUserProfile이 내부적으로 updateUserInfo를 호출한 후 세션을 업데이트하는 래퍼 메서드입니다. updateUserInfoUserRepositoryImpl에서만 사용되고 외부에서는 직접 호출되지 않으므로, 공개 인터페이스에 노출할 필요가 없습니다. 두 메서드를 함께 노출하면 개발자가 어느 메서드를 사용해야 하는지 혼동할 수 있습니다.

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

In `@core/src/main/java/com/example/core/repository/UserRepository.kt` around
lines 49 - 75, Remove updateUserInfo from the public UserRepository interface
(or mark it internal) because it is only used by the implementation as a helper
for updateUserProfile; updateUserProfile should remain the public API. Update
the interface to delete or change visibility of suspend fun updateUserInfo(...)
and ensure UserRepositoryImpl still implements the helper method (or retains it
as private/internal) while keeping suspend fun updateUserProfile(...) as the
exposed method that calls it and performs session updates.
feature/mypage/src/main/java/com/example/mypage/MyPageViewModel.kt (1)

127-145: ⚠️ Potential issue | 🟠 Major

deleteUser() 이후 로컬 인증 상태 초기화 누락 확인됨 - 수정 필요

UserRepositoryImpldeleteUser() 구현(275-279줄)이 authPreference.clear()sessionStore.clear()를 포함하지 않습니다. 반면 logout()(297-302줄)은 두 작업을 모두 수행합니다. 회원 탈퇴 후 서버 계정은 삭제되지만 디바이스의 토큰과 세션 데이터는 남아 있어 보안 위험이 됩니다.

deleteUser() 메서드에 다음과 같은 정리 로직을 추가하세요:

authPreference.clear()
sessionStore.clear()

또는 logout()처럼 deleteUser() 내부에서 이 두 작업을 먼저 수행한 후 서버 API를 호출하도록 변경하세요.

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

In `@feature/mypage/src/main/java/com/example/mypage/MyPageViewModel.kt` around
lines 127 - 145, UserRepositoryImpl's deleteUser() currently deletes the server
account but does not clear local auth/session data; update the deleteUser()
implementation in UserRepositoryImpl to also clear local state by calling
authPreference.clear() and sessionStore.clear() (either before calling the
server API or at the end), or simply invoke the existing logout() logic from
deleteUser() so tokens and session are removed from the device after account
deletion.
🟡 Minor comments (11)
feature/login/src/main/java/com/example/login/ui/screen/email/InterestContentScreen.kt-1-1 (1)

1-1: ⚠️ Potential issue | 🟡 Minor

패키지 변경 및 Interest 코어 모델 이전 — 확인 완료

패키지 경로 변경(com.example.login.ui.screencom.example.login.ui.screen.email)과 Interest enum 이전(com.example.core.model.auth.Interest)이 올바르게 완료되었습니다. 코어 모델에 displayName 프로퍼티와 요구되는 12개 상수(BUSINESS, DESIGN, STUDY, WRITING, IT, SOCIETY, STARTUP, COLLECT, CURRENT_EVENTS, PSYCHOLOGY, CAREER, INSIGHTS)가 모두 정의되어 있으며, 의존 파일(EmailLoginScreen.kt, SignUpViewModel.kt 등)의 import도 모두 올바르게 갱신되었습니다.

다만 몇 가지 코드 품질 개선사항이 남아있습니다:

  • 라인 194: val shiftY = 0.dp — 이 변수는 항상 0.dp로 할당되어 실제 사용되지 않는 데드 코드입니다. 제거하세요.
  • 라인 210: remember { with(density) { initialOffsetDp.roundToPx() } }density 값 변경 시 기억된 값이 재계산되도록 density를 remember의 키로 명시하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/InterestContentScreen.kt`
at line 1, Remove the dead variable declaration "val shiftY = 0.dp" from
InterestContentScreen.kt, and modify the remember call that computes the pixel
offset so it re-computes when density changes by adding density as a key: change
remember { with(density) { initialOffsetDp.roundToPx() } } to remember(density)
{ with(density) { initialOffsetDp.roundToPx() } }; reference symbols: shiftY,
initialOffsetDp, density, and the remember call in InterestContentScreen.
app/src/main/java/com/example/linku_android/Splash.kt-54-59 (1)

54-59: ⚠️ Potential issue | 🟡 Minor

DisposableEffect를 조건문 내부에서 조건부 호출 — Compose 규칙 위반

DisposableEffect는 Composable 함수이므로 if 블록 내부에서 호출하면 Compose의 Rules of Composables ("Do not call composable functions inside conditions")를 위반합니다. isPreviewsystemBarController가 런타임에 변경되지 않아 즉각적인 버그는 없지만, Compose 내부 슬롯 테이블 일관성 보장이 깨질 수 있습니다. HomeScreen.kt·EmailLoginScreen.kt는 올바르게 조건 검사를 효과 본문 내부에 두고 있습니다.

🛠️ 수정 제안
-    if (!isPreview && systemBarController != null) {
-        DisposableEffect(Unit) {
-            systemBarController.setSystemBarMode(SystemBarMode.HIDDEN)
-            onDispose { }
-        }
-    }
+    DisposableEffect(Unit) {
+        if (!isPreview && systemBarController != null) {
+            systemBarController.setSystemBarMode(SystemBarMode.HIDDEN)
+        }
+        onDispose { }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/example/linku_android/Splash.kt` around lines 54 - 59,
DisposableEffect is being called conditionally which violates Compose rules;
unconditionally call DisposableEffect (e.g.,
DisposableEffect(systemBarController, isPreview) or DisposableEffect(Unit)) in
Splash.kt and move the if (!isPreview && systemBarController != null) check
inside the DisposableEffect block, then call
systemBarController.setSystemBarMode(SystemBarMode.HIDDEN) only when that
condition is true and provide a proper onDispose (restore previous mode or
no-op) to mirror resource cleanup.
app/src/main/java/com/example/linku_android/MainApp.kt-96-103 (1)

96-103: ⚠️ Potential issue | 🟡 Minor

소셜 로그인 디버그 로그 릴리스 전 제거 필요

Lines 98–100, 109, 111의 Log.d("SOCIAL_LOGIN", ...) 로그는 개발 추적용입니다. 릴리스 빌드에서는 제거하거나 BuildConfig.DEBUG 조건으로 감싸야 합니다.

🛠️ 수정 제안
     LaunchedEffect(socialLoginData) {
         socialLoginData?.let { data ->
-            Log.d("SOCIAL_LOGIN", "=== MainApp에서 소셜 로그인 처리 ===")
-            Log.d("SOCIAL_LOGIN", "provider: ${data.provider}")
-            Log.d("SOCIAL_LOGIN", "token 길이: ${data.token.length}")
             loginViewModel.handleSocialLoginToken(data.token, data.provider)
         }
     }
 
     val loginState by loginViewModel.loginState.collectAsStateWithLifecycle()
 
     LaunchedEffect(loginState) {
-        Log.d("SOCIAL_LOGIN", "loginState 변경: $loginState")
         if (loginState is LoginState.Success && socialLoginData != null) {
-            Log.d("MainApp", "소셜 로그인 성공! 홈으로 이동")
             navigator.navigate(NavigationRoute.Home.route) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/example/linku_android/MainApp.kt` around lines 96 -
103, The debug Log.d statements in the LaunchedEffect handling socialLoginData
(the three Log.d calls before calling loginViewModel.handleSocialLoginToken)
should not be emitted in release builds; either remove them or wrap each in a
BuildConfig.DEBUG check. Locate the LaunchedEffect block that references
socialLoginData and the Log.d("SOCIAL_LOGIN", ...) calls and replace them so the
logs run only when BuildConfig.DEBUG is true (or delete them entirely), keeping
the call to loginViewModel.handleSocialLoginToken(data.token, data.provider)
unchanged.
data/src/main/java/com/example/data/api/dto/server/SocialProfileRequestDTO.kt-3-9 (1)

3-9: ⚠️ Potential issue | 🟡 Minor

SocialProfileRequestDTO@Json 어노테이션 추가 필요 — 필드명 매핑 누락

프로젝트의 다른 DTO 파일들(예: JoinDTO)과 달리 이 클래스는 Moshi @Json 어노테이션이 없습니다. JoinDTO를 보면 동일한 필드 구조(nickName, gender, jobId, purposeList, interestList)에 모두 @Json(name = "...") 어노테이션이 붙어 있으며, 서버는 필드명이 camelCase 형태를 기대합니다.

moshi-kotlin-codegen:1.15.2(KSP 활성화)를 사용 중이므로, 어노테이션 없는 클래스는 리플렉션 폴백에 의존하게 되어 예상치 못한 동작이 발생할 수 있습니다. JoinDTO의 패턴을 따라 각 필드에 @Json 어노테이션을 추가하세요:

수정 예시
import com.squareup.moshi.Json

data class SocialProfileRequestDTO(
    `@Json`(name = "nickName")
    val nickName: String,
    
    `@Json`(name = "gender")
    val gender: Int,
    
    `@Json`(name = "jobId")
    val jobId: Int,
    
    `@Json`(name = "purposeList")
    val purposeList: List<String>,
    
    `@Json`(name = "interestList")
    val interestList: List<String>
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/src/main/java/com/example/data/api/dto/server/SocialProfileRequestDTO.kt`
around lines 3 - 9, Add Moshi `@Json` annotations to SocialProfileRequestDTO so
KSP-generated adapters are used instead of reflection: import
com.squareup.moshi.Json and annotate each property in data class
SocialProfileRequestDTO (nickName, gender, jobId, purposeList, interestList)
with `@Json`(name = "...") using the exact camelCase names (e.g., `@Json`(name =
"nickName") on nickName) following the same pattern as JoinDTO.
feature/login/src/main/java/com/example/login/ui/screen/social/SocialNicknameScreen.kt-37-42 (1)

37-42: ⚠️ Potential issue | 🟡 Minor

닉네임 유효성 검사가 표시되는 규칙과 일치하지 않습니다.

규칙 텍스트는 "국문/영문 6자 이하"로 표시되지만, 실제 검증 로직은 isNotBlank() && length <= 6만 확인합니다. 숫자, 특수문자, 이모지 등이 허용되어 서버 측 검증 실패 시 사용자 경험이 저하될 수 있습니다.

🔧 정규식 기반 검증 추가 제안
     val isNicknameValid =
-        nickname.isNotBlank() && nickname.length <= 6
+        nickname.isNotBlank() && nickname.length <= 6 && nickname.matches(Regex("^[가-힣a-zA-Zㄱ-ㅎㅏ-ㅣ]+$"))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/social/SocialNicknameScreen.kt`
around lines 37 - 42, The current validation (isNicknameValid) only checks
non-blank and length, but the UI text requires "국문/영문 6자 이하"; update
isNicknameValid to enforce a regex that allows only Korean Hangul syllables and
English letters (for example: pattern matching characters in ranges
\uAC00-\uD7A3 and A–Z/a–z) and still enforce length <= 6 and non-blank (i.e.,
nickname.matches(<hangul+latin regex>) && nickname.length <= 6 &&
nickname.isNotBlank()); keep the isButtonEnabled logic (isNicknameValid &&
nicknameState == NicknameCheckState.Available) unchanged so the button still
depends on availability state.
feature/login/src/main/java/com/example/login/ui/screen/social/SocialGenderScreen.kt-54-58 (1)

54-58: ⚠️ Potential issue | 🟡 Minor

소셜 온보딩 플로우에서 totalSteps 값이 불일치합니다.

SocialNicknameScreen에서는 totalSteps = 4로 설정되어 있으나, SocialGenderScreen, SocialJobScreen, SocialPurposeScreen, SocialInterestScreen 등 이후 화면들에서는 모두 totalSteps = 6으로 설정되어 있습니다. StepIndicator가 올바르게 진행 상황을 표시하려면 전체 플로우의 totalSteps 값이 통일되어야 합니다. 정확한 총 스텝 개수를 확인하고 모든 화면에서 동일한 값으로 수정해 주세요.

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

In
`@feature/login/src/main/java/com/example/login/ui/screen/social/SocialGenderScreen.kt`
around lines 54 - 58, The StepIndicator totalSteps value is inconsistent across
onboarding screens; determine the correct total step count for the social
onboarding flow and make it consistent. Update the StepIndicator call in
SocialGenderScreen (StepIndicator(currentStep = 2, totalSteps = ...)), and also
change the totalSteps argument in the StepIndicator usages in
SocialNicknameScreen, SocialJobScreen, SocialPurposeScreen, and
SocialInterestScreen so they all use the same confirmed total; ensure only the
numeric totalSteps value is changed and currentStep remains appropriate per
screen.
app/src/main/java/com/example/linku_android/MainActivity.kt-31-32 (1)

31-32: ⚠️ Potential issue | 🟡 Minor

동일한 딥링크 데이터가 다시 들어오면 LaunchedEffect(socialLoginData)가 재실행되지 않습니다.

socialLoginStatemutableStateOf<SocialLoginData?>이므로, onNewIntent에서 동일한 provider/token 조합이 들어오면 값이 변경되지 않아 MainAppLaunchedEffect(socialLoginData)가 트리거되지 않습니다.

고유성 보장을 위해 타임스탬프 등을 추가하거나, 처리 후 null로 리셋하는 패턴을 고려하세요.

처리 후 리셋 패턴 예시 (MainApp 측)
// SocialLoginData에 고유 ID 추가
data class SocialLoginData(
    val provider: String,
    val token: String,
    val timestamp: Long = System.currentTimeMillis()
)

Also applies to: 42-42, 52-58

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

In `@app/src/main/java/com/example/linku_android/MainActivity.kt` around lines 31
- 32, socialLoginState (mutableStateOf<SocialLoginData?>) can ignore identical
deep-link payloads so LaunchedEffect(socialLoginData) in MainApp won't re-run;
to fix, modify SocialLoginData to include a uniqueness field (e.g., timestamp or
id) and populate it when creating the instance in onNewIntent, or alternatively
after MainApp handles the login inside LaunchedEffect(socialLoginData) reset
socialLoginState to null so the same provider/token pair can be re-emitted;
update references to SocialLoginData construction (where onNewIntent sets
socialLoginState) and the LaunchedEffect handler in MainApp to perform the
null-reset after processing.
feature/login/src/main/java/com/example/login/viewmodel/SocialAuthViewModel.kt-152-160 (1)

152-160: ⚠️ Potential issue | 🟡 Minor

completeSocialProfile 호출 시 닉네임 외 필수 필드 검증이 누락되었습니다.

닉네임만 빈 값 체크하고, gender == Gender.NONE, job == Job.NONE, purposes/interests 빈 리스트는 검증하지 않습니다. 서버에서 거부할 수 있지만 ViewModel 단에서 사전 검증하면 불필요한 API 호출을 줄일 수 있습니다.

검증 강화 예시
     fun completeSocialProfile(
         socialToken: String,
         onSuccess: () -> Unit
     ) {
-        if (_nickname.value.isBlank()) {
-            Log.w(TAG, "닉네임이 비어 있음")
+        val validationError = when {
+            _nickname.value.isBlank() -> "닉네임이 비어 있음"
+            _gender.value == Gender.NONE -> "성별이 선택되지 않음"
+            _job.value == Job.NONE -> "직업이 선택되지 않음"
+            _purposes.value.isEmpty() -> "목적이 선택되지 않음"
+            _interests.value.isEmpty() -> "관심사가 선택되지 않음"
+            else -> null
+        }
+        if (validationError != null) {
+            Log.w(TAG, validationError)
+            _error.value = IllegalArgumentException(validationError)
             return
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/viewmodel/SocialAuthViewModel.kt`
around lines 152 - 160, The completeSocialProfile function currently only checks
_nickname but must also validate gender (ensure gender != Gender.NONE), job (job
!= Job.NONE), and that purposes and interests lists are non-empty before
proceeding; update completeSocialProfile to perform these additional checks (use
the existing properties/fields named gender, job, purposes, interests), log or
surface a clear warning (e.g., via Log.w(TAG, ...)) for each failing validation
and return early to avoid making the API call when required fields are missing.
data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt-317-345 (1)

317-345: ⚠️ Potential issue | 🟡 Minor

completeSocialProfile에서 일반 Exception 미처리 및 withAuth 패턴 미사용.

두 가지 문제가 있습니다:

  1. ApiError만 catch하고 있어 IOException 등 네트워크 예외 발생 시 호출자에게 전파됩니다. SocialAuthViewModel.completeSocialProfileApiErrorException을 모두 catch하고 있어 크래시는 아니지만, 이 메서드 자체가 false를 반환하겠다는 계약을 일관되게 지키지 못합니다.
  2. 파일 내 다른 인증 API 호출은 serverApi.withAuth(authPreference)를 사용하는데, 여기서는 userApi를 직접 호출하며 수동으로 "Bearer $socialToken"을 설정합니다. 소셜 토큰이 일반 accessToken과 다른 경우라면 이해되지만, 이 의도를 주석으로 명시하면 좋겠습니다.
Exception catch 추가 제안
         return try {
             userApi.completeSocialProfile(
                 authorization = "Bearer $socialToken",
                 body = request
             )
             Log.d(TAG, "[소셜 프로필 완료] 성공")
             true
         } catch (e: ApiError) {
             Log.e(TAG, "[소셜 프로필 완료 실패] ${e.message}")
             false
+        } catch (e: Exception) {
+            Log.e(TAG, "[소셜 프로필 완료 실패 - 네트워크] ${e.message}")
+            false
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt`
around lines 317 - 345, completeSocialProfile currently only catches ApiError
and manually injects "Bearer $socialToken", which breaks the method's contract
of returning false on failure and diverges from the file's withAuth pattern;
update completeSocialProfile to (1) wrap the network call in a try/catch that
catches Exception (or IOException + ApiError) so any network/runtime error
returns false, and (2) switch to the existing withAuth pattern (e.g., call
userApi.withAuth(authPreference) or serverApi.withAuth(authPreference) used
elsewhere) instead of manually setting the Authorization header—if the
socialToken is intentionally different from the normal access token, add a clear
comment near completeSocialProfile documenting that intent. Use identifiers:
completeSocialProfile, SocialProfileMapper, userApi, withAuth, authPreference,
ApiError, and SocialAuthViewModel.completeSocialProfile to locate the relevant
code.
data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt-192-201 (1)

192-201: ⚠️ Potential issue | 🟡 Minor

mapNotNull이 실제로 아무 항목도 필터링하지 않습니다.

Purpose.fromServerKey(serverKey)?.displayName ?: serverKey는 항상 non-null String을 반환하므로 mapNotNullmap과 동일하게 동작합니다. 알 수 없는 서버 키가 원본 그대로 UI에 노출됩니다.

의도에 따라 두 가지 선택지가 있습니다:

  • 알 수 없는 키를 제외하려면: elvis 제거 후 mapNotNull만 사용
  • 알 수 없는 키를 원본 유지하려면: map으로 변경하여 의도를 명확히
옵션 A: 알 수 없는 키 제외
-        val displayPurposes = dto.purposes.mapNotNull { serverKey ->
-            Purpose.fromServerKey(serverKey)?.displayName ?: serverKey.also {
-                Log.w(TAG, "알 수 없는 Purpose serverKey: $serverKey")
-            }
-        }
-        val displayInterests = dto.interests.mapNotNull { serverKey ->
-            Interest.fromServerKey(serverKey)?.displayName ?: serverKey.also {
-                Log.w(TAG, "알 수 없는 Interest serverKey: $serverKey")
-            }
-        }
+        val displayPurposes = dto.purposes.mapNotNull { serverKey ->
+            Purpose.fromServerKey(serverKey)?.displayName.also {
+                if (it == null) Log.w(TAG, "알 수 없는 Purpose serverKey: $serverKey")
+            }
+        }
+        val displayInterests = dto.interests.mapNotNull { serverKey ->
+            Interest.fromServerKey(serverKey)?.displayName.also {
+                if (it == null) Log.w(TAG, "알 수 없는 Interest serverKey: $serverKey")
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt`
around lines 192 - 201, The current use of mapNotNull with the elvis (?:
serverKey) in UserRepositoryImpl.kt means unknown server keys are never filtered
out—decide which behavior you want and fix accordingly: if you want to exclude
unknown keys, remove the elvis fallback and keep mapNotNull {
Purpose.fromServerKey(serverKey)?.displayName } and similarly for
Interest.fromServerKey in displayInterests; if you want to keep the original
serverKey when unknown, change mapNotNull to map so the elvis fallback (?:
serverKey) remains effective (apply the same change for the interests mapping).
feature/login/src/main/java/com/example/login/ui/screen/social/SocialInterestScreen.kt-114-118 (1)

114-118: ⚠️ Potential issue | 🟡 Minor

StepIndicator의 라벨 오프셋이 3단계 이상을 지원하지 않습니다.

StepIndicator의 라벨 위치 로직(선 시작 패딩)은 currentStep이 1, 2, 3일 때만 특정 값(각각 6.dp, 68.dp, 122.dp)을 사용하고 그 외는 2.dp로 처리합니다. SocialPurposeScreen에서 currentStep = 4, SocialInterestScreen에서 currentStep = 5를 전달하면 라벨이 의도대로 정렬되지 않습니다.

StepIndicatorwhen() 문에 단계 4, 5, 6에 대한 라벨 오프셋을 추가해야 합니다.

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

In
`@feature/login/src/main/java/com/example/login/ui/screen/social/SocialInterestScreen.kt`
around lines 114 - 118, The StepIndicator's label offset logic currently only
handles currentStep values 1–3, so update the when() in the StepIndicator
composable to handle currentStep == 4, 5, and 6 (the same value used from
SocialPurposeScreen and SocialInterestScreen) and return appropriate start
padding offsets for those steps (follow the existing spacing pattern used for
1–3 so labels align correctly); locate the when() that switches on currentStep
inside the StepIndicator implementation and add cases for 4, 5, and 6 with the
calculated dp offsets.

Comment on lines 106 to 117
val loginState by loginViewModel.loginState.collectAsStateWithLifecycle()

LaunchedEffect(loginState) {
Log.d("SOCIAL_LOGIN", "loginState 변경: $loginState")
if (loginState is LoginState.Success && socialLoginData != null) {
Log.d("MainApp", "소셜 로그인 성공! 홈으로 이동")
navigator.navigate(NavigationRoute.Home.route) {
popUpTo(0) { inclusive = true }
launchSingleTop = true
}
}
}
Copy link

@coderabbitai coderabbitai bot Feb 21, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

socialLoginData 소비 후 미초기화 — 후속 LoginState.Success에서 의도치 않은 홈 이동 발생 가능

LaunchedEffect(loginState)loginState is LoginState.Success && socialLoginData != null 조건을 만족할 때마다 홈 화면으로 이동하고 전체 백스택을 제거합니다. 그러나 소셜 로그인 처리 완료 후 socialLoginDatanull로 초기화하는 코드가 없습니다.

문제 시나리오: 사용자가 소셜 로그인을 완료하여 홈으로 이동한 뒤, 앱이 백그라운드에서 토큰을 갱신하거나 다른 이유로 LoginState.Success가 다시 방출되면, socialLoginData가 여전히 비어 있지 않아 이 effect가 다시 실행되어 전체 백스택이 삭제됩니다.

socialLoginData를 소비한 직후 MainActivity에서 해당 상태를 null로 초기화(또는 일회성 이벤트 채널 패턴)하거나, 아래처럼 로컬 플래그로 처리를 추적해야 합니다.

🛠️ 임시 방편 — 처리 완료 플래그 추가
+    var socialLoginHandled by remember { mutableStateOf(false) }
+
     LaunchedEffect(loginState) {
         Log.d("SOCIAL_LOGIN", "loginState 변경: $loginState")
-        if (loginState is LoginState.Success && socialLoginData != null) {
+        if (loginState is LoginState.Success && socialLoginData != null && !socialLoginHandled) {
+            socialLoginHandled = true
             Log.d("MainApp", "소셜 로그인 성공! 홈으로 이동")
             navigator.navigate(NavigationRoute.Home.route) {
                 popUpTo(0) { inclusive = true }
                 launchSingleTop = true
             }
         }
     }

근본적으로는 MainActivity에서 소셜 로그인 상태를 소비 후 null로 갱신하는 것이 권장됩니다.

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

In `@app/src/main/java/com/example/linku_android/MainApp.kt` around lines 106 -
117, The LaunchedEffect observing loginState causes repeated navigation when
loginState becomes LoginState.Success and socialLoginData remains non-null;
update the code to consume and clear socialLoginData immediately after handling
success (or implement a one-time event flag) so subsequent emissions of
LoginState.Success do not retrigger navigator.navigate in MainActivity;
specifically, modify the LaunchedEffect block that checks "loginState is
LoginState.Success && socialLoginData != null" to reset socialLoginData to null
(or set a local handled flag) right after calling navigator.navigate to ensure
the action is only performed once.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

현재 테스트 중으로, 추후 삭제하겠습니다.

Copy link

Choose a reason for hiding this comment

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

@KateteDeveloper, 이해했습니다! 테스트 코드라면 괜찮습니다. 추후 삭제하실 때 관련 코드도 함께 정리해 주시면 되겠습니다. 👍

Comment on lines 300 to 304
composable("social_login_gate") { entry ->

val signUpVm: SignUpViewModel = hiltViewModel(
navController.getBackStackEntry("social_auth_graph")
)
Copy link

@coderabbitai coderabbitai bot Feb 21, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

getBackStackEntry("social_auth_graph") 호출 시 예외 처리가 없어 크래시 위험이 있습니다.

이메일 인증 플로우에서는 rememberAuthParentEntry를 통해 try-catch로 안전하게 처리하고 있지만, 소셜 인증 플로우의 모든 컴포저블(lines 300, 344, 366, 388, 414, 421, 428, 435, 446)에서는 navController.getBackStackEntry("social_auth_graph")를 직접 호출하고 있습니다.

getBackStackEntry는 해당 엔트리가 백스택에 없으면 IllegalArgumentException을 던집니다. 빠른 네비게이션이나 configuration change 시 크래시가 발생할 수 있습니다.

이메일 플로우와 동일한 안전 패턴을 적용하거나, 소셜 그래프 전용 헬퍼를 만들어 일관성을 유지하세요.

🛡️ 소셜 그래프용 안전 헬퍼 예시
`@Composable`
private fun rememberSocialParentEntry(
    navController: NavHostController,
    currentEntry: NavBackStackEntry
): NavBackStackEntry? {
    return remember(currentEntry) {
        try {
            navController.getBackStackEntry("social_auth_graph")
        } catch (e: Exception) {
            null
        }
    }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/login/src/main/java/com/example/login/LoginApp.kt` around lines 300 -
304, Replace direct calls to
navController.getBackStackEntry("social_auth_graph") inside the social
composables (e.g., composable "social_login_gate" where SignUpViewModel is
obtained) with a safe helper like rememberSocialParentEntry that wraps
getBackStackEntry in a try-catch and returns a nullable NavBackStackEntry; then
use hiltViewModel(parentEntry) only when the returned entry is non-null (or
provide a fallback), mirroring the rememberAuthParentEntry pattern used in the
email flow to avoid IllegalArgumentException on missing back stack entries.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

수정했습니다.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if a safe helper function for social auth graph was added
echo "=== Checking for rememberSocialParentEntry or similar helper ==="
rg -n "rememberSocialParentEntry" feature/login/src/main/java/com/example/login/LoginApp.kt

echo -e "\n=== Checking all getBackStackEntry calls for social_auth_graph ==="
rg -n -C3 'getBackStackEntry\("social_auth_graph"\)' feature/login/src/main/java/com/example/login/LoginApp.kt

echo -e "\n=== Checking for try-catch around social_auth_graph entry retrieval ==="
rg -n -A5 -B5 'social_auth_graph.*try|try.*social_auth_graph' feature/login/src/main/java/com/example/login/LoginApp.kt

Length of output: 499


🏁 Script executed:

#!/bin/bash
# Check if a safe helper function for social auth graph was added
echo "=== Checking for rememberSocialParentEntry or similar helper ==="
rg -n "rememberSocialParentEntry" feature/login/src/main/java/com/example/login/LoginApp.kt

echo -e "\n=== Checking all getBackStackEntry calls for social_auth_graph ==="
rg -n -C3 'getBackStackEntry\("social_auth_graph"\)' feature/login/src/main/java/com/example/login/LoginApp.kt

echo -e "\n=== Checking for try-catch around social_auth_graph entry retrieval ==="
rg -n -A5 -B5 'social_auth_graph.*try|try.*social_auth_graph' feature/login/src/main/java/com/example/login/LoginApp.kt

echo -e "\n=== Checking the rememberAuthParentEntry helper for comparison ==="
rg -n -A10 "fun rememberAuthParentEntry" feature/login/src/main/java/com/example/login/LoginApp.kt

Length of output: 665


🏁 Script executed:

#!/bin/bash
# Check if we're in a repository
pwd
ls -la

# Find the LoginApp.kt file
echo "=== Finding LoginApp.kt ==="
fd -t f "LoginApp.kt"

# List the feature/login directory structure
echo -e "\n=== Checking feature/login structure ==="
ls -R feature/login/src/main/java/com/example/login/ 2>/dev/null || echo "Directory not found"

Length of output: 3398


🏁 Script executed:

#!/bin/bash
# Check the NavEntryHelper.kt file (likely where the helper was added)
echo "=== Checking NavEntryHelper.kt ==="
cat feature/login/src/main/java/com/example/login/navigation/NavEntryHelper.kt

echo -e "\n\n=== Checking for getBackStackEntry usage in LoginApp.kt ==="
rg -n 'getBackStackEntry\("social_auth_graph"\)' feature/login/src/main/java/com/example/login/navigation/LoginApp.kt

echo -e "\n=== Checking for rememberSocialParentEntry usage in LoginApp.kt ==="
rg -n "rememberSocialParentEntry\|rememberSocialAuthEntry" feature/login/src/main/java/com/example/login/navigation/LoginApp.kt

echo -e "\n=== Checking hiltViewModel calls in social flow (around line 300) ==="
rg -n -A2 -B2 'social_login_gate.*hiltViewModel|hiltViewModel.*social_auth_graph' feature/login/src/main/java/com/example/login/navigation/LoginApp.kt | head -30

Length of output: 1447


🏁 Script executed:

#!/bin/bash
# Check imports in LoginApp.kt
echo "=== Checking imports in LoginApp.kt ==="
rg -n "^import.*NavEntryHelper|^import.*rememberSocialParentEntry" feature/login/src/main/java/com/example/login/navigation/LoginApp.kt

# Check specific lines around the social flow where ViewModels are obtained
echo -e "\n=== Checking lines 295-310 (social_login_gate composable) ==="
sed -n '295,310p' feature/login/src/main/java/com/example/login/navigation/LoginApp.kt

# Check a few more social composables
echo -e "\n=== Checking lines 340-350 (another social screen) ==="
sed -n '340,350p' feature/login/src/main/java/com/example/login/navigation/LoginApp.kt

# Search for all hiltViewModel calls in the file
echo -e "\n=== All hiltViewModel patterns in social_auth_graph section ==="
rg -n "hiltViewModel" feature/login/src/main/java/com/example/login/navigation/LoginApp.kt | grep -A2 -B2 "30[0-9]:\|3[1-9][0-9]:\|4[0-4][0-9]:"

Length of output: 1869


🏁 Script executed:

#!/bin/bash
# Find the socialComposable function definition
echo "=== Finding socialComposable function definition ==="
rg -n -A15 "fun.*socialComposable|private fun socialComposable|internal fun socialComposable" feature/login/src/main/java/com/example/login/navigation/LoginApp.kt

# Also check if there's a similar helper for auth_graph
echo -e "\n=== Checking for similar composable helpers ==="
rg -n -A10 "fun NavGraphBuilder\.(.*Composable|composable)" feature/login/src/main/java/com/example/login/navigation/LoginApp.kt | head -40

Length of output: 880


🏁 Script executed:

#!/bin/bash
# Double-check the exact current signature and implementation
echo "=== Exact rememberSocialParentEntry signature and implementation ==="
rg -n -A8 "fun rememberSocialParentEntry" feature/login/src/main/java/com/example/login/navigation/NavEntryHelper.kt

echo -e "\n=== Verify the null check in LoginApp.kt ==="
sed -n '246,252p' feature/login/src/main/java/com/example/login/navigation/LoginApp.kt

# Check if maybe there are multiple versions or the file was just updated
echo -e "\n=== Full NavEntryHelper.kt content hash ==="
wc -l feature/login/src/main/java/com/example/login/navigation/NavEntryHelper.kt
md5sum feature/login/src/main/java/com/example/login/navigation/NavEntryHelper.kt

Length of output: 1048


@KateteDeveloper 노력해주셔서 감사합니다! 하지만 아직 수정이 완전하지 않습니다. 🔍

발견된 문제:

NavEntryHelper.ktrememberSocialParentEntry 함수가:

  1. Non-nullable 타입을 반환하지만 (NavBackStackEntry)
  2. try-catch 없이 getBackStackEntry를 직접 호출하고 있어 여전히 크래시 위험이 있습니다
  3. LoginApp.kt에서 if (parentEntry == null) 체크를 하지만, non-nullable 타입이라 이 체크가 작동하지 않습니다

올바른 수정 방법:

NavEntryHelper.ktrememberSocialParentEntry를 다음과 같이 수정해주세요:

`@Composable`
fun rememberSocialParentEntry(
    navController: NavHostController,
    entry: NavBackStackEntry
): NavBackStackEntry? = remember(entry) {
    runCatching { navController.getBackStackEntry("social_auth_graph") }.getOrNull()
}

변경사항:

  • 반환 타입을 NavBackStackEntry?로 변경 (nullable)
  • runCatching { }.getOrNull()로 예외를 안전하게 처리
  • rememberAuthParentEntry와 동일한 패턴 사용

이렇게 수정하면 LoginApp.kt의 null 체크가 제대로 작동하고, 백스택에 엔트리가 없을 때 안전하게 로그인 화면으로 이동합니다.

Comment on lines 210 to 244
fun handleSocialLoginToken(token: String, provider: String) {
viewModelScope.launch {
try {
Log.d("SOCIAL_LOGIN", "=== ViewModel 토큰 처리 시작 ===")
Log.d("SOCIAL_LOGIN", "provider: $provider")
Log.d("SOCIAL_LOGIN", "token 길이: ${token.length}")
_loginState.value = LoginState.Loading
Log.d(TAG, "소셜 로그인 처리: $provider")

// TODO: 서버에서 userId도 같이 받아야 함!
// 지금은 임시로 0L 사용
authPreference.saveTokens(
accessToken = token,
refreshToken = "", // 서버 응답에 따라
userId = 0L // 서버에서 userId 받으면 수정
)
Log.d("SOCIAL_LOGIN", "토큰 저장 완료")


_loginState.value = LoginState.Success(
LoginResult(
accessToken = token,
refreshToken = "",
userId = 0,
status = "SUCCESS",
inactiveDate = null
)
)
Log.d("SOCIAL_LOGIN", " LoginState.Success 설정 완료")

} catch (e: Exception) {
Log.e(TAG, "소셜 로그인 실패", e)
_loginState.value = LoginState.Error(LoginErrorType.UNKNOWN_ERROR)
}
}
Copy link

@coderabbitai coderabbitai bot Feb 21, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check isLoggedIn implementation
rg -n "isLoggedIn" --type=kt -C 3

Repository: LinkYou-2025/LinkU_Android

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Search for isLoggedIn implementation without file type filter
rg -n "isLoggedIn" -C 3

Repository: LinkYou-2025/LinkU_Android

Length of output: 3840


🏁 Script executed:

#!/bin/bash
# Search for tryAutoLogin and login methods
rg -n "fun tryAutoLogin" -A 30

Repository: LinkYou-2025/LinkU_Android

Length of output: 3565


🏁 Script executed:

#!/bin/bash
# Search for login method to compare with handleSocialLoginToken
rg -n "fun login" -A 40 | head -100

Repository: LinkYou-2025/LinkU_Android

Length of output: 11350


🏁 Script executed:

#!/bin/bash
# Search for fetchAndSaveUserSession implementation
rg -n "fun fetchAndSaveUserSession" -A 25

Repository: LinkYou-2025/LinkU_Android

Length of output: 3085


handleSocialLoginToken: 서버 응답 누락으로 인한 자동 로그인 불가.

현재 refreshToken=""을 저장하고 있어, 다음과 같은 문제가 발생합니다:

  1. 자동 로그인이 작동하지 않음: authPreference.isLoggedIn!refreshToken.isNullOrBlank()로 정의되어 있습니다. 빈 문자열은 blank 처리되므로 isLoggedIn = false가 되어, tryAutoLogin()이 초기 단계에서 실패합니다.
  2. 세션 미설정: 일반 login() 메서드와 달리 fetchAndSaveUserSession()을 호출하지 않아, SessionStore에 사용자 정보가 저장되지 않습니다. 주석(Lines 22-33)에 문서화된 로그인 흐름과 불일치합니다.

근본 원인은 소셜 로그인 시 서버에서 userIdrefreshToken을 반환하지 않기 때문입니다. TODO 주석은 있지만, 이 상태로 머지되면 소셜 로그인이 완전히 작동하지 않을 수 있습니다.

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

In `@feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt`
around lines 210 - 244, handleSocialLoginToken currently saves an empty
refreshToken and doesn't populate SessionStore, so auto-login and session setup
fail; update handleSocialLoginToken to exchange the social provider token with
your backend (or call the existing login() endpoint) to obtain a real
refreshToken and userId, then call authPreference.saveTokens(accessToken =
<serverAccess>, refreshToken = <serverRefresh>, userId = <serverUserId>) and
invoke fetchAndSaveUserSession() (or the equivalent SessionStore initialization
used by login()) before setting _loginState to Success; if the backend cannot
return refreshToken/userId, surface an error or route through the normal login()
flow instead of persisting an empty refresh token.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

백엔드와 상의 후 수정하겠습니다.

Copy link

Choose a reason for hiding this comment

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

Rate Limit Exceeded

@KateteDeveloper have exceeded the limit for the number of chat messages per hour. Please wait 2 minutes and 8 seconds before sending another message.

- **내비게이션 구조 개편**: `LoginApp.kt`를 `navigation` 패키지로 이동하고, 중복 코드를 `NavEntryHelper.kt`로 분리하여 공통화
- **소셜 로그인 고도화**:
    - 딥링크를 통한 `socialToken` 및 `status` 전달 방식을 `navArgument`로 변경
    - `SocialAuthViewModel` 및 `SocialEntryScreen`에 딥링크 처리 로직 추가
    - `WelcomeSocialScreen`에 로그인 성공 콜백(`onLoginSuccess`) 연동 및 재시도 로직 구현
- **보안 및 설정 관리**: 하드코딩된 서버 URL을 `ServerConfig.kt` 상수로 분리
- **버그 수정 및 안정화**:
    - `rememberSaveable`을 사용하여 회원가입 요청 상태 유지 (리컴포지션 대응)
    - `social_auth_graph` 백스택 접근 시 예외 처리 추가
    - `UserRepositoryImpl`에서 발생한 `ApiError`를 상위 레이어로 전파하도록 수정
- **코드 정리**: `MainActivity` 및 `MainApp`에 흩어져 있던 소셜 로그인 처리 로직을 관련 모듈로 캡슐화 및 불필요한 주석 삭제
Comment on lines 106 to 117
val loginState by loginViewModel.loginState.collectAsStateWithLifecycle()

LaunchedEffect(loginState) {
Log.d("SOCIAL_LOGIN", "loginState 변경: $loginState")
if (loginState is LoginState.Success && socialLoginData != null) {
Log.d("MainApp", "소셜 로그인 성공! 홈으로 이동")
navigator.navigate(NavigationRoute.Home.route) {
popUpTo(0) { inclusive = true }
launchSingleTop = true
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

현재 테스트 중으로, 추후 삭제하겠습니다.

Comment on lines 190 to 206
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.padding(start = 20.scaler, end = 20.scaler, bottom = bottomPadding)
.height(50.scaler)
.background(Color.White, shape = RoundedCornerShape(18.dp)),
contentAlignment = Alignment.Center
) {
Text(
text = "로그인 하러가기",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
style = TextStyle(brush = colorTheme.maincolor),
fontFamily = Paperlogy.font
)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

중복된 부분 수정했습니다.

Comment on lines 300 to 304
composable("social_login_gate") { entry ->

val signUpVm: SignUpViewModel = hiltViewModel(
navController.getBackStackEntry("social_auth_graph")
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

수정했습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
feature/login/src/main/java/com/example/login/ui/screen/email/WelcomeScreen.kt (1)

106-106: ⚠️ Potential issue | 🟡 Minor

isSignUpRequested = false 재설정이 성공 경로에서 의미 없습니다.

SignUpState.Success 분기에서 navigator.navigate(...) 호출 직후 isSignUpRequested = false로 리셋하지만, popUpTo("auth_graph") { inclusive = true }로 이 화면이 백스택에서 제거되므로 해당 값이 참조될 기회가 없습니다. 불필요한 상태 변경으로 혼란을 줄 수 있습니다.

🔧 수정 제안
             is SignUpState.Success -> {
                 Log.d("WelcomeScreen", "회원가입 성공")
                 navigator.navigate("email_login") {
                     popUpTo("auth_graph") { inclusive = true }
                 }
-                isSignUpRequested = false
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/email/WelcomeScreen.kt`
at line 106, In the SignUpState.Success branch inside WelcomeScreen.kt, remove
the redundant assignment isSignUpRequested = false after navigator.navigate(...)
with popUpTo("auth_graph") { inclusive = true } because the screen is removed
from the back stack and that state will never be read; if you actually need to
reset the flag for downstream logic, perform the reset before calling
navigator.navigate or handle it in the ViewModel instead (references:
isSignUpRequested, SignUpState.Success, navigator.navigate,
popUpTo("auth_graph")).
feature/curation/src/main/java/com/example/curation/ui/screen/CurationDetailScreen.kt (1)

83-83: ⚠️ Potential issue | 🟡 Minor

collectAsState 대신 collectAsStateWithLifecycle 사용 필요

Line 83의 nicknameStatecollectAsState를 사용하고 있으며, 동일한 컴포저블 내 나머지 상태들(Lines 91-95)은 collectAsStateWithLifecycle을 사용합니다. 비생명주기 인식 버전은 앱이 백그라운드에 있을 때도 계속 수집하여 불필요한 리소스를 소비합니다.

🔧 수정 제안
-    val nicknameState = homeViewModel.nickname.collectAsState(initial = "")
+    val nicknameState by homeViewModel.nickname.collectAsStateWithLifecycle(initialValue = "")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/curation/src/main/java/com/example/curation/ui/screen/CurationDetailScreen.kt`
at line 83, Replace the non-lifecycle-aware collection for nicknameState: locate
the declaration val nicknameState =
homeViewModel.nickname.collectAsState(initial = "") in CurationDetailScreen (the
composable using homeViewModel.nickname) and change it to use
collectAsStateWithLifecycle so it matches the other state flows (Lines 91-95)
and stops collecting while the app is backgrounded; ensure you import and call
the lifecycle-aware extension on the Flow/StateFlow from homeViewModel.nickname.
🟡 Minor comments (13)
core/src/main/java/com/example/core/util/UrlUtils.kt-3-5 (2)

3-5: ⚠️ Potential issue | 🟡 Minor

빈 문자열 또는 공백 입력 처리 미흡

raw"" 이거나 공백(" ")인 경우 "https://" 또는 "https:// " 같은 잘못된 URL이 반환됩니다. 호출부에서 검증이 보장되지 않는다면 가드 조건을 추가하는 것이 안전합니다.

🛡️ 수정 제안
-fun ensureHttpScheme(raw: String): String =
-    if (raw.startsWith("http://") || raw.startsWith("https://")) raw
-    else "https://$raw"
+fun ensureHttpScheme(raw: String): String {
+    if (raw.isBlank()) return raw
+    return if (raw.startsWith("http://", ignoreCase = true) ||
+               raw.startsWith("https://", ignoreCase = true)) raw
+    else "https://$raw"
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/example/core/util/UrlUtils.kt` around lines 3 - 5,
ensureHttpScheme currently produces invalid URLs for empty or whitespace-only
inputs; update ensureHttpScheme to first val trimmed = raw.trim(), then if
trimmed.isBlank() return "" (or otherwise return raw unchanged per project
convention), otherwise check trimmed.startsWith("http://") or "https://" and
return trimmed or "https://$trimmed" accordingly so you don't produce "https://
" for blank inputs.

3-5: ⚠️ Potential issue | 🟡 Minor

startsWith 대소문자 구분 및 비-HTTP 스킴 중복 접두사 문제

두 가지 엣지 케이스가 있습니다.

  1. 대소문자 구분: Kotlin의 startsWith는 기본적으로 대소문자를 구분하므로, "HTTP://..." 또는 "HTTPS://..." 같은 입력이 else 분기로 빠져 "https://HTTP://..." 형태의 손상된 URL을 반환합니다.
  2. 비-HTTP 스킴: "ftp://example.com" 또는 "file:///..." 등 다른 스킴도 else 분기를 타게 되어 "https://ftp://example.com" 같은 이중 스킴 URL이 생성됩니다.

딥링크 컨텍스트에서 당장 발생 가능성이 낮더라도, 방어적으로 처리하는 것이 좋습니다.

🛡️ 수정 제안
 fun ensureHttpScheme(raw: String): String =
-    if (raw.startsWith("http://") || raw.startsWith("https://")) raw
+    if (raw.startsWith("http://", ignoreCase = true) ||
+        raw.startsWith("https://", ignoreCase = true) ||
+        raw.contains("://")) raw
     else "https://$raw"

contains("://") 조건을 추가하면 ftp://, file:// 등 다른 스킴도 그대로 통과시켜 이중 스킴 생성을 방지할 수 있습니다.

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

In `@core/src/main/java/com/example/core/util/UrlUtils.kt` around lines 3 - 5, The
ensureHttpScheme function currently uses case-sensitive startsWith checks and
doesn't guard against non-HTTP schemes, causing outputs like
"https://HTTP://..." or "https://ftp://..."; update ensureHttpScheme to return
raw unchanged if it already has any scheme (e.g., raw.contains("://")) or if it
starts with "http://" or "https://" in a case-insensitive way (use
startsWith(..., ignoreCase = true) for "http" checks), otherwise prepend
"https://". Locate and modify ensureHttpScheme to apply these conditions and
trim surrounding whitespace before checks to avoid false negatives.
app/src/main/java/com/example/linku_android/deeplink/SocialDeepLinkHandler.kt-15-17 (1)

15-17: ⚠️ Potential issue | 🟡 Minor

tokenprovider가 빈 문자열("")인 경우에 대한 방어 로직을 고려해 주세요.

getQueryParameter는 쿼리에 키가 존재하지만 값이 비어 있을 때 빈 문자열을 반환합니다. 현재 null 체크만 수행하므로, ?token=&path=kakao 같은 케이스에서 빈 토큰이 유효한 데이터로 처리될 수 있습니다.

💡 빈 문자열 방어 예시
-            val provider = uri.getQueryParameter("path")
-            val token    = uri.getQueryParameter("token")
+            val provider = uri.getQueryParameter("path")?.takeIf { it.isNotBlank() }
+            val token    = uri.getQueryParameter("token")?.takeIf { it.isNotBlank() }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/example/linku_android/deeplink/SocialDeepLinkHandler.kt`
around lines 15 - 17, The current null-only check in SocialDeepLinkHandler
(where SocialLoginData is created) allows empty strings like
"?token=&path=kakao" to be treated as valid; change the guard to reject blank
values by using an isNullOrBlank-style check for both token and provider before
constructing SocialLoginData so empty strings are treated as missing (i.e., skip
creation/return null or handle as error) in the method that builds/returns
SocialLoginData.
feature/curation/src/main/java/com/example/curation/CurationViewModel.kt-48-49 (1)

48-49: ⚠️ Potential issue | 🟡 Minor

실제 이름처럼 보이는 "세나"를 기본값·폴백으로 사용하는 것은 UX 문제입니다.

"세나"는 개발용 테스트 값으로 보이며, 현재 코드에서는 다음 세 가지 경우에 사용자에게 그대로 노출됩니다:

  • ViewModel 생성 직후 (초기값, Line 48)
  • uid ≤ 0이어서 로그인되지 않은 상태 (Line 94)
  • API 호출 실패 (Line 100의 ?:, Line 104)

로그인하지 않은 모든 사용자 또는 닉네임 로드에 실패한 사용자에게 타인의 이름처럼 보이는 값이 표시됩니다. 빈 문자열("") 또는 중립적인 플레이스홀더(예: "사용자")로 교체하고, 같은 문자열이 4군데(48, 94, 100, 104)에 중복되어 있으므로 상수로 추출하는 것이 좋습니다.

🛠️ 제안 수정
+    companion object {
+        private const val DEFAULT_NICKNAME = ""   // 또는 "사용자"
+    }

-    private val _nickname = MutableStateFlow("세나")
+    private val _nickname = MutableStateFlow(DEFAULT_NICKNAME)
     val nickname: StateFlow<String> = _nickname
     fun loadNickname() {
         viewModelScope.launch {
             val uid = requireUserId()
             if (uid <= 0L) {
-                _nickname.value = "세나"
+                _nickname.value = DEFAULT_NICKNAME
                 return@launch
             }
             runCatching { userRepository.getNickname(uid) }
                 .onSuccess { nick ->
-                    _nickname.value = nick ?: "세나"
+                    _nickname.value = nick ?: DEFAULT_NICKNAME
                 }
                 .onFailure { e ->
                     Log.e("UserRepository", "닉네임 가져오기 실패", e)
-                    _nickname.value = "세나"
+                    _nickname.value = DEFAULT_NICKNAME
                 }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/curation/src/main/java/com/example/curation/CurationViewModel.kt`
around lines 48 - 49, 현재 ViewModel의 `_nickname` 초기값과 로그인/로드 실패 시 하드코딩된 "세나"가 UX
문제를 일으키므로, `_nickname`의 기본값, 로그인 미인증 분기(uid ≤ 0), API 실패의 Elvis 대체값(사용되는 3곳)을 모두
빈 문자열 또는 중립 플레이스홀더("사용자")로 바꾸고 중복 문자열을 하나의 상수로 추출하세요; 구체적으로 `private val
_nickname = MutableStateFlow("세나")`, 공개 `val nickname`, 그리고 닉네임을 설정하는 로직(현재 uid
체크와 API 결과 처리 지점)에 하드코딩된 "세나"를 찾아 `DEFAULT_NICKNAME`(예: "" 또는 "사용자") 상수로 대체하도록
수정하세요.
data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt-247-252 (1)

247-252: ⚠️ Potential issue | 🟡 Minor

알 수 없는 displayName이 경고 없이 무시되어 데이터가 조용히 손실될 수 있습니다.

signUp()(87~99줄)은 매핑 실패 시 Log.w로 경고하고 require(...isNotEmpty())로 빈 리스트를 차단하는 반면, updateUserInfo()는 동일한 mapNotNull 패턴을 사용하면서도 두 가지 안전장치가 모두 없습니다. 모든 displayName이 매핑에 실패하면 서버에 빈 리스트가 전송되어 사용자의 목적·관심사 데이터가 소거될 수 있습니다.

추가로, updateUserProfile()은 이 함수가 서버를 갱신한 뒤 원본 purposes/interests 문자열로 sessionStore.updateProfile()을 호출하므로, 미지의 displayName이 있을 경우 서버 상태와 로컬 세션이 불일치하게 됩니다.

🛡️ 수정 제안 — 경고 로그 및 빈 리스트 가드 추가
-        val mappedPurposes = purposes.mapNotNull { displayName ->
-            Purpose.fromDisplayName(displayName)?.serverKey
-        }
-        val mappedInterests = interests.mapNotNull { displayName ->
-            Interest.fromDisplayName(displayName)?.serverKey
-        }
+        val mappedPurposes = purposes.mapNotNull { displayName ->
+            Purpose.fromDisplayName(displayName)?.serverKey.also {
+                if (it == null) Log.w(TAG, "알 수 없는 Purpose: $displayName")
+            }
+        }
+        val mappedInterests = interests.mapNotNull { displayName ->
+            Interest.fromDisplayName(displayName)?.serverKey.also {
+                if (it == null) Log.w(TAG, "알 수 없는 Interest: $displayName")
+            }
+        }
+
+        require(mappedPurposes.isNotEmpty()) { "purposes 매핑 결과가 비어 있습니다." }
+        require(mappedInterests.isNotEmpty()) { "interests 매핑 결과가 비어 있습니다." }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt`
around lines 247 - 252, updateUserInfo() currently uses the same mapNotNull
logic as signUp() but lacks logging and empty-list protection, so unknown
displayName values can be dropped silently and cause server/local inconsistency
when updateUserProfile() later calls sessionStore.updateProfile() with the
original strings; modify updateUserInfo() to (1) detect unmapped displayNames by
comparing the original lists to mappedPurposes/mappedInterests and emit Log.w
with the unknown names, (2) guard against empty mapped lists (mirror signUp()
behavior: require(mappedPurposes.isNotEmpty()) and
require(mappedInterests.isNotEmpty()) or return/fail early), and (3) ensure
sessionStore.updateProfile() is called with the same canonical server keys
(mappedPurposes/mappedInterests) or otherwise keep server and local session in
sync; refer to Purpose.fromDisplayName, Interest.fromDisplayName,
mappedPurposes, mappedInterests, signUp(), updateUserInfo(), and
updateUserProfile() to locate and apply these changes.
feature/login/src/main/java/com/example/login/ui/screen/social/WelcomeSocialScreen.kt-82-95 (1)

82-95: ⚠️ Potential issue | 🟡 Minor

isSignUpRequestedremember로 선언되어 프로세스 복원 시 중복 호출 가능.

PR 목표에서 rememberSaveable로 회원가입 요청 상태를 유지한다고 언급되어 있습니다. 현재 remember를 사용하므로 구성 변경이나 프로세스 사망/복원 시 상태가 초기화되어 signUp()이 다시 호출될 수 있습니다.

♻️ rememberSaveable 사용 제안
-    var isSignUpRequested by remember { mutableStateOf(false) } //중복 호출 방자용 상태 추가
+    var isSignUpRequested by rememberSaveable { mutableStateOf(false) } //중복 호출 방지용 상태 추가
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/social/WelcomeSocialScreen.kt`
around lines 82 - 95, The isSignUpRequested flag is declared with remember so it
won't survive process death or configuration restore, causing signUp() in the
LaunchedEffect(Unit) to potentially be called multiple times; change the
declaration of isSignUpRequested to use rememberSaveable (e.g., replace remember
{ mutableStateOf(false) } with rememberSaveable { mutableStateOf(false) }) so
the flag persists across process restore and prevents duplicate calls to
signUpViewModel?.signUp() inside the LaunchedEffect(Unit).
feature/login/src/main/java/com/example/login/navigation/LoginApp.kt-262-294 (1)

262-294: ⚠️ Potential issue | 🟡 Minor

socialToken이 빈 문자열일 때의 처리가 없습니다.

status는 빈 문자열 체크(Line 279)를 하지만, socialToken은 빈 값이어도 그대로 SocialEntryScreen에 전달됩니다. 빈 socialToken으로 이후 completeSocialProfile API 호출 시 실패할 수 있습니다.

🛡️ socialToken 검증 추가 제안
-                if (status.isBlank()) {
+                if (status.isBlank() || socialToken.isBlank()) {
                     LaunchedEffect(Unit) {
                         navController.navigate("login") {
                             popUpTo("social_auth_graph") { inclusive = true }
                         }
                     }
                     return@composable
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/login/src/main/java/com/example/login/navigation/LoginApp.kt` around
lines 262 - 294, The composable route extracts socialToken but doesn't validate
it, so an empty socialToken may be passed into SocialEntryScreen and later fail
API calls (e.g., completeSocialProfile); add the same guard used for status:
check if socialToken.isBlank() inside the composable, and if so launch a
navigation back to "login" using LaunchedEffect with
navController.navigate("login") { popUpTo("social_auth_graph") { inclusive =
true } } and return@composable to avoid rendering SocialEntryScreen with an
empty token.
feature/login/src/main/java/com/example/login/viewmodel/SocialAuthViewModel.kt-151-159 (1)

151-159: ⚠️ Potential issue | 🟡 Minor

completeSocialProfile() 메서드에 gender, job, purposes 필드에 대한 검증이 없습니다.

현재 nickname만 검증하고 있으며, gender/job/purposes/interests 필드는 검증 없이 서버로 전송됩니다. UI 단계에서는 각 화면의 버튼이 다음과 같이 제어됩니다:

  • SocialGenderScreen: selectedGender != Gender.NONE일 때만 활성화
  • SocialJobScreen: selectedJob != Job.NONE일 때만 활성화
  • SocialPurposeScreen: selectedPurposes.isNotEmpty()일 때만 활성화

따라서 정상 흐름에서는 default 값이 도달하지 않습니다. 다만 방어적 프로그래밍 관점에서 completeSocialProfile() 메서드 내에도 이들 필드에 대한 검증을 추가하는 것이 좋습니다.

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

In
`@feature/login/src/main/java/com/example/login/viewmodel/SocialAuthViewModel.kt`
around lines 151 - 159, The method completeSocialProfile currently only
validates _nickname; add defensive checks inside completeSocialProfile to ensure
selectedGender != Gender.NONE, selectedJob != Job.NONE, and
selectedPurposes.isNotEmpty() (and selectedInterests.isNotEmpty() if you track
interests) before proceeding; for each failed check, Log.w with a clear message
(similar to the existing Log.w(TAG, "닉네임이 비어 있음")) and return early so invalid
data is not sent to the server. Ensure you reference the same properties used by
the UI (selectedGender, selectedJob, selectedPurposes, selectedInterests) and
keep the onSuccess call only after all validations pass.
feature/curation/src/main/java/com/example/curation/ui/util/CurationConstants.kt-3-4 (1)

3-4: ⚠️ Potential issue | 🟡 Minor

미사용 임포트 (Dp, dp)

Dpdp가 임포트되어 있지만, 파일 내 어떤 상수도 Dp 타입이나 dp 확장 함수를 사용하지 않습니다. 불필요한 임포트를 제거하세요.

🔧 수정 제안
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/curation/src/main/java/com/example/curation/ui/util/CurationConstants.kt`
around lines 3 - 4, The file contains unused imports Dp and dp; remove the
import lines for androidx.compose.ui.unit.Dp and androidx.compose.ui.unit.dp
from CurationConstants.kt (and ensure no remaining references to Dp or dp in
constants or code); afterwards run the IDE's optimize imports/formatting to
clean up any leftover unused imports.
feature/curation/build.gradle.kts-80-80 (1)

80-80: ⚠️ Potential issue | 🟡 Minor

accompanist-systemuicontroller는 활발하게 사용 중이며, 하드코딩된 버전 문자열을 버전 카탈로그로 이동하세요.

CurationMonthDetailScreen.kt에서 rememberSystemUiController()를 통해 상태 바 색상을 관리하고 있으므로, 이 의존성은 현재 필수적입니다. 다만 하드코딩된 버전 문자열 "com.google.accompanist:accompanist-systemuicontroller:0.34.0"은 프로젝트의 다른 모든 의존성처럼 gradle/libs.versions.toml의 버전 카탈로그로 이동하여 일관성을 유지하세요.

참고: accompanist-systemuicontroller는 deprecated 상태이므로, 향후 Compose의 네이티브 시스템 바 제어 API로 마이그레이션할 계획을 고려하세요.

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

In `@feature/curation/build.gradle.kts` at line 80, Replace the hardcoded
dependency string "com.google.accompanist:accompanist-systemuicontroller:0.34.0"
in feature/curation/build.gradle.kts with a version-catalog reference (e.g.,
implementation(libs.accompanist.systemuicontroller)); to do this add an entry
for accompanist-systemuicontroller under [libraries] in
gradle/libs.versions.toml mapping the same coordinates and version, then update
the build.gradle.kts to use that libs key; note that
CurationMonthDetailScreen.kt uses rememberSystemUiController so keep the
dependency but manage its version via the catalog and plan future migration off
the deprecated API.
feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt-107-107 (1)

107-107: ⚠️ Potential issue | 🟡 Minor

하드코딩된 폴백 닉네임 "세나"는 리소스 문자열로 교체해 주세요.

특정 사용자 이름을 기본값으로 사용하면 혼란을 줄 수 있습니다. strings.xml에 일반적인 플레이스홀더(예: "사용자")를 정의하는 것이 적절합니다.

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

In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt` at
line 107, Replace the hardcoded fallback nickname "세나" with a string resource:
add a placeholder like default_nickname (e.g., "사용자") to strings.xml and update
the usage in CurationScreen (the nickname assignment where nickname =
nickname.ifBlank { "세나" }) to fetch that resource (using stringResource(...) in
Compose or context.getString(R.string.default_nickname) where appropriate) so
the default is localized and not hardcoded.
feature/curation/src/main/java/com/example/curation/ui/main_card/CurationMainCardPager.kt-83-83 (1)

83-83: ⚠️ Potential issue | 🟡 Minor

scale 변수가 계산만 되고 사용되지 않습니다.

graphicsLayer에 적용하려던 것으로 보이는데, 현재 어디에도 참조되지 않습니다. 의도한 동작이 있다면 적용하고, 아니라면 삭제해 주세요.

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

In
`@feature/curation/src/main/java/com/example/curation/ui/main_card/CurationMainCardPager.kt`
at line 83, The local variable scale (computed from pageOffset) is never used;
either apply it to the item's Modifier.graphicsLayer or remove it. Locate the
calculation of scale (the line with val scale = 1f - (pageOffset *
0.3f).coerceIn(0f, 0.3f)) inside the CurationMainCardPager/pager item composable
and either: a) add it to the Modifier.graphicsLayer call for that card (set
scaleX = scale and scaleY = scale inside graphicsLayer) so the visual scaling is
applied, or b) delete the unused scale variable if no scaling is intended.
feature/curation/src/main/java/com/example/curation/ui/screen/detail/CurationMonthDetailScreen.kt-111-111 (1)

111-111: ⚠️ Potential issue | 🟡 Minor

backgroundAlpha 변수가 선언만 되고 사용되지 않습니다.

Line 122에서 animationProgress를 직접 사용하고 있어 backgroundAlpha는 데드 코드입니다. 삭제하거나, 의도적으로 분리한 것이라면 실제로 활용해 주세요.

🧹 제안
-    // 배경 알파 - 뒤로가기 시 서서히 투명해짐
-    val backgroundAlpha = animationProgress
-
     val resolvedImageUrl = imageUrl
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/curation/src/main/java/com/example/curation/ui/screen/detail/CurationMonthDetailScreen.kt`
at line 111, Remove the unused local variable backgroundAlpha or wire it into
the UI so it isn't dead code: either delete the declaration "val backgroundAlpha
= animationProgress" from CurationMonthDetailScreen.kt, or replace direct uses
of animationProgress in the composable (where the background alpha is applied)
with backgroundAlpha so the intent of separating the animated value is
preserved; ensure any styling or alpha parameters reference backgroundAlpha
instead of animationProgress if you keep the variable.
🧹 Nitpick comments (29)
feature/login/src/main/java/com/example/login/constants/ServerConfig.kt (2)

11-14: BuildConfig를 이용한 환경별 URL 분리를 우선적으로 적용하세요.

현재 BASE_URL이 프로덕션 서버만 가리키므로, 이 상태로 배포되면 개발/스테이징 환경에서도 항상 프로덕션 서버로 요청이 전송됩니다. TODO로만 남겨 두지 말고 조기에 반영하는 것을 권장합니다.

♻️ BuildConfig 기반 환경별 분리 제안

feature/login/build.gradle.kts에 환경별 buildConfigField 추가:

 android {
     buildTypes {
         debug {
+            buildConfigField("String", "BASE_URL", "\"https://dev.linkuserver.store\"")
         }
         release {
+            buildConfigField("String", "BASE_URL", "\"https://linkuserver.store\"")
         }
     }
+    buildFeatures {
+        buildConfig = true
+    }
 }

그리고 ServerConfig.kt를 다음과 같이 수정:

 object ServerConfig {
-    private const val BASE_URL = "https://linkuserver.store"
+    private val BASE_URL = BuildConfig.BASE_URL

     const val KAKAO_LOGIN_URL  = "$BASE_URL/login/kakao"
     const val GOOGLE_LOGIN_URL = "$BASE_URL/login/google"
 }

참고: BuildConfig 필드를 사용하면 const val을 유지할 수 없으므로 val로 변경이 필요합니다.

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

In `@feature/login/src/main/java/com/example/login/constants/ServerConfig.kt`
around lines 11 - 14, ServerConfig.kt currently hardcodes BASE_URL to the
production endpoint causing all builds to target prod; update to read the base
URL from BuildConfig (e.g., replace private const val BASE_URL with a
BuildConfig-provided value and change const val to val where needed) and use
that BuildConfig base for KAKAO_LOGIN_URL and GOOGLE_LOGIN_URL so different
buildTypes/flavors can set buildConfigField values; ensure you add
environment-specific buildConfigField entries in feature/login/build.gradle.kts
and update the symbols BASE_URL, KAKAO_LOGIN_URL, and GOOGLE_LOGIN_URL
accordingly.

1-1: com.example 패키지명은 실제 앱 네임스페이스로 교체하는 것을 권장합니다.

com.example은 Android Studio 프로젝트 생성 시 기본으로 생성되는 플레이스홀더 패키지입니다. 프로덕션 앱에서는 실제 도메인 기반 패키지명(예: com.linkyu.android.login.constants)으로 교체해야 Play Store 심사 및 앱 서명 시 충돌 위험을 방지할 수 있습니다. 이미 프로젝트 전체가 com.example을 사용하고 있다면 프로젝트 차원의 일괄 변경이 필요합니다.

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

In `@feature/login/src/main/java/com/example/login/constants/ServerConfig.kt` at
line 1, 파일 ServerConfig.kt의 패키지 선언(package com.example.login.constants)이 기본
플레이스홀더인 com.example를 사용하고 있으니 실제 앱 네임스페이스로 교체하세요; 예를 들어 package
com.linkyu.android.login.constants 같은 도메인 기반 패키지명으로 변경하거나 프로젝트 전체에 걸쳐 동일한
네임스페이스로 일괄 리팩터링(패키지 이동/리네임)해 모든 참조(imports, AndroidManifest, Gradle 설정 등)가 일치하도록
수정하십시오.
feature/curation/src/main/java/com/example/curation/ui/effect/skeleton/ShimmerBrush.kt (2)

30-58: grayShimmerBrushpinkShimmerBrush의 중복 코드 → 공통 함수로 추출 권장

두 함수는 colors 목록만 다르고 나머지 로직이 완전히 동일하며, 각각 독립된 rememberInfiniteTransition 인스턴스를 생성합니다. 두 브러시가 동일한 컴포저블 내에서 함께 사용될 경우, 불필요하게 두 개의 애니메이션 인스턴스가 생성됩니다.

colors를 파라미터로 받는 공통 함수로 추출하면 중복을 제거하고 애니메이션 인스턴스도 하나로 줄일 수 있습니다.

♻️ 공통 함수 추출 제안
+@Composable
+fun shimmerBrush(colors: List<Color>): Brush {
+    val translate = shimmerAnimation()
+    return Brush.linearGradient(
+        colors = colors,
+        start = Offset(translate - 300f, 0f),
+        end = Offset(translate, 0f)
+    )
+}
+
 /** 기본 그레이 쉬머 */
 `@Composable`
-fun grayShimmerBrush(): Brush {
-    val translate = shimmerAnimation()
-
-    return Brush.linearGradient(
-        colors = listOf(
-            Color(0xFFF4F5F7),
-            Color(0xFFE9EAEE),
-            Color(0xFFF4F5F7)
-        ),
-        start = Offset(translate - 300f, 0f),
-        end = Offset(translate, 0f)
-    )
-}
+fun grayShimmerBrush(): Brush = shimmerBrush(
+    colors = listOf(Color(0xFFF4F5F7), Color(0xFFE9EAEE), Color(0xFFF4F5F7))
+)
 
 /** 밝은 핑크 쉬머 */
 `@Composable`
-fun pinkShimmerBrush(): Brush {
-    val translate = shimmerAnimation()
-
-    return Brush.linearGradient(
-        colors = listOf(
-            Color(0xFFFBEEFF),
-            Color(0xFFE3A3F5),
-            Color(0xFFFBEEFF)
-        ),
-        start = Offset(translate - 300f, 0f),
-        end = Offset(translate, 0f)
-    )
-}
+fun pinkShimmerBrush(): Brush = shimmerBrush(
+    colors = listOf(Color(0xFFFBEEFF), Color(0xFFE3A3F5), Color(0xFFFBEEFF))
+)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/curation/src/main/java/com/example/curation/ui/effect/skeleton/ShimmerBrush.kt`
around lines 30 - 58, Extract a single composable helper (e.g., shimmerBrush or
shimmerGradientBrush) that accepts the colors list and reuses the existing
shimmerAnimation() call so only one rememberInfiniteTransition is created;
replace grayShimmerBrush and pinkShimmerBrush to call this new helper with their
respective color lists, keeping the same start/end Offsets and linearGradient
construction and referencing the existing shimmerAnimation() function to locate
where to reuse the animation logic.

13-23: 매직 넘버를 상수로 추출 권장

1000f, 1200, 300f가 하드코딩되어 있어 의미 파악과 일괄 수정이 어렵습니다.

♻️ 상수 추출 제안

파일 상단(imports 아래)에 상수 추가:

+private const val SHIMMER_DURATION_MS = 1200
+private const val SHIMMER_TARGET_VALUE = 1000f
+private const val SHIMMER_GRADIENT_WIDTH = 300f
+
 `@Composable`
 private fun shimmerAnimation(): Float {
     val transition = rememberInfiniteTransition(label = "shimmer")
 
     val translate by transition.animateFloat(
         initialValue = 0f,
-        targetValue = 1000f,
+        targetValue = SHIMMER_TARGET_VALUE,
         animationSpec = infiniteRepeatable(
             animation = tween(
-                durationMillis = 1200,
+                durationMillis = SHIMMER_DURATION_MS,
                 easing = LinearEasing
             ),

그리고 Offset 계산 부분:

-        start = Offset(translate - 300f, 0f),
-        end = Offset(translate, 0f)
+        start = Offset(translate - SHIMMER_GRADIENT_WIDTH, 0f),
+        end = Offset(translate, 0f)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/curation/src/main/java/com/example/curation/ui/effect/skeleton/ShimmerBrush.kt`
around lines 13 - 23, 하드코딩된 매직 넘버(1000f, 1200, 300f)를 상단 상수로 추출하여 가독성과 유지보수성을
높이세요: ShimmerBrush.kt 파일에서 transition.animateFloat의 targetValue(현재 1000f),
tween의 durationMillis(현재 1200) 및 Offset 계산에 사용되는 300f 값을 의미있는 이름(EX:
SHIMMER_TRANSLATE_DISTANCE, SHIMMER_DURATION_MS, SHIMMER_BRUSH_WIDTH 등)으로 선언하고
기존의 리터럴을 각각 val translate / transition.animateFloat, tween, Offset 계산 위치(라벨
"shimmerTranslate" 사용 부분 포함)에 대체하세요.
app/src/main/java/com/example/linku_android/deeplink/SocialLoginData.kt (1)

8-20: 주석 처리된 향후 구현 계획은 이슈 트래커로 관리하는 것을 권장합니다.

코드에 특정 날짜와 담당자 이름이 포함된 대규모 주석 블록은 시간이 지나면 맥락을 잃기 쉽습니다. GitHub Issue에 향후 SocialLoginData 확장 스펙을 기록하고, 코드에는 해당 이슈 번호만 간략히 남기는 방식이 유지보수에 유리합니다.

또한 line 16에 (배제함. 보안 이슈)val userId: Long? 같은 보안 관련 의사결정 사항은 ADR(Architecture Decision Record)이나 이슈에 남기는 것이 더 적절합니다.

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

In `@app/src/main/java/com/example/linku_android/deeplink/SocialLoginData.kt`
around lines 8 - 20, Remove the large dated/author comment block describing
future fields for SocialLoginData and instead create a tracked issue (or ADR)
that documents the proposed fields and security decision about userId; then
replace the comment in the file with a one-line reference to that issue/ADR
(e.g., "See issue `#1234` for SocialLoginData expansion and userId security
rationale") so the SocialLoginData plan and the security decision are recorded
in the tracker and only the issue number remains in the code.
app/src/main/java/com/example/linku_android/deeplink/SocialDeepLinkHandler.kt (2)

29-53: 주석 처리된 대체 구현 블록은 이슈 트래커로 이동하는 것이 좋습니다.

SocialLoginData.kt과 동일하게, 향후 변경 예정인 전체 함수 구현체를 주석으로 남기기보다는 GitHub Issue에 기록하고 코드에는 간단한 TODO 주석과 이슈 번호만 남기는 것을 권장합니다.

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

In
`@app/src/main/java/com/example/linku_android/deeplink/SocialDeepLinkHandler.kt`
around lines 29 - 53, Large commented alternative implementation of
extractSocialDeepLinkData (the block that reconstructs SocialLoginData) should
be removed from SocialDeepLinkHandler.kt and instead recorded as a GitHub Issue;
add a single-line TODO in the code referencing that issue (e.g., "TODO: See
GH-<issue-number> for proposed social deep link implementation") and keep only
the minimal current implementation; reference the function name
extractSocialDeepLinkData and the data class SocialLoginData in the issue so
reviewers can find the proposed changes.

11-11: BASE_URL 상수를 공유 가능하도록 구조화 필요

ServerConfig.ktBASE_URLprivate으로 설정되어 있어 다른 모듈에서 직접 참조할 수 없습니다. 하드코딩된 호스트명을 제거하려면 다음 중 하나가 필요합니다:

  • ServerConfigBASE_URLpublic으로 변경하거나, 호스트명만 추출한 공개 상수 추가
  • 또는 공용 모듈(예: data 모듈)에 서버 관련 상수를 중앙화하여 모든 모듈에서 재사용 가능하도록 구조화

현재 app 모듈의 deeplink handler에서 직접 참조할 수 있도록 상수를 정리하면, 서버 주소 변경 시 누락을 방지할 수 있습니다.

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

In
`@app/src/main/java/com/example/linku_android/deeplink/SocialDeepLinkHandler.kt`
at line 11, The hardcoded host in SocialDeepLinkHandler (the condition uri.host
== "linkuserver.store" && uri.path == "/auth") should use a shared constant
instead of a literal; update ServerConfig.kt to expose a public constant (either
make BASE_URL public or add a public HOST constant derived from BASE_URL) or
move server constants to a common module (e.g., data) and then replace the
literal in SocialDeepLinkHandler with that shared constant to ensure centralized
server address management.
feature/curation/src/main/java/com/example/curation/CurationViewModel.kt (3)

151-167: 대량의 주석 처리된 코드를 제거해 주세요.

loadMonthlyCuration의 이전 버전 4개, toggleHighlightLike의 이전 버전, loadHomeRecommendedLinksTop2의 이전 버전 등이 주석으로 남아 있어 파일이 크게 비대해졌습니다. 버전 이력은 Git이 관리하므로 주석 처리된 코드는 삭제하는 것이 좋습니다.

Also applies to: 202-348, 393-420, 461-480, 531-539

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

In `@feature/curation/src/main/java/com/example/curation/CurationViewModel.kt`
around lines 151 - 167, Remove the large blocks of commented-out legacy code to
shrink the file: delete the commented previous implementations inside
CurationViewModel (notably the old variants of loadMonthlyCuration,
toggleHighlightLike, and loadHomeRecommendedLinksTop2 and the other commented
ranges referenced around lines 202-348, 393-420, 461-480, 531-539) so only the
current active implementations remain; rely on Git history for prior versions
and ensure you do not remove any active code or TODOs—only pure commented-out
legacy code.

72-75: ?: null은 불필요한 중복 표현입니다.

?.let { ... }는 수신 객체가 null이면 이미 null을 반환합니다. 뒤에 ?: null을 추가해도 동작은 동일하며 코드만 복잡해집니다.

♻️ 제안 수정
-        recent?.let { r -> likedList.any { it.id == r.id } } ?: null
+        recent?.let { r -> likedList.any { it.id == r.id } }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/curation/src/main/java/com/example/curation/CurationViewModel.kt`
around lines 72 - 75, The expression for highlightLiked contains a redundant "?:
null" after using recent?.let { ... }; remove the unnecessary Elvis operator and
let the combine lambda simply return recent?.let { r -> _likedCurations.any {
it.id == r.id } } (i.e., keep symbols highlightLiked, _recentCuration,
_likedCurations, combine, stateIn) so a null recent yields null without the
extra "?: null".

137-145: _isGenerating.value = false 중복 설정 (Line 143)

setEmptyCurationState()(Line 646)는 내부에서 이미 _isGenerating.value = false를 수행하고, finally 블록(Line 197)도 동일하게 처리합니다. Line 143의 명시적 설정은 불필요합니다.

♻️ 제안 수정
             if (item == null || item.id <= 0L) {
                 setEmptyCurationState(markPrefetched = true)
-                _isGenerating.value = false   // 무한로딩 강제종료
                 return@launch
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/curation/src/main/java/com/example/curation/CurationViewModel.kt`
around lines 137 - 145, The assignment _isGenerating.value = false at the
early-return after repository.getMyRecentCuration(uid) is redundant because
setEmptyCurationState(markPrefetched = true) already sets _isGenerating.value =
false internally and the coroutine's finally block also clears it; remove the
explicit `_isGenerating.value = false` on that branch and keep only
setEmptyCurationState(...) and the return so the state is managed in one place
(refer to repository.getMyRecentCuration(uid), setEmptyCurationState, and the
existing finally block for where the flag is consistently handled).
data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt (2)

303-310: clearAuthData()는 더 이상 호출되지 않는 데드 코드입니다.

logout()이 직접 authPreference.clear() / sessionStore.clear()를 호출하도록 변경되면서 clearAuthData()는 어디에서도 호출되지 않습니다. 혼란을 방지하기 위해 제거를 권장합니다.

♻️ 수정 제안 — 미사용 메서드 제거
-    private suspend fun clearAuthData() {
-        // 중복 실행 방지함. 이미 로그아웃 상태면 아무것도 하지 않음
-        if (authPreference.userId == null && !authPreference.isLoggedIn) return
-
-        authPreference.clear()
-        sessionStore.clear()
-        Log.d(TAG, "모든 로컬 세션 데이터 삭제 완료")
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt`
around lines 303 - 310, The method clearAuthData() is dead code after logout()
was updated to call authPreference.clear() and sessionStore.clear() directly;
remove the unused clearAuthData() function from UserRepositoryImpl to avoid
confusion and dead code, and ensure no references remain to clearAuthData()
(search for clearAuthData and delete the method and any imports/comments that
solely referenced it).

7-8: 25줄의 와일드카드 임포트 com.example.core.model.auth.*로 이미 포함되는 중복 임포트입니다.

InterestPurpose는 25줄의 와일드카드로 이미 임포트되므로 7~8줄의 명시적 임포트는 제거해도 됩니다.

♻️ 수정 제안 — 중복 임포트 제거
-import com.example.core.model.auth.Interest
-import com.example.core.model.auth.Purpose
 import com.example.core.repository.UserRepository
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt`
around lines 7 - 8, 파일 상단에 com.example.core.model.auth.* 와일드카드 임포트가 이미 존재하므로 명시적
임포트인 Interest와 Purpose 선언(import com.example.core.model.auth.Interest, import
com.example.core.model.auth.Purpose)을 삭제하세요; 즉, UserRepositoryImpl의 상단에서 두 개의
중복된 import 문을 제거하여 중복 임포트를 없애고 빌드/IDE 경고가 사라지는지 확인하세요.
feature/curation/src/main/java/com/example/curation/ui/screen/CurationDetailScreen.kt (1)

69-73: 주석 처리된 코드를 정리하세요.

Lines 69–73의 hiltViewModel() 파라미터들과 Line 117의 toggleHighlightLike() 호출이 주석으로 남아 있습니다. 더 이상 필요하지 않다면 제거하거나, 추후 필요한 경우 이슈로 트래킹하세요.

Also applies to: 117-117

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

In
`@feature/curation/src/main/java/com/example/curation/ui/screen/CurationDetailScreen.kt`
around lines 69 - 73, Remove stale commented code and either delete or track it
as an issue: delete the commented hiltViewModel() parameter variants around the
CurationDetailScreen signature so only the active parameters (detailViewModel:
CurationDetailViewModel, homeViewModel: CurationViewModel) remain, and remove
the commented-out toggleHighlightLike() call (or create a ticket if you want to
preserve it for future work); search for and clean any remaining commented
references to toggleHighlightLike, CurationDetailViewModel = hiltViewModel(),
and CurationViewModel = hiltViewModel() in this file to keep the codebase tidy.
feature/curation/src/main/java/com/example/curation/ui/main_card/CurationMainCard.kt (2)

37-37: 백그라운드 색상을 테마 컬러 시스템에서 가져오는 것을 고려하세요.

Color(0xFFF2F2F2)는 하드코딩된 색상입니다. 프로젝트에서 LocalColorTheme을 사용하고 있으므로, 해당 팔레트에 적절한 스켈레톤/플레이스홀더 색상이 있다면 이를 활용하면 다크 모드 등 테마 변경 시 자동으로 적용됩니다.

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

In
`@feature/curation/src/main/java/com/example/curation/ui/main_card/CurationMainCard.kt`
at line 37, Replace the hardcoded background color used in the CurationMainCard
Modifier.chain (the .background(Color(0xFFF2F2F2)) call) with the appropriate
theme color from your LocalColorTheme palette (e.g., skeleton/placeholder color
exposed by LocalColorTheme) so the component automatically respects dark mode
and theme changes; update the Modifier.background call to read the color from
LocalColorTheme (or MaterialTheme color alias if preferred) instead of using the
literal Color(...) in CurationMainCard.

32-36: CurationConstants에 정의된 상수를 사용하세요 (DRY 위반)

346432는 이미 CurationConstants.CARD_WIDTH_VALUECurationConstants.CARD_HEIGHT_VALUE로 정의되어 있는데, 여기서는 직접 매직 넘버를 사용하고 있습니다. 상수를 재사용하지 않으면 값이 변경될 때 두 곳을 각각 수정해야 합니다.

♻️ 수정 제안
+import com.example.curation.ui.util.CurationConstants
 
     Box(
         modifier = modifier
             .size(
-                width = 346.scaler,
-                height = 432.scaler
+                width = CurationConstants.CARD_WIDTH_VALUE.scaler,
+                height = CurationConstants.CARD_HEIGHT_VALUE.scaler
             )
-            .clip(RoundedCornerShape(24.scaler))
+            .clip(RoundedCornerShape(24.scaler))  // 필요 시 CurationConstants에 코너 반경 상수 추가 고려
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/curation/src/main/java/com/example/curation/ui/main_card/CurationMainCard.kt`
around lines 32 - 36, Replace the magic numbers in the size() call inside
CurationMainCard with the constants from CurationConstants: use
CurationConstants.CARD_WIDTH_VALUE (with .scaler) for the width and
CurationConstants.CARD_HEIGHT_VALUE (with .scaler) for the height so the
.size(...) call reads the values from the shared constants instead of hard-coded
346 and 432; this keeps the RoundedCornerShape clipping and other chain calls
unchanged.
app/src/main/java/com/example/linku_android/MainActivity.kt (1)

25-26: 불필요한 주석 처리된 코드를 제거하세요.

Lines 25–26의 WindowCompat.setDecorFitsSystemWindows/enableEdgeToEdge() 및 Line 42의 socialLoginState.value = extractDeepLinkData(intent) 주석이 남아 있습니다. 더 이상 사용하지 않는다면 코드 가독성을 위해 삭제하세요.

Also applies to: 42-42

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

In `@app/src/main/java/com/example/linku_android/MainActivity.kt` around lines 25
- 26, Remove the leftover commented-out code in MainActivity: delete the
commented calls to WindowCompat.setDecorFitsSystemWindows(window, false) and
enableEdgeToEdge() and the commented assignment socialLoginState.value =
extractDeepLinkData(intent) if they are no longer needed; if any of these are
required for future work, replace with a short explanatory TODO comment instead
of full commented code. Ensure you only remove the comments around the exact
symbols WindowCompat.setDecorFitsSystemWindows, enableEdgeToEdge, and
socialLoginState.value = extractDeepLinkData so related logic remains intact.
feature/curation/src/main/java/com/example/curation/ui/util/CurationConstants.kt (1)

6-22: .scaler 수동 적용 요구사항은 오류 발생 가능성이 높습니다.

상수를 Int로 정의하고 "사용 시 .scaler 적용 필요"라는 주석에 의존하는 방식은 누락 위험이 있습니다. 실제로 CurationMainCard.kt에서 이 상수들을 사용하지 않고 346.scaler, 432.scaler를 직접 하드코딩하여 DRY 원칙이 이미 위반되고 있습니다.

필요하다면 아래처럼 @Composable 프로퍼티로 래핑하거나, 최소한 각 사용 위치에서 CurationConstants.CARD_WIDTH_VALUE.scaler와 같이 상수를 명시적으로 참조하도록 팀 내 합의를 권장합니다.

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

In
`@feature/curation/src/main/java/com/example/curation/ui/util/CurationConstants.kt`
around lines 6 - 22, CurationConstants currently exposes raw Int dp values
(CARD_TOP_OFFSET_VALUE, CARD_HORIZONTAL_PADDING_VALUE, CARD_WIDTH_VALUE,
CARD_HEIGHT_VALUE, DETAIL_CARD_HEIGHT_VALUE) and relies on a comment to remind
callers to call .scaler, which is error-prone; either convert these to
composable-scaled properties (e.g., expose `@Composable` vals that return the
scaled Dp values) or update all call sites (e.g., CurationMainCard.kt) to
reference the constants with explicit scaling
(CurationConstants.CARD_WIDTH_VALUE.scaler, etc.); locate the object
CurationConstants and either replace the Int constants with `@Composable`
properties returning the scaled result or change usages to reference the
constants with .scaler to maintain DRY and prevent hardcoded literals.
feature/curation/build.gradle.kts (1)

54-55: androidx.foundation.layoutandroidx.compose.foundation의 전이 의존성이므로 명시적 선언을 제거하세요.

55번 줄의 libs.androidx.foundation.layout 선언은 불필요합니다. 54번 줄의 libs.androidx.compose.foundation을 추가하면 foundation-layout이 자동으로 전이 의존성으로 포함됩니다. 프로젝트에서 이미 Compose BOM(48번 줄)을 사용 중이므로, 버전 관리도 자동으로 처리됩니다. 55번 줄을 제거하면 빌드 파일을 간결하게 유지할 수 있습니다.

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

In `@feature/curation/build.gradle.kts` around lines 54 - 55, Remove the redundant
explicit dependency libs.androidx.foundation.layout from the build file because
libs.androidx.compose.foundation already brings foundation-layout as a
transitive dependency (and the Compose BOM handles versions); keep only
implementation(libs.androidx.compose.foundation) and delete the
implementation(libs.androidx.foundation.layout) line to simplify the
dependencies.
feature/curation/src/main/java/com/example/curation/CurationApp.kt (2)

12-14: 사용되지 않는 import를 정리해 주세요.

CurationDetailViewModel(Line 12)과 CurationDetailScreen(Line 14)은 활성 코드에서 참조되지 않습니다. 주석 처리된 코드 블록 제거와 함께 삭제해 주세요.

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

In `@feature/curation/src/main/java/com/example/curation/CurationApp.kt` around
lines 12 - 14, Remove the unused imports CurationDetailViewModel and
CurationDetailScreen from CurationApp.kt and delete the associated commented-out
code block that referenced them; keep only imports and code actively used (e.g.,
CurationViewModel). Ensure no remaining references to CurationDetailViewModel or
CurationDetailScreen remain in the file before committing.

61-115: 주석 처리된 코드 블록을 제거해 주세요.

이전 내비게이션 구조가 주석으로 남아 있습니다. 버전 관리 시스템에서 히스토리를 확인할 수 있으므로, 최종 코드에서는 삭제하는 것이 좋습니다.

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

In `@feature/curation/src/main/java/com/example/curation/CurationApp.kt` around
lines 61 - 115, Remove the entire commented-out navigation block for
NavGraphBuilder.curationGraph (the large commented function that references
CurationViewModel, CurationDetailViewModel, CurationScreen and
CurationDetailScreen) so the file only contains active code; delete the
commented lines from the start of the /* or // block through its end, ensuring
no leftover commented navigation code remains and leaving imports/other
declarations intact.
feature/curation/src/main/java/com/example/curation/ui/main_card/CurationCardItem.kt (2)

26-27: imageUrl 해석 로직이 여러 파일에 중복됩니다.

?.takeIf { it.isNotBlank() && it != "null" } 패턴이 CurationCardItem, CurationMonthDetailScreen, CurationMonthDetailOverlay 3곳에서 반복됩니다. 유틸 확장 함수로 추출하면 일관성 유지와 향후 변경이 수월해집니다.

♻️ 제안 예시
// e.g. in a shared util file
fun String?.resolveImageUrl(): String? =
    this?.takeIf { it.isNotBlank() && it != "null" }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/curation/src/main/java/com/example/curation/ui/main_card/CurationCardItem.kt`
around lines 26 - 27, Extract the repeated nullable string check into a shared
extension like String?.resolveImageUrl() and replace the inline pattern used in
CurationCardItem (resolvedImageUrl), CurationMonthDetailScreen and
CurationMonthDetailOverlay with calls to that extension; ensure the extension
returns this?.takeIf { it.isNotBlank() && it != "null" } so callers like
resolvedImageUrl = imageUrl.resolveImageUrl() keep behavior consistent.

18-19: 개발 중 남은 주석을 정리해 주세요.

// CurationMainCardPager.kt// private 키워드 제거!는 개발 과정의 메모로, 최종 코드에는 불필요합니다.

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

In
`@feature/curation/src/main/java/com/example/curation/ui/main_card/CurationCardItem.kt`
around lines 18 - 19, Remove the leftover developer comment lines "//
CurationMainCardPager.kt" and "// private 키워드 제거!" from CurationCardItem.kt;
search within the CurationCardItem class/file for these inline comments and
delete them so only relevant production comments or documentation remain.
feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt (2)

63-63: SharedTransitionLayout이 실제 공유 전환 없이 사용되고 있습니다.

@OptIn(ExperimentalSharedTransitionApi::class)를 추가하고 SharedTransitionLayout으로 감쌌지만, 내부에서 sharedElement, sharedBounds, AnimatedVisibilityScope 등의 공유 전환 API가 사용되지 않습니다. 향후 사용 계획이 없다면 불필요한 복잡성이므로 제거를 고려해 주세요.

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

In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt` at
line 63, The file wraps UI in SharedTransitionLayout without using any shared
transition APIs (e.g., sharedElement, sharedBounds, AnimatedVisibilityScope);
either remove SharedTransitionLayout and the
`@OptIn`(ExperimentalSharedTransitionApi::class) annotation and replace it with a
simple container composable (Column/Box/whatever CurationScreen currently
needs), or implement the expected shared transition usage inside the
SharedTransitionLayout by adding sharedElement/sharedBounds calls where
transitions are required; locate the SharedTransitionLayout usage in
CurationScreen.kt and update accordingly.

170-175: imageUrlslistOf(null, null, null)로 하드코딩되어 실제 데이터와 연동되지 않습니다.

Line 110의 // TODO: 실제 curationId 주석과 함께, 실제 이미지 URL을 ViewModel에서 가져오는 작업이 필요합니다. 현재 상태로는 항상 폴백 이미지만 표시됩니다. 후속 작업으로 추적해 주세요.

이 TODO를 추적하기 위한 이슈를 생성해 드릴까요?

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

In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt`
around lines 170 - 175, The CurationMainCardPager is being fed a hardcoded
imageUrls = listOf(null, null, null); replace this with the real image list from
the ViewModel (e.g., observe/collect the ViewModel property that exposes image
URLs for the current curationId referenced by the "// TODO: 실제 curationId"
comment) and pass that state into CurationMainCardPager instead of the stub;
ensure you handle loading/fallback (nulls) and update the place where curationId
is determined to fetch the correct images from the ViewModel so the UI reflects
real data rather than always showing the fallback images.
feature/curation/src/main/java/com/example/curation/ui/screen/detail/CurationMonthDetailOverlay.kt (2)

110-127: 들여쓰기가 일관되지 않습니다.

Line 112의 if (resolvedImageUrl == null) 블록이 부모 Box 내부에서 한 레벨 덜 들여쓰기되어 있습니다. 기능에는 영향 없지만 가독성을 위해 정리해 주세요.

🧹 제안
         ) {
-
-        if (resolvedImageUrl == null) {
+            if (resolvedImageUrl == null) {
                 Image(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/curation/src/main/java/com/example/curation/ui/screen/detail/CurationMonthDetailOverlay.kt`
around lines 110 - 127, The if/else that switches between Image and
SubcomposeAsyncImage is misaligned inside the parent Box; reindent the block so
the if (resolvedImageUrl == null) { ... } else { ... } lines are indented to the
same level as other Box children, ensuring the Image and SubcomposeAsyncImage
calls (and their modifiers) are nested correctly under the Box; locate the usage
of resolvedImageUrl, Image, and SubcomposeAsyncImage in
CurationMonthDetailOverlay and fix the indentation to match surrounding
composables for consistent formatting.

144-157: CurationMonthDetailContent가 두 파일에 각각 다른 시그니처로 존재합니다.

이 파일에서는 CurationMonthDetailContent(page: Int)이고, CurationMonthDetailScreen.kt에서는 CurationMonthDetailContent(curationId: Long)입니다. 둘 다 private이라 컴파일 문제는 없지만, 동일한 이름에 다른 의미의 파라미터를 사용하면 혼동을 줄 수 있습니다. 향후 공통 컴포저블로 통합하는 것을 고려해 주세요.

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

In
`@feature/curation/src/main/java/com/example/curation/ui/screen/detail/CurationMonthDetailOverlay.kt`
around lines 144 - 157, The function CurationMonthDetailContent(page: Int)
conflicts in intent with the other CurationMonthDetailContent(curationId: Long)
in CurationMonthDetailScreen.kt; rename this variant to a distinct, descriptive
name (e.g., CurationMonthDetailPreviewContent or
CurationMonthDetailContentForPage) or change its parameter name to something
explicit (e.g., monthPage) and update all local callers; if you intend a single
shared composable, align the signature to accept the correct parameter
(curationId: Long) and migrate callers accordingly (update the function name and
parameter references in this file and any usages).
feature/curation/src/main/java/com/example/curation/ui/main_card/CurationMainCardPager.kt (1)

126-134: 인디케이터에서 dp를 직접 사용하고 있어 나머지 코드의 .scaler 패턴과 불일치합니다.

페이저 본문은 모두 .scaler를 사용하는데, 인디케이터는 7.dp, 28.dp, 3.5.dp 등 원시 dp를 쓰고 있습니다. 디바이스별 스케일링이 다르게 적용될 수 있습니다.

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

In
`@feature/curation/src/main/java/com/example/curation/ui/main_card/CurationMainCardPager.kt`
around lines 126 - 134, The indicator currently uses raw dp values (7.dp, 28.dp,
3.5.dp) in the Box/Modifier and RoundedCornerShape, which breaks the project's
`.scaler` sizing convention; update the Box sizing and shape to use the same
scaler extension/pattern used elsewhere in this module (replace 7.dp, 28.dp, and
3.5.dp with the corresponding scaled values via the project's scaler extension),
keeping the conditional logic on isSelected intact and applying the scaled sizes
to Modifier.width, Modifier.height and RoundedCornerShape so the indicator
respects device scaling like the rest of the pager UI.
feature/curation/src/main/java/com/example/curation/ui/screen/detail/CurationMonthDetailScreen.kt (2)

28-163: CurationMonthDetailScreenCurationMonthDetailOverlay의 애니메이션 로직이 거의 동일합니다.

두 파일 간 animationState/animationProgress/contentProgress 상태 머신, BackHandler 처리, 이미지 해석, 카드 렌더링, 콘텐츠 슬라이드 로직이 대부분 복사-붙여넣기 수준으로 중복됩니다. 차이점은 CurationConstants 사용 여부와 systemUiController 정도입니다.

공통 애니메이션 로직을 커스텀 훅이나 공유 컴포저블로 추출하면 유지보수가 크게 개선됩니다. 현재 PR 범위에서 어렵다면 후속 이슈로 추적해 주세요.

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

In
`@feature/curation/src/main/java/com/example/curation/ui/screen/detail/CurationMonthDetailScreen.kt`
around lines 28 - 163, The CurationMonthDetailScreen duplicates the same
animation/back-navigation/image/card rendering logic found in
CurationMonthDetailOverlay; extract the shared state and behavior
(animationState, animationProgress, contentProgress, BackHandler logic,
resolvedImageUrl resolution and card layout parameters like
startHeight/endHeight/horizontalPadding/topOffset) into a single reusable unit
(e.g., a useCurationDetailAnimation hook or a CurationDetailAnimation composable
that returns progress values and a trigger API) and have both
CurationMonthDetailScreen and CurationMonthDetailOverlay call it, wiring in
differences (CurationConstants usage, systemUiController) via parameters; if
extraction cannot be completed in this PR, encapsulate the duplicated logic into
a small helper and open a follow-up task to fully refactor into a shared
composable.

25-25: accompanist-systemuicontroller는 더 이상 지원되지 않는 라이브러리입니다.

Accompanist 0.32.0부터 systemuicontroller는 deprecated되었으며, 다음의 마이그레이션이 권장됩니다:

  • ComponentActivity.enableEdgeToEdge() (AndroidX Activity): 엣지-투-엣지 + 시스템 바 색상/아이콘 처리
  • Compose WindowInsets API: statusBarsPadding(), navigationBarsPadding(), imePadding() 등으로 시스템 바 하단의 콘텐츠 레이아웃 처리
  • WindowInsetsControllerCompat / WindowCompat: 표시 및 동작 제어가 필요한 경우 (Activity/Window 레벨에서 사용)

향후 완전히 제거될 수 있으므로 조기 마이그레이션을 권장합니다.

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

In
`@feature/curation/src/main/java/com/example/curation/ui/screen/detail/CurationMonthDetailScreen.kt`
at line 25, The file currently imports and likely uses
rememberSystemUiController (accompanist.systemuicontroller); remove that import
and migrate to the AndroidX/Compose APIs: call
ComponentActivity.enableEdgeToEdge() from the hosting Activity (or ensure
WindowCompat usage) to handle edge-to-edge and system bar coloring, replace uses
of rememberSystemUiController-driven padding/visibility with Compose
WindowInsets helpers such as statusBarsPadding(), navigationBarsPadding(),
imePadding() in CurationMonthDetailScreen, and if you need programmatic bar
visibility/behavior use WindowInsetsControllerCompat/WindowCompat at the
Activity/Window level instead of rememberSystemUiController.

Comment on lines 8 to 9
Log.d("SOCIAL_LOGIN", "URI 전체: $uri")
Log.d("SOCIAL_LOGIN", "host: ${uri.host}, path: ${uri.path}")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

보안 이슈: 전체 URI 로깅 시 인증 토큰이 노출됩니다.

uri에는 token 쿼리 파라미터가 포함되어 있으므로, Log.d로 전체 URI를 출력하면 logcat에 민감한 인증 토큰이 평문으로 기록됩니다. 루팅된 기기나 디버그 빌드에서 다른 앱이 접근할 수 있습니다.

릴리즈 빌드에서는 토큰이 포함된 로그를 제거하거나, 토큰을 마스킹 처리해야 합니다.

🔒 토큰 마스킹 예시
-        Log.d("SOCIAL_LOGIN", "URI 전체: $uri")
-        Log.d("SOCIAL_LOGIN", "host: ${uri.host}, path: ${uri.path}")
+        Log.d("SOCIAL_LOGIN", "host: ${uri.host}, path: ${uri.path}")
+        // 토큰은 로그에 출력하지 않음
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/example/linku_android/deeplink/SocialDeepLinkHandler.kt`
around lines 8 - 9, 로그에 전체 uri를 직접 출력하는 Log.d 호출(SocialDeepLinkHandler의 uri 변수
사용)은 쿼리 파라미터 token을 노출하므로 릴리즈에서는 제거하거나 마스킹해야 합니다; 수정 방법은 SocialDeepLinkHandler에서
현재 Log.d("SOCIAL_LOGIN", "URI 전체: $uri") 및 Log.d("SOCIAL_LOGIN", "host:
${uri.host}, path: ${uri.path}")를 변경하여 BuildConfig.DEBUG 여부를 체크해 디버그에서만 민감정보 없이
호스트/경로만 출력하거나, uri.queryParameter("token")를 추출해 토큰을 고정 길이로 대체(예: 앞/뒤 일부만 남기거나
******** 처리)한 뒤 마스킹된 URI를 로그에 남기도록 구현하고, 릴리즈 빌드에서는 전체 URI 로그를 완전히 제거하거나 마스크된 값만
로그에 남기세요.

Comment on lines +192 to +201
val displayPurposes = dto.purposes.mapNotNull { serverKey ->
Purpose.fromServerKey(serverKey)?.displayName ?: serverKey.also {
Log.w(TAG, "알 수 없는 Purpose serverKey: $serverKey")
}
}
val displayInterests = dto.interests.mapNotNull { serverKey ->
Interest.fromServerKey(serverKey)?.displayName ?: serverKey.also {
Log.w(TAG, "알 수 없는 Interest serverKey: $serverKey")
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

mapNotNull이 항상 no-op으로 동작하여 알 수 없는 서버 키가 UI에 그대로 노출됩니다.

?: serverKey 엘비스 연산자의 오른쪽 피연산자가 non-null String이므로 람다의 결과는 절대 null이 되지 않습니다. mapNotNull은 변환 함수의 null 결과만 제거하는 연산자인데, 여기서는 항상 non-null을 반환하므로 실질적으로 map과 동일하게 동작합니다. 결과적으로 열거형에 매핑되지 않는 서버 키가 그대로 UserInfo.purposes / UserInfo.interests에 포함되어 UI 레이어까지 내부 API 식별자가 노출될 수 있습니다.

signUp()(87~96줄)의 패턴처럼 폴백 없이 미지의 값을 필터링하거나, 폴백 의도가 맞다면 map을 사용해 의도를 명시해야 합니다.

🐛 수정 제안 — 미지의 서버 키 필터링 (signUp과 일관성 유지)
-        val displayPurposes = dto.purposes.mapNotNull { serverKey ->
-            Purpose.fromServerKey(serverKey)?.displayName ?: serverKey.also {
-                Log.w(TAG, "알 수 없는 Purpose serverKey: $serverKey")
-            }
-        }
-        val displayInterests = dto.interests.mapNotNull { serverKey ->
-            Interest.fromServerKey(serverKey)?.displayName ?: serverKey.also {
-                Log.w(TAG, "알 수 없는 Interest serverKey: $serverKey")
-            }
-        }
+        val displayPurposes = dto.purposes.mapNotNull { serverKey ->
+            Purpose.fromServerKey(serverKey)?.displayName.also {
+                if (it == null) Log.w(TAG, "알 수 없는 Purpose serverKey: $serverKey")
+            }
+        }
+        val displayInterests = dto.interests.mapNotNull { serverKey ->
+            Interest.fromServerKey(serverKey)?.displayName.also {
+                if (it == null) Log.w(TAG, "알 수 없는 Interest serverKey: $serverKey")
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt`
around lines 192 - 201, The current mapping for displayPurposes and
displayInterests uses ?: serverKey which always returns a non-null String so
mapNotNull never filters unknown server keys; change the lambdas to return the
enum displayName when Purpose.fromServerKey(serverKey) /
Interest.fromServerKey(serverKey) is non-null and return null (while logging)
when mapping fails so unknown server keys get filtered out (consistent with
signUp’s pattern), i.e., update the displayPurposes and displayInterests
computations to call Purpose.fromServerKey/Interest.fromServerKey, use their
?.displayName when present, and otherwise Log.w(...) and return null so
UserInfo.purposes/interests won’t expose internal server keys.

Comment on lines +334 to +346
return try {
userApi.completeSocialProfile(
authorization = "Bearer $socialToken",
body = request
)
Log.d(TAG, "[소셜 프로필 완료] 성공")
true
} catch (e: ApiError) {
// data 레이어 예외를 core/일반 예외로 변환 후 던짐
Log.e(TAG, "[소셜 프로필 완료 실패] ${e.message}")
throw Exception(e.message ?: "소셜 프로필 완료 실패")
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

ApiError를 삼키고 Exception으로 재포장해 예외 타입 정보가 소실되며, 반환 타입 Boolean도 부정확합니다.

두 가지 문제가 있습니다:

  1. 예외 삼킴(detekt SwallowedException 경고): catch(e: ApiError)에서 원인 예외를 전달하지 않고 Exception(e.message)로 재포장하면 ApiError의 구체적인 서브타입(예: BusinessError)과 스택 트레이스가 손실됩니다. 상위 레이어(ViewModel)에서 오류 유형을 구분할 수 없게 됩니다.

  2. 의미 없는 Boolean 반환 타입: 이 함수는 성공 시 항상 true를 반환하고, 실패 시 예외를 던집니다. false를 반환하는 경로가 없으므로 반환 타입은 Unit이 적절합니다.

🐛 수정 제안 — 원인 예외 보존 및 반환 타입 수정
-    override suspend fun completeSocialProfile(
-        ...
-    ): Boolean {
+    override suspend fun completeSocialProfile(
+        ...
+    ) {

         val request = SocialProfileMapper.toRequest(...)

-        return try {
+        try {
             userApi.completeSocialProfile(
                 authorization = "Bearer $socialToken",
                 body = request
             )
             Log.d(TAG, "[소셜 프로필 완료] 성공")
-            true
         } catch (e: ApiError) {
             Log.e(TAG, "[소셜 프로필 완료 실패] ${e.message}")
-            throw Exception(e.message ?: "소셜 프로필 완료 실패")
+            throw Exception(e.message ?: "소셜 프로필 완료 실패", e)
         }
     }

반환 타입을 Unit으로 변경하려면 UserRepository 인터페이스 및 호출부도 함께 수정해야 합니다.

🧰 Tools
🪛 detekt (1.23.8)

[warning] 341-341: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

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

In
`@data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt`
around lines 334 - 346, The method completeSocialProfile in UserRepositoryImpl
currently returns Boolean and catches ApiError then rethrows a new Exception,
losing the original exception type and stack trace; change the repository API
and implementation to return Unit (update UserRepository interface and all
callers), remove the arbitrary "true" return, and in the catch preserve the
original cause and type by rethrowing the ApiError (or rethrowing a new domain
exception that wraps e as the cause) instead of throw Exception(e.message),
while keeping the Log statements for success and failure.

Comment on lines +25 to +31
@Composable
fun rememberSocialParentEntry(
navController: NavHostController,
entry: NavBackStackEntry
): NavBackStackEntry = remember(entry) {
navController.getBackStackEntry("social_auth_graph")
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

rememberSocialParentEntryrememberAuthParentEntry와 달리 예외 안전하지 않습니다.

rememberAuthParentEntryrunCatching으로 감싸서 백스택 진입 실패 시 null을 반환하지만, rememberSocialParentEntrygetBackStackEntry를 직접 호출하여 social_auth_graph가 백스택에 없으면 크래시합니다.

LoginApp.kt의 socialComposable 헬퍼(Line 249)에서 if (parentEntry == null) 체크를 하고 있지만, 이 함수의 반환 타입이 non-nullable NavBackStackEntry이므로 해당 null 체크는 도달 불가능한 코드입니다.

🐛 rememberAuthParentEntry와 동일한 안전 패턴 적용
 `@Composable`
 fun rememberSocialParentEntry(
     navController: NavHostController,
     entry: NavBackStackEntry
-): NavBackStackEntry = remember(entry) {
-    navController.getBackStackEntry("social_auth_graph")
-}
+): NavBackStackEntry? = remember(entry) {
+    runCatching { navController.getBackStackEntry("social_auth_graph") }.getOrNull()
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/login/src/main/java/com/example/login/navigation/NavEntryHelper.kt`
around lines 25 - 31, The rememberSocialParentEntry function is not
exception-safe and returns a non-nullable NavBackStackEntry causing crashes and
making the null-check in socialComposable unreachable; update
rememberSocialParentEntry to mirror rememberAuthParentEntry by changing its
return type to nullable (NavBackStackEntry?) and wrap
navController.getBackStackEntry("social_auth_graph") in runCatching { ... }. Use
getOrNull() inside the remember block so it returns null on failure, keeping the
remember(entry) behavior and allowing the existing if (parentEntry == null)
check in socialComposable to work.

Comment on lines +27 to +35
try{
navController
.getBackStackEntry("social_auth_graph")
.savedStateHandle["socialToken"] = socialToken
} catch (e: IllegalAccessException){
// social_auth_graph가 백스택에 없는 경우 처리
navController.popBackStack()
return@LaunchedEffect
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# 파일이 존재하는지 확인하고 내용 확인
find . -name "SocialEntryScreen.kt" -type f

Repository: LinkYou-2025/LinkU_Android

Length of output: 155


🏁 Script executed:

# IllegalAccessException과 IllegalArgumentException의 사용 위치 확인
rg -n "IllegalAccessException|IllegalArgumentException" --type=kt -B 2 -A 2

Repository: LinkYou-2025/LinkU_Android

Length of output: 96


🏁 Script executed:

# NavController 관련 import 및 getBackStackEntry 사용 패턴 확인
rg -n "getBackStackEntry" --type=kt -B 2 -A 2

Repository: LinkYou-2025/LinkU_Android

Length of output: 96


🏁 Script executed:

# 파일 내용 확인
cat -n ./feature/login/src/main/java/com/example/login/ui/screen/social/SocialEntryScreen.kt

Repository: LinkYou-2025/LinkU_Android

Length of output: 2100


🏁 Script executed:

# 예외 타입 검색 - 타입 필터 없이
rg "IllegalAccessException|IllegalArgumentException" -B 2 -A 2

Repository: LinkYou-2025/LinkU_Android

Length of output: 1474


🏁 Script executed:

# getBackStackEntry 사용 확인
rg "getBackStackEntry" -B 3 -A 3

Repository: LinkYou-2025/LinkU_Android

Length of output: 4564


예외 타입 수정: IllegalAccessExceptionIllegalArgumentException

NavController.getBackStackEntry()는 해당 route가 백스택에 없을 때 IllegalArgumentException을 던집니다. 현재 코드는 리플렉션 관련 예외인 IllegalAccessException을 catch하고 있어, 실제 예외 발생 시 catch 블록이 실행되지 않고 앱이 크래시됩니다.

🐛 예외 타입 수정
-        } catch (e: IllegalAccessException){
+        } catch (e: IllegalArgumentException) {
             // social_auth_graph가 백스택에 없는 경우 처리
             navController.popBackStack()
             return@LaunchedEffect
         }
📝 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
try{
navController
.getBackStackEntry("social_auth_graph")
.savedStateHandle["socialToken"] = socialToken
} catch (e: IllegalAccessException){
// social_auth_graph가 백스택에 없는 경우 처리
navController.popBackStack()
return@LaunchedEffect
}
try{
navController
.getBackStackEntry("social_auth_graph")
.savedStateHandle["socialToken"] = socialToken
} catch (e: IllegalArgumentException) {
// social_auth_graph가 백스택에 없는 경우 처리
navController.popBackStack()
return@LaunchedEffect
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/social/SocialEntryScreen.kt`
around lines 27 - 35, The catch clause in the LaunchedEffect block is catching
IllegalAccessException but NavController.getBackStackEntry(...) throws
IllegalArgumentException when the route is missing; update the catch to catch
IllegalArgumentException (or add an additional catch for
IllegalArgumentException) around the call to
navController.getBackStackEntry("social_auth_graph") and the savedStateHandle
assignment so the popBackStack() + return@LaunchedEffect path runs when the back
stack entry is absent.

Comment on lines +97 to +121
// 서버 응답 감지
LaunchedEffect(signUpState) {
when (signUpState) {
is SignUpState.Success -> {
Log.d("WelcomeScreen", "회원가입 성공")
onLoginSuccess() // MainApp의 홈 이동 로직 호출
navigator.navigate("email_login") {
popUpTo("auth_graph") { inclusive = true }
}
isSignUpRequested = false
}
// 재시도 문제가 있음. 사용자에게 회원가입 시패시 안내를 하고 재시도를 하도록 해야함.
is SignUpState.Error -> {
val message = (signUpState as SignUpState.Error).message
Log.e("WelcomeScreen", "회원가입 실패: $message")
isSignUpRequested = false
}
is SignUpState.Loading -> {
Log.d("WelcomeScreen", "회원가입 진행 중...")
}
is SignUpState.Idle -> {
// 초기 상태
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Success 시 onLoginSuccess()navigate("email_login") 이중 네비게이션 충돌.

Line 102에서 onLoginSuccess()를 호출하여 홈으로 이동한 후, Line 103-105에서 navigate("email_login")을 다시 호출합니다. onLoginSuccess()가 이미 상위 NavGraph를 정리하므로 후속 navigate 호출은 예측 불가능한 동작을 유발하거나 무시될 수 있습니다.

소셜 회원가입 성공 시에는 onLoginSuccess()만 호출하면 충분합니다.

🐛 불필요한 navigate 제거
             is SignUpState.Success -> {
                 Log.d("WelcomeScreen", "회원가입 성공")
                 onLoginSuccess()  //  MainApp의 홈 이동 로직 호출
-                navigator.navigate("email_login") {
-                    popUpTo("auth_graph") { inclusive = true }
-                }
                 isSignUpRequested = false
             }
📝 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
// 서버 응답 감지
LaunchedEffect(signUpState) {
when (signUpState) {
is SignUpState.Success -> {
Log.d("WelcomeScreen", "회원가입 성공")
onLoginSuccess() // MainApp의 홈 이동 로직 호출
navigator.navigate("email_login") {
popUpTo("auth_graph") { inclusive = true }
}
isSignUpRequested = false
}
// 재시도 문제가 있음. 사용자에게 회원가입 시패시 안내를 하고 재시도를 하도록 해야함.
is SignUpState.Error -> {
val message = (signUpState as SignUpState.Error).message
Log.e("WelcomeScreen", "회원가입 실패: $message")
isSignUpRequested = false
}
is SignUpState.Loading -> {
Log.d("WelcomeScreen", "회원가입 진행 중...")
}
is SignUpState.Idle -> {
// 초기 상태
}
}
}
// 서버 응답 감지
LaunchedEffect(signUpState) {
when (signUpState) {
is SignUpState.Success -> {
Log.d("WelcomeScreen", "회원가입 성공")
onLoginSuccess() // MainApp의 홈 이동 로직 호출
isSignUpRequested = false
}
// 재시도 문제가 있음. 사용자에게 회원가입 시패시 안내를 하고 재시도를 하도록 해야함.
is SignUpState.Error -> {
val message = (signUpState as SignUpState.Error).message
Log.e("WelcomeScreen", "회원가입 실패: $message")
isSignUpRequested = false
}
is SignUpState.Loading -> {
Log.d("WelcomeScreen", "회원가입 진행 중...")
}
is SignUpState.Idle -> {
// 초기 상태
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/social/WelcomeSocialScreen.kt`
around lines 97 - 121, In the SignUpState.Success branch inside the
LaunchedEffect in WelcomeSocialScreen (handle for SignUpState), remove the
redundant navigator.navigate("email_login") { popUpTo("auth_graph") { inclusive
= true } } call—only call onLoginSuccess() and set isSignUpRequested = false;
ensure no other navigation is triggered after onLoginSuccess() to avoid
double-navigation conflicts.

Comment on lines +192 to +238
// 2. 하단 버튼 레이어
Box(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.padding(start = 20.scaler, end = 20.scaler, bottom = bottomPadding)
.height(50.scaler)
.background(Color.White, shape = RoundedCornerShape(18.dp))
.clickable {
when (signUpState) {
is SignUpState.Error -> {
// 실패 시 재시도
isSignUpRequested = false
signUpViewModel?.signUp()
}

is SignUpState.Success -> onLoginSuccess()
else -> {} // Loading 중엔 무시
}
},
contentAlignment = Alignment.Center
) {
// 상태에 따라 버튼 내용 변경
when (signUpState) {
is SignUpState.Loading -> CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = Color(0xFFC800FF),
strokeWidth = 2.dp
)

is SignUpState.Error -> Text(
text = "다시 시도하기",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
style = TextStyle(brush = colorTheme.maincolor),
fontFamily = Paperlogy.font
)

else -> Text(
text = "홈으로 이동하기",
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
style = TextStyle(brush = colorTheme.maincolor),
fontFamily = Paperlogy.font
)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

else 분기에서 onLoginSuccess()가 호출되지 않아 사용자가 화면에 갇힙니다.

LoginApp.kt에서 WelcomeSocialScreensignUpViewModel을 전달하지 않으므로 signUpState는 항상 Idle입니다. clickableelse -> {} 분기가 실행되어 "홈으로 이동하기" 버튼이 아무 동작도 하지 않습니다. BackHandler가 뒤로가기도 차단하므로 사용자가 이 화면에서 빠져나갈 수 없습니다.

🐛 Idle 상태에서 홈 이동 처리 추가
                         is SignUpState.Error -> {
                             // 실패 시 재시도
                             isSignUpRequested = false
                             signUpViewModel?.signUp()
                         }

                         is SignUpState.Success -> onLoginSuccess()
-                        else -> {} // Loading 중엔 무시
+                        is SignUpState.Loading -> {} // Loading 중엔 무시
+                        is SignUpState.Idle -> onLoginSuccess() // signUpViewModel 없는 경우 홈 이동
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/login/src/main/java/com/example/login/ui/screen/social/WelcomeSocialScreen.kt`
around lines 192 - 238, The click handler in WelcomeSocialScreen currently
ignores the Idle (default) signUpState causing the "홈으로 이동하기" button to do
nothing; update the clickable lambda that switches on signUpState so the else
branch calls onLoginSuccess() (or explicitly handle SignUpState.Idle to call
onLoginSuccess()), and ensure the same behavior is used when signUpViewModel is
null by invoking onLoginSuccess() directly in the Idle path; locate the switch
on signUpState inside the Box clickable and replace the empty else -> {} with a
call to onLoginSuccess() (or add an explicit is SignUpState.Idle ->
onLoginSuccess()) so users can exit the screen.

Comment on lines 37 to +38
foundationLayout = "1.10.0"
foundationLayoutVersion = "1.10.2"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

동일한 아티팩트에 대한 중복 라이브러리 별칭 및 버전 충돌 가능성

androidx.compose.foundation:foundation-layout 아티팩트에 대해 두 개의 별칭이 정의되어 있습니다:

  • Line 90: androidx-compose-foundation-layout → 버전 1.10.0 (foundationLayout)
  • Line 91: androidx-foundation-layout → 버전 1.10.2 (foundationLayoutVersion)

동일한 Maven 좌표를 가리키는 두 별칭이 서로 다른 버전을 참조하므로, Gradle이 버전 충돌 경고를 발생시키거나 모듈 간에 서로 다른 버전이 혼용될 수 있습니다. 기존 androidx-compose-foundation-layoutfoundationLayout = "1.10.0" 항목을 제거하고 단일 별칭으로 통합하세요.

🔧 통합 제안
-foundationLayout = "1.10.0"
 foundationLayoutVersion = "1.10.2"
-androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayout" }
 androidx-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "foundationLayoutVersion" }

Also applies to: 90-91

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

In `@gradle/libs.versions.toml` around lines 37 - 38, There are two aliases
pointing to androidx.compose.foundation:foundation-layout with conflicting
versions (foundationLayout = "1.10.0" and foundationLayoutVersion = "1.10.2");
remove the duplicate/older alias (androidx-compose-foundation-layout /
foundationLayout = "1.10.0") and consolidate to a single alias (keep
androidx-foundation-layout / foundationLayoutVersion = "1.10.2" or rename to a
single clear alias) so the TOML defines one alias referencing version "1.10.2"
and update any usages to that single alias.

- AndroidManifest에 linku:// 딥링크 스킴 등록
- MainActivity에서 소셜 딥링크 수신 및 파싱 처리
- SocialDeepLinkBus(replay=1)로 타이밍 문제 해결
- SocialLoginData/SocialLoginEvent 코어 모듈로 이동
- LoginViewModel에 handleSocialDeepLink 추가
  - ACTIVE: 토큰 저장 후 LoginState.Success → 홈 이동
  - TEMP: socialToken 전달 후 회원가입 플로우 진입
- MainApp에서 LoginState.Success 감지 → 홈 이동 처리
- extractSocialDeepLinkData path 체크 제거 (linku://auth 구조 대응)

TODO: /api/users/me 완성 후 userId 실제값 저장 및 세션 풀 세팅
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

♻️ Duplicate comments (5)
app/src/main/java/com/example/linku_android/deeplink/SocialDeepLinkHandler.kt (1)

10-13: 보안 이슈: 전체 URI 로깅 시 인증 토큰이 노출됩니다.

Line 10에서 전체 URI를 로깅하면 accessToken, refreshToken, socialToken 등의 쿼리 파라미터가 logcat에 평문으로 기록됩니다. 이전 리뷰에서도 지적된 사항입니다.

🔒 토큰 마스킹 제안
-    Log.d("SOCIAL_LOGIN", "URI 전체: $uri")
-    Log.d("SOCIAL_LOGIN", "scheme: ${uri.scheme}")
-    Log.d("SOCIAL_LOGIN", "host: ${uri.host}")
-    Log.d("SOCIAL_LOGIN", "path: ${uri.path}")
+    Log.d("SOCIAL_LOGIN", "host: ${uri.host}, path: ${uri.path}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/example/linku_android/deeplink/SocialDeepLinkHandler.kt`
around lines 10 - 13, The current logging in SocialDeepLinkHandler (where Log.d
prints "URI 전체: $uri", "scheme", "host", "path") exposes sensitive query
parameters (accessToken, refreshToken, socialToken); remove or replace the
full-URI log and instead log only safe parts (scheme, host, path) and/or a
sanitized URI where any token query params are stripped or masked (e.g., replace
values with "***" or remove those keys) before calling Log.d; update the code in
the method that handles the incoming Uri in SocialDeepLinkHandler.kt to perform
this sanitization/masking for any sensitive query parameter names before
logging.
app/src/main/java/com/example/linku_android/MainActivity.kt (1)

24-24: 딥링크 URI 전체를 로깅하면 인증 토큰이 노출됩니다.

이전 리뷰에서도 지적된 사항입니다. intent.data 전체를 로깅하면 소셜 로그인 딥링크의 토큰 쿼리 파라미터가 logcat에 기록됩니다.

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

In `@app/src/main/java/com/example/linku_android/MainActivity.kt` at line 24, The
current logging of intent?.data in MainActivity (onCreate) exposes sensitive
query params (auth tokens); change the log to emit a sanitized URI (e.g., log
only scheme/host/path or the result of clearing the query) instead of the full
intent.data, and remove any duplicate logging of the same deep link to ensure
tokens are not written to logcat.
feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt (1)

247-252: userId = 0L로 저장 시 자동 로그인이 불가능합니다.

이전 리뷰에서 지적된 사항이며, 개발자가 백엔드 수정 후 대응 예정임을 확인했습니다. TODO 주석은 충분하나, 이 상태로 머지될 경우 소셜 로그인 사용자의 자동 로그인과 세션 설정(fetchAndSaveUserSession)이 작동하지 않음을 유의하세요.

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

In `@feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt`
around lines 247 - 252, The code currently saves userId as 0L in
authPreference.saveTokens, which prevents automatic login and breaks
fetchAndSaveUserSession; update LoginViewModel to call the API that returns the
current user (e.g., GET /api/users/me) after receiving social auth tokens,
extract the real userId from that response, then pass that userId into
authPreference.saveTokens (instead of 0L) and only then invoke
fetchAndSaveUserSession so session and nickname data are correctly populated;
locate the token-handling flow around authPreference.saveTokens in
LoginViewModel and wire the user-info fetch into that path.
feature/login/src/main/java/com/example/login/navigation/LoginApp.kt (2)

423-428: onLoginSuccess 콜백 전달 수정 확인 완료

이전 리뷰에서 제기된 WelcomeSocialScreenonLoginSuccess 콜백 미전달 문제가 정상적으로 수정되었습니다. onLoginSuccess = onLoginSuccess가 올바르게 전달되고 있습니다.

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

In `@feature/login/src/main/java/com/example/login/navigation/LoginApp.kt` around
lines 423 - 428, The previous issue where WelcomeSocialScreen was not receiving
the onLoginSuccess callback has been resolved; confirm that
composable("social_welcome") passes onLoginSuccess to WelcomeSocialScreen (check
the WelcomeSocialScreen invocation and the onLoginSuccess parameter) and approve
the change—no further code edits required since onLoginSuccess = onLoginSuccess
is now correctly supplied.

254-268: ⚠️ Potential issue | 🟠 Major

rememberSocialParentEntry의 null 체크가 여전히 도달 불가능합니다.

Line 243의 코드 주석 "NavEntryHelper.kt의 rememberSocialParentEntry에 try-catch 추가 필요"는 아직 적용되지 않았음을 시사합니다. rememberSocialParentEntry가 non-nullable NavBackStackEntry를 반환하는 한 Line 262의 if (parentEntry == null) 분기는 절대 실행되지 않으며, social_auth_graph가 백스택에 없을 경우 안전 처리 대신 예외가 발생합니다.

NavEntryHelper.kt에서 반환 타입을 NavBackStackEntry?로 변경하고, 진입 불가 시 null을 반환하는 구현이 필요합니다.

#!/bin/bash
# Description: rememberSocialParentEntry 및 rememberAuthParentEntry의 반환 타입 확인
rg -n "fun rememberSocialParentEntry\|fun rememberAuthParentEntry" --type=kotlin -A 5
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/login/src/main/java/com/example/login/navigation/LoginApp.kt` around
lines 254 - 268, The null-check in socialComposable is unreachable because
rememberSocialParentEntry currently returns a non-null NavBackStackEntry; change
rememberSocialParentEntry (and similarly rememberAuthParentEntry) signature to
return NavBackStackEntry? and update its implementation in NavEntryHelper.kt to
catch the absence/any exception (wrap lookup in try-catch), returning null when
the social_auth_graph parent entry cannot be found or an error occurs so the if
(parentEntry == null) branch in socialComposable (and analogous callers) can
execute safely.
🧹 Nitpick comments (4)
core/src/main/java/com/example/core/model/auth/SocialLoginData.kt (1)

13-24: 중복된 주석 블록을 정리하세요.

Lines 13-24의 주석은 바로 위의 data class 정의와 거의 동일한 내용을 반복합니다. 필드별 의미 설명이 필요하다면 각 프로퍼티에 KDoc을 추가하는 것이 더 적합합니다.

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

In `@core/src/main/java/com/example/core/model/auth/SocialLoginData.kt` around
lines 13 - 24, Remove the duplicated block comment that repeats the
SocialLoginData data class fields (the comment spanning lines 13-24) and keep
the actual data class declaration only; if field-level explanations are
required, move them into KDoc on the SocialLoginData class and/or KDoc for each
property (e.g., provider, result, status, accessToken, refreshToken,
socialToken, errorCode) instead of maintaining a redundant standalone comment
block.
app/src/main/java/com/example/linku_android/MainApp.kt (1)

128-136: consumeSocialLoginEvent() 호출 시점이 네비게이션 후입니다.

Line 131에서 navigator.navigate가 호출된 후 Line 135에서 consumeSocialLoginEvent()가 실행됩니다. 네비게이션이 recomposition을 유발하면 이 LaunchedEffect가 소비 전에 다시 실행될 가능성이 있습니다. 순서를 바꾸는 것을 권장합니다.

♻️ 순서 변경 제안
     LaunchedEffect(socialLoginEvent) {
         val event = socialLoginEvent as? SocialLoginEvent.NavigateToSocialEntry ?: return@LaunchedEffect
         pendingSocialToken = event.socialToken
+        loginViewModel.consumeSocialLoginEvent()
         navigator.navigate("login_root") {
             popUpTo(0) { inclusive = true }
             launchSingleTop = true
         }
-        loginViewModel.consumeSocialLoginEvent()
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/example/linku_android/MainApp.kt` around lines 128 -
136, The LaunchedEffect handling socialLoginEvent currently calls
navigator.navigate(...) before loginViewModel.consumeSocialLoginEvent(), which
can cause the effect to retrigger on recomposition and re-process the same
event; reorder the operations in the LaunchedEffect so you first set
pendingSocialToken = event.socialToken, then call
loginViewModel.consumeSocialLoginEvent(), and only after that invoke
navigator.navigate("login_root") { popUpTo(0) { inclusive = true };
launchSingleTop = true }, ensuring the event is consumed before any
navigation-induced recomposition.
app/src/main/AndroidManifest.xml (1)

44-53: 주석에 개발자 이름이 포함되어 있습니다.

코드 주석에 "서원이"와 같은 개인 이름 대신, 작업 내용이나 티켓 번호를 참조하는 것이 유지보수에 더 적합합니다. Line 54의 TODO에도 동일하게 적용됩니다.

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

In `@app/src/main/AndroidManifest.xml` around lines 44 - 53, Replace the personal
name in the AndroidManifest comment above the intent-filter with a neutral
description or ticket reference; update the comment mentioning "소셜 로그인 딥링크" to
reference the task/issue ID or describe the change (e.g., "Replace
app.deeplink.base-url with linku:// so social login redirect is handled by the
app; register scheme=linku and host=auth") and change the Line 54 TODO to the
same neutral task/ticket wording—do this around the intent-filter block to
remove the developer name "서원이" while keeping the implementation notes intact.
feature/login/src/main/java/com/example/login/navigation/LoginApp.kt (1)

151-183: authsocial 약관 루프에서 when(route) 패턴이 중복됩니다.

termsSteps(Lines 151–183)와 socialTermsSteps(Lines 344–377)의 when(route) 디스패치 블록이 거의 동일합니다. 공통 헬퍼 컴포저블(예: TermsScreenDispatcher(route, onBack, onAgreeClicked))로 추출하면 유지보수성이 향상됩니다.

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

In `@feature/login/src/main/java/com/example/login/navigation/LoginApp.kt` around
lines 151 - 183, Extract the duplicated when(route) dispatch logic used for
termsSteps and socialTermsSteps into a shared composable (e.g.,
TermsScreenDispatcher) that takes parameters (route: String, parentEntry:
NavBackStackEntry, onBack: () -> Unit, onAgreeClicked: (SignUpViewModel) ->
Unit) and internally calls the correct screen (ServiceTermsScreen,
PrivacyTermsScreenFixed, MarketingTermsScreenComposable) wiring onBack and an
agree handler that invokes agreeAction(vm) then onBack(); then replace the
in-place when blocks inside authComposable and the social equivalent to call
TermsScreenDispatcher(route, parentEntry, onBack, agreeAction) and obtain the
SignUpViewModel via hiltViewModel inside the dispatcher to keep behavior
identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/example/linku_android/deeplink/SocialDeepLinkBus.kt`:
- Around line 8-14: The SharedFlow currently uses replay=1 so old
SocialLoginData can be re-emitted; add a cache-clearing API on SocialDeepLinkBus
that calls _flow.resetReplayCache() to drop the stored replayed value (e.g., fun
clear() { _flow.resetReplayCache() }) and call this after processing a deep
link; reference symbols: SocialDeepLinkBus, _flow, flow, emit.

In `@app/src/main/java/com/example/linku_android/MainActivity.kt`:
- Around line 53-54: MainActivity is logging the entire SocialLoginData
(including the token) via Log.d("DEEPLINK", "… $data"); change this to avoid
calling SocialLoginData.toString() or printing the token: either log only
non-sensitive fields (e.g., userId, provider) or create a redacted accessor
(e.g., SocialLoginData.toRedactedString() or SocialLoginData.getSafeLogFields())
and use that in the Log.d call; additionally, update SocialLoginData.toString()
to not include the token (or ensure tokens are omitted/masked) so future
accidental logs are safe; target symbols: MainActivity Log.d call,
SocialLoginData.toString(), and SocialDeepLinkBus.emit usage.
- Around line 47-57: handleDeepLinkIntent currently only checks uri.host ==
"auth" so HTTPS links like https://linkuserver.store/auth (where uri.host ==
"linkuserver.store" and uri.path == "/auth") are ignored; update the routing to
accept either host == "auth" OR path == "/auth" (or trim leading slash) before
calling extractSocialDeepLinkData and SocialDeepLinkBus.emit; modify the when/if
in handleDeepLinkIntent to inspect uri.path (and normalize it) in addition to
uri.host so both linku://auth and https://linkuserver.store/auth are handled.

In `@app/src/main/java/com/example/linku_android/MainApp.kt`:
- Around line 117-123: The log call that interpolates "$data" inside the
LaunchedEffect (SocialDeepLinkBus.flow.collect) exposes sensitive tokens because
SocialLoginData.toString() prints accessToken/refreshToken/socialToken; remove
or change this log so it never prints full SocialLoginData (e.g., stop logging
data, log only a non-sensitive field or a masked representation), or wrap the
Log.d call with a build-type check (only in debug builds) and ensure
SocialLoginData.toString() is updated to mask tokens; update references in
MainApp.kt (LaunchedEffect, SocialDeepLinkBus.flow.collect,
loginViewModel.handleSocialDeepLink) and the SocialLoginData.toString()
implementation accordingly.
- Around line 142-155: The LaunchedEffect reacting to loginViewModel.loginState
(checking LoginState.Success) causes duplicate navigation because onLoginSuccess
also navigates; change the LaunchedEffect in MainApp.kt to only perform the home
navigation for social logins by adding a guard that checks the login source
(e.g., a new or existing flag on loginViewModel like isSocialLogin or a property
on LoginState indicating social vs email) before calling
homeViewModel.refreshAfterLogin(), mypageViewModel.refreshUserInfo(), setting
showNavBar/currentLinkuNavigationItem, and navigator.navigate; alternatively,
clear or consume the social-login-only event after handling so onLoginSuccess
and LaunchedEffect do not both navigate.

In `@core/src/main/java/com/example/core/model/auth/SocialLoginData.kt`:
- Around line 3-11: The SocialLoginData data class exposes tokens via the
auto-generated toString(); override toString() in SocialLoginData to mask
sensitive fields (accessToken, refreshToken, socialToken) before logging (e.g.,
show fixed masked value like "****" or a short prefix/suffix only). Modify the
class declaration for SocialLoginData by adding an override fun toString() that
returns a string including provider, result, status, and errorCode but replaces
accessToken/refreshToken/socialToken with masked values so any Log.d("$data") no
longer prints raw tokens.

In `@feature/login/src/main/java/com/example/login/navigation/LoginApp.kt`:
- Around line 354-359: The onBack handler defined as val onBack: () -> Unit
currently calls navController.popBackStack() before accessing
navController.currentBackStackEntry.savedStateHandle, which can race; change the
order so you first get
navController.currentBackStackEntry?.savedStateHandle?.set("show_terms_sheet",
true) and only then call navController.popBackStack(), mirroring the auth_graph
pattern; update the onBack definition in the social terms flow (the val onBack
in LoginApp / socialTermsSteps) to set the savedStateHandle before popping the
back stack.
- Around line 54-63: The code assumes initialSocialToken implies TEMP status
which is brittle; update the navigation to accept an explicit status instead of
hardcoding "TEMP": add an optional parameter (e.g., initialSocialStatus: String?
= null) to the LoginApp entry and pass that status into navController.navigate
(the social_entry query param) rather than the literal "TEMP", and update
callers to provide the real status coming from
LoginViewModel.handleSocialDeepLink / LoginState.Success; alternatively, if you
cannot change the signature now, add a clear top-of-file comment above the
LaunchedEffect and the initialSocialToken usage documenting the invariant ("only
TEMP tokens are passed here") and ensure any code that sets initialSocialToken
enforces that contract.

In `@feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt`:
- Line 225: The debug log prints the entire SocialLoginData (including token) in
handleSocialDeepLink via Log.d("SOCIAL_VM", "handleSocialDeepLink 호출됨: $data");
update SocialLoginData.toString() to mask or omit the token (e.g., replace token
chars with ***** or only show last4) and then change the log to use the masked
representation (or explicitly log non-sensitive fields), ensuring no raw token
is ever logged; refer to the SocialLoginData class and the handleSocialDeepLink
call site to apply the change.
- Around line 229-285: The when block handling social login outcomes can fall
through (leaving _loginState as Loading) when data.result or data.status are
unexpected or null; add a final else/default branch in the same when that logs
the unexpected values (use TAG and data.result/data.status), and sets
_loginState.value to a safe terminal state (e.g.,
LoginState.Error(LoginErrorType.UNKNOWN_ERROR) or LoginState.Idle) so the UI
won't stay in Loading; update the when around the code referenced (the when
handling data.result/data.status inside the coroutine that sets _loginState and
_socialLoginEvent) to include this default case.

---

Duplicate comments:
In
`@app/src/main/java/com/example/linku_android/deeplink/SocialDeepLinkHandler.kt`:
- Around line 10-13: The current logging in SocialDeepLinkHandler (where Log.d
prints "URI 전체: $uri", "scheme", "host", "path") exposes sensitive query
parameters (accessToken, refreshToken, socialToken); remove or replace the
full-URI log and instead log only safe parts (scheme, host, path) and/or a
sanitized URI where any token query params are stripped or masked (e.g., replace
values with "***" or remove those keys) before calling Log.d; update the code in
the method that handles the incoming Uri in SocialDeepLinkHandler.kt to perform
this sanitization/masking for any sensitive query parameter names before
logging.

In `@app/src/main/java/com/example/linku_android/MainActivity.kt`:
- Line 24: The current logging of intent?.data in MainActivity (onCreate)
exposes sensitive query params (auth tokens); change the log to emit a sanitized
URI (e.g., log only scheme/host/path or the result of clearing the query)
instead of the full intent.data, and remove any duplicate logging of the same
deep link to ensure tokens are not written to logcat.

In `@feature/login/src/main/java/com/example/login/navigation/LoginApp.kt`:
- Around line 423-428: The previous issue where WelcomeSocialScreen was not
receiving the onLoginSuccess callback has been resolved; confirm that
composable("social_welcome") passes onLoginSuccess to WelcomeSocialScreen (check
the WelcomeSocialScreen invocation and the onLoginSuccess parameter) and approve
the change—no further code edits required since onLoginSuccess = onLoginSuccess
is now correctly supplied.
- Around line 254-268: The null-check in socialComposable is unreachable because
rememberSocialParentEntry currently returns a non-null NavBackStackEntry; change
rememberSocialParentEntry (and similarly rememberAuthParentEntry) signature to
return NavBackStackEntry? and update its implementation in NavEntryHelper.kt to
catch the absence/any exception (wrap lookup in try-catch), returning null when
the social_auth_graph parent entry cannot be found or an error occurs so the if
(parentEntry == null) branch in socialComposable (and analogous callers) can
execute safely.

In `@feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt`:
- Around line 247-252: The code currently saves userId as 0L in
authPreference.saveTokens, which prevents automatic login and breaks
fetchAndSaveUserSession; update LoginViewModel to call the API that returns the
current user (e.g., GET /api/users/me) after receiving social auth tokens,
extract the real userId from that response, then pass that userId into
authPreference.saveTokens (instead of 0L) and only then invoke
fetchAndSaveUserSession so session and nickname data are correctly populated;
locate the token-handling flow around authPreference.saveTokens in
LoginViewModel and wire the user-info fetch into that path.

---

Nitpick comments:
In `@app/src/main/AndroidManifest.xml`:
- Around line 44-53: Replace the personal name in the AndroidManifest comment
above the intent-filter with a neutral description or ticket reference; update
the comment mentioning "소셜 로그인 딥링크" to reference the task/issue ID or describe
the change (e.g., "Replace app.deeplink.base-url with linku:// so social login
redirect is handled by the app; register scheme=linku and host=auth") and change
the Line 54 TODO to the same neutral task/ticket wording—do this around the
intent-filter block to remove the developer name "서원이" while keeping the
implementation notes intact.

In `@app/src/main/java/com/example/linku_android/MainApp.kt`:
- Around line 128-136: The LaunchedEffect handling socialLoginEvent currently
calls navigator.navigate(...) before loginViewModel.consumeSocialLoginEvent(),
which can cause the effect to retrigger on recomposition and re-process the same
event; reorder the operations in the LaunchedEffect so you first set
pendingSocialToken = event.socialToken, then call
loginViewModel.consumeSocialLoginEvent(), and only after that invoke
navigator.navigate("login_root") { popUpTo(0) { inclusive = true };
launchSingleTop = true }, ensuring the event is consumed before any
navigation-induced recomposition.

In `@core/src/main/java/com/example/core/model/auth/SocialLoginData.kt`:
- Around line 13-24: Remove the duplicated block comment that repeats the
SocialLoginData data class fields (the comment spanning lines 13-24) and keep
the actual data class declaration only; if field-level explanations are
required, move them into KDoc on the SocialLoginData class and/or KDoc for each
property (e.g., provider, result, status, accessToken, refreshToken,
socialToken, errorCode) instead of maintaining a redundant standalone comment
block.

In `@feature/login/src/main/java/com/example/login/navigation/LoginApp.kt`:
- Around line 151-183: Extract the duplicated when(route) dispatch logic used
for termsSteps and socialTermsSteps into a shared composable (e.g.,
TermsScreenDispatcher) that takes parameters (route: String, parentEntry:
NavBackStackEntry, onBack: () -> Unit, onAgreeClicked: (SignUpViewModel) ->
Unit) and internally calls the correct screen (ServiceTermsScreen,
PrivacyTermsScreenFixed, MarketingTermsScreenComposable) wiring onBack and an
agree handler that invokes agreeAction(vm) then onBack(); then replace the
in-place when blocks inside authComposable and the social equivalent to call
TermsScreenDispatcher(route, parentEntry, onBack, agreeAction) and obtain the
SignUpViewModel via hiltViewModel inside the dispatcher to keep behavior
identical.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba5758a and cbcfd3d.

📒 Files selected for processing (10)
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/com/example/linku_android/MainActivity.kt
  • app/src/main/java/com/example/linku_android/MainApp.kt
  • app/src/main/java/com/example/linku_android/deeplink/SocialDeepLinkBus.kt
  • app/src/main/java/com/example/linku_android/deeplink/SocialDeepLinkHandler.kt
  • app/src/main/java/com/example/linku_android/deeplink/SocialLoginData.kt
  • core/src/main/java/com/example/core/model/auth/SocialLoginData.kt
  • core/src/main/java/com/example/core/model/auth/SocialLoginEvent.kt
  • feature/login/src/main/java/com/example/login/navigation/LoginApp.kt
  • feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/java/com/example/linku_android/deeplink/SocialLoginData.kt

Comment on lines +8 to +14
object SocialDeepLinkBus {
private val _flow = MutableSharedFlow<SocialLoginData>(
extraBufferCapacity = 1,
replay = 1 // 늦게 구독해도 마지막 값 받을 수 있음
)
val flow: SharedFlow<SocialLoginData> = _flow.asSharedFlow()
fun emit(data: SocialLoginData) { _flow.tryEmit(data) }
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

replay=1로 인해 오래된 딥링크 데이터가 재처리될 수 있습니다.

replay=1은 늦은 구독자가 마지막 값을 받을 수 있게 하지만, 소셜 로그인 처리 완료 후에도 해당 값이 캐시에 남습니다. MainAppLaunchedEffect(Unit)가 재실행되거나 화면 재구성 시 동일한 딥링크 데이터가 다시 처리될 수 있습니다.

처리 완료 후 replay 캐시를 초기화하는 메서드를 추가하는 것을 권장합니다.

♻️ 캐시 초기화 메서드 추가 제안
 object SocialDeepLinkBus {
     private val _flow = MutableSharedFlow<SocialLoginData>(
         extraBufferCapacity = 1,
         replay = 1
     )
     val flow: SharedFlow<SocialLoginData> = _flow.asSharedFlow()
     fun emit(data: SocialLoginData) { _flow.tryEmit(data) }
+    fun clear() { _flow.resetReplayCache() }
 }
📝 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
object SocialDeepLinkBus {
private val _flow = MutableSharedFlow<SocialLoginData>(
extraBufferCapacity = 1,
replay = 1 // 늦게 구독해도 마지막 값 받을 수 있음
)
val flow: SharedFlow<SocialLoginData> = _flow.asSharedFlow()
fun emit(data: SocialLoginData) { _flow.tryEmit(data) }
object SocialDeepLinkBus {
private val _flow = MutableSharedFlow<SocialLoginData>(
extraBufferCapacity = 1,
replay = 1 // 늦게 구독해도 마지막 값 받을 수 있음
)
val flow: SharedFlow<SocialLoginData> = _flow.asSharedFlow()
fun emit(data: SocialLoginData) { _flow.tryEmit(data) }
fun clear() { _flow.resetReplayCache() }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/example/linku_android/deeplink/SocialDeepLinkBus.kt`
around lines 8 - 14, The SharedFlow currently uses replay=1 so old
SocialLoginData can be re-emitted; add a cache-clearing API on SocialDeepLinkBus
that calls _flow.resetReplayCache() to drop the stored replayed value (e.g., fun
clear() { _flow.resetReplayCache() }) and call this after processing a deep
link; reference symbols: SocialDeepLinkBus, _flow, flow, emit.

Comment on lines +47 to 57
private fun handleDeepLinkIntent(intent: Intent) {
val uri = intent.data ?: return

when (uri.host) {
"auth" -> {
val data = extractSocialDeepLinkData(intent) ?: return
Log.d("DEEPLINK", "소셜 로그인 딥링크 수신: $data")
SocialDeepLinkBus.emit(data) // ← 다음 단계에서 만들 파일
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

https://linkuserver.store/auth 딥링크가 소셜 로그인으로 라우팅되지 않습니다.

handleDeepLinkIntenturi.host == "auth"만 검사합니다. linku://auth의 경우 host"auth"이므로 동작하지만, https://linkuserver.store/auth의 경우 host"linkuserver.store"이고 path"/auth"이므로 이 분기에 진입하지 않습니다.

AndroidManifest에 https://linkuserver.store/auth intent-filter가 등록되어 있으므로 (Line 65-73), 해당 딥링크도 처리해야 합니다.

🐛 HTTPS 딥링크 대응 수정 제안
     private fun handleDeepLinkIntent(intent: Intent) {
         val uri = intent.data ?: return
 
         when (uri.host) {
             "auth" -> {
                 val data = extractSocialDeepLinkData(intent) ?: return
-                Log.d("DEEPLINK", "소셜 로그인 딥링크 수신: $data")
+                Log.d("DEEPLINK", "소셜 로그인 딥링크 수신 (custom scheme)")
+                SocialDeepLinkBus.emit(data)
+            }
+            "linkuserver.store" -> {
+                if (uri.path == "/auth") {
+                    val data = extractSocialDeepLinkData(intent) ?: return
+                    Log.d("DEEPLINK", "소셜 로그인 딥링크 수신 (https)")
+                    SocialDeepLinkBus.emit(data)
+                }
+            }
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/example/linku_android/MainActivity.kt` around lines 47
- 57, handleDeepLinkIntent currently only checks uri.host == "auth" so HTTPS
links like https://linkuserver.store/auth (where uri.host == "linkuserver.store"
and uri.path == "/auth") are ignored; update the routing to accept either host
== "auth" OR path == "/auth" (or trim leading slash) before calling
extractSocialDeepLinkData and SocialDeepLinkBus.emit; modify the when/if in
handleDeepLinkIntent to inspect uri.path (and normalize it) in addition to
uri.host so both linku://auth and https://linkuserver.store/auth are handled.

Comment on lines +53 to +54
Log.d("DEEPLINK", "소셜 로그인 딥링크 수신: $data")
SocialDeepLinkBus.emit(data) // ← 다음 단계에서 만들 파일
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

로그에 $data로 토큰이 노출됩니다.

SocialLoginDatatoString()에 토큰이 포함되므로 Line 53에서도 민감 정보가 로깅됩니다.

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

In `@app/src/main/java/com/example/linku_android/MainActivity.kt` around lines 53
- 54, MainActivity is logging the entire SocialLoginData (including the token)
via Log.d("DEEPLINK", "… $data"); change this to avoid calling
SocialLoginData.toString() or printing the token: either log only non-sensitive
fields (e.g., userId, provider) or create a redacted accessor (e.g.,
SocialLoginData.toRedactedString() or SocialLoginData.getSafeLogFields()) and
use that in the Log.d call; additionally, update SocialLoginData.toString() to
not include the token (or ensure tokens are omitted/masked) so future accidental
logs are safe; target symbols: MainActivity Log.d call,
SocialLoginData.toString(), and SocialDeepLinkBus.emit usage.

Comment on lines +117 to +123
LaunchedEffect(Unit) {
Log.d("SOCIAL_VM", "Bus 구독 시작")
SocialDeepLinkBus.flow.collect { data ->
Log.d("SOCIAL_VM", "MainApp Bus 수신: $data")
loginViewModel.handleSocialDeepLink(data)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bus에서 수신한 데이터 로깅 시 토큰이 노출됩니다.

Line 120의 "$data"SocialLoginData.toString()을 통해 accessToken, refreshToken, socialToken이 logcat에 기록됩니다. SocialLoginDatatoString() 마스킹과 함께 이 로그도 제거하거나 디버그 빌드로 제한하세요.

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

In `@app/src/main/java/com/example/linku_android/MainApp.kt` around lines 117 -
123, The log call that interpolates "$data" inside the LaunchedEffect
(SocialDeepLinkBus.flow.collect) exposes sensitive tokens because
SocialLoginData.toString() prints accessToken/refreshToken/socialToken; remove
or change this log so it never prints full SocialLoginData (e.g., stop logging
data, log only a non-sensitive field or a masked representation), or wrap the
Log.d call with a build-type check (only in debug builds) and ensure
SocialLoginData.toString() is updated to mask tokens; update references in
MainApp.kt (LaunchedEffect, SocialDeepLinkBus.flow.collect,
loginViewModel.handleSocialDeepLink) and the SocialLoginData.toString()
implementation accordingly.

Comment on lines +142 to +155
val loginState by loginViewModel.loginState.collectAsStateWithLifecycle()
LaunchedEffect(loginState) {
if (loginState is LoginState.Success) {
Log.d("SOCIAL_VM", "LoginState.Success 감지 → 홈 이동")
homeViewModel.refreshAfterLogin()
mypageViewModel.refreshUserInfo()
showNavBar = true
currentLinkuNavigationItem = LinkuNavigationItem.HOME
navigator.navigate(NavigationRoute.Home.route) {
popUpTo(0) { inclusive = true }
launchSingleTop = true
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

LoginState.Success 감지가 소셜 로그인뿐 아니라 모든 로그인 성공에 반응하여 중복 네비게이션이 발생합니다.

LaunchedEffectloginState is LoginState.Success일 때 무조건 홈으로 이동합니다. 그러나 일반 이메일/비밀번호 로그인 시에도 login() 메서드가 LoginState.Success를 설정하므로, LoginApponLoginSuccess 콜백(Line 317-343)과 이 LaunchedEffect동시에 홈 네비게이션을 실행합니다.

소셜 로그인 성공에 대해서만 반응하도록 조건을 추가하거나, onLoginSuccess 콜백과 역할을 분리해야 합니다.

🛠️ 소셜 로그인 전용 조건 추가 제안
     LaunchedEffect(loginState) {
-        if (loginState is LoginState.Success) {
+        val success = loginState as? LoginState.Success ?: return@LaunchedEffect
+        // 소셜 로그인(ACTIVE 딥링크)에서만 여기서 홈 이동 처리
+        if (success.result.status == "ACTIVE" && pendingSocialToken == null) {
             Log.d("SOCIAL_VM", "LoginState.Success 감지 → 홈 이동")
             homeViewModel.refreshAfterLogin()
             mypageViewModel.refreshUserInfo()
             showNavBar = true
             currentLinkuNavigationItem = LinkuNavigationItem.HOME
             navigator.navigate(NavigationRoute.Home.route) {
                 popUpTo(0) { inclusive = true }
                 launchSingleTop = true
             }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/example/linku_android/MainApp.kt` around lines 142 -
155, The LaunchedEffect reacting to loginViewModel.loginState (checking
LoginState.Success) causes duplicate navigation because onLoginSuccess also
navigates; change the LaunchedEffect in MainApp.kt to only perform the home
navigation for social logins by adding a guard that checks the login source
(e.g., a new or existing flag on loginViewModel like isSocialLogin or a property
on LoginState indicating social vs email) before calling
homeViewModel.refreshAfterLogin(), mypageViewModel.refreshUserInfo(), setting
showNavBar/currentLinkuNavigationItem, and navigator.navigate; alternatively,
clear or consume the social-login-only event after handling so onLoginSuccess
and LaunchedEffect do not both navigate.

Comment on lines +3 to +11
data class SocialLoginData(
val provider: String,//카카오, 네이버, 구글
val result: String,
val status: String?,
val accessToken: String?,
val refreshToken: String?,
val socialToken: String?,
val errorCode: String?
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

보안 이슈: data classtoString()이 토큰을 평문으로 노출합니다.

Kotlin data class는 모든 프로퍼티를 포함한 toString()을 자동 생성합니다. 이 객체가 여러 곳에서 Log.d로 출력되고 있어("$data" 형태) accessToken, refreshToken, socialToken이 logcat에 그대로 기록됩니다:

  • MainActivity.kt: Log.d("DEEPLINK", "소셜 로그인 딥링크 수신: $data")
  • MainApp.kt: Log.d("SOCIAL_VM", "MainApp Bus 수신: $data")
  • LoginViewModel.kt: Log.d("SOCIAL_VM", "handleSocialDeepLink 호출됨: $data")

toString()을 오버라이드하여 토큰을 마스킹하세요.

🔒 toString 마스킹 제안
 data class SocialLoginData(
     val provider: String,
     val result: String,
     val status: String?,
     val accessToken: String?,
     val refreshToken: String?,
     val socialToken: String?,
     val errorCode: String?
-)
+) {
+    override fun toString(): String =
+        "SocialLoginData(provider=$provider, result=$result, status=$status, errorCode=$errorCode)"
+}
📝 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
data class SocialLoginData(
val provider: String,//카카오, 네이버, 구글
val result: String,
val status: String?,
val accessToken: String?,
val refreshToken: String?,
val socialToken: String?,
val errorCode: String?
)
data class SocialLoginData(
val provider: String,//카카오, 네이버, 구글
val result: String,
val status: String?,
val accessToken: String?,
val refreshToken: String?,
val socialToken: String?,
val errorCode: String?
) {
override fun toString(): String =
"SocialLoginData(provider=$provider, result=$result, status=$status, errorCode=$errorCode)"
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/com/example/core/model/auth/SocialLoginData.kt` around
lines 3 - 11, The SocialLoginData data class exposes tokens via the
auto-generated toString(); override toString() in SocialLoginData to mask
sensitive fields (accessToken, refreshToken, socialToken) before logging (e.g.,
show fixed masked value like "****" or a short prefix/suffix only). Modify the
class declaration for SocialLoginData by adding an override fun toString() that
returns a string including provider, result, status, and errorCode but replaces
accessToken/refreshToken/socialToken with masked values so any Log.d("$data") no
longer prints raw tokens.

Comment on lines +54 to +63
LaunchedEffect(initialSocialToken) {
if (!initialSocialToken.isNullOrBlank()) {
navController.navigate(
"social_entry?socialToken=$initialSocialToken&status=TEMP"
) {
popUpTo("auth_graph") { inclusive = true }
launchSingleTop = true
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

status=TEMP 하드코딩으로 인한 묵시적 계약 위반 위험

initialSocialToken이 항상 TEMP 상태 사용자에게만 전달된다는 가정이 코드에 명시되지 않았습니다. LoginViewModel.handleSocialDeepLink가 ACTIVE 토큰을 LoginState.Success로 전환하고, TEMP 토큰만 initialSocialToken으로 전달한다는 계약이 깨지면(리팩터링, 타이밍 이슈 등), ACTIVE 사용자가 홈 대신 소셜 회원가입 플로우로 잘못 진입합니다.

두 가지 중 하나를 권장합니다:

  1. LoginAppinitialSocialStatus: String? = null 파라미터를 추가하고 실제 status를 전달
  2. 또는 현재 방식을 유지하되 최소한 상단에 명시적인 주석으로 "TEMP 토큰만 전달됨"을 보장하는 계약을 문서화
✨ 권장 수정 방안 1: 실제 status 파라미터 전달
 `@Composable`
 fun LoginApp(
     onLoginSuccess: () -> Unit,
     loginViewModel: LoginViewModel,
     showNavBar: (Boolean) -> Unit,
     initialSocialToken: String? = null,
+    initialSocialStatus: String? = null,
 ) {
     val navController = rememberNavController()

     LaunchedEffect(initialSocialToken) {
-        if (!initialSocialToken.isNullOrBlank()) {
+        if (!initialSocialToken.isNullOrBlank() && !initialSocialStatus.isNullOrBlank()) {
             navController.navigate(
-                "social_entry?socialToken=$initialSocialToken&status=TEMP"
+                "social_entry?socialToken=$initialSocialToken&status=$initialSocialStatus"
             ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/login/src/main/java/com/example/login/navigation/LoginApp.kt` around
lines 54 - 63, The code assumes initialSocialToken implies TEMP status which is
brittle; update the navigation to accept an explicit status instead of
hardcoding "TEMP": add an optional parameter (e.g., initialSocialStatus: String?
= null) to the LoginApp entry and pass that status into navController.navigate
(the social_entry query param) rather than the literal "TEMP", and update
callers to provide the real status coming from
LoginViewModel.handleSocialDeepLink / LoginState.Success; alternatively, if you
cannot change the signature now, add a clear top-of-file comment above the
LaunchedEffect and the initialSocialToken usage documenting the invariant ("only
TEMP tokens are passed here") and ensure any code that sets initialSocialToken
enforces that contract.

Comment on lines +354 to +359
val onBack: () -> Unit = {
navController.popBackStack()
navController.currentBackStackEntry
?.savedStateHandle
?.set("show_terms_sheet", true)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

socialTermsSteps.onBack에서 popBackStack 후 savedStateHandle 접근 — 순서 역전으로 인한 잠재적 경쟁 상태

auth_graph의 동일 패턴(Lines 161–164)과 달리, 소셜 약관에서는 popBackStack() 이후에 currentBackStackEntry에 접근합니다. popBackStack() 직후의 currentBackStackEntry 값이 social_login_gate를 가리키더라도, 상태 변경이 그 항목의 recomposition 이전에 반영되지 않으면 약관 시트가 재표시되지 않습니다.

auth_graph 패턴과 동일하게 pop 전에 대상 항목의 savedStateHandle에 먼저 접근하는 방식으로 수정하세요.

🐛 수정 제안
-    val onBack: () -> Unit = {
-        navController.popBackStack()
-        navController.currentBackStackEntry
-            ?.savedStateHandle
-            ?.set("show_terms_sheet", true)
-    }
+    val onBack: () -> Unit = {
+        try {
+            navController.getBackStackEntry("social_login_gate")
+                .savedStateHandle["show_terms_sheet"] = true
+        } catch (_: IllegalArgumentException) { }
+        navController.popBackStack()
+    }
📝 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 onBack: () -> Unit = {
navController.popBackStack()
navController.currentBackStackEntry
?.savedStateHandle
?.set("show_terms_sheet", true)
}
val onBack: () -> Unit = {
try {
navController.getBackStackEntry("social_login_gate")
.savedStateHandle["show_terms_sheet"] = true
} catch (_: IllegalArgumentException) { }
navController.popBackStack()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/login/src/main/java/com/example/login/navigation/LoginApp.kt` around
lines 354 - 359, The onBack handler defined as val onBack: () -> Unit currently
calls navController.popBackStack() before accessing
navController.currentBackStackEntry.savedStateHandle, which can race; change the
order so you first get
navController.currentBackStackEntry?.savedStateHandle?.set("show_terms_sheet",
true) and only then call navController.popBackStack(), mirroring the auth_graph
pattern; update the onBack definition in the social terms flow (the val onBack
in LoginApp / socialTermsSteps) to set the savedStateHandle before popping the
back stack.


fun handleSocialDeepLink(data: SocialLoginData) {
viewModelScope.launch {
Log.d("SOCIAL_VM", "handleSocialDeepLink 호출됨: $data")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

로그에 $data로 토큰이 노출됩니다.

SocialLoginDatatoString()에 토큰이 포함됩니다. SocialLoginDatatoString() 마스킹과 함께 이 로그도 정리하세요.

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

In `@feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt` at
line 225, The debug log prints the entire SocialLoginData (including token) in
handleSocialDeepLink via Log.d("SOCIAL_VM", "handleSocialDeepLink 호출됨: $data");
update SocialLoginData.toString() to mask or omit the token (e.g., replace token
chars with ***** or only show last4) and then change the log to use the masked
representation (or explicitly log non-sensitive fields), ensuring no raw token
is ever logged; refer to the SocialLoginData class and the handleSocialDeepLink
call site to apply the change.

Comment on lines +229 to +285
when {
// 기존 유저 - 바로 홈으로
data.result == "SUCCESS" && data.status == "ACTIVE" -> {
Log.d("SOCIAL_VM", "ACTIVE 케이스 진입")
val accessToken = data.accessToken ?: run {
_loginState.value = LoginState.Error(LoginErrorType.UNKNOWN_ERROR)
return@launch
}
val refreshToken = data.refreshToken ?: run {
_loginState.value = LoginState.Error(LoginErrorType.UNKNOWN_ERROR)
return@launch
}
// TODO: 서원이 /api/users/me API 확인 후 아래 작업 필요
// 1. GET /api/users/me 호출 → 실제 userId 조회
// 2. authPreference.saveTokens(userId = 실제값) 으로 교체
// 3. fetchAndSaveUserSession(userId) 호출 → 세션 풀 세팅
// 4. 현재는 userId=0L 임시값이라 자동 로그인 불가 상태

// TODO: 서원이 /api/users/me 확인 후 userId 실제값으로 교체
authPreference.saveTokens(
accessToken = accessToken,
refreshToken = refreshToken,
userId = 0L // TODO: 실제 userId로 교체 필요 - 지금 자동 로그인 불가, 닉네임 제대로 안 내려옴.
)
Log.d(TAG, "소셜 ACTIVE 토큰 저장 완료")

_loginState.value = LoginState.Success(
LoginResult(
accessToken = accessToken,
refreshToken = refreshToken,
userId = 0,
status = "ACTIVE",
inactiveDate = null
)
)
}

// 신규 유저 - 프로필 입력 화면으로
data.result == "SUCCESS" && data.status == "TEMP" -> {
val socialToken = data.socialToken ?: run {
_loginState.value = LoginState.Error(LoginErrorType.UNKNOWN_ERROR)
return@launch
}
Log.d(TAG, "소셜 TEMP → SocialEntry로 이동")

_socialLoginEvent.value = SocialLoginEvent.NavigateToSocialEntry(
socialToken = socialToken,
provider = data.provider
)
_loginState.value = LoginState.Idle
}

data.result == "FAIL" -> {
Log.e(TAG, "소셜 로그인 실패: ${data.errorCode}")
_loginState.value = LoginState.Error(LoginErrorType.UNKNOWN_ERROR)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

when 블록에 기본(else) 분기가 없어 LoginState.Loading이 영구 지속될 수 있습니다.

현재 처리되는 조건:

  • SUCCESS + ACTIVE
  • SUCCESS + TEMP
  • FAIL

result"SUCCESS"이지만 statusnull이거나 예상 외 값인 경우, 또는 result"SUCCESS""FAIL"도 아닌 경우 어떤 분기에도 진입하지 않아 loginStateLoading 상태로 남습니다. 사용자는 무한 로딩을 경험하게 됩니다.

🐛 기본 분기 추가 제안
                     data.result == "FAIL" -> {
                         Log.e(TAG, "소셜 로그인 실패: ${data.errorCode}")
                         _loginState.value = LoginState.Error(LoginErrorType.UNKNOWN_ERROR)
                     }
+
+                    else -> {
+                        Log.e(TAG, "소셜 로그인: 예상치 못한 응답 (result=${data.result}, status=${data.status})")
+                        _loginState.value = LoginState.Error(LoginErrorType.UNKNOWN_ERROR)
+                    }
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt`
around lines 229 - 285, The when block handling social login outcomes can fall
through (leaving _loginState as Loading) when data.result or data.status are
unexpected or null; add a final else/default branch in the same when that logs
the unexpected values (use TAG and data.result/data.status), and sets
_loginState.value to a safe terminal state (e.g.,
LoginState.Error(LoginErrorType.UNKNOWN_ERROR) or LoginState.Idle) so the UI
won't stay in Loading; update the when around the code referenced (the when
handling data.result/data.status inside the coroutine that sets _loginState and
_socialLoginEvent) to include this default case.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (5)
app/src/main/java/com/example/linku_android/MainApp.kt (2)

117-121: ⚠️ Potential issue | 🟠 Major

민감 토큰 로그 노출 제거 필요

Line 120의 "$data"SocialLoginData.toString() 경유로 토큰이 로그에 남을 수 있습니다. 운영/디버그 모두에서 토큰 필드는 절대 출력하지 않게 바꿔주세요.

🔧 제안 수정
-            Log.d("SOCIAL_VM", "MainApp Bus 수신: $data")
+            Log.d("SOCIAL_VM", "MainApp Bus 수신: socialStatus=${data.status}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/example/linku_android/MainApp.kt` around lines 117 -
121, Remove sensitive token exposure from logs by stopping the direct
interpolation of SocialLoginData into Log.d; in the LaunchedEffect where
SocialDeepLinkBus.flow is collected (SocialDeepLinkBus.flow and
loginViewModel.handleSocialDeepLink), replace Log.d("SOCIAL_VM", "MainApp Bus
수신: $data") with a safe log that omits or redacts token fields (e.g., log only
non-sensitive fields like provider or an event marker, or call a scrubber that
returns a redacted representation), ensuring SocialLoginData.toString() is never
used for logging.

142-155: ⚠️ Potential issue | 🔴 Critical

LoginState.Success 전역 감지로 네비게이션 중복/경합 발생

현재 조건은 소셜 여부와 무관하게 항상 홈 이동을 실행합니다. LoginApp.onLoginSuccess(일반 로그인) 및 딥링크 로그인 분기와 충돌해 중복 navigate가 발생할 수 있습니다. 소셜 딥링크 성공 “전용” 일회성 이벤트로 분리해 처리해 주세요.

🔧 방향성 예시 (소셜 딥링크 전용 가드)
     // TEMP 토큰 임시 보관
     var pendingSocialToken by remember { mutableStateOf<String?>(null) }
+    var socialDeepLinkInFlight by remember { mutableStateOf(false) }

     LaunchedEffect(Unit) {
         Log.d("SOCIAL_VM", "Bus 구독 시작")
         SocialDeepLinkBus.flow.collect { data ->
+            socialDeepLinkInFlight = true
             loginViewModel.handleSocialDeepLink(data)
         }
     }

     LaunchedEffect(socialLoginEvent) {
         val event = socialLoginEvent as? SocialLoginEvent.NavigateToSocialEntry ?: return@LaunchedEffect
+        socialDeepLinkInFlight = false
         pendingSocialToken = event.socialToken
         navigator.navigate("login_root") {
             popUpTo(0) { inclusive = true }
             launchSingleTop = true
         }
         loginViewModel.consumeSocialLoginEvent()
     }

     LaunchedEffect(loginState) {
-        if (loginState is LoginState.Success) {
+        if (loginState is LoginState.Success && socialDeepLinkInFlight) {
+            socialDeepLinkInFlight = false
             homeViewModel.refreshAfterLogin()
             mypageViewModel.refreshUserInfo()
             showNavBar = true
             currentLinkuNavigationItem = LinkuNavigationItem.HOME
             navigator.navigate(NavigationRoute.Home.route) {
                 popUpTo(0) { inclusive = true }
                 launchSingleTop = true
             }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/example/linku_android/MainApp.kt` around lines 142 -
155, The LaunchedEffect reacting to loginViewModel.loginState
(LoginState.Success) causes duplicate navigator.navigate calls by also being
triggered by non-social login flows (LoginApp.onLoginSuccess and deep link
handling); restrict this block to only handle the one-time social-deep-link
success: add or use a one-shot indicator (e.g., a new LoginState.SocialSuccess
variant or a Boolean/SingleLiveEvent like loginViewModel.isSocialDeepLinkEvent),
check that flag inside the LaunchedEffect before calling
homeViewModel.refreshAfterLogin(), mypageViewModel.refreshUserInfo(), setting
showNavBar/currentLinkuNavigationItem, and navigator.navigate(...), and then
consume/reset the flag immediately after navigation so it won’t fire again for
normal LoginState.Success paths handled elsewhere (e.g.,
LoginApp.onLoginSuccess).
data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt (2)

192-201: ⚠️ Potential issue | 🟠 Major

mapNotNull이 사실상 필터링을 못 하고 있어 미지의 서버 키가 그대로 노출됩니다.

?: serverKey 때문에 람다 결과가 항상 non-null이라 mapNotNullmap처럼 동작합니다. 의도가 “미지 값 제외”라면 null을 반환해야 합니다.

수정 제안
-        val displayPurposes = dto.purposes.mapNotNull { serverKey ->
-            Purpose.fromServerKey(serverKey)?.displayName ?: serverKey.also {
-                Log.w(TAG, "알 수 없는 Purpose serverKey: $serverKey")
-            }
-        }
-        val displayInterests = dto.interests.mapNotNull { serverKey ->
-            Interest.fromServerKey(serverKey)?.displayName ?: serverKey.also {
-                Log.w(TAG, "알 수 없는 Interest serverKey: $serverKey")
-            }
-        }
+        val displayPurposes = dto.purposes.mapNotNull { serverKey ->
+            Purpose.fromServerKey(serverKey)?.displayName.also {
+                if (it == null) Log.w(TAG, "알 수 없는 Purpose serverKey: $serverKey")
+            }
+        }
+        val displayInterests = dto.interests.mapNotNull { serverKey ->
+            Interest.fromServerKey(serverKey)?.displayName.also {
+                if (it == null) Log.w(TAG, "알 수 없는 Interest serverKey: $serverKey")
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt`
around lines 192 - 201, The current lambdas for displayPurposes and
displayInterests always return a non-null value because of "?: serverKey", so
mapNotNull doesn't filter unknown keys; change the Elvis branch to return null
after logging so unknown serverKey values are excluded: use
Purpose.fromServerKey(serverKey)?.displayName ?: run { Log.w(TAG, "알 수 없는
Purpose serverKey: $serverKey"); null } (and similarly for
Interest.fromServerKey) so logging still occurs but mapNotNull will filter out
unknown entries.

334-345: ⚠️ Potential issue | 🟠 Major

예외 타입 소실(SwallowedException)과 Boolean 반환 계약이 현재 구현과 맞지 않습니다.

성공 시 항상 true, 실패 시 예외 throw 구조라 Boolean은 의미가 약하고, Exception(e.message)로 재포장하면 원인 타입/스택이 유실됩니다.

수정 제안
-    ): Boolean {
+    ) {
@@
-        return try {
+        try {
             userApi.completeSocialProfile(
                 authorization = "Bearer $socialToken",
                 body = request
             )
             Log.d(TAG, "[소셜 프로필 완료] 성공")
-            true
         } catch (e: ApiError) {
-            // data 레이어 예외를 core/일반 예외로 변환 후 던짐
             Log.e(TAG, "[소셜 프로필 완료 실패] ${e.message}")
-            throw Exception(e.message ?: "소셜 프로필 완료 실패")
+            throw e
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt`
around lines 334 - 345, The method in UserRepositoryImpl that calls
userApi.completeSocialProfile should not return a meaningless Boolean nor
swallow the original ApiError; change the repository method signature to return
Unit (or a Result type) instead of Boolean, remove the try/catch that throws a
generic Exception, and either let ApiError propagate or rethrow a domain
exception that preserves the original as the cause (e.g., throw
DomainApiException("소셜 프로필 완료 실패", cause = e)); also update the Log.e call
(using TAG) to include the throwable (stacktrace) rather than only e.message so
the original error details are preserved.
feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt (1)

246-910: 주석 처리된 ~660줄의 코드를 제거해 주세요.

이전 리뷰에서도 지적된 사항입니다. Git 히스토리로 복구할 수 있으므로 삭제를 권장합니다.

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

In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt`
around lines 246 - 910, Remove the large commented-out block (~660 lines) that
contains unused commented declarations and previews (including
CurationScreenContent, PreviewCurationScreenFull, the CurationScreen
implementation, toLabel/ensureHttpScheme helpers, LikedCurationEmptyState,
LinksPreparingHome, PreviewCurationScreenExact and related preview data); delete
those commented lines entirely so the file only contains live code (you can
recover via git if needed) and ensure no leftover commented references to those
functions remain.
🧹 Nitpick comments (4)
data/src/main/java/com/example/data/api/UserApi.kt (1)

67-69: getUserInfo 시그니처에서 주석 처리된 파라미터를 제거해 API 계약을 명확히 해주세요.

현재 /me 기반으로 전환된 상태라 주석으로 남아있는 userId 파라미터는 오해를 유발할 수 있습니다. 인터페이스 정의는 최종 계약만 남기는 편이 유지보수에 안전합니다.

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

In `@data/src/main/java/com/example/data/api/UserApi.kt` around lines 67 - 69,
Remove the commented-out parameter inside the suspend function getUserInfo so
the interface signature clearly represents the current /me contract; open the
getUserInfo declaration, delete the commented line "// `@Path`("userId") userId:
Long" and, if present, update any KDoc or method-level comments to reflect that
this endpoint uses the authenticated user context rather than a userId path
parameter.
feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt (3)

131-131: 파일명과 동일한 인라인 주석 제거

// CurationScreen.kt 는 파일명과 중복되는 불필요한 주석입니다.

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

In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt` at
line 131, Remove the redundant inline comment "// CurationScreen.kt" that
duplicates the filename; locate it near the top of the CurationScreen file
(class/function symbol CurationScreen) and delete that single-line comment
only—no other code changes or logic updates are needed.

110-115: TODO: curationId 매핑 미구현

onCardClick에서 selectedPage 인덱스만 기록되고 실제 curationId는 세팅되지 않습니다. CurationMonthDetailOverlay가 정확한 큐레이션 데이터를 표시하려면, ViewModel에서 받아온 큐레이션 목록의 curationId를 인덱스 기반으로 조회해 함께 전달해야 합니다.

curationId 연동 로직 생성을 도와드릴까요? 새로운 이슈를 열어 추적하길 원하신다면 알려 주세요.

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

In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt`
around lines 110 - 115, In onCardClick (the lambda setting
selectedPage/selectedImageUrl/showDetail) also look up the corresponding
curationId from the ViewModel's list (e.g., curationList or monthCurations)
using the passed index and assign it to the state that
CurationMonthDetailOverlay reads (e.g., selectedCurationId); update the state
variable used by CurationMonthDetailOverlay so it receives the curationId along
with selectedPage and selectedImageUrl; ensure null/ bounds checks when indexing
the ViewModel list (use safe access or guard against index out of range) and
keep naming consistent with existing symbols (onCardClick, selectedPage,
selectedImageUrl, showDetail, selectedCurationId, CurationMonthDetailOverlay,
and the ViewModel's curation list).

39-39: SharedTransitionLayoutsharedElement/sharedBounds 없이 사용되고 있음

@OptIn(ExperimentalSharedTransitionApi::class) 로 실험적 API를 활성화했지만, SharedTransitionLayout은 공유 전환을 활성화하는 외부 레이아웃이며, 내부 컴포저블이 공유 요소 모디파이어를 사용해야만 의미가 있습니다. 현재는 Modifier.sharedElement()Modifier.sharedBounds() 호출이 전혀 없으므로 이 래퍼는 아무런 효과가 없습니다.

공유 전환이 아직 구현 준비가 되지 않았다면, SharedTransitionLayout@OptIn을 제거하여 불필요한 실험적 API 의존을 줄이는 것이 좋습니다. Preview(lines 193-244)에서도 동일하게 적용됩니다.

Also applies to: 63-129

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

In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt` at
line 39, The file enables the experimental Shared Transition API with
`@OptIn`(ExperimentalSharedTransitionApi::class) but wraps UI in
SharedTransitionLayout while no child composables call Modifier.sharedElement()
or Modifier.sharedBounds(), so the wrapper is a no-op; either remove the
SharedTransitionLayout and the `@OptIn` annotation (and the same removal in the
Preview block) to eliminate the unused experimental dependency, or implement
shared transitions by adding Modifier.sharedElement() / Modifier.sharedBounds()
to the appropriate child composables referenced inside SharedTransitionLayout
(ensure matching keys) — update the CurationScreen composable and its Preview
accordingly to reflect the chosen approach.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/example/linku_android/MainApp.kt`:
- Around line 126-128: Remove the stray literal token causing the syntax error:
delete the standalone "4" between the collectAsStateWithLifecycle() line and the
LaunchedEffect(...) invocation in MainApp.kt so that the val socialLoginEvent by
loginViewModel.socialLoginEvent.collectAsStateWithLifecycle() and the
LaunchedEffect(socialLoginEvent) block are contiguous and compile correctly.

In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt`:
- Around line 48-51: The pager is using hardcoded placeholders: replace
pageCount = { 3 } and imageUrls = listOf(null, null, null) with the
ViewModel-backed collection (e.g., obtain val curationItems by subscribing to
ViewModel state via collectAsState/observeAsState before creating pagerState),
then set pageCount = { curationItems.size } and derive imageUrls =
curationItems.map { it.thumbnailUrl } (handling nulls/empty lists as needed);
ensure you reference rememberPagerState, pagerState, imageUrls, and your
ViewModel property (e.g., curationItems or curationList) so the UI recomposes
when the ViewModel data changes.

In
`@feature/curation/src/main/java/com/example/curation/ui/screen/CurationDetailScreen.kt`:
- Around line 103-106: monthLabel is using remember without a key so
LocalDate.now() runs only at first composition and won't update across month
boundaries; change the remember call to include a key that changes when the
month changes (for example remember(LocalDate.now().monthValue) {
LocalDate.now().format(DateTimeFormatter.ofPattern("M월", Locale.KOREAN)) }) so
monthLabel is recomputed when the month changes (or alternatively use a state +
LaunchedEffect that updates at midnight if you need automatic refresh).

---

Duplicate comments:
In `@app/src/main/java/com/example/linku_android/MainApp.kt`:
- Around line 117-121: Remove sensitive token exposure from logs by stopping the
direct interpolation of SocialLoginData into Log.d; in the LaunchedEffect where
SocialDeepLinkBus.flow is collected (SocialDeepLinkBus.flow and
loginViewModel.handleSocialDeepLink), replace Log.d("SOCIAL_VM", "MainApp Bus
수신: $data") with a safe log that omits or redacts token fields (e.g., log only
non-sensitive fields like provider or an event marker, or call a scrubber that
returns a redacted representation), ensuring SocialLoginData.toString() is never
used for logging.
- Around line 142-155: The LaunchedEffect reacting to loginViewModel.loginState
(LoginState.Success) causes duplicate navigator.navigate calls by also being
triggered by non-social login flows (LoginApp.onLoginSuccess and deep link
handling); restrict this block to only handle the one-time social-deep-link
success: add or use a one-shot indicator (e.g., a new LoginState.SocialSuccess
variant or a Boolean/SingleLiveEvent like loginViewModel.isSocialDeepLinkEvent),
check that flag inside the LaunchedEffect before calling
homeViewModel.refreshAfterLogin(), mypageViewModel.refreshUserInfo(), setting
showNavBar/currentLinkuNavigationItem, and navigator.navigate(...), and then
consume/reset the flag immediately after navigation so it won’t fire again for
normal LoginState.Success paths handled elsewhere (e.g.,
LoginApp.onLoginSuccess).

In
`@data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt`:
- Around line 192-201: The current lambdas for displayPurposes and
displayInterests always return a non-null value because of "?: serverKey", so
mapNotNull doesn't filter unknown keys; change the Elvis branch to return null
after logging so unknown serverKey values are excluded: use
Purpose.fromServerKey(serverKey)?.displayName ?: run { Log.w(TAG, "알 수 없는
Purpose serverKey: $serverKey"); null } (and similarly for
Interest.fromServerKey) so logging still occurs but mapNotNull will filter out
unknown entries.
- Around line 334-345: The method in UserRepositoryImpl that calls
userApi.completeSocialProfile should not return a meaningless Boolean nor
swallow the original ApiError; change the repository method signature to return
Unit (or a Result type) instead of Boolean, remove the try/catch that throws a
generic Exception, and either let ApiError propagate or rethrow a domain
exception that preserves the original as the cause (e.g., throw
DomainApiException("소셜 프로필 완료 실패", cause = e)); also update the Log.e call
(using TAG) to include the throwable (stacktrace) rather than only e.message so
the original error details are preserved.

In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt`:
- Around line 246-910: Remove the large commented-out block (~660 lines) that
contains unused commented declarations and previews (including
CurationScreenContent, PreviewCurationScreenFull, the CurationScreen
implementation, toLabel/ensureHttpScheme helpers, LikedCurationEmptyState,
LinksPreparingHome, PreviewCurationScreenExact and related preview data); delete
those commented lines entirely so the file only contains live code (you can
recover via git if needed) and ensure no leftover commented references to those
functions remain.

---

Nitpick comments:
In `@data/src/main/java/com/example/data/api/UserApi.kt`:
- Around line 67-69: Remove the commented-out parameter inside the suspend
function getUserInfo so the interface signature clearly represents the current
/me contract; open the getUserInfo declaration, delete the commented line "//
`@Path`("userId") userId: Long" and, if present, update any KDoc or method-level
comments to reflect that this endpoint uses the authenticated user context
rather than a userId path parameter.

In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt`:
- Line 131: Remove the redundant inline comment "// CurationScreen.kt" that
duplicates the filename; locate it near the top of the CurationScreen file
(class/function symbol CurationScreen) and delete that single-line comment
only—no other code changes or logic updates are needed.
- Around line 110-115: In onCardClick (the lambda setting
selectedPage/selectedImageUrl/showDetail) also look up the corresponding
curationId from the ViewModel's list (e.g., curationList or monthCurations)
using the passed index and assign it to the state that
CurationMonthDetailOverlay reads (e.g., selectedCurationId); update the state
variable used by CurationMonthDetailOverlay so it receives the curationId along
with selectedPage and selectedImageUrl; ensure null/ bounds checks when indexing
the ViewModel list (use safe access or guard against index out of range) and
keep naming consistent with existing symbols (onCardClick, selectedPage,
selectedImageUrl, showDetail, selectedCurationId, CurationMonthDetailOverlay,
and the ViewModel's curation list).
- Line 39: The file enables the experimental Shared Transition API with
`@OptIn`(ExperimentalSharedTransitionApi::class) but wraps UI in
SharedTransitionLayout while no child composables call Modifier.sharedElement()
or Modifier.sharedBounds(), so the wrapper is a no-op; either remove the
SharedTransitionLayout and the `@OptIn` annotation (and the same removal in the
Preview block) to eliminate the unused experimental dependency, or implement
shared transitions by adding Modifier.sharedElement() / Modifier.sharedBounds()
to the appropriate child composables referenced inside SharedTransitionLayout
(ensure matching keys) — update the CurationScreen composable and its Preview
accordingly to reflect the chosen approach.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cbcfd3d and 18d2ac8.

📒 Files selected for processing (13)
  • app/src/main/java/com/example/linku_android/MainApp.kt
  • data/src/main/java/com/example/data/api/UserApi.kt
  • data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt
  • feature/curation/src/main/java/com/example/curation/CurationFragment.kt
  • feature/curation/src/main/java/com/example/curation/CurationTypography.kt
  • feature/curation/src/main/java/com/example/curation/LinkItem.kt
  • feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt
  • feature/curation/src/main/java/com/example/curation/ui/detail_card/HighlightCard.kt
  • feature/curation/src/main/java/com/example/curation/ui/list_card/CurationLikedSection.kt
  • feature/curation/src/main/java/com/example/curation/ui/main_card/CurationHighlightSection.kt
  • feature/curation/src/main/java/com/example/curation/ui/recommend_list/CurationRecommendedLinksSection.kt
  • feature/curation/src/main/java/com/example/curation/ui/screen/CurationDetailScreen.kt
  • feature/curation/src/main/java/com/example/curation/ui/top/bar/CurationTopBar.kt
💤 Files with no reviewable changes (6)
  • feature/curation/src/main/java/com/example/curation/CurationFragment.kt
  • feature/curation/src/main/java/com/example/curation/ui/top/bar/CurationTopBar.kt
  • feature/curation/src/main/java/com/example/curation/ui/main_card/CurationHighlightSection.kt
  • feature/curation/src/main/java/com/example/curation/LinkItem.kt
  • feature/curation/src/main/java/com/example/curation/ui/list_card/CurationLikedSection.kt
  • feature/curation/src/main/java/com/example/curation/CurationTypography.kt

Comment on lines +126 to +128
val socialLoginEvent by loginViewModel.socialLoginEvent.collectAsStateWithLifecycle()
4
LaunchedEffect(socialLoginEvent) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n app/src/main/java/com/example/linku_android/MainApp.kt | sed -n '122,132p'

Repository: LinkYou-2025/LinkU_Android

Length of output: 534


불필요한 리터럴 토큰 제거 필수

Line 127의 단독 숫자 4는 Kotlin 구문 오류이며 컴파일 블로커입니다. 이 라인을 제거하세요.

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

In `@app/src/main/java/com/example/linku_android/MainApp.kt` around lines 126 -
128, Remove the stray literal token causing the syntax error: delete the
standalone "4" between the collectAsStateWithLifecycle() line and the
LaunchedEffect(...) invocation in MainApp.kt so that the val socialLoginEvent by
loginViewModel.socialLoginEvent.collectAsStateWithLifecycle() and the
LaunchedEffect(socialLoginEvent) block are contiguous and compile correctly.

Comment on lines +48 to +51
val pagerState = rememberPagerState(
initialPage = 0,
pageCount = { 3 }
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

실제 데이터 미연동 — pageCountimageUrls가 하드코딩됨

pageCount = { 3 }imageUrls = listOf(null, null, null)은 플레이스홀더 값으로, ViewModel의 실제 큐레이션 목록과 연결되어 있지 않습니다. 사용자는 항상 빈 카드 3장만 보게 됩니다.

  • pageCount는 ViewModel에서 제공하는 큐레이션 아이템 수로 교체되어야 합니다.
  • imageUrls는 실제 서버에서 받아온 썸네일 URL 리스트로 교체되어야 합니다.

ViewModel 상태 연동 코드를 생성해 드릴까요? 새로운 이슈를 열어 추적하길 원하신다면 알려 주세요.

Also applies to: 171-171

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

In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt`
around lines 48 - 51, The pager is using hardcoded placeholders: replace
pageCount = { 3 } and imageUrls = listOf(null, null, null) with the
ViewModel-backed collection (e.g., obtain val curationItems by subscribing to
ViewModel state via collectAsState/observeAsState before creating pagerState),
then set pageCount = { curationItems.size } and derive imageUrls =
curationItems.map { it.thumbnailUrl } (handling nulls/empty lists as needed);
ensure you reference rememberPagerState, pagerState, imageUrls, and your
ViewModel property (e.g., curationItems or curationList) so the UI recomposes
when the ViewModel data changes.

Comment on lines 103 to 106
val monthLabel = remember {
java.time.LocalDate.now()
.format(java.time.format.DateTimeFormatter.ofPattern("M월", java.util.Locale.KOREAN))
LocalDate.now()
.format(DateTimeFormatter.ofPattern("M월", Locale.KOREAN))
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

monthLabelremember 키 누락 — 월 경계에서 값이 갱신되지 않음

remember {} 에 키가 없으므로 컴포저블이 최초 구성될 때만 LocalDate.now()가 호출됩니다. 앱이 월말 자정을 넘겨도 레이블이 업데이트되지 않습니다. 실용적인 영향은 낮지만, 의도를 명확히 하려면 키 없는 remember가 맞는지 확인하세요.

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

In
`@feature/curation/src/main/java/com/example/curation/ui/screen/CurationDetailScreen.kt`
around lines 103 - 106, monthLabel is using remember without a key so
LocalDate.now() runs only at first composition and won't update across month
boundaries; change the remember call to include a key that changes when the
month changes (for example remember(LocalDate.now().monthValue) {
LocalDate.now().format(DateTimeFormatter.ofPattern("M월", Locale.KOREAN)) }) so
monthLabel is recomputed when the month changes (or alternatively use a state +
LaunchedEffect that updates at midnight if you need automatic refresh).

- `CalendarBox`, `CalendarIconBox` 컴포넌트 신규 구현
- `CurationScreen` 내 '지난 큐레이션' 섹션 및 캘린더 UI 추가
- 캘린더 아이콘 전용 런드리 고딕(Laundrygothic) 폰트 및 이미지 리소스 추가
- 기존 큐레이션 타이틀 패딩 수치 조정 (20.scaler -> 24.scaler)
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt (2)

268-932: 파일 내 대규모 레거시 주석 블록은 정리하는 편이 좋습니다.

이전 구현/프리뷰 코드가 주석으로 장기간 남아 있어 현재 코드 경로 파악이 어렵습니다. Git 히스토리로 복구 가능하므로 소스에서는 제거해 주세요.

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

In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt`
around lines 268 - 932, Remove the large legacy commented sections (notably the
commented CurationScreenContent block, PreviewCurationScreenFull,
PreviewCurationScreenExact and long commented imports/utility functions) so the
file only contains active code; if you want to preserve the old snippets,
extract them to a separate archival file or a Gist/branch instead of leaving
them commented inline (search for symbols like CurationScreenContent,
PreviewCurationScreenFull, PreviewCurationScreenExact, LikedCurationEmptyState
to find the blocks), then run a build to ensure no referenced symbols were
accidentally removed.

49-52: ⚠️ Potential issue | 🟠 Major

실데이터 미연동으로 메인 카드가 항상 플레이스홀더 3장만 표시됩니다.

pageCount = { 3 }imageUrls = listOf(null, null, null)가 고정되어 있어 ViewModel 상태가 UI에 반영되지 않습니다. 카드 개수와 썸네일 URL을 동일한 실제 데이터 소스에서 파생하도록 연결해 주세요.

Also applies to: 171-173

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

In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt`
around lines 49 - 52, The UI currently hardcodes pager count and placeholder
thumbnails (pagerState with pageCount = { 3 } and imageUrls = listOf(null, null,
null)), so ViewModel state changes never reflect; change both the
rememberPagerState pageCount and the imageUrls source in CurationScreen (and the
duplicate block near the bottom) to derive from the same ViewModel collection
(e.g., use viewModel.items or viewModel.imageList): set pageCount = { items.size
} and build imageUrls by mapping items to their thumbnail URL (preserving nulls
if missing), and ensure remember/LaunchedEffect uses that state so the pager and
thumbnails update when the ViewModel changes.
🧹 Nitpick comments (6)
design/src/main/java/com/example/design/theme/font/Laundrygothic.kt (3)

11-16: data object 대신 object 사용을 권장합니다.

data object는 sealed class 상태(Loading, Empty 등) 처럼 동등 비교·해시·로깅이 필요한 싱글턴 분기에 권장되는 패턴입니다. 반면 object는 매니저/헬퍼, 상태 없는 유틸리티, 설정 홀더(Configuration holders) 등에 적합한 선택입니다. 이 폰트 스킴 싱글턴은 상태나 비교 로직이 없으므로 data object가 제공하는 equals()/hashCode()/toString() 자동 생성은 실질적인 이점이 없습니다. 일반 object를 사용하는 것이 더 관용적인 Kotlin 코드입니다.

♻️ 제안 변경 사항
-data object Laundrygothic : ThemeFontScheme(
+object Laundrygothic : ThemeFontScheme(
     font = FontFamily(
         Font(R.font.laundrygothic_bold, FontWeight.Bold, FontStyle.Normal),
         Font(R.font.laundrygothic_regular, FontWeight.Normal, FontStyle.Normal)
     )
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@design/src/main/java/com/example/design/theme/font/Laundrygothic.kt` around
lines 11 - 16, Replace the Kotlin "data object" declaration for the font scheme
with a plain singleton "object": change the declaration of Laundrygothic (which
extends ThemeFontScheme and constructs the FontFamily with
Font(R.font.laundrygothic_bold...) and Font(R.font.laundrygothic_regular...))
from a data object to an object, keeping the constructor and contents identical
so equals/hashCode/toString auto-generation from data object is not used for
this stateless font scheme.

9-9: 코드 내 커밋 메시지성 주석을 제거하거나 KDoc으로 교체하세요.

"큐레이션 화면 달력 아이콘 폰트체 추가했습니다." 는 코드를 설명하는 문서가 아니라 커밋 메시지에 가까운 내용입니다. 이런 주석은 코드 히스토리를 오염시키며, 파일 목적을 설명하려면 아래처럼 간결한 KDoc이 더 적합합니다.

♻️ 제안 변경 사항
-// 큐레이션 화면 달력 아이콘 폰트체 추가했습니다.
-
+/** 큐레이션 화면 달력 아이콘에 사용되는 Laundrygothic 폰트 스킴 */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@design/src/main/java/com/example/design/theme/font/Laundrygothic.kt` at line
9, Remove the commit-style inline comment "큐레이션 화면 달력 아이콘 폰트체 추가했습니다." from
Laundrygothic.kt and either delete it or replace it with a concise KDoc block
that describes the file's purpose (e.g., "Provides the Laundry Gothic font used
for the curation screen calendar icon"). Ensure the KDoc appears above the
relevant top-level declaration (the font object/class or file-level declaration)
so tooling and readers get a proper description instead of a commit message.

13-14: FontStyle.Normal은 기본값이므로 생략 가능합니다.

Font(resId: Int, weight: FontWeight = FontWeight.Normal, style: FontStyle = FontStyle.Normal)style 파라미터의 기본값이 이미 FontStyle.Normal입니다. 명시적으로 전달하는 것은 오류는 아니지만 불필요한 boilerplate입니다.

추가로, italic 변형(FontStyle.Italic)이 등록되지 않았습니다. 현재 캘린더 아이콘 용도로만 사용된다면 문제없지만, 향후 이탤릭 스타일이 적용될 경우 Android가 폰트 합성(font synthesis)으로 대체하여 렌더링이 달라질 수 있습니다.

♻️ 제안 변경 사항 (불필요한 기본값 제거)
 data object Laundrygothic : ThemeFontScheme(
     font = FontFamily(
-        Font(R.font.laundrygothic_bold, FontWeight.Bold, FontStyle.Normal),
-        Font(R.font.laundrygothic_regular, FontWeight.Normal, FontStyle.Normal)
+        Font(R.font.laundrygothic_bold, FontWeight.Bold),
+        Font(R.font.laundrygothic_regular, FontWeight.Normal)
     )
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@design/src/main/java/com/example/design/theme/font/Laundrygothic.kt` around
lines 13 - 14, Remove the redundant FontStyle.Normal arguments in the two
Font(...) calls (leave Font(R.font.laundrygothic_bold, FontWeight.Bold) and
Font(R.font.laundrygothic_regular, FontWeight.Normal)) and, if an italic font
file exists or will be needed, register it as Font(R.font.laundrygothic_italic,
FontWeight.Normal, FontStyle.Italic); ensure the Font(...) usages in
Laundrygothic.kt reference these symbols exactly (Font, FontWeight.Bold,
FontWeight.Normal, FontStyle.Italic) so Android won't fall back to font
synthesis when italic is required.
feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt (1)

141-199: 세로 스크롤 컨테이너가 없어 작은 화면에서 하단 섹션 접근이 어려울 수 있습니다.

현재 고정 높이 요소를 Column에 누적하고 있으며(제목 이미지, 텍스트, 카드 페이저, 캘린더), CalendarBox는 고정된 80.scaler 높이 제약이 있어 디바이스 높이에 따라 콘텐츠가 잘릴 수 있습니다. verticalScroll 또는 LazyColumn 기반으로 전환을 권장합니다.

♻️ 제안 수정안
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
...
-    Column(
-        modifier = modifier.fillMaxSize()
-    ) {
+    Column(
+        modifier = modifier
+            .fillMaxSize()
+            .verticalScroll(rememberScrollState())
+    ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt`
around lines 141 - 199, The Column in CurationScreen is not scrollable so lower
content like CalendarBox can be clipped on small screens; change the root Column
to a vertically scrollable container (either add
Modifier.verticalScroll(rememberScrollState()) to the current Column or refactor
the layout into a LazyColumn) and ensure children such as the Image, Text
blocks, CurationMainCardPager (using pagerState, isDetailOpen, onCardClick) and
CalendarBox can measure naturally instead of fixed height constraints; remove or
revise the fixed 80.scaler height constraint on CalendarBox so it can layout
correctly within the scrollable parent.
feature/curation/src/main/java/com/example/curation/ui/calendar/CalendarBox.kt (2)

47-48: 사용자 노출 문자열은 stringResource로 분리해 주세요.

현재 하드코딩은 i18n 및 카피 변경 대응이 어렵습니다. strings.xml로 이동하는 게 좋습니다.

수정 예시
+import androidx.compose.ui.res.stringResource
+import com.example.curation.R
@@
-                text = "지난 월간 큐레이션을 다시 볼 수 있어요",
+                text = stringResource(R.string.curation_monthly_archive_desc),
@@
-                text = "월간 큐레이션 모아보기",
+                text = stringResource(R.string.curation_monthly_archive_title),
<!-- res/values/strings.xml -->
<string name="curation_monthly_archive_desc">지난 월간 큐레이션을 다시 볼 수 있어요</string>
<string name="curation_monthly_archive_title">월간 큐레이션 모아보기</string>

Also applies to: 61-62

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

In
`@feature/curation/src/main/java/com/example/curation/ui/calendar/CalendarBox.kt`
around lines 47 - 48, Replace hardcoded user-visible strings in CalendarBox.kt
with stringResource lookups: move the literal "지난 월간 큐레이션을 다시 볼 수 있어요" (and the
other hardcoded string around lines 61-62) into res/values/strings.xml (e.g.,
curation_monthly_archive_desc and curation_monthly_archive_title) and update the
Text composable usages in the CalendarBox composable (the Text(...) calls where
text = "...") to use stringResource(R.string.curatio... ) for those keys; ensure
you import androidx.compose.ui.res.stringResource and reference the exact string
resource IDs in the Text composable parameters (and keep existing TextStyle and
other properties unchanged).

35-35: TODO는 머지 전 이슈로 승격하거나 제거해 주세요.

아이콘 사이즈 이슈가 미해결 상태로 남아 있어 추적 누락 가능성이 있습니다. 이 PR/후속 이슈 중 하나로 명확히 관리하는 것을 권장합니다.

원하시면 TODO 내용을 바로 GitHub 이슈 템플릿 형태로 정리해드릴게요.

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

In
`@feature/curation/src/main/java/com/example/curation/ui/calendar/CalendarBox.kt`
at line 35, The inline TODO in CalendarBox.kt about the calendar icon size must
not remain as an untracked comment—either convert it into a formal GitHub issue
(including desired target size from Figma, current size, expected behavior, and
assignee/designer link) and replace the TODO with the issue number/reference, or
remove the TODO and implement the agreed size now; update the CalendarBox (class
or composable named CalendarBox) source to reference the created issue ID (e.g.,
"// ISSUE `#123`: ...") if you file one, or change the icon size values to the
agreed Figma dimensions and delete the TODO before merging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@feature/curation/src/main/java/com/example/curation/ui/calendar/CalendarIconBox.kt`:
- Around line 31-33: MONTH_LABELS contains "JUNE" and "JULY" as four-letter
month strings which breaks the three-letter alignment used by the icon; change
those entries in the MONTH_LABELS list (variable MONTH_LABELS in
CalendarIconBox.kt) from "JUNE" to "JUN" and "JULY" to "JUL" so all month labels
are three characters and text alignment remains consistent.
- Around line 42-44: 현재 이전 달을 계산할 때 prevMonth를 remember {
LocalDate.now().minusMonths(1) }로 캐시하고 있어 리컴포지션 시 값이 갱신되지 않습니다; CalendarIconBox의
prevMonth 계산에서 remember를 제거하고 prevMonth = LocalDate.now().minusMonths(1)처럼 직접
계산하도록 변경하여 항상 최신 날짜 기반으로 이전 달을 반환하도록 수정하세요.

---

Duplicate comments:
In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt`:
- Around line 268-932: Remove the large legacy commented sections (notably the
commented CurationScreenContent block, PreviewCurationScreenFull,
PreviewCurationScreenExact and long commented imports/utility functions) so the
file only contains active code; if you want to preserve the old snippets,
extract them to a separate archival file or a Gist/branch instead of leaving
them commented inline (search for symbols like CurationScreenContent,
PreviewCurationScreenFull, PreviewCurationScreenExact, LikedCurationEmptyState
to find the blocks), then run a build to ensure no referenced symbols were
accidentally removed.
- Around line 49-52: The UI currently hardcodes pager count and placeholder
thumbnails (pagerState with pageCount = { 3 } and imageUrls = listOf(null, null,
null)), so ViewModel state changes never reflect; change both the
rememberPagerState pageCount and the imageUrls source in CurationScreen (and the
duplicate block near the bottom) to derive from the same ViewModel collection
(e.g., use viewModel.items or viewModel.imageList): set pageCount = { items.size
} and build imageUrls by mapping items to their thumbnail URL (preserving nulls
if missing), and ensure remember/LaunchedEffect uses that state so the pager and
thumbnails update when the ViewModel changes.

---

Nitpick comments:
In `@design/src/main/java/com/example/design/theme/font/Laundrygothic.kt`:
- Around line 11-16: Replace the Kotlin "data object" declaration for the font
scheme with a plain singleton "object": change the declaration of Laundrygothic
(which extends ThemeFontScheme and constructs the FontFamily with
Font(R.font.laundrygothic_bold...) and Font(R.font.laundrygothic_regular...))
from a data object to an object, keeping the constructor and contents identical
so equals/hashCode/toString auto-generation from data object is not used for
this stateless font scheme.
- Line 9: Remove the commit-style inline comment "큐레이션 화면 달력 아이콘 폰트체 추가했습니다."
from Laundrygothic.kt and either delete it or replace it with a concise KDoc
block that describes the file's purpose (e.g., "Provides the Laundry Gothic font
used for the curation screen calendar icon"). Ensure the KDoc appears above the
relevant top-level declaration (the font object/class or file-level declaration)
so tooling and readers get a proper description instead of a commit message.
- Around line 13-14: Remove the redundant FontStyle.Normal arguments in the two
Font(...) calls (leave Font(R.font.laundrygothic_bold, FontWeight.Bold) and
Font(R.font.laundrygothic_regular, FontWeight.Normal)) and, if an italic font
file exists or will be needed, register it as Font(R.font.laundrygothic_italic,
FontWeight.Normal, FontStyle.Italic); ensure the Font(...) usages in
Laundrygothic.kt reference these symbols exactly (Font, FontWeight.Bold,
FontWeight.Normal, FontStyle.Italic) so Android won't fall back to font
synthesis when italic is required.

In
`@feature/curation/src/main/java/com/example/curation/ui/calendar/CalendarBox.kt`:
- Around line 47-48: Replace hardcoded user-visible strings in CalendarBox.kt
with stringResource lookups: move the literal "지난 월간 큐레이션을 다시 볼 수 있어요" (and the
other hardcoded string around lines 61-62) into res/values/strings.xml (e.g.,
curation_monthly_archive_desc and curation_monthly_archive_title) and update the
Text composable usages in the CalendarBox composable (the Text(...) calls where
text = "...") to use stringResource(R.string.curatio... ) for those keys; ensure
you import androidx.compose.ui.res.stringResource and reference the exact string
resource IDs in the Text composable parameters (and keep existing TextStyle and
other properties unchanged).
- Line 35: The inline TODO in CalendarBox.kt about the calendar icon size must
not remain as an untracked comment—either convert it into a formal GitHub issue
(including desired target size from Figma, current size, expected behavior, and
assignee/designer link) and replace the TODO with the issue number/reference, or
remove the TODO and implement the agreed size now; update the CalendarBox (class
or composable named CalendarBox) source to reference the created issue ID (e.g.,
"// ISSUE `#123`: ...") if you file one, or change the icon size values to the
agreed Figma dimensions and delete the TODO before merging.

In `@feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt`:
- Around line 141-199: The Column in CurationScreen is not scrollable so lower
content like CalendarBox can be clipped on small screens; change the root Column
to a vertically scrollable container (either add
Modifier.verticalScroll(rememberScrollState()) to the current Column or refactor
the layout into a LazyColumn) and ensure children such as the Image, Text
blocks, CurationMainCardPager (using pagerState, isDetailOpen, onCardClick) and
CalendarBox can measure naturally instead of fixed height constraints; remove or
revise the fixed 80.scaler height constraint on CalendarBox so it can layout
correctly within the scrollable parent.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 18d2ac8 and 3295ea2.

⛔ Files ignored due to path filters (3)
  • design/src/main/res/font/laundrygothic_bold.ttf is excluded by !**/*.ttf
  • design/src/main/res/font/laundrygothic_regular.ttf is excluded by !**/*.ttf
  • feature/curation/src/main/res/drawable/img_curation_calendar.png is excluded by !**/*.png
📒 Files selected for processing (4)
  • design/src/main/java/com/example/design/theme/font/Laundrygothic.kt
  • feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt
  • feature/curation/src/main/java/com/example/curation/ui/calendar/CalendarBox.kt
  • feature/curation/src/main/java/com/example/curation/ui/calendar/CalendarIconBox.kt

Comment on lines +31 to +33
private val MONTH_LABELS = listOf(
"JAN", "FEB", "MAR", "APR", "MAY", "JUNE",
"JULY", "AUG", "SEP", "OCT", "NOV", "DEC"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

월 라벨 포맷을 3글자로 통일해 주세요.

JUNE, JULY만 4글자라 아이콘 폭 기준에서 텍스트 정렬이 달라질 수 있습니다. JUN, JUL로 맞추는 편이 안전합니다.

수정 예시
 private val MONTH_LABELS = listOf(
-    "JAN", "FEB", "MAR", "APR", "MAY", "JUNE",
-    "JULY", "AUG", "SEP", "OCT", "NOV", "DEC"
+    "JAN", "FEB", "MAR", "APR", "MAY", "JUN",
+    "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private val MONTH_LABELS = listOf(
"JAN", "FEB", "MAR", "APR", "MAY", "JUNE",
"JULY", "AUG", "SEP", "OCT", "NOV", "DEC"
private val MONTH_LABELS = listOf(
"JAN", "FEB", "MAR", "APR", "MAY", "JUN",
"JUL", "AUG", "SEP", "OCT", "NOV", "DEC"
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/curation/src/main/java/com/example/curation/ui/calendar/CalendarIconBox.kt`
around lines 31 - 33, MONTH_LABELS contains "JUNE" and "JULY" as four-letter
month strings which breaks the three-letter alignment used by the icon; change
those entries in the MONTH_LABELS list (variable MONTH_LABELS in
CalendarIconBox.kt) from "JUNE" to "JUN" and "JULY" to "JUL" so all month labels
are three characters and text alignment remains consistent.

Comment on lines +42 to +44
val prevMonth = remember {
LocalDate.now().minusMonths(1)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "CalendarIconBox.kt" -type f

Repository: LinkYou-2025/LinkU_Android

Length of output: 154


🏁 Script executed:

# Also look for any test files that might give context
fd -t f -i "calendar" | grep -i "test\|spec" | head -20

Repository: LinkYou-2025/LinkU_Android

Length of output: 52


🌐 Web query:

Jetpack Compose remember LocalDate.now() stale value recomposition

💡 Result:

LocalDate.now() becomes “stale” in Compose when you wrap it in remember because remember { … } runs its lambda only on the first composition; on later recompositions it returns the previously stored value instead of re-evaluating it. Also, “time passing” does not trigger recomposition by itself—only state changes do. [1], [2]

What to do instead

A) If you only meant “recompute on recomposition” (e.g., some other state already recomposes your UI), then don’t use remember:

val today = LocalDate.now()

This updates when recomposition happens, not at midnight automatically. [1]

B) If you need it to update when the date changes (e.g., at midnight), create state that updates on a timer (so Compose has a state change to observe):

`@Composable`
fun rememberToday(): State<LocalDate> = produceState(LocalDate.now()) {
    while (true) {
        val now = java.time.ZonedDateTime.now()
        val nextMidnight = now.toLocalDate().plusDays(1).atStartOfDay(now.zone)
        val millis = java.time.Duration.between(now, nextMidnight).toMillis()
        kotlinx.coroutines.delay(millis)
        value = LocalDate.now()
    }
}

Usage:

val today by rememberToday()

Note: remember(key) { … } only re-runs when the key changes and a recomposition occurs—it still won’t update “by time” unless something triggers recomposition. [1]

Also note: rememberUpdatedState is for keeping a long-lived effect/lambda seeing the latest parameter across recompositions; it doesn’t solve “midnight update” by itself. [3]

Sources: [1] https://developer.android.com/develop/ui/compose/state • [2] https://developer.android.com/develop/ui/compose/state-lifespans • [3] https://stackoverflow.com/questions/69085027/difference-between-remember-and-rememberupdatedstate-in-jetpack-compose


🌐 Web query:

Compose remember { LocalDate.now() } caching behavior month change

💡 Result:

In Compose, remember { LocalDate.now() } is evaluated once per composition and then cached across recompositions. It will not automatically update when the month/day changes—because time passing does not trigger recomposition, and remember returns the previously remembered value until it’s forgotten (the composable leaves the composition) or its keys change. [1]

So if the screen stays composed across a month boundary (e.g., Jan 31 → Feb 1), remember { LocalDate.now() } can keep returning the old date/month indefinitely, until something causes the composable instance to be disposed and re-added (or you restart the process). [1]

To make it update, you need to drive state (e.g., update a mutableStateOf(LocalDate.now()) on a timer / at midnight via LaunchedEffect), or deliberately invalidate the remembered value by changing keys (but keys still only re-evaluate when a recomposition happens). [1]


🏁 Script executed:

cat -n ./feature/curation/src/main/java/com/example/curation/ui/calendar/CalendarIconBox.kt

Repository: LinkYou-2025/LinkU_Android

Length of output: 4494


🏁 Script executed:

# Get more context - read relevant sections
head -80 ./feature/curation/src/main/java/com/example/curation/ui/calendar/CalendarIconBox.kt | tail -50

Repository: LinkYou-2025/LinkU_Android

Length of output: 1513


remember를 제거하여 현재 날짜를 기반으로 항상 계산하세요.

remember { LocalDate.now().minusMonths(1) } 패턴은 첫 번째 컴포지션에서만 값을 계산하고 이후 리컴포지션에서도 캐시된 값을 반환합니다. 화면이 월을 넘어서도 계속 구성된 상태에 있으면 이전 달 정보가 갱신되지 않습니다. 이 컴포넌트는 상태 기반 매개변수가 없는 스테이트리스 컴포넌트이므로, remember 없이 직접 계산하는 것이 안전합니다.

수정 예시
-    val prevMonth = remember {
-        LocalDate.now().minusMonths(1)
-    }
+    val prevMonth = LocalDate.now().minusMonths(1)
📝 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 prevMonth = remember {
LocalDate.now().minusMonths(1)
}
val prevMonth = LocalDate.now().minusMonths(1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/curation/src/main/java/com/example/curation/ui/calendar/CalendarIconBox.kt`
around lines 42 - 44, 현재 이전 달을 계산할 때 prevMonth를 remember {
LocalDate.now().minusMonths(1) }로 캐시하고 있어 리컴포지션 시 값이 갱신되지 않습니다; CalendarIconBox의
prevMonth 계산에서 remember를 제거하고 prevMonth = LocalDate.now().minusMonths(1)처럼 직접
계산하도록 변경하여 항상 최신 날짜 기반으로 이전 달을 반환하도록 수정하세요.

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.

1 participant