Feature/#72 : 로그인 뷰모델, ui 일부 수정#74
Conversation
- 색상 리스트 열 때, 바텀 시트가 내려가는 에러 수정 - sheetState: SheetState = rememberModalBottomSheetState() 매개변수 추가해서 오류 픽스 및 각 바텀 시트에서 상태를 수정할 수 있도록 수정 - TextFieldFileBottomSheet: modifier.padding(top = 14.dp) 위치를 AnimatedVisibility에서 내부의 VerticalGrid로 이동
- FileScreen()을 FileApp()으로 변경
- `gradle/libs.versions.toml`에 `foundationLayout` 버전 및 라이브러리 정의 추가 - `feature/file` 모듈의 `build.gradle.kts`에 `androidx-compose-foundation-layout` 의존성 추가
- `enabled`, `onClickLabel`, `role` 파라미터를 추가하여 `clickable` 기능 확장 및 접근성 지원 - KDoc 주석을 추가하여 함수 및 각 파라미터의 역할 명시 - 내부 `clickable` 구현에서 고정되어 있던 인자들을 전달받은 파라미터로 동작하도록 수정
- `gradientTint` 함수의 파라미터 및 동작 방식에 대한 상세 설명 추가 - `graphicsLayer`와 `drawWithCache`를 사용한 구현 방식 및 `BlendMode.SrcIn` 기본값에 대한 설명 명시
- `BottomFolderEditBottomSheet`에서 `TextFieldFileBottomSheet` 호출 시 `sheetState` 인자를 추가하여 상태 관리 개선 - `rememberModalBottomSheetState`에 `skipPartiallyExpanded = true` 옵션을 적용하여 바텀 시트가 항상 전체 확장 상태로 열리도록 수정 - 불필요한 `FileViewModel` 임포트 제거
- `linkList.isNotEmpty()` 조건을 추가하여 "분류되지 않은 링크" 텍스트와 링크 그리드가 데이터가 있을 때만 표시되도록 수정
- 새로운 메뉴 체크 아이콘 에셋(`ic_top_folders_menu.xml`) 추가 - `TopFolderListMenu` 비즈니스 로직과 UI 레이아웃(`TopFolderListMenuLayout`) 분리 - 메뉴의 각 항목을 담당하는 `TopFolderListMenuRow` 컴포저블 추가 - `noRippleClickable`, `gradientTint` 등 커스텀 Modifier를 적용하여 코드 간결화 및 재사용성 향상 - 각 컴포저블 및 파라미터에 대한 상세 KDoc 주석 추가 - 드롭다운 메뉴 너비(150dp -> 180dp) 및 내부 패딩 등 세부 UI 스타일 수정
- **기능 추가 및 개선**
- 검색 결과가 없을 때 표시할 안내 문구 및 아이콘(`ic_search_bar_caution`) 추가
- 검색어 입력창에 텍스트가 있을 때만 삭제(`X`) 버튼이 보이도록 노출 로직 수정
- 탑 시트가 닫힐 때(`visible == false`), 뒤로가기 버튼 클릭, 배경 딤(Dim) 클릭 시 입력 텍스트 및 수정 모드 상태를 초기화하도록 개선
- 검색어 입력 시 `collectLatest`를 사용하여 불필요한 이전 검색 요청을 취소하도록 로직 최적화
- **UI/UX 및 스타일 수정**
- 최근 검색어의 삭제 버튼 아이콘을 전용 리소스(`ic_recent_search_x`)로 교체 및 색상 조정
- 뒤로가기 아이콘을 기본 Material Vector에서 커스텀 리소스(`ic_back`)로 변경
- 최근 검색어 영역의 "최근 검색" 타이틀 폰트 두께를 `Bold`로 변경하고 간격 및 패딩 미세 조정
- `HighlightedText` 컴포넌트의 가독성을 위해 정규식 처리 및 스타일 적용 로직 정리
- **코드 리팩터링 및 가독성**
- 컴포넌트 및 파라미터에 상세 KDoc 주석 추가
- `RecentQueryItem` -> `RecentQueryChip`, `FastSearchItem` -> `FastSearchItemRow` 등 컴포저블 함수명 직관화
- 변수명 오타 수정 (`recentQuerys` -> `recentQueries`) 및 중복 코드 함수화(`resetAndDismiss`)
- **리소스 추가**
- `ic_recent_search_x.xml`, `ic_search_bar_caution.xml` 벡터 드로어블 추가
Walkthrough자동 로그인 시도(tryAutoLogin)/LoginState 기반 로그인 상태 머신과 로그인 네비게이션 재구성, ApiError·withAuth 계층으로 서버 인증·토큰 재발급 중앙화, SessionStore·AuthPreference 확장, SearchBarTopSheet/TopBar 포함 디자인·리소스·하단시트 리팩터링이 포함된 대규모 변경입니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant EmailLogin as EmailLoginScreen
participant VM as LoginViewModel
participant Repo as UserRepository
participant API as ServerApi
participant Pref as AuthPreference
participant Main as MainApp
User->>EmailLogin: 로그인 시도(email,password)
EmailLogin->>VM: login(email,password)
activate VM
VM->>Repo: login API 호출 (withErrorHandling)
activate Repo
Repo->>API: 서버 요청
API-->>Repo: BaseResponse<LoginResult>
Repo-->>VM: LoginResult(accessToken,refreshToken,userId)
deactivate Repo
VM->>Pref: saveTokens(accessToken,refreshToken,userId)
VM-->>EmailLogin: loginState = Success
deactivate VM
EmailLogin->>Main: onLoginSuccess() 호출 (네비게이션)
Main->>API: pending-share 소비 등 (withAuth)
Main->>Pref: 토큰 확인/사용
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested Reviewers
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Important Action Needed: IP Allowlist UpdateIf your organization protects your Git platform with IP whitelisting, please add the new CodeRabbit IP address to your allowlist:
Failure to add the new IP will result in interrupted reviews. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
feature/file/src/main/java/com/example/file/ui/bottom/sheet/TextFieldFileBottomSheet.kt (1)
85-109: 재오픈 시 colorId 미초기화로 상태 불일치가 발생할 수 있습니다.
닫힘 후 다시 열면 UI는 초기색(Gray300)인데 colorId는 이전 값이라 저장 시 잘못된 색상으로 업데이트될 수 있어요.✅ 제안 수정
LaunchedEffect(visible) { if (visible) { text = "" // 바텀 시트 열릴 때 초기화 + colorId = -1 + selectedColor = Gray300 + expanded = false } } @@ onDismiss = { selectedColor = Gray300 expanded = false + colorId = -1 onDismiss() }feature/file/src/main/java/com/example/file/ui/bottom/sheet/BottomFolderEditBottomSheet.kt (1)
15-19: placeholderText에 “에러” 노출은 사용자 UX에 부적절합니다.
null일 때 시트를 숨기거나 안전한 기본값으로 처리하는 편이 좋아요.✅ 제안 수정
- TextFieldFileBottomSheet( + val readyFolder = folderStateViewModel.readyToUpdateBottomFolder + TextFieldFileBottomSheet( title = "폴더명을 변경하시겠습니까?", body = "변경할 폴더명을 입력해주세요!", - placeholderText = folderStateViewModel.readyToUpdateBottomFolder?.folderName?:"에러", - visible = folderStateViewModel.bottomFolderEditBottomSheetVisible, + placeholderText = readyFolder?.folderName.orEmpty(), + visible = folderStateViewModel.bottomFolderEditBottomSheetVisible && readyFolder != null,feature/file/src/main/java/com/example/file/ui/bottom/sheet/TopFolderEditBottomSheet.kt (1)
25-30: readyToUpdateTopFolder!! 사용은 NPE 위험이 있습니다.
가시성 상태와 데이터 준비 타이밍이 엇갈리면 크래시가 날 수 있어 안전 호출로 방어하는 편이 좋습니다.✅ 제안 수정
onColorIdDeliver = { colorId -> - fileViewModel.updateCategoryColor( - categoryName = folderStateViewModel.readyToUpdateTopFolder!!.folderName, - colorId = (colorId + 1).toLong(), - colorStyle = CategoryColorStyle.categoryStyleList[colorId] - ) + folderStateViewModel.readyToUpdateTopFolder?.let { folder -> + fileViewModel.updateCategoryColor( + categoryName = folder.folderName, + colorId = (colorId + 1).toLong(), + colorStyle = CategoryColorStyle.categoryStyleList[colorId] + ) + } },
🤖 Fix all issues with AI agents
In `@app/src/main/java/com/example/linku_android/MainApp.kt`:
- Around line 802-808: Remove the duplicate debug log for loginState in MainApp:
keep a single Log.d("MainApp", "loginState: $loginState") and delete the other
redundant call so loginState (from
loginViewModel.loginState.collectAsStateWithLifecycle()) is only logged once;
locate the two identical Log.d entries near the loginState collection and remove
one.
In `@design/src/main/java/com/example/design/top/search/SearchBarTopSheet.kt`:
- Around line 112-128: The current resetAndDismiss() function calls onDismiss(),
causing duplicate dismiss callbacks when visible becomes false; update the logic
so state-reset and dismissal are separated: remove the onDismiss() call from
resetAndDismiss() (or rename it to resetState()), create or use a dedicated
dismiss method that only calls onDismiss(), and change the
LaunchedEffect(visible) block to call only the reset function when !visible;
ensure UI handlers that should trigger a user-initiated close (e.g., close
button/back/dim) call the dismiss method instead of resetAndDismiss().
- Around line 149-156: The current LaunchedEffect(text) flow filters out inputs
with trimmed length < 2, so onQueryChange is not called and fastSearchItems may
retain prior results; update the logic in LaunchedEffect(text) that builds the
snapshotFlow { text } pipeline (or add a pre-check on the collected trimmed
value) to call onQueryChange("") whenever the trimmed input length is less than
2, and only use the debounce/distinctUntilChanged/collectLatest pipeline for
inputs with length >= 2; reference the existing LaunchedEffect(text),
snapshotFlow { text }, and onQueryChange symbols when locating and updating the
code.
In `@feature/file/build.gradle.kts`:
- Line 56: The foundation-layout dependency is pinned in the version catalog
which overrides the Compose BOM; remove the explicit version reference for the
androidx-compose-foundation-layout entry in libs.versions.toml (the key
currently named foundationLayout) so it reads without a version.ref and lets the
BOM manage its version, and keep the build.gradle.kts usage
(implementation(libs.androidx.compose.foundation.layout)) as-is; also verify the
Compose BOM entry is present and applied so the BOM controls versions for ui,
ui-graphics, material3 and foundation-layout.
In
`@feature/file/src/main/java/com/example/file/ui/top/bar/component/TopFolderListMenu.kt`:
- Line 75: 주석에 있는 변수명 오타를 수정하세요: TopFolderListMenu.kt 파일 내에 있는 주석에서 잘못 쓴
`isShredFolders`를 올바른 이름인 `isSharedFolders`로 바꿔주십시오(문제가 보고된 위치에 해당하는 모든 인스턴스 —
예: 해당 파일의 주석들 around the occurrences noted).
In `@feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt`:
- Around line 88-99: The catch-all Exception currently swallows IO/network
errors; update LoginViewModel to import java.io.IOException and add a separate
catch (e: IOException) before the generic Exception handler that logs using TAG
and sets _loginState.value = LoginState.Error(LoginErrorType.NETWORK_ERROR);
keep the existing catch (e: HttpException) mapping for HTTP codes and only use
the final catch (e: Exception) for unknown errors so network errors are mapped
to NETWORK_ERROR correctly.
- Around line 122-154: In tryAutoLogin, don't unconditionally call
clearAuthData() for every exception; instead distinguish between transient
errors and auth-invalid errors: if the caught exception is an IOException
(network/transient) or a generic non-auth error, keep tokens and treat as
failure without clearing them, but only clearAuthData() when the failure
indicates an invalid refresh token (e.g., the exception from
userRepository.reissue signals HTTP 401 or 403 — check the exception type or
HTTP status code on the thrown exception and match 401/403). Update the catch
block in tryAutoLogin to branch on exception type/status and only call
clearAuthData() for auth-invalid cases, adding the java.io.IOException import to
detect network errors.
In `@gradle/libs.versions.toml`:
- Line 37: Remove the duplicate Foundation dependency declaration and let the
Compose BOM manage Foundation versions: delete the redundant foundation entry
(the duplicate declaring androidx.compose.foundation — e.g., the symbol
referenced as androidx-compose-foundation / androidx-foundation), remove
individual version keys foundation, foundationLayout, and foundationVersion so
they no longer pin foundation versions, update composeBom to "2026.01.00", and
ensure downstream dependency usage imports the BOM via platform() so Foundation
versions are resolved from the BOM rather than hardcoded.
🧹 Nitpick comments (8)
feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt (1)
112-138: 중복된 에러 텍스트 스타일은 공통화하면 유지보수에 유리합니다.
Line 112-138의Duplicated/Error분기가 동일한 텍스트 스타일을 반복하므로, 공통 컴포저블로 묶으면 향후 스타일 변경 시 드리프트를 줄일 수 있습니다.♻️ 예시 리팩터링
+@Composable +private fun NicknameErrorText(text: String) { + Text( + text = text, + fontSize = 13.sp, + lineHeight = 15.sp, + fontWeight = FontWeight(400), + fontFamily = Paperlogy.font, + color = Color(0xFFFF5E5E), + modifier = Modifier.padding(start = (12.scaler)) + ) +} ... -Text( - text = "이미 사용 중인 닉네임입니다.", - fontSize = 13.sp, - lineHeight = 15.sp, - fontWeight = FontWeight(400), - fontFamily = Paperlogy.font, - color = Color(0xFFFF5E5E), - modifier = Modifier.padding(start = (12.scaler)) -) +NicknameErrorText("이미 사용 중인 닉네임입니다.") ... -Text( - text = (nicknameState as NicknameCheckState.Error).message, - fontSize = 13.sp, - lineHeight = 15.sp, - fontWeight = FontWeight(400), - fontFamily = Paperlogy.font, - color = Color(0xFFFF5E5E), - modifier = Modifier.padding(start = (12.scaler)) -) +NicknameErrorText((nicknameState as NicknameCheckState.Error).message)feature/file/src/main/java/com/example/file/ui/top/bar/component/TopFolderListMenu.kt (2)
129-129: 하드코딩된 문자열을 문자열 리소스로 추출하는 것을 권장합니다.
"나의 폴더","공유받은 폴더"문자열이 여러 곳에서 하드코딩되어 있습니다. 향후 다국어 지원(i18n)을 고려하여strings.xml로 추출하면 유지보수성이 향상됩니다.♻️ 리팩토링 제안
res/values/strings.xml에 추가:<string name="my_folders">나의 폴더</string> <string name="shared_folders">공유받은 폴더</string>컴포저블에서 사용:
val myFoldersText = stringResource(R.string.my_folders) val sharedFoldersText = stringResource(R.string.shared_folders) val selectedText = if (isSharedFolders) sharedFoldersText else myFoldersTextAlso applies to: 155-163
240-243: FontWeight에 명명된 상수 사용을 권장합니다.가독성 향상을 위해 매직 넘버 대신
FontWeight명명된 상수를 사용하는 것이 좋습니다.♻️ 리팩토링 제안
// 폰트 굵기 - fontWeight = FontWeight( - weight = if (selectedOption == selectedText) 500 - else 400 - ), + fontWeight = if (selectedOption == selectedText) FontWeight.Medium + else FontWeight.Normal,feature/login/src/main/java/com/example/login/ui/screen/SignUpPasswordScreen.kt (1)
56-159: SignUpPasswordScreen/Content 중복으로 UI 드리프트 위험현재
SignUpPasswordScreen이 화면 UI를 직접 재구성하면서SignUpPasswordScreenContent와 두 곳에서 동일 UI를 관리하게 됐습니다. 이미 규칙 문구/패딩/에러 문구가 서로 달라지기 시작해 유지보수 리스크가 커집니다. 한 곳을 단일 소스로 유지하도록 통합(공용 컴포저블 위임)하는 편이 안전합니다.design/src/main/java/com/example/design/modifier/GradientTint.kt (1)
20-27: 중복된 인라인 주석 제거 고려KDoc이 이미
brush와blendMode파라미터를 문서화하고 있으므로, 함수 내부의 인라인 주석(lines 21-24)은 중복됩니다. 코드 정리 시 제거를 고려해 주세요.♻️ 제안된 수정
fun Modifier.gradientTint( - /* - * brush: 그라데이션 색상을 적용할 브러시 - * blendMode: 그라데이션 색상을 적용할 때 사용할 블렌드 모드 - * */ brush: Brush, blendMode: BlendMode = BlendMode.SrcIn ): Modifier = this.graphicsLayer(alpha = 0.99f)feature/login/src/main/java/com/example/login/ui/screen/EmailLoginScreen.kt (2)
187-205: 스마트 캐스트 안전성 개선 고려현재
is체크 후as캐스트를 사용하고 있습니다. 동일 컴포지션 내에서는 안전하지만,when표현식을 사용하면 더 안전하고 관용적입니다.♻️ 제안된 수정
- if (loginState is LoginState.Error) { + (loginState as? LoginState.Error)?.let { errorState -> Spacer(Modifier.height(12.scaler)) Box( modifier = Modifier.fillMaxWidth() ) { Text( - text = (loginState as LoginState.Error).errorType.message, + text = errorState.errorType.message, style = TextStyle( fontSize = 13.sp, lineHeight = 15.sp, fontFamily = Paperlogy.font, fontWeight = FontWeight(400), color = Color(0xFFFF5E5E) ), modifier = Modifier.padding(start = 22.scaler) ) } }
288-292: 불필요한 중첩 Box 구조
Box안에Column이 있고, 마지막에 닫는 괄호가 여러 개 있습니다. 코드 구조상 불필요한 중첩이 있어 보입니다. 들여쓰기와 괄호 매칭을 확인해 주세요.app/src/main/java/com/example/linku_android/MainApp.kt (1)
451-466: 주석 처리된 코드 정리 권장로그인 성공 로직이
onLoginSuccess콜백으로 이동되었으므로, 이 주석 처리된 코드 블록은 PR 병합 후 제거하는 것이 좋습니다. 현재 상태에서는 히스토리 추적용으로 남겨두는 것도 괜찮습니다.
| /** | ||
| * 공통 초기화 + 닫기 처리 | ||
| * (뒤로가기 / 딤 클릭 / 닫기 버튼에서 중복 제거) | ||
| */ | ||
| fun resetAndDismiss() { | ||
| text = "" | ||
| isEditMode = false | ||
| keyboardController?.hide() | ||
| onDismiss() | ||
| } | ||
|
|
||
| // 입력 변화 디바운스 수집 (2자 이상 + 350ms) | ||
| // - mapLatest: 새 입력이 오면 이전 요청(코루틴 Job) 자동 취소 → 레이스 방지 | ||
| // 닫힐 때 상태 초기화 로직 (visible이 false가 될 때) | ||
| LaunchedEffect(visible) { | ||
| if (!visible) { | ||
| resetAndDismiss() | ||
| } | ||
| } |
There was a problem hiding this comment.
visible=false에서 onDismiss가 재호출될 수 있습니다.
Line 124-127에서 visible이 false일 때 resetAndDismiss()가 onDismiss를 다시 호출합니다. 상위에서 이미 닫았다면 중복 dismiss로 네비게이션/콜백이 2회 실행될 수 있어요. 상태 초기화와 dismiss를 분리해 visible 변화에서는 reset만 수행하세요.
🛠️ 수정 제안
- fun resetAndDismiss() {
- text = ""
- isEditMode = false
- keyboardController?.hide()
- onDismiss()
- }
+ fun resetState() {
+ text = ""
+ isEditMode = false
+ keyboardController?.hide()
+ }
+
+ fun resetAndDismiss() {
+ resetState()
+ onDismiss()
+ }
LaunchedEffect(visible) {
if (!visible) {
- resetAndDismiss()
+ resetState()
}
}📝 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.
| /** | |
| * 공통 초기화 + 닫기 처리 | |
| * (뒤로가기 / 딤 클릭 / 닫기 버튼에서 중복 제거) | |
| */ | |
| fun resetAndDismiss() { | |
| text = "" | |
| isEditMode = false | |
| keyboardController?.hide() | |
| onDismiss() | |
| } | |
| // 입력 변화 디바운스 수집 (2자 이상 + 350ms) | |
| // - mapLatest: 새 입력이 오면 이전 요청(코루틴 Job) 자동 취소 → 레이스 방지 | |
| // 닫힐 때 상태 초기화 로직 (visible이 false가 될 때) | |
| LaunchedEffect(visible) { | |
| if (!visible) { | |
| resetAndDismiss() | |
| } | |
| } | |
| /** | |
| * 공통 초기화 + 닫기 처리 | |
| * (뒤로가기 / 딤 클릭 / 닫기 버튼에서 중복 제거) | |
| */ | |
| fun resetState() { | |
| text = "" | |
| isEditMode = false | |
| keyboardController?.hide() | |
| } | |
| fun resetAndDismiss() { | |
| resetState() | |
| onDismiss() | |
| } | |
| // 닫힐 때 상태 초기화 로직 (visible이 false가 될 때) | |
| LaunchedEffect(visible) { | |
| if (!visible) { | |
| resetState() | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@design/src/main/java/com/example/design/top/search/SearchBarTopSheet.kt`
around lines 112 - 128, The current resetAndDismiss() function calls
onDismiss(), causing duplicate dismiss callbacks when visible becomes false;
update the logic so state-reset and dismissal are separated: remove the
onDismiss() call from resetAndDismiss() (or rename it to resetState()), create
or use a dedicated dismiss method that only calls onDismiss(), and change the
LaunchedEffect(visible) block to call only the reset function when !visible;
ensure UI handlers that should trigger a user-initiated close (e.g., close
button/back/dim) call the dismiss method instead of resetAndDismiss().
| LaunchedEffect(text) { | ||
| snapshotFlow { text } | ||
| .map { it.trim() } | ||
| .filter { it.length >= 2 } | ||
| .debounce(350) | ||
| .distinctUntilChanged() | ||
| .collectLatest(onQueryChange) | ||
| } |
There was a problem hiding this comment.
2자 미만 입력 시 이전 결과가 남습니다.
Line 149-156에서 2자 미만 입력이 filter로 제거되어 onQueryChange가 호출되지 않습니다. 그 결과 fastSearchItems가 이전 검색 결과로 남을 수 있습니다. 2자 미만이면 결과를 비우도록 onQueryChange("")를 호출해주세요.
🛠️ 수정 제안
LaunchedEffect(text) {
snapshotFlow { text }
.map { it.trim() }
- .filter { it.length >= 2 }
- .debounce(350)
.distinctUntilChanged()
- .collectLatest(onQueryChange)
+ .debounce(350)
+ .collectLatest { q ->
+ if (q.length >= 2) onQueryChange(q) else onQueryChange("")
+ }
}🤖 Prompt for AI Agents
In `@design/src/main/java/com/example/design/top/search/SearchBarTopSheet.kt`
around lines 149 - 156, The current LaunchedEffect(text) flow filters out inputs
with trimmed length < 2, so onQueryChange is not called and fastSearchItems may
retain prior results; update the logic in LaunchedEffect(text) that builds the
snapshotFlow { text } pipeline (or add a pre-check on the collected trimmed
value) to call onQueryChange("") whenever the trimmed input length is less than
2, and only use the debounce/distinctUntilChanged/collectLatest pipeline for
inputs with length >= 2; reference the existing LaunchedEffect(text),
snapshotFlow { text }, and onQueryChange symbols when locating and updating the
code.
| implementation(libs.androidx.ui.tooling.preview) | ||
| implementation(libs.androidx.material3) | ||
| implementation(libs.androidx.navigation.compose) | ||
| implementation(libs.androidx.compose.foundation.layout) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "libs.versions.toml" -o -name "gradle.properties" | head -20Repository: LinkYou-2025/LinkU_Android
Length of output: 117
🏁 Script executed:
cat feature/file/build.gradle.ktsRepository: LinkYou-2025/LinkU_Android
Length of output: 2585
🏁 Script executed:
rg -n "foundation\.layout|compose\.foundation\.layout|foundation-layout" --type tomlRepository: LinkYou-2025/LinkU_Android
Length of output: 239
🏁 Script executed:
rg -n "foundationLayout" gradle/libs.versions.tomlRepository: LinkYou-2025/LinkU_Android
Length of output: 244
🏁 Script executed:
cat gradle/libs.versions.toml | head -100Repository: LinkYou-2025/LinkU_Android
Length of output: 6322
Compose BOM과 foundation-layout 버전 관리 불일치 해결 필요
BOM(2024.09.00)을 사용할 때, androidx-compose-foundation-layout은 명시적 버전을 지정하면 안 됩니다. libs.versions.toml에서 foundationLayout = "1.10.0"으로 고정되어 있어 BOM의 버전 관리가 무시될 수 있습니다.
해결책: 다른 Compose 라이브러리(ui, ui-graphics, material3)처럼 foundation-layout 정의에서 version.ref를 제거하고 BOM이 버전을 관리하도록 수정하세요.
androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" }
🤖 Prompt for AI Agents
In `@feature/file/build.gradle.kts` at line 56, The foundation-layout dependency
is pinned in the version catalog which overrides the Compose BOM; remove the
explicit version reference for the androidx-compose-foundation-layout entry in
libs.versions.toml (the key currently named foundationLayout) so it reads
without a version.ref and lets the BOM manage its version, and keep the
build.gradle.kts usage (implementation(libs.androidx.compose.foundation.layout))
as-is; also verify the Compose BOM entry is present and applied so the BOM
controls versions for ui, ui-graphics, material3 and foundation-layout.
| * 나의 폴더들을 보이게 하는 로직 | ||
| * */ | ||
|
|
||
| // isShredFolders를 false로 수정 |
There was a problem hiding this comment.
주석에 오타가 있습니다.
isShredFolders → isSharedFolders로 수정이 필요합니다.
📝 수정 제안
- // isShredFolders를 false로 수정
+ // isSharedFolders를 false로 수정
folderStateViewModel.updateIsSharedFolders(false)- // isShredFolders를 true로 수정
+ // isSharedFolders를 true로 수정
folderStateViewModel.updateIsSharedFolders(true)Also applies to: 94-94
🤖 Prompt for AI Agents
In
`@feature/file/src/main/java/com/example/file/ui/top/bar/component/TopFolderListMenu.kt`
at line 75, 주석에 있는 변수명 오타를 수정하세요: TopFolderListMenu.kt 파일 내에 있는 주석에서 잘못 쓴
`isShredFolders`를 올바른 이름인 `isSharedFolders`로 바꿔주십시오(문제가 보고된 위치에 해당하는 모든 인스턴스 —
예: 해당 파일의 주석들 around the occurrences noted).
feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt
Show resolved
Hide resolved
feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt
Outdated
Show resolved
Hide resolved
| composeTesting = "1.0.0-alpha09" | ||
| toolsCore = "1.0.0-alpha14" | ||
| foundationVersion = "1.10.1" | ||
| foundationLayout = "1.10.0" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n gradle/libs.versions.toml | head -100Repository: LinkYou-2025/LinkU_Android
Length of output: 7022
🌐 Web query:
androidx compose foundation latest stable version 2024 2025 BOM best practices
💡 Result:
Latest stable (as of Jan 26, 2026)
- androidx.compose.foundation (Foundation):
1.10.1(released Jan 14, 2026) [1] - Latest stable Compose BOM:
androidx.compose:compose-bom:2026.01.00[2]- The BOM mapping page shows what library versions (Foundation/UI/Runtime/etc.) each BOM resolves to, and it currently includes
2026.01.00(page last updated 2026-01-19 UTC) [3].
- The BOM mapping page shows what library versions (Foundation/UI/Runtime/etc.) each BOM resolves to, and it currently includes
2024–2025 “what version was stable?”
- Compose 1.9 (stable): Aug 13, 2025 → BOM
2025.08.00[4] - Compose 1.10 (stable): Dec 3, 2025 → BOM
2025.12.00[5]
BOM best practices (recommended by Android docs)
- Import the BOM with
platform(...), and do not specify versions on Compose artifacts (includingfoundation). Also import the BOM fortestandandroidTestconfigurations. [2] - Use the BOM mapping page to confirm exactly which Foundation/UI/etc. versions your BOM resolves to. [3]
- If you need pre-stable bits, use
compose-bom-betaorcompose-bom-alpha(suffix on the artifact name), but stable BOM has no suffix. [2]
Sources: [1] [2] [3] [4] [5]
Foundation 라이브러리 중복 선언 및 버전 불일치 해결 필요
현재 코드에 다음 문제들이 있습니다:
-
Foundation 라이브러리 중복 선언: Line 85와 Line 88에서 동일한
androidx.compose.foundation:foundation라이브러리가 서로 다른 버전으로 두 번 정의되어 있습니다.- Line 85:
androidx-compose-foundation(1.9.5 사용) - Line 88:
androidx-foundation(1.10.1 사용)
- Line 85:
-
버전 불일치: Foundation 관련 라이브러리들이 세 가지 다른 버전을 사용하고 있습니다:
foundation= "1.9.5" (Line 33)foundationLayout= "1.10.0" (Line 37)foundationVersion= "1.10.1" (Line 36)
-
Compose BOM 활용 미흡:
composeBom = "2024.09.00"이 정의되어 있지만, 개별 버전을 명시하면 BOM의 버전 관리가 무시될 수 있습니다. 또한 현재 BOM이 4개월 이상 오래되었습니다(최신: 2026.01.00).
권장 사항:
- 중복된 Foundation 라이브러리 선언 중 하나 제거
- Compose BOM을 최신 버전(2026.01.00)으로 업데이트
- Foundation 관련 버전을 BOM이 관리하도록 수정 (개별 버전 지정 제거)
- BOM 사용 시
platform()적용
🤖 Prompt for AI Agents
In `@gradle/libs.versions.toml` at line 37, Remove the duplicate Foundation
dependency declaration and let the Compose BOM manage Foundation versions:
delete the redundant foundation entry (the duplicate declaring
androidx.compose.foundation — e.g., the symbol referenced as
androidx-compose-foundation / androidx-foundation), remove individual version
keys foundation, foundationLayout, and foundationVersion so they no longer pin
foundation versions, update composeBom to "2026.01.00", and ensure downstream
dependency usage imports the BOM via platform() so Foundation versions are
resolved from the BOM rather than hardcoded.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt (1)
296-311:SearchBarTopSheet가Scaffold블록 외부에 위치함
Scaffold가 Line 296에서 닫히고SearchBarTopSheet가 그 다음에 호출되고 있습니다. 이로 인해 중괄호 불일치 문제가 발생했습니다.CurationScreen함수의 닫는 중괄호(})가 Line 311에 있어야 하는데, 이 구조가 올바른지 확인이 필요합니다.현재 코드 구조상
SearchBarTopSheet는Scaffold와 동일한 레벨에서 렌더링되어야 하므로, 전체를Box나 다른 컨테이너로 감싸야 합니다.🔧 제안된 수정
`@Composable` fun CurationScreen( viewModel: CurationViewModel = hiltViewModel(), onOpenDetail: (Long, Long) -> Unit = { _, _ -> } ) { - // 추가: TopSheet 표시 여부 - var showSearch by remember { mutableStateOf(false) } val uri = LocalUriHandler.current // ... (중략) + Box(modifier = Modifier.fillMaxSize()) { Scaffold( // ... Scaffold 내용 ) { innerPadding -> // ... LazyColumn 내용 } - } - // 검색창 탑 시트 SearchBarTopSheet( // ... SearchBarTopSheet 파라미터들 ) + } }
🤖 Fix all issues with AI agents
In
`@feature/curation/src/main/java/com/example/curation/ui/top/bar/CurationTopBar.kt`:
- Around line 1-206: This file contains a fully commented-out CurationTopBar
implementation (symbols: CurationTopBar, PreviewCurationTopBar,
TOPBAR_SEARCH_HEIGHT) and should be deleted; remove
feature/curation/src/main/java/.../CurationTopBar.kt from the repo, ensure there
are no remaining references/imports to CurationTopBar or its preview functions
elsewhere, and rely on the new TopBar implementation
(design/src/main/java/com/example/design/top/bar/TopBar.kt) instead — use Git
history to restore if needed.
In `@feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt`:
- Around line 110-125: saveUserSession currently writes empty/placeholder values
into sessionStore.saveLogin using an incomplete LoginResult, overwriting any
existing cached profile; fix by ensuring full profile data is available before
calling saveLogin: either extend LoginResult to include
nickname/email/gender/jobId/jobName/myLinku/myFolder/myAiLinku and populate them
from the login API, or call a separate user profile fetch (e.g.,
fetchUserProfile or loadUserProfile) after successful login to obtain those
fields and then call sessionStore.saveLogin with the complete values;
alternatively, add and call a partial update method on sessionStore (e.g.,
updateLoginFields) to only update fields present in LoginResult and avoid
clobbering existing values; update authPreference.userId assignment to use the
same validated userId value when saving the full session.
🧹 Nitpick comments (9)
app/src/main/java/com/example/linku_android/MainApp.kt (2)
429-443:parentEntry가 null인 경우 로깅 추가 권장
runCatching으로 예외를 삼키면auth_graph엔트리가 없는 경우를 조용히 무시합니다.parentEntry가 null이면 약관 동의 시트가 표시되지 않고 회원가입 플로우가 작동하지 않아 UX 문제가 발생할 수 있습니다.디버깅을 위해 null인 경우 로그를 남기는 것이 좋습니다.
♻️ 제안된 수정
val parentEntry = remember(navigator.currentBackStackEntry) { runCatching { navigator.getBackStackEntry("auth_graph") - }.getOrNull() + }.onFailure { + Log.w("MainApp", "auth_graph entry not found", it) + }.getOrNull() }
451-466: 주석 처리된 코드 블록 제거 권장이 로직은
onLoginSuccess콜백으로 이동되었으므로, 주석 처리된 코드를 완전히 제거하는 것이 좋습니다. 주석으로 남겨두면 코드 가독성이 떨어지고 유지보수가 어려워집니다.design/src/main/java/com/example/design/top/bar/TopBar.kt (4)
1-4: 불필요한 빈 줄 정리패키지 선언 후 불필요한 빈 줄이 2개 있습니다.
♻️ 제안된 수정
package com.example.design.top.bar - - import androidx.compose.foundation.background
7-7: 와일드카드 import 사용 지양
foundation.layout.*와일드카드 import는 명시적 import로 변경하는 것이 좋습니다. 실제 사용되는 클래스만 명시하면 코드 가독성과 빌드 최적화에 도움이 됩니다.
32-42: KDoc 주석 형식 수정 필요KDoc 주석은
/**로 시작해야 합니다. 현재/*로 시작되어 IDE에서 문서로 인식되지 않습니다.♻️ 제안된 수정
-/* - 공통 TopBar 컴포넌트 - * +/** + * 공통 TopBar 컴포넌트 + * * `@param` modifier Modifier * `@param` showSearchBar 검색바 표시 여부 (false면 로고+알림만) * `@param` logoBrush 로고 텍스트 색상/그라데이션 (null이면 테마 기본값) * `@param` searchBarBrush 검색바 배경 색상/그라데이션 (null이면 테마 기본값) * `@param` backgroundColor 전체 배경색 (기본값: 흰색, null이면 배경 없음 = 투명) * `@param` onClickSearch 검색바 클릭 콜백 * `@param` onClickAlarm 알림 아이콘 클릭 콜백 */
106-116: 알림 아이콘 접근성 개선알림 아이콘에
contentDescription이 설정되어 있어 좋습니다. 다만noRippleClickable은 시각적 피드백이 없어 사용자 경험이 저하될 수 있습니다. 알림처럼 중요한 기능은 ripple 효과를 유지하는 것이 권장됩니다.feature/curation/src/main/java/com/example/curation/ui/CurationScreen.kt (2)
82-84: 사용되지 않는 상태 변수
showSearch변수가 선언되었지만 사용되지 않습니다. Line 301에서viewModel.searchTopSheetVisible을 직접 사용하고 있으므로 이 변수는 불필요합니다.♻️ 제안된 수정
fun CurationScreen( viewModel: CurationViewModel = hiltViewModel(), onOpenDetail: (Long, Long) -> Unit = { _, _ -> } ) { - // 추가: TopSheet 표시 여부 - var showSearch by remember { mutableStateOf(false) } val uri = LocalUriHandler.current
458-463: Preview 코드 포맷팅 개선Preview 내
TopBar호출의 들여쓰기가 일관되지 않습니다.♻️ 제안된 수정
item { TopBar( - showSearchBar = true, - onClickSearch = {}, // Preview라서 빈 람다 - onClickAlarm = {} - ) } + showSearchBar = true, + onClickSearch = {}, // Preview라서 빈 람다 + onClickAlarm = {} + ) + }feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt (1)
61-72: 중복 로그인 요청 가드 추가를 권장합니다.빠른 연타로
login()이 여러 번 호출되면 중복 요청이 발생할 수 있습니다.Loading상태일 때는 early-return 하는 방어 로직을 고려해주세요.♻️ 제안 수정안
fun login(email: String, password: String) { + if (_loginState.value is LoginState.Loading) return // 1. 입력 검증 if (email.isBlank() || password.isBlank()) { _loginState.value = LoginState.Error(LoginErrorType.INVALID_CREDENTIALS) return }
| //package com.example.curation.ui.top_bar | ||
| // | ||
| //import androidx.compose.foundation.background | ||
| //import androidx.compose.foundation.border | ||
| //import androidx.compose.foundation.layout.* | ||
| //import androidx.compose.foundation.shape.RoundedCornerShape | ||
| //import androidx.compose.material3.Icon | ||
| //import androidx.compose.material3.Text | ||
| //import androidx.compose.runtime.Composable | ||
| //import androidx.compose.ui.Alignment | ||
| //import androidx.compose.ui.Modifier | ||
| //import androidx.compose.ui.draw.clip | ||
| //import androidx.compose.ui.graphics.Brush | ||
| //import androidx.compose.ui.res.painterResource | ||
| //import androidx.compose.ui.text.SpanStyle | ||
| //import androidx.compose.ui.text.buildAnnotatedString | ||
| //import androidx.compose.ui.text.font.FontWeight | ||
| //import androidx.compose.ui.text.withStyle | ||
| //import androidx.compose.ui.tooling.preview.Preview | ||
| //import androidx.compose.ui.unit.dp | ||
| //import androidx.compose.ui.unit.sp | ||
| //import com.example.design.theme.font.Paperlogy | ||
| //import com.example.design.modifier.noRippleClickable | ||
| //import com.example.design.theme.LocalColorTheme | ||
| //import com.example.design.R as Res | ||
| //import com.example.design.theme.font.Taebaek | ||
| //import com.example.design.util.scaler | ||
| //import androidx.compose.ui.graphics.Color | ||
| // | ||
| ///* | ||
| // 공통 TopBar 컴포넌트 | ||
| // * | ||
| // * @param modifier Modifier | ||
| // * @param showSearchBar 검색바 표시 여부 (false면 로고+알림만) | ||
| // * @param logoBrush 로고 텍스트 색상/그라데이션 (null이면 테마 기본값) | ||
| // * @param searchBarBrush 검색바 배경 색상/그라데이션 (null이면 테마 기본값) | ||
| // * @param backgroundColor 전체 배경색 (기본값: 흰색, null이면 배경 없음 = 투명) | ||
| // * @param onClickSearch 검색바 클릭 콜백 | ||
| // * @param onClickAlarm 알림 아이콘 클릭 콜백 | ||
| // */ | ||
| // | ||
| //private const val TOPBAR_SIMPLE_HEIGHT = 77.4f // 로고 + 알림만 | ||
| //private const val TOPBAR_SEARCH_HEIGHT = 139f // 검색바 포함 //기존 파일은 206 | ||
| // | ||
| //private val DEFAULT_BACKGROUND = Color.White // 기본 배경 흰색 | ||
| // | ||
| //@Composable | ||
| //fun CurationTopBar( | ||
| // modifier: Modifier = Modifier, | ||
| // showSearchBar: Boolean = true, | ||
| // logoBrush: Brush? = null, | ||
| // searchBarBrush: Brush? = null, | ||
| // searchBarBorderColor: Color? = null, | ||
| // backgroundColor: Color? = DEFAULT_BACKGROUND, // 기본값: 흰색, null이면 투명 | ||
| // onClickSearch: () -> Unit = {}, | ||
| // onClickAlarm: () -> Unit = {} | ||
| //) { | ||
| // //디자인 모듈 불러오기 | ||
| // val colorTheme = LocalColorTheme.current | ||
| // | ||
| // // 기본값 설정 - 파일 제외 모두 기본값은 동일합니다. | ||
| // val actualLogoBrush = logoBrush ?: colorTheme.maincolor | ||
| // val actualSearchBarBrush = searchBarBrush ?: colorTheme.maincolor | ||
| // val actualBackgroundColor = backgroundColor ?: colorTheme.white | ||
| // | ||
| // // 로고 + 알림만 있을 때, 탑 바 높이를 77.4.scaler 일반적일 때는 139 | ||
| // val topBarHeight = | ||
| // if (showSearchBar) TOPBAR_SEARCH_HEIGHT.scaler | ||
| // else TOPBAR_SIMPLE_HEIGHT.scaler | ||
| // | ||
| // // null이면 배경 없음, 아니면 해당 색상 적용 | ||
| // val backgroundModifier = if (backgroundColor != null) { | ||
| // Modifier.background(backgroundColor) | ||
| // } else { | ||
| // Modifier // 배경 없음 (투명) | ||
| // } | ||
| // | ||
| // // 파일 탭과 동일한 규격이나 반응형으로 수정함. | ||
| // Box( | ||
| // modifier = modifier | ||
| // .fillMaxWidth() | ||
| // .height(topBarHeight) | ||
| // .then(backgroundModifier) | ||
| // ) { | ||
| // | ||
| // //링큐 로고 텍스트 | ||
| // Text( | ||
| // modifier = Modifier | ||
| // .align(Alignment.TopStart) | ||
| // .padding(start = 35.scaler, top = 52.scaler), | ||
| // text = buildAnnotatedString { | ||
| // withStyle( | ||
| // SpanStyle( | ||
| // fontSize = 24.sp, | ||
| // fontFamily = Taebaek.font, | ||
| // fontWeight = FontWeight(400), | ||
| // brush = actualLogoBrush | ||
| // ) | ||
| // ) { | ||
| // append("링큐") | ||
| // } | ||
| // } | ||
| // ) | ||
| // | ||
| // // 알림 | ||
| // Icon( | ||
| // painter = painterResource(id = Res.drawable.ic_alarm), | ||
| // contentDescription = "알림", | ||
| // tint = colorTheme.gray[300], | ||
| // modifier = Modifier | ||
| // .align(Alignment.TopEnd) | ||
| // .padding(end = 29.8f.scaler, top = 50.38f.scaler) | ||
| // .size(width = 22.26f.scaler, height = 27.18f.scaler) | ||
| // .noRippleClickable { onClickAlarm() } | ||
| // ) | ||
| // | ||
| // // 빠른 링크 검색바. (showSearchBar가 true일 때만 표시가 됩니다. 마이페이지 탑바 생성시 참고 부탁드립니다.) | ||
| // if (showSearchBar) { | ||
| // // 테두리 Modifier 조건부 적용 | ||
| // val borderModifier = if (searchBarBorderColor != null) { | ||
| // Modifier.border( | ||
| // width = 1.scaler, | ||
| // color = searchBarBorderColor, | ||
| // shape = RoundedCornerShape(18.scaler) | ||
| // ) | ||
| // } else { | ||
| // Modifier //테두리 없음. | ||
| // } | ||
| // Box( | ||
| // modifier = Modifier | ||
| // .align(Alignment.TopCenter) | ||
| // .padding(top = 91.scaler, start = 16.scaler, end = 16.scaler) | ||
| // .fillMaxWidth() | ||
| // .height(48.scaler) | ||
| // .clip(RoundedCornerShape(18.scaler)) | ||
| // .background(brush = actualSearchBarBrush) | ||
| // .then(borderModifier) //테두리 적용 추가. | ||
| // .noRippleClickable { onClickSearch() }, | ||
| // contentAlignment = Alignment.CenterStart | ||
| // ) { | ||
| // Row( | ||
| // verticalAlignment = Alignment.CenterVertically, | ||
| // horizontalArrangement = Arrangement.spacedBy(13.scaler) | ||
| // ) { | ||
| // Icon( | ||
| // painter = painterResource(id = Res.drawable.ic_logo_white), | ||
| // contentDescription = null, | ||
| // tint = colorTheme.white, | ||
| // modifier = Modifier | ||
| // .padding(start = 18.5f.scaler, top = 15.scaler, bottom = 16.scaler) | ||
| // .width(23.97571f.scaler) | ||
| // .height(17f.scaler) | ||
| // ) | ||
| // | ||
| // Text( | ||
| // text = "빠른 링크 검색", | ||
| // color = colorTheme.white, | ||
| // fontFamily = Paperlogy.font, | ||
| // fontSize = 16.sp, | ||
| // lineHeight = 20.sp, | ||
| // fontWeight = FontWeight.Medium | ||
| // ) | ||
| // } | ||
| // } | ||
| // } | ||
| // } | ||
| //} | ||
| // | ||
| //@Preview(showBackground = true, name = "기본 (검색바 포함)") | ||
| //@Composable | ||
| //fun PreviewCurationTopBar() { | ||
| // CurationTopBar() | ||
| //} | ||
| // | ||
| //@Preview(showBackground = true, name = "로고 + 알림만") | ||
| //@Composable | ||
| //fun PreviewCurationTopBarSimple() { | ||
| // CurationTopBar(showSearchBar = false) | ||
| //} | ||
| // | ||
| //@Preview(showBackground = true, name = "커스텀 컬러 (FileTopBar 스타일 프리뷰)") | ||
| //@Composable | ||
| //fun PreviewCurationTopBarCustom() { | ||
| // val colorTheme = LocalColorTheme.current | ||
| // | ||
| // Box( | ||
| // modifier = Modifier | ||
| // .fillMaxWidth() | ||
| // .height(139.scaler) | ||
| // .background(brush = colorTheme.maincolor) | ||
| // ) { | ||
| // CurationTopBar( | ||
| // backgroundColor = null, // 투명 | ||
| // | ||
| // logoBrush = Brush.linearGradient( | ||
| // listOf(Color.White, Color.White) | ||
| // ), | ||
| // | ||
| // searchBarBrush = Brush.linearGradient( | ||
| // listOf(Color(0x26FFFFFF), Color(0x26FFFFFF)) | ||
| // ), | ||
| // | ||
| // searchBarBorderColor = Color.White | ||
| // ) | ||
| // } | ||
| //} No newline at end of file |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
주석 처리된 코드 대신 파일 삭제 권장
전체 파일이 주석 처리되어 있습니다. 새로운 TopBar 컴포넌트(design/src/main/java/com/example/design/top/bar/TopBar.kt)로 대체되었으므로, 이 파일은 삭제하는 것이 좋습니다.
버전 관리 시스템(Git)이 히스토리를 보존하므로 필요시 언제든 복구할 수 있습니다. 주석 처리된 코드는 유지보수 부담과 혼란을 야기합니다.
🤖 Prompt for AI Agents
In
`@feature/curation/src/main/java/com/example/curation/ui/top/bar/CurationTopBar.kt`
around lines 1 - 206, This file contains a fully commented-out CurationTopBar
implementation (symbols: CurationTopBar, PreviewCurationTopBar,
TOPBAR_SEARCH_HEIGHT) and should be deleted; remove
feature/curation/src/main/java/.../CurationTopBar.kt from the repo, ensure there
are no remaining references/imports to CurationTopBar or its preview functions
elsewhere, and rely on the new TopBar implementation
(design/src/main/java/com/example/design/top/bar/TopBar.kt) instead — use Git
history to restore if needed.
feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/com/example/linku_android/MainApp.kt (1)
204-204:⚠️ Potential issue | 🟡 Minor동일 타입의 ViewModel이 두 개 생성되어 상태가 분리됩니다.
Line 109의
loginViewModel과 Line 204의loginVM이 각각 다른 스코프에서 생성되어 상태를 공유하지 않습니다. 스플래시에서loginVM으로 자동 로그인을 시도하고, 이메일 로그인 화면에서는loginViewModel을 사용하므로 상태 불일치가 발생할 수 있습니다.🔧 수정 제안
Line 204의
loginVM을 제거하고 Line 109의loginViewModel을 사용하도록 통합하세요:- val loginVM: LoginViewModel = hiltViewModel()그리고 Line 241에서:
- loginVM.tryAutoLogin( + loginViewModel.tryAutoLogin(
🤖 Fix all issues with AI agents
In
`@data/src/main/java/com/example/data/implementation/preference/AuthPreferenceImpl.kt`:
- Around line 18-19: The code uses plain SharedPreferences via the pref variable
(context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)), which is
insecure for storing access/refresh tokens; replace this with a platform-backed
encryption flow: create and persist an AES key in AndroidKeyStore (use
KeyGenerator with "AndroidKeyStore"), use standard JCA Cipher (e.g.,
"AES/GCM/NoPadding") to encrypt/decrypt token bytes, and store only ciphertext
(and IV) in SharedPreferences/DataStore under the same PREF_NAME keys; update
methods in AuthPreferenceImpl (the usages that read/write pref) to call
encrypt/decrypt, and add exception handling for KeyStore key
invalidation/UnrecoverableKeyException to trigger key rotation/clear tokens and
re-auth flow. Ensure key creation, encryption, decryption, and recovery logic
are encapsulated (e.g., KeyStore helper + encrypt/decrypt helpers) and
referenced from AuthPreferenceImpl where pref is used.
In `@feature/login/src/main/java/com/example/login/ui/item/WrongIndicator.kt`:
- Around line 34-39: The Image in WrongIndicator.kt currently sets
contentDescription = "wrong", which exposes a decorative icon as screen-reader
noise; change the Image call in the WrongIndicator composable to use
contentDescription = null so the icon is ignored by accessibility services
(ensure no other accessibility semantics rely on this image being announced).
In
`@feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt`:
- Around line 94-108: The when branch over nicknameState currently lumps
Idle/Checking/Available/Error into else and thus fails to show loading or error
messages; update the when on NicknameCheckState to handle is
NicknameCheckState.Duplicated (use WrongRuleItem), is NicknameCheckState.Error
(render an error message UI item or ErrorRuleItem using the state's message), is
NicknameCheckState.Checking (render a loading indicator instead of
PasswordRuleItem), and keep PasswordRuleItem only for the default
valid/idle/available case; reference NicknameCheckState, WrongRuleItem,
PasswordRuleItem and the new Error/Checking UI components when making the
change.
🧹 Nitpick comments (8)
data/src/main/java/com/example/data/implementation/repository/LinkuRepositoryImpl.kt (2)
23-26: 주석 문구가 과하게 단정적입니다.이 클래스 내부에
withAuth()호출 지점이 여러 곳인데 “최초 호출 지점”이라고 단정하면 오해 소지가 있습니다. 표현을 완화하는 게 좋겠습니다.🔧 문구 완화 제안
- * 모든 인증처리는 여기 withAuth()에서 시작함. withAuth() 최초 호출 지점은 여기임. + * 모든 인증처리는 여기 withAuth()에서 시작함. (앱 내 주요 진입 지점 중 하나)
145-145: 홈 진입 “최초” 표현은 사실과 다를 수 있습니다.실제 호출 순서는 화면/플로우에 따라 달라질 수 있어 “가장 먼저/처음” 표현을 완화하는 편이 안전합니다.
🔧 문구 완화 제안
- //홈에서 가장 먼저 호출하는 api 여기서 withAuth 처음 진입함. + //홈 진입 시 자주 먼저 호출되는 API (withAuth 주요 진입 지점 중 하나).feature/login/src/main/java/com/example/login/ui/item/WrongRuleItem.kt (1)
46-52: 하드코딩 색상 대신 테마 컬러 사용을 권장합니다.
다크모드/테마 일관성을 위해Color.Negative(또는 theme 색상)로 통일하는 편이 안전합니다.🎨 수정 제안
- Text( - text = text, - fontSize = 13.sp, - fontWeight = FontWeight(400), - fontFamily = Paperlogy.font, - color = Color(0xFFFF5E5E) - ) + Text( + text = text, + fontSize = 13.sp, + fontWeight = FontWeight(400), + fontFamily = Paperlogy.font, + color = Color.Negative + )feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt (1)
32-32: TODO 항목은 이슈로 분리하는 걸 권장합니다.
필요하시면 티켓/구현안 정리 도와드릴게요.data/src/main/java/com/example/data/di/api/ServerApiModule.kt (1)
35-49: 중복 코드: OkHttpClient 설정이 두 provider에서 반복됩니다.
provideServerApi와provideUserApi에서 동일한 인터셉터 로직을 가진 OkHttpClient를 각각 생성하고 있습니다. 이 중복을 제거하면 유지보수성이 향상됩니다.♻️ OkHttpClient를 별도 Provider로 추출하는 리팩토링
`@Module` `@InstallIn`(SingletonComponent::class) object ServerApiModule { + `@Provides` + `@Singleton` + fun provideOkHttpClient(authPreference: AuthPreference): OkHttpClient { + val logger = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + return OkHttpClient.Builder() + .addNetworkInterceptor { chain -> + val requestBuilder = chain.request().newBuilder() + authPreference.accessToken?.let { token -> + requestBuilder.addHeader("Authorization", "Bearer $token") + } + chain.proceed(requestBuilder.build()) + } + .addInterceptor(logger) + .build() + } + `@Provides` `@Singleton` fun provideServerApi( - authPreference: AuthPreference, + client: OkHttpClient, moshi: Moshi, ): ServerApi { - val logger = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - } - - val client = OkHttpClient.Builder() - .addNetworkInterceptor { - val request = it.request() - .newBuilder() - .let { builder -> - authPreference.accessToken?.let { token -> - builder.addHeader("Authorization", "Bearer $token") - } ?: builder - } - .build() - it.proceed(request) - } - .addInterceptor(logger) - .build() - return Retrofit.Builder() .baseUrl(BuildConfig.SERVER_BASE_URL) .addConverterFactory(MoshiConverterFactory.create(moshi)) .client(client) .build() .create(ServerApi::class.java) } `@Provides` `@Singleton` - fun provideUserApi(authPreference: AuthPreference, moshi: Moshi): UserApi { - val logger = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - } - - val client = OkHttpClient.Builder() - .addNetworkInterceptor { - val request = it.request() - .newBuilder() - .let { builder -> - authPreference.accessToken?.let { token -> - builder.addHeader("Authorization", "Bearer $token") - } ?: builder - } - .build() - it.proceed(request) - } - .addInterceptor(logger) - .build() - + fun provideUserApi(client: OkHttpClient, moshi: Moshi): UserApi { return Retrofit.Builder() .baseUrl(BuildConfig.SERVER_BASE_URL) .addConverterFactory(MoshiConverterFactory.create(moshi)) .client(client) .build() .create(UserApi::class.java) } }Also applies to: 68-81
feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt (1)
19-19:sealed키워드 뒤 공백 오타 수정 필요.
sealed class에 공백이 두 개 있습니다.🔧 수정 제안
-sealed class LoginState { +sealed class LoginState {app/src/main/java/com/example/linku_android/MainApp.kt (2)
454-469: 주석 처리된 코드 블록을 제거하세요.
onLoginSuccess콜백으로 대체된 중복 로직이 주석으로 남아있습니다. 버전 관리 시스템에 히스토리가 보존되므로 주석 처리된 코드는 삭제하는 것이 좋습니다.🧹 정리 제안
- // 로그인 성공 시 즉시 재로드 -> 중복 로직을 주석처리 했습니다. -// LaunchedEffect(loginState) { -// if (loginState is LoginState.Success) { // Success 타입 체크 -// // 큐레이션 재시도 가능하게 잠금 해제 후 로드 -// curationViewModel.invalidate() -// curationViewModel.loadMonthlyCuration() -// -// homeViewModel.refreshAfterLogin() -// -// // 그리고 홈으로 이동 -// navigator.navigate(NavigationRoute.Home.route) { -// popUpTo(NavigationRoute.Login.route) { inclusive = true } -// launchSingleTop = true -// } -// } -// } -
805-817: 주석 처리된 코드를 정리하세요.Lines 807, 813-816에 주석 처리된 코드가 남아있습니다.
LoginState.Success타입 체크로 전환된 로직은 정상이지만, 사용하지 않는 주석은 제거하는 것이 좋습니다.🧹 정리 제안
val loginState by loginViewModel.loginState.collectAsStateWithLifecycle() - //Log.d("MainApp", "loginState: $loginState") - Log.d("MainApp", "loginState: $loginState") LaunchedEffect(loginState) { -// val loggedIn = (loginState.result != null) && (loginState.errorTag == null) && !loginState.loading -// Log.d("MainApp", "loggedIn (deeplink): $loggedIn") - -// if (loggedIn) { if (loginState is LoginState.Success) {
| // 실제 저장소. | ||
| private val pref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Android EncryptedSharedPreferences best practices 2025
💡 Result:
Key 2025 reality check (important)
EncryptedSharedPreferencesis deprecated (deprecated inandroidx.security:security-crypto:1.1.0, released July 30, 2025) and Google says there will be no subsequent releases ofsecurity-crypto/security-crypto-ktx. Use platform cryptography APIs + Android Keystore directly instead. [1][2][4]
Best practices if you still use EncryptedSharedPreferences (maintenance mode only)
- Exclude the prefs file from backups/restore
- Android docs warn the encrypted prefs file should not be backed up, because on restore the key might not exist anymore; exclude it via backup rules / data extraction rules. [3]
- Use the non-deprecated
create(Context, fileName, MasterKey, …)overload- The deprecated overload that takes a masterKeyAlias can be not thread-safe when the key is created on-demand (race on first run). Prefer the overload that takes a
MasterKeyinstance. [3]
- The deprecated overload that takes a masterKeyAlias can be not thread-safe when the key is created on-demand (race on first run). Prefer the overload that takes a
- Prefer modern schemes (AES-256-GCM master key)
- The official sample uses
MasterKey.KeyScheme.AES256_GCM. (Still deprecated overall, but if you’re using it, match the documented configuration.) [3]
- The official sample uses
- Design for keystore/key invalidation
- Treat
GeneralSecurityException/IOExceptionduring init as expected cases: implement a recovery path (e.g., wipe and re-create encrypted storage, or re-auth/login), because keystore keys can become unusable (OS update, lock screen changes, hardware/firmware issues, etc.). (This is implied by the API throwing these exceptions.) [3]
- Treat
Recommended 2025+ approach (what to do for new work)
- Don’t adopt EncryptedSharedPreferences for new code. Google’s guidance is to use platform APIs and Android Keystore directly. [1][2][4]
- Typical modern pattern:
- Generate/store a key in Android Keystore (via
javax.crypto.KeyGenerator/AndroidKeyStore) - Encrypt your data using standard JCA (
Cipherwith an AEAD mode like AES/GCM/NoPadding) and store ciphertext in regular storage (SharedPreferences / DataStore / file / DB). [1][4]
- Generate/store a key in Android Keystore (via
Sources: Android cryptography guidance + Jetpack Security release notes + EncryptedSharedPreferences API docs. [1][2][3][4]
🏁 Script executed:
# Let's examine the AuthPreferenceImpl.kt file to understand the context better
find . -name "AuthPreferenceImpl.kt" -type fRepository: LinkYou-2025/LinkU_Android
Length of output: 155
🏁 Script executed:
cat -n ./data/src/main/java/com/example/data/implementation/preference/AuthPreferenceImpl.ktRepository: LinkYou-2025/LinkU_Android
Length of output: 2263
민감한 토큰 저장에 보안이 취약한 일반 SharedPreferences 사용
Access token과 refresh token은 민감한 인증 정보입니다. 일반 SharedPreferences는 루팅된 기기에서 평문으로 노출될 수 있습니다.
EncryptedSharedPreferences는 2025년 7월부로 deprecated되었으며 Google은 더 이상 지원하지 않습니다. 대신 Android Keystore + 표준 JCA Cipher API를 사용한 플랫폼 기반 암호화를 적용해야 합니다:
- Android Keystore에 AES 키 생성 (
javax.crypto.KeyGenerator/AndroidKeyStore사용) - 표준 JCA Cipher (
AES/GCM/NoPadding등)로 토큰 데이터 암호화 - 암호화된 데이터를 SharedPreferences/DataStore에 저장
토큰 저장 시 KeyStore 키 무효화 등의 예외 상황에 대한 복구 로직도 필요합니다.
🤖 Prompt for AI Agents
In
`@data/src/main/java/com/example/data/implementation/preference/AuthPreferenceImpl.kt`
around lines 18 - 19, The code uses plain SharedPreferences via the pref
variable (context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)), which
is insecure for storing access/refresh tokens; replace this with a
platform-backed encryption flow: create and persist an AES key in
AndroidKeyStore (use KeyGenerator with "AndroidKeyStore"), use standard JCA
Cipher (e.g., "AES/GCM/NoPadding") to encrypt/decrypt token bytes, and store
only ciphertext (and IV) in SharedPreferences/DataStore under the same PREF_NAME
keys; update methods in AuthPreferenceImpl (the usages that read/write pref) to
call encrypt/decrypt, and add exception handling for KeyStore key
invalidation/UnrecoverableKeyException to trigger key rotation/clear tokens and
re-auth flow. Ensure key creation, encryption, decryption, and recovery logic
are encapsulated (e.g., KeyStore helper + encrypt/decrypt helpers) and
referenced from AuthPreferenceImpl where pref is used.
There was a problem hiding this comment.
구현 난이도가 있어서 고려해보겠습니다.
There was a problem hiding this comment.
Rate Limit Exceeded
@KateteDeveloper have exceeded the limit for the number of chat messages per hour. Please wait 2 minutes and 17 seconds before sending another message.
| Image( | ||
| painter = painterResource(id = R.drawable.ic_login_wrong), | ||
| contentDescription = "wrong", | ||
| modifier = Modifier | ||
| .width(9.dp) | ||
| .height(9.dp) |
There was a problem hiding this comment.
contentDescription는 장식용이면 null로 처리해주세요.
오류 텍스트가 함께 노출되므로 이 아이콘은 스크린리더에 불필요한 노이즈가 될 수 있습니다.
✅ 수정 제안
- Image(
- painter = painterResource(id = R.drawable.ic_login_wrong),
- contentDescription = "wrong",
- modifier = Modifier
- .width(9.dp)
- .height(9.dp)
- )
+ Image(
+ painter = painterResource(id = R.drawable.ic_login_wrong),
+ contentDescription = null,
+ modifier = Modifier
+ .width(9.dp)
+ .height(9.dp)
+ )📝 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.
| Image( | |
| painter = painterResource(id = R.drawable.ic_login_wrong), | |
| contentDescription = "wrong", | |
| modifier = Modifier | |
| .width(9.dp) | |
| .height(9.dp) | |
| Image( | |
| painter = painterResource(id = R.drawable.ic_login_wrong), | |
| contentDescription = null, | |
| modifier = Modifier | |
| .width(9.dp) | |
| .height(9.dp) | |
| ) |
🤖 Prompt for AI Agents
In `@feature/login/src/main/java/com/example/login/ui/item/WrongIndicator.kt`
around lines 34 - 39, The Image in WrongIndicator.kt currently sets
contentDescription = "wrong", which exposes a decorative icon as screen-reader
noise; change the Image call in the WrongIndicator composable to use
contentDescription = null so the icon is ignored by accessibility services
(ensure no other accessibility semantics rely on this image being announced).
| // 상태에 따라 다른 컴포넌트 표시 => 수정사항 반영. | ||
| when (nicknameState) { | ||
| is NicknameCheckState.Duplicated -> { | ||
| Spacer(Modifier.height((6.scaler))) | ||
| Text( | ||
| WrongRuleItem( | ||
| text = "이미 사용 중인 닉네임입니다.", | ||
| fontSize = 13.sp, | ||
| lineHeight = 15.sp, | ||
| fontWeight = FontWeight(400), | ||
| fontFamily = Paperlogy.font, | ||
| color = Color(0xFFFF5E5E) | ||
| modifier = Modifier.padding(start = (12.scaler)) | ||
| ) | ||
| } | ||
|
|
||
|
|
||
| is NicknameCheckState.Error -> { | ||
| Spacer(Modifier.height((6.scaler))) | ||
| Text( | ||
| text = (nicknameState as NicknameCheckState.Error).message, //뷰모델 에러 메시지 사용. | ||
| //text = "서버 요청에 실패했습니다.", | ||
| fontSize = 13.sp, | ||
| lineHeight = 15.sp, | ||
| fontWeight = FontWeight(400), | ||
| fontFamily = Paperlogy.font, | ||
| color = Color(0xFFFF5E5E) | ||
| else -> { | ||
| PasswordRuleItem( | ||
| text = "국문/영문 6자 이하", | ||
| satisfied = isNicknameValid, | ||
| modifier = Modifier.padding(start = (12.scaler)) | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "sealed class NicknameCheckState|enum class NicknameCheckState|class NicknameCheckState" --type kotlinRepository: LinkYou-2025/LinkU_Android
Length of output: 181
🏁 Script executed:
rg -n "NicknameCheckState\." --type kotlin -A 2 -B 2Repository: LinkYou-2025/LinkU_Android
Length of output: 4875
🏁 Script executed:
rg -n "is NicknameCheckState" --type kotlin -A 1 -B 1Repository: LinkYou-2025/LinkU_Android
Length of output: 442
🏁 Script executed:
sed -n '39,70p' feature/login/src/main/java/com/example/login/viewmodel/SignUpViewModel.ktRepository: LinkYou-2025/LinkU_Android
Length of output: 990
Error와 Checking 상태가 else 분기에 의해 의도하지 않게 처리되고 있습니다.
NicknameCheckState의 모든 변수는 Idle, Checking, Available, Duplicated, Error(message)입니다. 현재 코드는:
Duplicated→ WrongRuleItem (중복 메시지)else(Idle, Checking, Available, Error) → PasswordRuleItem (유효성 규칙)
이는 문제가 있습니다:
- Error 상태에서 에러 메시지를 사용자에게 표시하지 않음
- Checking 상태(로딩 중)에서 유효성 규칙을 표시함 (로딩 표시기가 필요할 수 있음)
Error와 Checking 상태는 명시적으로 처리하는 것을 권장합니다:
when (nicknameState) {
is NicknameCheckState.Duplicated -> { /* WrongRuleItem */ }
is NicknameCheckState.Error -> { /* ErrorRuleItem 또는 에러 메시지 표시 */ }
is NicknameCheckState.Checking -> { /* 로딩 표시 */ }
else -> { /* PasswordRuleItem */ }
}🤖 Prompt for AI Agents
In
`@feature/login/src/main/java/com/example/login/ui/screen/SignUpNicknameScreen.kt`
around lines 94 - 108, The when branch over nicknameState currently lumps
Idle/Checking/Available/Error into else and thus fails to show loading or error
messages; update the when on NicknameCheckState to handle is
NicknameCheckState.Duplicated (use WrongRuleItem), is NicknameCheckState.Error
(render an error message UI item or ErrorRuleItem using the state's message), is
NicknameCheckState.Checking (render a loading indicator instead of
PasswordRuleItem), and keep PasswordRuleItem only for the default
valid/idle/available case; reference NicknameCheckState, WrongRuleItem,
PasswordRuleItem and the new Error/Checking UI components when making the
change.
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Fix all issues with AI agents
In `@core/src/main/java/com/example/core/session/SessionStore.kt`:
- Around line 103-107: The function updateNickname unnecessarily calls
session.first() and assigns it to current without using it, causing an extra I/O
operation; remove the unused call and variable (the session.first() and current)
and simply perform the DataStore update via context.dataStore.edit { p ->
p[Keys.USER_NICK] = nickname } inside updateNickname, or if you intended to
validate the session first, replace the unused call with an explicit check using
session.firstOrNull() and handle the null case before calling
context.dataStore.edit; references: updateNickname, session.first(), current,
context.dataStore.edit, Keys.USER_NICK.
In `@data/src/main/java/com/example/data/di/api/ServerApiModule.kt`:
- Around line 31-35: The provideLoggingInterceptor() currently sets
HttpLoggingInterceptor.Level.BODY which can expose sensitive request/response
bodies in production; change it to conditionally select the level (e.g., use
BODY only when running a debug build and NONE or BASIC otherwise). Update
provideLoggingInterceptor() to read a build/config flag (such as
BuildConfig.DEBUG or an injected isDebug boolean) and set level = Level.BODY
when debug is true, otherwise Level.NONE (or BASIC) to avoid logging tokens and
payloads in production.
- Around line 45-58: The skipAuthPaths matching uses substring checks
(path.contains) which wrongly matches routes like /api/login-history; update the
matching in the interceptor that builds newRequest so it only treats intended
endpoints as matches (skipAuthPaths). Replace the contains logic with a stricter
check such as exact path equality or segment-aware checks (for example compare
path == skipPath or path.startsWith("$skipPath/") or match path segments) when
evaluating skipAuthPaths.any { ... } so only the exact endpoints
("/reissue","/login","/join") are skipped for adding the Authorization header.
In
`@data/src/main/java/com/example/data/implementation/preference/AuthPreferenceImpl.kt`:
- Around line 3-5: The code currently imports android.content.ContentValues.TAG
which yields an incorrect fixed string; remove that import and add a private TAG
constant inside the AuthPreferenceImpl companion object (e.g., private const val
TAG = "AuthPreferenceImpl") alongside the existing PREF_NAME and other constants
so existing Log.d/e calls in AuthPreferenceImpl keep using TAG correctly; ensure
you delete the erroneous import line and place the new TAG constant in the
companion object.
In
`@data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt`:
- Around line 177-178: The Log.d call in requestTempPassword(email: String)
exposes user PII by logging the raw email; update the logging to avoid sensitive
data by removing the plain email from the log or replacing it with a
non-identifying value (e.g., masked email, truncated/hashed value, or a request
id). Modify the implementation in requestTempPassword (and any similar uses of
TAG) to log only safe context (e.g., "임시PW 요청 received" plus a maskedEmail or
correlationId) instead of the full email string.
- Around line 170-173: The code in UserRepositoryImpl constructs a
TokenReissueResult and silently substitutes empty strings for null tokens
(response.accessToken/response.refreshToken), which hides failures; update the
logic in the token reissue path (where TokenReissueResult is created) to
validate that response.accessToken and response.refreshToken are non-null and,
if either is null, throw a clear exception (e.g., IllegalStateException or a
domain-specific exception) or return a Result/Failure that callers can handle
instead of returning empty strings so token reissue errors are surfaced.
- Around line 224-225: The current use of mapNotNull on purposes and interests
silently drops unmapped values; change the logic around
mappedPurposes/mappedInterests to detect unmapped items by comparing the
original lists (purposes, interests) against the keys present in
purposeMap/interestMap, and if any unmapped items exist either log a clear
warning (including the unmapped values and the maps' keys) or throw a
descriptive exception (e.g., IllegalArgumentException) to prevent silent data
loss; update the code paths in UserRepositoryImpl where mappedPurposes and
mappedInterests are produced to perform this check and handle failures
consistently.
- Around line 97-103: The LoginResult construction in UserRepositoryImpl is
using a magic fallback (-1) for response.userId and can overflow when converting
Long to Int; instead, validate response.userId explicitly: if response.userId is
null throw an IllegalStateException (or similar) indicating missing userId, then
check the Long value is within Int.MIN_VALUE..Int.MAX_VALUE and throw if out of
range, finally convert to Int only after those checks and use that value when
constructing LoginResult (update the userId assignment where
response.userId?.toInt() ?: -1 is used).
- Around line 73-80: The nickname availability check in the checkNickname call
uses response.result?.contains("사용 가능"), which is fragile; update the logic in
UserRepositoryImpl (the block using serverApi.withErrorHandlingRaw {
checkNickname(nickname) }) to determine availability from response.isSuccess
instead, adjust the Log.d message to reflect the new boolean, and return that
isSuccess value so this method matches the pattern used by sendEmailCode,
requestTempPassword, and updateUserInfo.
🧹 Nitpick comments (5)
core/src/main/java/com/example/core/session/SessionStore.kt (1)
87-101: 폴백 값-1L사용 시 주의세션이 비어있거나 유효하지 않을 때
-1L이나 빈 문자열로 저장되면 데이터 무결성 문제가 발생할 수 있습니다.current.userId가null인 경우 업데이트를 건너뛰거나 예외를 던지는 것이 더 안전할 수 있습니다.feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt (1)
19-25:sealed class선언에 불필요한 공백Line 20에서
sealed class에 공백이 두 개 있습니다.🔧 수정 제안
-sealed class LoginState { +sealed class LoginState {data/src/main/java/com/example/data/di/api/ServerApiModule.kt (1)
64-72: 인터셉터 순서 확인 필요
authInterceptor가addNetworkInterceptor로,loggingInterceptor가addInterceptor로 추가되었습니다. 로깅이 실제 네트워크 요청(토큰 헤더 포함)을 캡처하려면loggingInterceptor도addNetworkInterceptor로 추가하는 것이 좋습니다.💡 참고
fun provideOkHttpClient( authInterceptor: Interceptor, loggingInterceptor: HttpLoggingInterceptor ): OkHttpClient { return OkHttpClient.Builder() .addNetworkInterceptor(authInterceptor) // 토큰 붙이기 - .addInterceptor(loggingInterceptor) // 로그 출력 + .addNetworkInterceptor(loggingInterceptor) // 실제 요청/응답 로그 .build() }data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt (1)
269-286: TODO 주석을 추적하거나 해결해야 합니다.
// logout? - TODO : 지현아... 세션으로 마이페이지 해야할 듯...주석이 남아있습니다. 이 작업이 완료되었거나 별도 이슈로 추적되어야 합니다.
runCatching으로 서버 로그아웃 실패를 무시하고 로컬 데이터를 정리하는 패턴은 적절합니다.이 TODO 항목을 추적하기 위한 GitHub 이슈를 생성해 드릴까요?
data/src/main/java/com/example/data/api/ServerApiExt.kt (1)
87-94: 알 수 없는 HTTP 코드를 BadRequest로 매핑하는 것은 부정확할 수 있습니다.
else -> ApiError.BadRequest(code(), message())에서 알 수 없는 HTTP 상태 코드(예: 418, 429, 사용자 정의 코드)를BadRequest로 처리하면 에러 원인을 잘못 나타낼 수 있습니다.💡 제안된 수정
private fun HttpException.toApiError(): ApiError = when (code()) { 400 -> ApiError.BadRequest(code(), message()) 401 -> ApiError.Unauthorized() 403 -> ApiError.Forbidden() 404 -> ApiError.NotFound() + 429 -> ApiError.BadRequest(code(), "요청이 너무 많습니다. 잠시 후 다시 시도해주세요") in 500..599 -> ApiError.ServerError(code(), message()) - else -> ApiError.BadRequest(code(), message()) + else -> ApiError.ServerError(code(), "예상치 못한 오류가 발생했습니다 (코드: ${code()})") }
data/src/main/java/com/example/data/implementation/preference/AuthPreferenceImpl.kt
Outdated
Show resolved
Hide resolved
data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt
Show resolved
Hide resolved
| return LoginResult( | ||
| userId = result.userId?.toInt() ?: -1, | ||
| token = result.accessToken ?: "", | ||
| status = result.status ?: "", | ||
| inactiveDate = result.inactiveDate?.toString() | ||
| userId = response.userId?.toInt() ?: -1, | ||
| accessToken = response.accessToken ?: "", | ||
| refreshToken = response.refreshToken ?: "", | ||
| status = response.status ?: "", | ||
| inactiveDate = response.inactiveDate?.toString() | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find the LoginResult model
fd -t f LoginResult --type fRepository: LinkYou-2025/LinkU_Android
Length of output: 275
🏁 Script executed:
# Search for LoginResult class definition
rg "data class LoginResult|class LoginResult" -A 10Repository: LinkYou-2025/LinkU_Android
Length of output: 1665
🏁 Script executed:
# Also look at UserRepositoryImpl to see the full context
fd UserRepositoryImpl.ktRepository: LinkYou-2025/LinkU_Android
Length of output: 153
🏁 Script executed:
# Check the response type and response.userId type
rg "response\.userId" -B 5 -A 5Repository: LinkYou-2025/LinkU_Android
Length of output: 1342
userId 변환 시 에러 처리 방식 개선 필요
response.userId?.toInt() ?: -1은 두 가지 문제가 있습니다:
-
Null 안전성:
-1을 매직 넘버로 사용하면 호출자가 유효하지 않은 userId를 처리하기 어렵습니다. userId가 필수 필드인 경우 null을 명시적으로 에러로 처리해야 합니다. -
타입 변환 오버플로우:
Long.toInt()변환 시 값이Int.MAX_VALUE를 초과하면 오버플로우가 발생합니다.
LoginResult는 userId를 Int (non-nullable)로 요구하므로, null userId는 비정상 상태입니다. 예외를 던지는 것이 명확한 처리 방식입니다:
제안된 수정
return LoginResult(
- userId = response.userId?.toInt() ?: -1,
+ userId = response.userId?.toInt() ?: throw IllegalStateException("userId가 없습니다"),
accessToken = response.accessToken ?: "",
refreshToken = response.refreshToken ?: "",
status = response.status ?: "",
inactiveDate = response.inactiveDate?.toString()
)🤖 Prompt for AI Agents
In
`@data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt`
around lines 97 - 103, The LoginResult construction in UserRepositoryImpl is
using a magic fallback (-1) for response.userId and can overflow when converting
Long to Int; instead, validate response.userId explicitly: if response.userId is
null throw an IllegalStateException (or similar) indicating missing userId, then
check the Long value is within Int.MIN_VALUE..Int.MAX_VALUE and throw if out of
range, finally convert to Int only after those checks and use that value when
constructing LoginResult (update the userId assignment where
response.userId?.toInt() ?: -1 is used).
There was a problem hiding this comment.
userId = response.userId?.toInt() ?: throw IllegalStateException("로그인 응답에 userId가 누락되었습니다."), 으로 수정하겠습니다.
There was a problem hiding this comment.
@KateteDeveloper, 네, 좋습니다! 해당 수정으로 null userId 처리가 명확해지고, 매직 넘버(-1) 사용 문제도 해결됩니다. 👍
참고로, Long.toInt() 변환 시 값이 Int 범위(약 ±21억)를 초과하면 하위 32비트만 사용하여 잘림 현상이 발생할 수 있습니다. 다만 실제 환경에서 userId가 Int.MAX_VALUE를 초과할 가능성은 낮으므로, 현재 수정안으로도 충분합니다.
수정 완료 후 알려주시면 확인하겠습니다!
data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt
Show resolved
Hide resolved
data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt
Outdated
Show resolved
Hide resolved
data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@data/src/main/java/com/example/data/di/api/ServerApiModule.kt`:
- Around line 41-86: The auth interceptor provideAuthInterceptor currently uses
addHeader and is registered as a network interceptor, which can cause duplicate
Authorization headers on redirects/retries; change the header insertion in
provideAuthInterceptor to use header("Authorization", "Bearer $token") (which
replaces existing values) and register the interceptor as an application
interceptor by switching addNetworkInterceptor(authInterceptor) to
addInterceptor(authInterceptor) in provideOkHttpClient so the auth logic runs
exactly once per logical request.
| // 인증 인터센터임. 토큰에 헤더를 자동 추가함. | ||
| @Provides | ||
| @Singleton | ||
| fun provideAuthInterceptor(authPreference: AuthPreference): Interceptor { | ||
| return Interceptor { chain -> | ||
| val originalRequest = chain.request() | ||
| val path = originalRequest.url.encodedPath | ||
|
|
||
| // UserApi 정의서에 기반한 토큰 미필요 경로 리스트 (정확한 경로 명시) | ||
| val skipAuthPaths = setOf( | ||
| "/api/users/reissue", | ||
| "/api/users/login", | ||
| "/api/users/join", | ||
| "/api/users/check-nickname", | ||
| "/api/users/emails/code", | ||
| "/api/users/emails/verify", | ||
| "/api/users/password/temp" | ||
| ) | ||
|
|
||
| val client = OkHttpClient.Builder() | ||
| .addNetworkInterceptor { | ||
| val request = it.request() | ||
| .newBuilder() | ||
| .let { builder -> | ||
| authPreference.accessToken?.let { token -> | ||
| builder.addHeader("Authorization", "Bearer $token") | ||
| } ?: builder | ||
| // path가 "/api/users/login"일 때만 true가 됨 | ||
| val isSkipPath = skipAuthPaths.contains(path) | ||
|
|
||
| val newRequest = if (isSkipPath) { | ||
| originalRequest | ||
| } else { | ||
| originalRequest.newBuilder().apply { | ||
| authPreference.accessToken?.let { token -> | ||
| addHeader("Authorization", "Bearer $token") | ||
| } | ||
| .build() | ||
| it.proceed(request) | ||
| }.build() | ||
| } | ||
| .addInterceptor(logger) | ||
| chain.proceed(newRequest) | ||
| } | ||
| } | ||
|
|
||
| //OkHttpClient : 네트워크 전송 | ||
| @Provides | ||
| @Singleton | ||
| fun provideOkHttpClient( | ||
| authInterceptor: Interceptor, | ||
| loggingInterceptor: HttpLoggingInterceptor | ||
| ): OkHttpClient { | ||
| return OkHttpClient.Builder() | ||
| .addNetworkInterceptor(authInterceptor) // 토큰 붙이기 | ||
| .addInterceptor(loggingInterceptor) // 로그 출력 | ||
| .build() |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
OkHttp addHeader vs header duplicate Authorization redirect
💡 Result:
Request.Builder.header(name, value)replaces any existing headers with that name (there will be at most one). (square.github.io)Request.Builder.addHeader(name, value)adds another header line with the same name (duplicates are kept). It’s meant for multi-valued headers likeCookie. (square.github.io)
Why you see duplicate Authorization
If you set auth in an Interceptor or Authenticator using addHeader("Authorization", ...), and the original request already had Authorization, you’ll end up sending two Authorization headers. Most servers will reject or mis-parse that. Use header("Authorization", ...) instead. (stackoverflow.com)
Redirect behavior (why Authorization “disappears”)
OkHttp intentionally removes Authorization on redirects that change scheme/host/port (“across hosts”) to avoid leaking credentials to a different origin. This is done in OkHttp’s redirect handling (RetryAndFollowUpInterceptor). (stackoverflow.com)
Practical guidance
- For
Authorization, preferheader("Authorization", token), notaddHeader. (square.github.io) - If you must preserve
Authorizationon cross-origin redirects, you generally can’t override OkHttp’s built-in redirect decision directly; the usual approach is to disable automatic redirects and handle 3xx manually, re-attaching the header only when you decide it’s safe. (stackoverflow.com)
Citations:
- 1: https://square.github.io/okhttp/3.x/okhttp/okhttp3/Request.Builder.html?utm_source=openai
- 2: https://square.github.io/okhttp/5.x/okhttp/okhttp3/-request/-builder/add-header.html?utm_source=openai
- 3: https://stackoverflow.com/questions/57872635/okhttp-authenticator-addheader-vs-header?utm_source=openai
- 4: https://stackoverflow.com/questions/52284568/does-okhttp-send-authorization-and-other-potentially-sensitive-headers-on-redire?utm_source=openai
- 5: https://square.github.io/okhttp/recipes/?utm_source=openai
- 6: https://stackoverflow.com/questions/77483238/okhttp-redirect-with-authorization-header-included?utm_source=openai
🌐 Web query:
OkHttp network interceptor vs application interceptor Authorization header best practice
💡 Result:
Best practice: add the Authorization header in an application interceptor (addInterceptor()), and use an Authenticator for handling 401 challenges / token refresh.
- Use an application interceptor for
Authorizationinjection because it runs once per logical call, is invoked even when the response is served from cache, and matches your app’s “original intent” (you’re setting a policy for all requests) rather than “on-the-wire” concerns. [1] - Avoid a network interceptor for auth header injection as the default because network interceptors aren’t invoked for cache hits and they run for each network attempt, including redirects and retries, which is usually not what you want for simple header injection. [1]
- Use
Authenticatorfor refresh/retry on 401 instead of doing refresh inside an interceptor; OkHttp provides it specifically for authentication challenges and automatic retry behavior. [4]
Rule of thumb
- “Always attach my bearer token” ⇒ application interceptor. [1]
- “If I get 401, refresh token and retry safely” ⇒ Authenticator. [4]
- “I must modify/inspect exactly what goes over the network (incl. redirects/retries)” ⇒ network interceptor. [1]
Sources: OkHttp interceptors docs (application vs network) [1]; OkHttp Authenticator vs interceptor discussion [4].
addHeader() 대신 header()를 사용하고, 헤더 주입은 application interceptor로 변경하세요.
Network interceptor에서 addHeader("Authorization", ...)를 사용하면 리다이렉트/재시도 시 network interceptor가 다시 실행되면서 Authorization 헤더가 중복될 수 있습니다. header()는 기존 헤더를 교체하므로 중복을 방지하고, application interceptor (addInterceptor)를 사용하는 것이 표준 방식입니다 — application interceptor는 논리적 요청당 한 번만 실행되고 auth 정책을 설정하기에 적합합니다.
✍️ 제안 수정
- val newRequest = if (isSkipPath) {
- originalRequest
- } else {
- originalRequest.newBuilder().apply {
- authPreference.accessToken?.let { token ->
- addHeader("Authorization", "Bearer $token")
- }
- }.build()
- }
+ val newRequest = if (isSkipPath) {
+ originalRequest
+ } else {
+ originalRequest.newBuilder().apply {
+ authPreference.accessToken?.let { token ->
+ header("Authorization", "Bearer $token")
+ }
+ }.build()
+ }- .addNetworkInterceptor(authInterceptor) // 토큰 붙이기
+ .addInterceptor(authInterceptor) // 토큰 붙이기📝 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.
| // 인증 인터센터임. 토큰에 헤더를 자동 추가함. | |
| @Provides | |
| @Singleton | |
| fun provideAuthInterceptor(authPreference: AuthPreference): Interceptor { | |
| return Interceptor { chain -> | |
| val originalRequest = chain.request() | |
| val path = originalRequest.url.encodedPath | |
| // UserApi 정의서에 기반한 토큰 미필요 경로 리스트 (정확한 경로 명시) | |
| val skipAuthPaths = setOf( | |
| "/api/users/reissue", | |
| "/api/users/login", | |
| "/api/users/join", | |
| "/api/users/check-nickname", | |
| "/api/users/emails/code", | |
| "/api/users/emails/verify", | |
| "/api/users/password/temp" | |
| ) | |
| val client = OkHttpClient.Builder() | |
| .addNetworkInterceptor { | |
| val request = it.request() | |
| .newBuilder() | |
| .let { builder -> | |
| authPreference.accessToken?.let { token -> | |
| builder.addHeader("Authorization", "Bearer $token") | |
| } ?: builder | |
| // path가 "/api/users/login"일 때만 true가 됨 | |
| val isSkipPath = skipAuthPaths.contains(path) | |
| val newRequest = if (isSkipPath) { | |
| originalRequest | |
| } else { | |
| originalRequest.newBuilder().apply { | |
| authPreference.accessToken?.let { token -> | |
| addHeader("Authorization", "Bearer $token") | |
| } | |
| .build() | |
| it.proceed(request) | |
| }.build() | |
| } | |
| .addInterceptor(logger) | |
| chain.proceed(newRequest) | |
| } | |
| } | |
| //OkHttpClient : 네트워크 전송 | |
| @Provides | |
| @Singleton | |
| fun provideOkHttpClient( | |
| authInterceptor: Interceptor, | |
| loggingInterceptor: HttpLoggingInterceptor | |
| ): OkHttpClient { | |
| return OkHttpClient.Builder() | |
| .addNetworkInterceptor(authInterceptor) // 토큰 붙이기 | |
| .addInterceptor(loggingInterceptor) // 로그 출력 | |
| .build() | |
| // 인증 인터센터임. 토큰에 헤더를 자동 추가함. | |
| `@Provides` | |
| `@Singleton` | |
| fun provideAuthInterceptor(authPreference: AuthPreference): Interceptor { | |
| return Interceptor { chain -> | |
| val originalRequest = chain.request() | |
| val path = originalRequest.url.encodedPath | |
| // UserApi 정의서에 기반한 토큰 미필요 경로 리스트 (정확한 경로 명시) | |
| val skipAuthPaths = setOf( | |
| "/api/users/reissue", | |
| "/api/users/login", | |
| "/api/users/join", | |
| "/api/users/check-nickname", | |
| "/api/users/emails/code", | |
| "/api/users/emails/verify", | |
| "/api/users/password/temp" | |
| ) | |
| // path가 "/api/users/login"일 때만 true가 됨 | |
| val isSkipPath = skipAuthPaths.contains(path) | |
| val newRequest = if (isSkipPath) { | |
| originalRequest | |
| } else { | |
| originalRequest.newBuilder().apply { | |
| authPreference.accessToken?.let { token -> | |
| header("Authorization", "Bearer $token") | |
| } | |
| }.build() | |
| } | |
| chain.proceed(newRequest) | |
| } | |
| } | |
| //OkHttpClient : 네트워크 전송 | |
| `@Provides` | |
| `@Singleton` | |
| fun provideOkHttpClient( | |
| authInterceptor: Interceptor, | |
| loggingInterceptor: HttpLoggingInterceptor | |
| ): OkHttpClient { | |
| return OkHttpClient.Builder() | |
| .addInterceptor(authInterceptor) // 토큰 붙이기 | |
| .addInterceptor(loggingInterceptor) // 로그 출력 | |
| .build() |
🤖 Prompt for AI Agents
In `@data/src/main/java/com/example/data/di/api/ServerApiModule.kt` around lines
41 - 86, The auth interceptor provideAuthInterceptor currently uses addHeader
and is registered as a network interceptor, which can cause duplicate
Authorization headers on redirects/retries; change the header insertion in
provideAuthInterceptor to use header("Authorization", "Bearer $token") (which
replaces existing values) and register the interceptor as an application
interceptor by switching addNetworkInterceptor(authInterceptor) to
addInterceptor(authInterceptor) in provideOkHttpClient so the auth logic runs
exactly once per logical request.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
feature/login/build.gradle.kts (1)
72-73:⚠️ Potential issue | 🟠 Major중복된 navigation-compose 의존성이 서로 다른 버전으로 선언되어 있습니다.
Line 73에
2.5.3버전이, Line 103에2.7.7버전이 선언되어 있어 버전 충돌이 발생할 수 있습니다. 하나의 버전으로 통일해주세요.🔧 수정 제안
- implementation("com.google.accompanist:accompanist-navigation-animation:0.32.0") - implementation ("androidx.navigation:navigation-compose:2.5.3") + implementation("com.google.accompanist:accompanist-navigation-animation:0.32.0") + implementation("androidx.navigation:navigation-compose:2.7.7")그리고 Line 103의 중복 선언을 제거하세요:
implementation("androidx.hilt:hilt-navigation-compose:1.2.0") - implementation("androidx.navigation:navigation-compose:2.7.7") }Also applies to: 103-103
app/src/main/java/com/example/linku_android/MainApp.kt (1)
110-110:⚠️ Potential issue | 🟠 MajorLoginViewModel이 두 번 생성되고 있습니다.
Line 110에서
loginViewModel이, Line 205에서loginVM이 각각hiltViewModel()로 생성됩니다. Composable 스코프가 다르므로 서로 다른 인스턴스가 될 수 있어 상태 불일치 문제가 발생할 수 있습니다.
loginVM은 Splash에서 자동 로그인에 사용되고,loginViewModel은login_root와 딥링크 로그인에 사용됩니다. 동일한 인스턴스를 사용하도록 통일해주세요.🔧 수정 제안
Line 205의 별도 생성을 제거하고 Line 110의
loginViewModel을 사용하세요:val app = LocalContext.current.applicationContext val deps = remember { EntryPointAccessors.fromApplication(app, SplashDeps::class.java) } - val loginVM: LoginViewModel = hiltViewModel()그리고 Splash 내
loginVM.tryAutoLogin을loginViewModel.tryAutoLogin으로 변경:- loginVM.tryAutoLogin( + loginViewModel.tryAutoLogin(Also applies to: 205-205
🤖 Fix all issues with AI agents
In `@feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt`:
- Around line 156-158: LoginViewModel에서 authPreference.userId가 null일 때 -1L을 대입해
getUserInfo(-1L)를 호출하는 것은 잘못된 API 호출을 유발하므로, authPreference.userId가 null이면 즉시
함수/처리에서 early return 하거나 에러/비정상 상태를 설정하도록 변경하세요; 구체적으로 LoginViewModel의 해당 블록에서
authPreference.userId를 안전하게 바인딩한 뒤 (예: val userId = authPreference.userId ?:
return) getUserInfo(userId)를 호출하거나 null일 때는 사용자 재인증/로그아웃/에러 상태를 트리거하도록 구현해 서버로
잘못된 id가 전달되지 않게 수정하세요.
🧹 Nitpick comments (6)
feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt (2)
19-25: sealed class 선언에 불필요한 공백이 있습니다.Line 20의
sealed class에 공백이 두 개 있습니다.✏️ 수정 제안
-sealed class LoginState { +sealed class LoginState {
191-246: 주석 처리된 코드 블록을 제거해주세요.55줄에 달하는 주석 처리된 코드가 남아 있습니다. 버전 관리 시스템(Git)에서 이력을 추적할 수 있으므로, 주석 처리된 코드는 제거하는 것이 코드 가독성과 유지보수에 좋습니다.
feature/login/src/main/java/com/example/login/LoginApp.kt (2)
36-39: 중복된 import와 주석 처리된 import를 정리해주세요.
- Line 36: 주석 처리된 import (
DeepLinkHandlerViewModel)- Lines 37-38:
composable과navigation이 Lines 12-13에서 이미 import되어 중복됩니다.🧹 수정 제안
import com.example.login.viewmodel.SignUpViewModel import com.example.home.HomeViewModel -//import com.example.linku_android.deeplink.DeepLinkHandlerViewModel -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.navigation +import androidx.navigation.compose.NavHost
43-48: 주석 처리된 파라미터를 제거해주세요.Line 44의
//navController: NavHostController는 사용되지 않으므로 제거하는 것이 좋습니다.✏️ 수정 제안
fun LoginApp( - //navController: NavHostController, onLoginSuccess: () -> Unit, loginViewModel: LoginViewModel, showNavBar: (Boolean) -> Unit ) {app/src/main/java/com/example/linku_android/MainApp.kt (2)
249-252: 주석 처리된 코드 블록들을 제거해주세요.파일 전체에 여러 주석 처리된 코드 블록이 있습니다:
- Lines 249-252:
auth_graph네비게이션- Lines 271-274:
auth_graph네비게이션- Lines 415-418: 로그아웃 네비게이션
- Lines 607-610: LoginState 체크
Git 히스토리에서 이전 코드를 확인할 수 있으므로, 주석 처리된 코드는 제거하는 것이 좋습니다.
Also applies to: 271-274, 415-418, 607-610
291-300: 딥링크 처리 시 주석 정리가 필요합니다.Line 291의 주석
// 딥링크 대기 작업 처리 //지민아 이거 정리해줄 수 있어?는 팀 내부 커뮤니케이션용으로 보입니다. 코드 리뷰/정리 후 제거하거나 명확한 TODO로 변경해주세요.
feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@feature/curation/src/main/java/com/example/curation/CurationApp.kt`:
- Line 1: 파일의 package 선언이 실제 경로와 불일치해 빌드 에러가 나고 있습니다; CurationApp.kt 파일의 상단
package 선언을 현재 경로에 맞게 com.example.curation으로 수정하거나, 의도한 패키지
com.example.linku_android.curation를 유지하려면 파일을 해당 디렉터리로 이동하도록 변경하세요 (수정할 위치:
CurationApp.kt의 package 선언).
In `@feature/mypage/src/main/java/com/example/mypage/MyPageApp.kt`:
- Around line 136-149: The code hardcodes jobName = "" when calling
viewModel.updateUserInfo so the UI never shows the selected job; update the
AccountSettingScreen's onSubmit callback to include the selected jobName (or
look it up from jobId) and pass that jobName into viewModel.updateUserInfo
instead of an empty string. Locate the AccountSettingScreen onSubmit usage and
its declaration, extend its signature to include jobName (or add a lookup in the
caller before calling updateUserInfo), then call
viewModel.updateUserInfo(nickname = ..., jobId = jobId, jobName = jobName,
purposes = ..., interests = ..., onSuccess = ..., onError = ...).
- Around line 114-115: Replace the hardcoded empty sets for initialPurposeTags
and initialContentTags with the user's saved tags from the current session:
locate the MyPageApp component construction where initialPurposeTags =
emptySet() and initialContentTags = emptySet(), and pass the corresponding
session properties (e.g., session.purposeTags and session.contentTags or the
actual field names on your session/user object) so the account settings screen
receives and displays the user's existing selections.
In `@feature/mypage/src/main/java/com/example/mypage/MyPageViewModel.kt`:
- Around line 156-165: In logout(), catch currently swallows exceptions; modify
the catch block in the logout function (inside viewModelScope.launch where
userRepository.logout() is called) to log the exception details before calling
onError -- e.g., use your logger or Android Log (referencing logout(),
viewModelScope.launch, and userRepository.logout()) to record e.message and the
stacktrace, then pass the user-facing message to onError.
🧹 Nitpick comments (11)
feature/curation/src/main/java/com/example/curation/CurationApp.kt (1)
49-53: 타입 안전한 네비게이션 인자 사용을 고려해 보세요.현재 경로 인자를
String으로 받아toLong()으로 변환하고 실패 시0L로 대체합니다. 이 방식은 잘못된 인자가 전달되어도 조용히 무시되어 디버깅이 어려울 수 있습니다.Navigation Compose의
navArgument와NavType.LongType을 사용하면 타입 안전성을 확보할 수 있습니다.♻️ 타입 안전한 인자 처리 예시
import androidx.navigation.NavType import androidx.navigation.navArgument // composable 정의 시 composable( route = "curation_detail/{userId}/{curationId}", arguments = listOf( navArgument("userId") { type = NavType.LongType }, navArgument("curationId") { type = NavType.LongType } ) ) { backStack -> val userId = backStack.arguments?.getLong("userId") ?: return@composable val curationId = backStack.arguments?.getLong("curationId") ?: return@composable // ... }core/src/main/java/com/example/core/session/SessionStore.kt (2)
89-94:clear()에서 불필요한 중복 작업
p.clear()가 모든 키를 삭제하므로, 그 전에p[Keys.LOGGED_IN] = false를 설정하는 것은 불필요합니다.♻️ 수정 제안
suspend fun clear() { context.dataStore.edit { p -> - p[Keys.LOGGED_IN] = false p.clear() // 모든 세션 데이터 한 번에 삭제 } }
84-85: 쉼표 구분자 사용 시 데이터 손실 위험
purposes와interests를 쉼표로 join/split하고 있습니다. 만약 항목 자체에 쉼표가 포함되어 있으면 데이터가 잘못 파싱됩니다.현재 매핑된 값들("CAREER", "STUDY" 등)은 쉼표를 포함하지 않지만, 향후 변경에 대비하여 JSON 직렬화나 다른 구분자(예:
|)를 고려해보세요.♻️ JSON 직렬화 예시
// 저장 시 import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json p[Keys.USER_PURPOSES] = Json.encodeToString(purposes) // 읽기 시 purposes = p[Keys.USER_PURPOSES]?.let { runCatching { Json.decodeFromString<List<String>>(it) }.getOrElse { emptyList() } } ?: emptyList()Also applies to: 162-163
data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt (1)
305-312:clearAuthData()의 idempotency 체크가 불완전할 수 있음
authPreference상태만 확인하고sessionStore상태는 확인하지 않습니다. 두 저장소의 상태가 불일치할 경우, 한쪽만 정리될 수 있습니다.♻️ 수정 제안
private suspend fun clearAuthData() { - // 중복 실행 방지함. 이미 로그아웃 상태면 아무것도 하지 않음 - if (authPreference.userId == null && !authPreference.isLoggedIn) return - + // 항상 양쪽 모두 정리 (clear()는 내부적으로 idempotent) authPreference.clear() sessionStore.clear() Log.d(TAG, "모든 로컬 세션 데이터 삭제 완료") }feature/mypage/src/main/java/com/example/mypage/MyPageViewModel.kt (1)
27-34:initialValue가독성 개선 권장
SessionSnapshot생성자에 많은null값이 위치 인자로 전달되어 가독성이 떨어집니다. Named parameters를 사용하면 더 명확합니다.♻️ 수정 제안
val sessionState: StateFlow<SessionStore.SessionSnapshot> = sessionStore.session .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), - initialValue = SessionStore.SessionSnapshot(false, null, - null, null, null, null, null, null, null, null, - emptyList(), emptyList() ) + initialValue = SessionStore.SessionSnapshot( + loggedIn = false, + userId = null, + nickname = null, + email = null, + gender = null, + jobId = null, + jobName = null, + myLinku = null, + myFolder = null, + myAiLinku = null, + purposes = emptyList(), + interests = emptyList() + ) )feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt (2)
189-194:tryAutoLogin에서 네트워크 오류를 구분하지 않음현재 일반
Exception으로 모든 오류를 처리하고 있어, 일시적 네트워크 오류와 다른 오류를 구분하지 못합니다. 사용자에게 더 명확한 피드백을 제공하려면IOException을 별도로 처리하는 것이 좋습니다.♻️ 수정 제안
} catch (e: ApiError.TokenExpired) { // 이 경우만 logout Log.e(TAG, "자동 로그인 실패: 토큰 만료") authPreference.clear() _autoLoginState.value = AutoLoginState.Failed onFail() + } catch (e: IOException) { + // 네트워크 오류 - 토큰 유지 + Log.e(TAG, "자동 로그인 실패: 네트워크 오류", e) + _autoLoginState.value = AutoLoginState.Failed + onFail() + } catch (e: Exception) { - // 나머지는 절대 logout 하지 않음 - Log.e(TAG, "자동 로그인 실패: ${e.message}") + // 기타 오류 + Log.e(TAG, "자동 로그인 실패: 기타 오류", e) _autoLoginState.value = AutoLoginState.Failed onFail() }
199-220:logout()에서 중복clear()호출
try블록에서authPreference.clear()를 호출하고,catch블록에서도 다시 호출합니다.sessionStore.clear()실패 시authPreference는 이미 정리되었으므로 중복입니다.또한
sessionStore.clear()실패 시 세션 데이터가 남아있게 됩니다.♻️ 수정 제안
fun logout(onComplete: () -> Unit) { viewModelScope.launch { try { - // 서버에 로그아웃 알림? 필요할까요? - // userRepository.logout() - - // 로컬 저장소 비우기 (토큰, 유저 아이디 삭제) authPreference.clear() - - // 인메모리 세션 스토어 비우기 -> 아예 비울 수 있도록. - sessionStore.clear() // SessionStore에 clear() 함수가 있다고 가정 - + sessionStore.clear() Log.d("LoginVM", "로그아웃 및 세션 정리 완료") - onComplete() } catch (e: Exception) { Log.e("LoginVM", "로그아웃 중 오류 발생", e) - // 에러가 나더라도 로컬 데이터는 지워야 함 - authPreference.clear() - onComplete() + } finally { + // 성공/실패 무관하게 항상 완료 콜백 호출 + onComplete() } } }app/src/main/java/com/example/linku_android/MainApp.kt (4)
110-112: LoginViewModel 중복 획득 정리 권장. 동일 스코프에서hiltViewModel()을 두 번 호출하면 스코프가 바뀌었을 때 인스턴스가 달라질 수 있어, 상단의loginViewModel을 재사용하는 편이 안전합니다.♻️ 제안된 수정
- val loginVM: LoginViewModel = hiltViewModel() + // 상단에서 생성한 loginViewModel 재사용 ... - loginVM.tryAutoLogin( + loginViewModel.tryAutoLogin(Also applies to: 205-210, 264-265
300-301: 개인 메모성 주석 정리 권장. 릴리즈 전엔 TODO로 정리하거나 삭제해 주세요.
502-505: 임시성 주석 문구 정리 권장. 기능 의도가 확정되면 정식 코멘트로 다듬는 편이 좋아요.
569-597: 로그인 성공 상태 재진입 처리 확인 필요. 로그인 ViewModel이 액티비티 스코프라면Success가 유지된 채로 로그인 화면에 재진입했을 때 곧바로 네비게이션이 트리거될 수 있으니, 성공 이벤트 소비/초기화 흐름을 검토해 주세요.
| //fun CurationApp(viewModel: CurationViewModel) { | ||
| // CurationScreen() | ||
| //} No newline at end of file | ||
| package com.example.linku_android.curation |
There was a problem hiding this comment.
패키지 선언이 파일 경로와 일치하지 않습니다.
파일 경로는 feature/curation/src/main/java/com/example/curation/CurationApp.kt인데, 패키지는 com.example.linku_android.curation으로 선언되어 있습니다. 이로 인해 빌드 오류가 발생합니다.
🐛 패키지 수정 제안
-package com.example.linku_android.curation
+package com.example.curation📝 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.
| package com.example.linku_android.curation | |
| package com.example.curation |
🤖 Prompt for AI Agents
In `@feature/curation/src/main/java/com/example/curation/CurationApp.kt` at line
1, 파일의 package 선언이 실제 경로와 불일치해 빌드 에러가 나고 있습니다; CurationApp.kt 파일의 상단 package 선언을
현재 경로에 맞게 com.example.curation으로 수정하거나, 의도한 패키지
com.example.linku_android.curation를 유지하려면 파일을 해당 디렉터리로 이동하도록 변경하세요 (수정할 위치:
CurationApp.kt의 package 선언).
| fun logout(onSuccess: () -> Unit, onError: (String) -> Unit) { | ||
| viewModelScope.launch { | ||
| try { | ||
| userRepository.logout() | ||
| onSuccess() | ||
| } catch (e: Exception) { | ||
| onError("로그아웃에 실패했습니다.") | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
예외가 삼켜지고 있음 (static analysis warning)
logout() 함수에서 예외 발생 시 로그 없이 메시지만 반환합니다. 디버깅을 위해 예외를 로깅하세요.
🔧 수정 제안
fun logout(onSuccess: () -> Unit, onError: (String) -> Unit) {
viewModelScope.launch {
try {
userRepository.logout()
onSuccess()
} catch (e: Exception) {
+ Log.e("MyPageViewModel", "로그아웃 실패", e)
onError("로그아웃에 실패했습니다.")
}
}
}🧰 Tools
🪛 detekt (1.23.8)
[warning] 161-161: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
🤖 Prompt for AI Agents
In `@feature/mypage/src/main/java/com/example/mypage/MyPageViewModel.kt` around
lines 156 - 165, In logout(), catch currently swallows exceptions; modify the
catch block in the logout function (inside viewModelScope.launch where
userRepository.logout() is called) to log the exception details before calling
onError -- e.g., use your logger or Android Log (referencing logout(),
viewModelScope.launch, and userRepository.logout()) to record e.message and the
stacktrace, then pass the user-facing message to onError.
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@core/src/main/java/com/example/core/session/SessionStore.kt`:
- Around line 115-124: Update the outdated example comment to match the new
SessionStore.updateProfile signature by adding the missing parameters `purposes`
and `interests` to the example call used in MyPageViewModel usage; locate the
example block in SessionStore.kt (the comment showing how to call
updateUserInfo/updateProfile) and modify the example invocation to include
appropriate placeholders/variables for `purposes` and `interests` so the comment
reflects the current method signature of updateProfile.
- Around line 41-49: Add a datastore migration that reads the legacy string keys
(those created via stringPreferencesKey) and converts them to Long into the new
longPreferencesKey entries: specifically handle USER_ID, USER_JOB_ID,
USER_MY_LINKU, USER_MY_FOLDER, USER_MY_AI_LINKU by attempting to read the old
string value, parseLong (with safe fallback), write into the new long key, and
then remove the legacy string key; implement this as part of the SessionStore
preferences migration path (where the datastore is initialized) so existing
users are upgraded on first run. Also replace the fragile joinToString(",")
storage for purposes and interests with a JSON array encoding/decoding (store as
a JSON string in the same stringPreferencesKey) to preserve values containing
commas and update read/write helpers to use JSON parse/serialize. Ensure all
changes reference the existing preference key symbols (USER_ID, USER_JOB_ID,
USER_MY_LINKU, USER_MY_FOLDER, USER_MY_AI_LINKU, and the purposes/interests
keys) and perform safe parsing with fallbacks to avoid breaking login/session
loading.
- Around line 70-85: The code stores purposes/interests by joining with commas
which corrupts items containing commas; replace the comma-joined string storage
for Keys.USER_PURPOSES and Keys.USER_INTERESTS with a safe representation:
either (preferred if order not required) use Preferences.Set via
stringSetPreferencesKey and save the List<String> as a Set<String>, or (if order
must be preserved) JSON-serialize the List<String> (e.g., via
kotlinx.serialization/Moshi/Gson) and store the JSON string under those keys;
update the corresponding read logic to parse the Set<String> back to List or
deserialize the JSON, and change references around the context.dataStore.edit
block and the read helpers that use Keys.USER_PURPOSES / Keys.USER_INTERESTS to
use the new storage format.
In
`@data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt`:
- Around line 70-83: The log currently prints the full nickname (in
CheckNickname override and the similar logging at lines ~292-304); change those
Log.d/Log.e calls to mask PII by transforming nickname into a safe display
(e.g., show only first and/or last character and replace the rest with asterisks
or a fixed placeholder) before interpolating it into messages in checkNickname
and the other nickname-related methods; locate uses of TAG and
serverApi.withErrorHandlingRaw (and any Log.* lines that include the raw
nickname) and replace the direct nickname interpolation with the masked version
so production logs never contain the full user identifier.
- Around line 209-237: The debug logs that print user preference data (the Log.d
calls using TAG that log dto.purposes, dto.interests, displayPurposes,
displayInterests and the session save logs before calling
sessionStore.saveLogin) must be restricted to debug builds or removed for
production; update the code around the UserRepositoryImpl mapping section to
either remove those Log.d statements or wrap them in a debug-only guard (e.g.,
BuildConfig.DEBUG or Log.isLoggable) and avoid printing raw sensitive arrays,
ensuring only non-sensitive/aggregated info is logged in production.
In `@feature/mypage/src/main/java/com/example/mypage/MyPageApp.kt`:
- Line 110: Remove the leftover debug log call in MyPageApp.kt: the
android.util.Log.d("MyPageApp", "태그 데이터 확인 - Purposes: ${session.purposes},
Interests: ${session.interests}") statement should not exist in production code;
either delete this line or replace it with a conditional/production-safe logger
(e.g., use BuildConfig.DEBUG or Log.isLoggable before logging, or switch to your
app's logger like Timber and log at an appropriate level) and ensure references
to session.purposes and session.interests remain available only when guarded by
the debug check.
🧹 Nitpick comments (5)
feature/mypage/src/main/java/com/example/mypage/screen/AccountSettingScreen.kt (1)
65-66:onSubmit시그니처에jobName파라미터 추가는 적절합니다.직업 이름을 콜백으로 전달하여 UI에 즉시 반영할 수 있게 되었습니다. 다만, 주석 처리된 이전 시그니처(line 66)는 삭제하는 것이 좋습니다.
,
♻️ 주석 코드 제거 제안
onSubmit: (nickname: String, jobId: Long, jobName: String, purposes: List<String>, interests: List<String>) -> Unit - //onSubmit: (nickname: String, jobId: Long, purposes: List<String>, interests: List<String>) -> Unit ) {feature/mypage/src/main/java/com/example/mypage/MyPageApp.kt (2)
135-138: 로딩 상태에 대한 UI 피드백이 없습니다.
session.nickname이 null일 때 빈 화면만 표시됩니다. 사용자에게 데이터 로딩 중임을 알리는CircularProgressIndicator등의 피드백을 제공하는 것이 좋습니다.♻️ 로딩 인디케이터 추가 제안
} else { - // 로딩 중일 때 보여줄 화면 (잠시 빈 화면 혹은 프로그레스바) - // 아무것도 안 써두면 데이터가 올 때까지 잠깐 멈춰있다가 나타납니다. + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } }필요한 import 추가:
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier
34-37: 대량의 주석 처리된 레거시 코드가 남아있습니다.주석 처리된 코드가 여러 곳에 걸쳐 약 100줄 이상 존재합니다. Git 히스토리에서 복구 가능하므로, 코드 가독성과 유지보수성을 위해 머지 전에 삭제하는 것을 권장합니다.
Also applies to: 39-39, 72-107, 139-181, 183-214
core/src/main/java/com/example/core/repository/UserRepository.kt (1)
8-34: 매핑 상수는 전용 모델/객체로 центра라이즈 권장.
UserRepository인터페이스 파일에 하드코딩된 라벨 매핑이 들어가 있어 레이어 간 드리프트 위험이 있습니다. 공용enum/sealed class또는object로 분리해 한 곳에서 관리하는 편이 안전합니다.data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt (1)
117-120: 매핑 실패 감지를 위한 로깅/검증을 권장합니다.
알 수 없는 값이 그대로 서버에 전달되면 실패 원인 추적이 어려워집니다.♻️ 제안된 수정
- val safePurposeList = purposeList.map { purposeMap[it] ?: it } - val safeInterestList = interestList.map { interestMap[it] ?: it } + val safePurposeList = purposeList.map { purpose -> + purposeMap[purpose] ?: run { + Log.w(TAG, "알 수 없는 purpose: $purpose") + purpose + } + } + val safeInterestList = interestList.map { interest -> + interestMap[interest] ?: run { + Log.w(TAG, "알 수 없는 interest: $interest") + interest + } + }- val mappedPurposes = purposes.map { purposeMap[it] ?: it } - val mappedInterests = interests.map { interestMap[it] ?: it } + val mappedPurposes = purposes.map { purpose -> + purposeMap[purpose] ?: run { + Log.w(TAG, "알 수 없는 purpose: $purpose") + purpose + } + } + val mappedInterests = interests.map { interest -> + interestMap[interest] ?: run { + Log.w(TAG, "알 수 없는 interest: $interest") + interest + } + }Also applies to: 261-263
| val USER_ID = longPreferencesKey("user_id") // String -> Long | ||
| val USER_NICK = stringPreferencesKey("user_nickname") | ||
| val USER_EMAIL = stringPreferencesKey("user_email") | ||
| val USER_GENDER = stringPreferencesKey("user_gender") | ||
| val USER_JOB_ID = stringPreferencesKey("user_job_id") | ||
| val USER_JOB_ID = longPreferencesKey("user_job_id") // String -> Long | ||
| val USER_JOB_NAME = stringPreferencesKey("user_job_name") | ||
| val USER_MY_LINKU = stringPreferencesKey("user_my_linku") | ||
| val USER_MY_FOLDER = stringPreferencesKey("user_my_folder") | ||
| val USER_MY_AI_LINKU = stringPreferencesKey("user_my_ai_linku") | ||
| } | ||
|
|
||
| suspend fun setLoggedIn(value: Boolean) { | ||
| context.dataStore.edit { p -> p[Keys.LOGGED_IN] = value } | ||
| val USER_MY_LINKU = longPreferencesKey("user_my_linku") // String -> Long | ||
| val USER_MY_FOLDER = longPreferencesKey("user_my_folder") // String -> Long | ||
| val USER_MY_AI_LINKU = longPreferencesKey("user_my_ai_linku") // String -> Long |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "SessionStore.kt" -type fRepository: LinkYou-2025/LinkU_Android
Length of output: 131
🏁 Script executed:
cat -n ./core/src/main/java/com/example/core/session/SessionStore.kt | head -100Repository: LinkYou-2025/LinkU_Android
Length of output: 4283
🏁 Script executed:
wc -l ./core/src/main/java/com/example/core/session/SessionStore.ktRepository: LinkYou-2025/LinkU_Android
Length of output: 135
🏁 Script executed:
cat -n ./core/src/main/java/com/example/core/session/SessionStore.kt | tail -80Repository: LinkYou-2025/LinkU_Android
Length of output: 3332
String → Long 타입 변경에 대한 마이그레이션 전략이 필요합니다.
user_id, user_job_id, user_my_linku, user_my_folder, user_my_ai_linku가 String 타입에서 Long 타입으로 변경되었습니다. 기존 사용자의 데이터스토어에 문자열로 저장된 값이 있다면, 업데이트 후 longPreferencesKey로 읽을 때 타입 불일치로 인해 null이 반환되어 로그인 상태 검증이나 사용자 정보 로드가 실패할 수 있습니다.
마이그레이션 전략이 필요합니다. 예를 들어:
- 레거시 String 키에서 Long으로 자동 변환하는 로직 추가
- 새로운 키명으로 마이그레이션 후 레거시 키 정리
또한 purposes와 interests는 joinToString(",") 방식으로 저장되는데, 데이터에 쉼표가 포함되면 분할 시 손상될 수 있으므로 JSON 또는 더 안전한 포맷 사용을 검토하세요.
🤖 Prompt for AI Agents
In `@core/src/main/java/com/example/core/session/SessionStore.kt` around lines 41
- 49, Add a datastore migration that reads the legacy string keys (those created
via stringPreferencesKey) and converts them to Long into the new
longPreferencesKey entries: specifically handle USER_ID, USER_JOB_ID,
USER_MY_LINKU, USER_MY_FOLDER, USER_MY_AI_LINKU by attempting to read the old
string value, parseLong (with safe fallback), write into the new long key, and
then remove the legacy string key; implement this as part of the SessionStore
preferences migration path (where the datastore is initialized) so existing
users are upgraded on first run. Also replace the fragile joinToString(",")
storage for purposes and interests with a JSON array encoding/decoding (store as
a JSON string in the same stringPreferencesKey) to preserve values containing
commas and update read/write helpers to use JSON parse/serialize. Ensure all
changes reference the existing preference key symbols (USER_ID, USER_JOB_ID,
USER_MY_LINKU, USER_MY_FOLDER, USER_MY_AI_LINKU, and the purposes/interests
keys) and perform safe parsing with fallbacks to avoid breaking login/session
loading.
| purposes: List<String>, // 추가 - 마이페이지 수정. | ||
| interests: List<String>, | ||
| ) { | ||
| context.dataStore.edit { p -> | ||
| p[Keys.LOGGED_IN] = true | ||
| p[Keys.USER_ID] = userId.toString() | ||
| p[Keys.USER_ID] = userId | ||
| p[Keys.USER_NICK] = nickname | ||
| p[Keys.USER_EMAIL] = email | ||
| p[Keys.USER_GENDER] = gender | ||
| p[Keys.USER_JOB_ID] = jobId.toString() | ||
| p[Keys.USER_JOB_ID] = jobId | ||
| p[Keys.USER_JOB_NAME] = jobName | ||
| p[Keys.USER_MY_LINKU] = myLinku.toString() | ||
| p[Keys.USER_MY_FOLDER] = myFolder.toString() | ||
| p[Keys.USER_MY_AI_LINKU] = myAiLinku.toString() | ||
| p[Keys.USER_MY_LINKU] = myLinku | ||
| p[Keys.USER_MY_FOLDER] = myFolder | ||
| p[Keys.USER_MY_AI_LINKU] = myAiLinku | ||
| p[Keys.USER_PURPOSES] = purposes.joinToString(",") //추가 - 마이페이지 수정. | ||
| p[Keys.USER_INTERESTS] = interests.joinToString(",") |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "SessionStore.kt" -type fRepository: LinkYou-2025/LinkU_Android
Length of output: 131
🏁 Script executed:
# Get the full SessionStore.kt file to understand the structure
git ls-files | grep -i sessionstoreRepository: LinkYou-2025/LinkU_Android
Length of output: 129
🏁 Script executed:
# Search for SessionStore in the codebase
fd -t f "SessionStore" --extension ktRepository: LinkYou-2025/LinkU_Android
Length of output: 129
🏁 Script executed:
wc -l core/src/main/java/com/example/core/session/SessionStore.ktRepository: LinkYou-2025/LinkU_Android
Length of output: 133
🏁 Script executed:
cat -n core/src/main/java/com/example/core/session/SessionStore.ktRepository: LinkYou-2025/LinkU_Android
Length of output: 7148
🏁 Script executed:
# Check if stringSetPreferencesKey is mentioned anywhere in the project
rg "stringSetPreferencesKey" --type ktRepository: LinkYou-2025/LinkU_Android
Length of output: 96
🏁 Script executed:
# Check gradle or build files for DataStore version
find . -name "build.gradle*" -o -name "build.gradle.kts" | head -10Repository: LinkYou-2025/LinkU_Android
Length of output: 352
🏁 Script executed:
# Look for DataStore dependency version
rg "datastore" -A 2 build.gradle.ktsRepository: LinkYou-2025/LinkU_Android
Length of output: 52
🏁 Script executed:
cat core/build.gradle.kts | grep -A 10 -B 2 "datastore"Repository: LinkYou-2025/LinkU_Android
Length of output: 208
🏁 Script executed:
# Search for stringSetPreferencesKey in the project
rg "stringSetPreferencesKey" .Repository: LinkYou-2025/LinkU_Android
Length of output: 52
🏁 Script executed:
# Check DataStore API documentation by examining imports and available keys
rg "import androidx.datastore.preferences.core" core/src/main/java/com/example/core/session/SessionStore.ktRepository: LinkYou-2025/LinkU_Android
Length of output: 363
🏁 Script executed:
# Check what keys are available in DataStore 1.1.1
web_search "androidx.datastore:datastore-preferences 1.1.1 stringSetPreferencesKey"Repository: LinkYou-2025/LinkU_Android
Length of output: 120
🏁 Script executed:
web_search "DataStore Preferences stringSetPreferencesKey availability android"Repository: LinkYou-2025/LinkU_Android
Length of output: 120
🌐 Web query:
androidx.datastore:datastore-preferences 1.1.1 stringSetPreferencesKey
💡 Result:
In androidx.datastore:datastore-preferences:1.1.1, the key factory you want is:
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.stringSetPreferencesKey
val TAGS_KEY: Preferences.Key<Set<String>> = stringSetPreferencesKey("tags")stringSetPreferencesKey(name: String) returns a Preferences.Key<Set<String>>. The Set<String> you read back from DataStore is unmodifiable, so when updating you must write a new set (e.g., oldSet + "newValue"). [1][2]
Sources: [1] [2]
🌐 Web query:
DataStore Preferences stringSetPreferencesKey Android API
💡 Result:
stringSetPreferencesKey() is the Preferences DataStore API that creates a typed key for storing a Set<String> in DataStore<Preferences>:
- Signature:
fun stringSetPreferencesKey(name: String): Preferences.Key<Set<String>>[1] - Import:
import androidx.datastore.preferences.core.stringSetPreferencesKey[1] - Artifact:
androidx.datastore:datastore-preferences-core(also available viaandroidx.datastore:datastore-preferences) [1][2] - Notes: don’t reuse the same name for different types (can cause
ClassCastException), and the returned sets are unmodifiable (mutating them throws) [1].
Example (Kotlin)
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringSetPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.map
private val Context.dataStore by preferencesDataStore(name = "settings")
private val TAGS = stringSetPreferencesKey("tags")
suspend fun addTag(context: Context, tag: String) {
context.dataStore.edit { prefs ->
val current = prefs[TAGS].orEmpty()
prefs[TAGS] = current + tag
}
}
val tagsFlow = context.dataStore.data.map { prefs ->
prefs[TAGS].orEmpty()
}Gradle (example):
implementation "androidx.datastore:datastore-preferences:1.0.0"[2]
Sources: Android API reference for androidx.datastore.preferences.core [1] and Android Developers codelab (dependency example) [2].
[1] https://developer.android.com/reference/kotlin/androidx/datastore/preferences/core/package-summary
[2] https://developer.android.com/codelabs/android-preferences-datastore
purposes/interests를 콤마 문자열로 저장하면 데이터 손상이 가능합니다.
항목 값에 콤마가 포함되면 분해 시 원본이 깨집니다. 순서가 중요하지 않다면 stringSetPreferencesKey로 저장하고, 순서가 필요하면 JSON 직렬화 등을 권장합니다.
♻️ 제안 변경안 (순서 불필요한 경우)
-import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.core.stringSetPreferencesKey
- val USER_PURPOSES = stringPreferencesKey("user_purposes")
- val USER_INTERESTS = stringPreferencesKey("user_interests")
+ val USER_PURPOSES = stringSetPreferencesKey("user_purposes")
+ val USER_INTERESTS = stringSetPreferencesKey("user_interests")
- p[Keys.USER_PURPOSES] = purposes.joinToString(",")
- p[Keys.USER_INTERESTS] = interests.joinToString(",")
+ p[Keys.USER_PURPOSES] = purposes.toSet()
+ p[Keys.USER_INTERESTS] = interests.toSet()
- p[Keys.USER_PURPOSES] = purposes.joinToString(",")
- p[Keys.USER_INTERESTS] = interests.joinToString(",")
+ p[Keys.USER_PURPOSES] = purposes.toSet()
+ p[Keys.USER_INTERESTS] = interests.toSet()
- purposes = p[Keys.USER_PURPOSES]?.split(",")?.map { it.trim() }?.filter { it.isNotBlank() } ?: emptyList(),
- interests = p[Keys.USER_INTERESTS]?.split(",")?.map { it.trim() }?.filter { it.isNotBlank() } ?: emptyList(),
+ purposes = p[Keys.USER_PURPOSES]?.toList() ?: emptyList(),
+ interests = p[Keys.USER_INTERESTS]?.toList() ?: emptyList(),적용 위치: 84-85, 110-111, 162-163
🤖 Prompt for AI Agents
In `@core/src/main/java/com/example/core/session/SessionStore.kt` around lines 70
- 85, The code stores purposes/interests by joining with commas which corrupts
items containing commas; replace the comma-joined string storage for
Keys.USER_PURPOSES and Keys.USER_INTERESTS with a safe representation: either
(preferred if order not required) use Preferences.Set via
stringSetPreferencesKey and save the List<String> as a Set<String>, or (if order
must be preserved) JSON-serialize the List<String> (e.g., via
kotlinx.serialization/Moshi/Gson) and store the JSON string under those keys;
update the corresponding read logic to parse the Set<String> back to List or
deserialize the JSON, and change references around the context.dataStore.edit
block and the read helpers that use Keys.USER_PURPOSES / Keys.USER_INTERESTS to
use the new storage format.
| /** MyPageViewModel에서 사용방법 | ||
| * fun updateUserInfo(nickname: String, jobId: Long, jobName: String) { | ||
| * viewModelScope.launch { | ||
| * // 1. 서버 API 호출 (UserRepository) | ||
| * val isSuccess = userRepository.updateUserInfo(nickname, jobId, ...) | ||
| * | ||
| * if (isSuccess) { | ||
| * // 2. 서버 성공 시 세션 스토어만 업데이트 (이것만 하면 UI가 알아서 바뀜!) | ||
| * sessionStore.updateProfile(nickname, jobId, jobName) | ||
| * } |
There was a problem hiding this comment.
사용 예시 주석이 변경된 시그니처와 불일치합니다.
updateProfile에 purposes, interests가 추가되었는데 예시 호출에는 누락되어 혼동될 수 있습니다. 주석을 최신 시그니처에 맞춰 주세요.
✍️ 주석 수정 예시
- * sessionStore.updateProfile(nickname, jobId, jobName)
+ * sessionStore.updateProfile(nickname, jobId, jobName, purposes, interests)🤖 Prompt for AI Agents
In `@core/src/main/java/com/example/core/session/SessionStore.kt` around lines 115
- 124, Update the outdated example comment to match the new
SessionStore.updateProfile signature by adding the missing parameters `purposes`
and `interests` to the example call used in MyPageViewModel usage; locate the
example block in SessionStore.kt (the comment showing how to call
updateUserInfo/updateProfile) and modify the example invocation to include
appropriate placeholders/variables for `purposes` and `interests` so the comment
reflects the current method signature of updateProfile.
| override suspend fun checkNickname(nickname: String): Boolean { | ||
| Log.d(TAG, "[API 호출] checkNickname nickname=$nickname") | ||
|
|
||
| // 서버 메시지에 따라 사용 가능 여부 결정 | ||
| response.isSuccess == true && response.result?.contains("사용 가능") == true | ||
| } catch (e: HttpException) { | ||
| Log.e("UserRepository", " [닉네임 API 오류] code=${e.code()} msg=${e.message()}") | ||
| false | ||
| } catch (e: Exception) { | ||
| Log.e("UserRepository", " [닉네임 API 호출 실패]", e) | ||
| return try { | ||
| val response = serverApi.withErrorHandlingRaw { | ||
| checkNickname(nickname) // ApiResponseString 반환 | ||
| } | ||
| // response가 ApiResponseString이면 그에 맞게 처리 | ||
| val isAvailable = response.isSuccess == true | ||
| Log.d(TAG, "[닉네임 API 응답] 사용가능=$isAvailable") | ||
| isAvailable | ||
| } catch (e: ApiError) { | ||
| Log.e(TAG, "[닉네임 API 오류] ${e.message}") | ||
| false |
There was a problem hiding this comment.
닉네임 로그는 PII 마스킹이 필요합니다.
프로덕션 로그에 사용자 식별 정보를 그대로 남기지 않는 것이 안전합니다.
🔒 제안된 수정
- Log.d(TAG, "[API 호출] checkNickname nickname=$nickname")
+ Log.d(TAG, "[API 호출] checkNickname nicknameLength=${nickname.length}")
...
- Log.d(TAG, "닉네임=$nick")
+ Log.d(TAG, "닉네임 길이=${nick?.length ?: 0}")Also applies to: 292-304
🤖 Prompt for AI Agents
In
`@data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt`
around lines 70 - 83, The log currently prints the full nickname (in
CheckNickname override and the similar logging at lines ~292-304); change those
Log.d/Log.e calls to mask PII by transforming nickname into a safe display
(e.g., show only first and/or last character and replace the rest with asterisks
or a fixed placeholder) before interpolating it into messages in checkNickname
and the other nickname-related methods; locate uses of TAG and
serverApi.withErrorHandlingRaw (and any Log.* lines that include the raw
nickname) and replace the direct nickname interpolation with the masked version
so production logs never contain the full user identifier.
| // 📍 서버 원본 데이터 확인 | ||
| Log.d(TAG, "📍 [서버 원본] purposes: ${dto.purposes}") | ||
| Log.d(TAG, "📍 [서버 원본] interests: ${dto.interests}") | ||
|
|
||
| val dto = response.result | ||
| ?: throw IllegalStateException("마이페이지 조회 실패: ${response.message}") | ||
|
|
||
| // 서버 enum → 한글 | ||
| // 서버에서 온 ENUM(CAREER 등)을 UI용 한글("취업 커리어 준비")로 변환 | ||
| val displayPurposes = dto.purposes.map { reversePurposeMap[it] ?: it } | ||
| val displayInterests = dto.interests.map { reverseInterestMap[it] ?: it } | ||
|
|
||
| // 📍 변환 후 데이터 확인 | ||
| Log.d(TAG, "📍 [변환 후] purposes: $displayPurposes") | ||
| Log.d(TAG, "📍 [변환 후] interests: $displayInterests") | ||
|
|
||
| return UserInfo( | ||
| nickname = dto.nickName.orEmpty(), | ||
| email = dto.email, | ||
| gender = dto.gender.value, | ||
| jobId = dto.job.id, | ||
| jobName = dto.job.name, | ||
| myLinku = dto.myLinku, | ||
| myFolder = dto.myFolder, | ||
| myAiLinku = dto.myAiLinku, | ||
| purposes = displayPurposes, | ||
| nickname = dto.nickName.orEmpty(), | ||
| email = dto.email, | ||
| gender = dto.gender.value, | ||
| jobId = dto.job.id.toLong(), | ||
| jobName = dto.job.name, | ||
| myLinku = dto.myLinku.toLong(), | ||
| myFolder = dto.myFolder.toLong(), | ||
| myAiLinku = dto.myAiLinku.toLong(), | ||
| purposes = displayPurposes, | ||
| interests = displayInterests | ||
| ) | ||
| ).also { userInfo -> | ||
| // 세션을 업데이트, 지현이가 편할 수 있게 | ||
| Log.d(TAG, "📍 [세션 저장] purposes: ${userInfo.purposes}") | ||
| Log.d(TAG, "📍 [세션 저장] interests: ${userInfo.interests}") | ||
| sessionStore.saveLogin( |
There was a problem hiding this comment.
사용자 선호 정보 로깅은 디버그 전용으로 제한하세요.
선호/관심사 정보는 민감할 수 있으니 프로덕션 로그 노출을 피하는 편이 안전합니다.
🔒 제안된 수정
- Log.d(TAG, "📍 [서버 원본] purposes: ${dto.purposes}")
- Log.d(TAG, "📍 [서버 원본] interests: ${dto.interests}")
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "📍 [서버 원본] purposes: ${dto.purposes}")
+ Log.d(TAG, "📍 [서버 원본] interests: ${dto.interests}")
+ }
...
- Log.d(TAG, "📍 [변환 후] purposes: $displayPurposes")
- Log.d(TAG, "📍 [변환 후] interests: $displayInterests")
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "📍 [변환 후] purposes: $displayPurposes")
+ Log.d(TAG, "📍 [변환 후] interests: $displayInterests")
+ }
...
- Log.d(TAG, "📍 [세션 저장] purposes: ${userInfo.purposes}")
- Log.d(TAG, "📍 [세션 저장] interests: ${userInfo.interests}")
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "📍 [세션 저장] purposes: ${userInfo.purposes}")
+ Log.d(TAG, "📍 [세션 저장] interests: ${userInfo.interests}")
+ }🤖 Prompt for AI Agents
In
`@data/src/main/java/com/example/data/implementation/repository/UserRepositoryImpl.kt`
around lines 209 - 237, The debug logs that print user preference data (the
Log.d calls using TAG that log dto.purposes, dto.interests, displayPurposes,
displayInterests and the session save logs before calling
sessionStore.saveLogin) must be restricted to debug builds or removed for
production; update the code around the UserRepositoryImpl mapping section to
either remove those Log.d statements or wrap them in a debug-only guard (e.g.,
BuildConfig.DEBUG or Log.isLoggable) and avoid printing raw sensitive arrays,
ensuring only non-sensitive/aggregated info is logged in production.
| composable("account") { | ||
| ui.userInfo?.let { user -> | ||
| // nickname이나 purposes가 로드될 때까지 기다립니다. | ||
| android.util.Log.d("MyPageApp", "태그 데이터 확인 - Purposes: ${session.purposes}, Interests: ${session.interests}") |
There was a problem hiding this comment.
프로덕션 코드에 디버그 로그가 남아있습니다.
android.util.Log.d() 호출은 개발 중 디버깅용으로만 사용하고, 머지 전에 제거하거나 조건부 로깅으로 변경해야 합니다.
🗑️ 제거 제안
composable("account") {
// nickname이나 purposes가 로드될 때까지 기다립니다.
- android.util.Log.d("MyPageApp", "태그 데이터 확인 - Purposes: ${session.purposes}, Interests: ${session.interests}")
if (session.nickname != null) {🤖 Prompt for AI Agents
In `@feature/mypage/src/main/java/com/example/mypage/MyPageApp.kt` at line 110,
Remove the leftover debug log call in MyPageApp.kt: the
android.util.Log.d("MyPageApp", "태그 데이터 확인 - Purposes: ${session.purposes},
Interests: ${session.interests}") statement should not exist in production code;
either delete this line or replace it with a conditional/production-safe logger
(e.g., use BuildConfig.DEBUG or Log.isLoggable before logging, or switch to your
app's logger like Timber and log at an appropriate level) and ensure references
to session.purposes and session.interests remain available only when guarded by
the debug check.
📝 설명
✔️ PR 유형
어떤 변경 사항이 있나요?
📎 관련 이슈 번호
Summary by CodeRabbit
새로운 기능
개선 사항
UI 리소스
버그 수정