-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/#72 : 로그인 뷰모델, ui 일부 수정 #74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
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/AutoLoginState 기반 로그인 상태 머신 도입, 중앙화된 API 오류·토큰 재발급 유틸(withAuth/ApiError) 추가, LoginApp·EmailLoginScreen 네비게이션/콜백 재배치, SearchBarTopSheet·TopBar 등 디자인·리소스·하단시트 리팩터링 포함. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant EmailLoginScreen as EmailLoginScreen
participant LoginViewModel as LoginViewModel
participant UserRepository as UserRepository
participant ServerApi as ServerApi
participant AuthPreference as AuthPreference
participant MainApp as MainApp
User->>EmailLoginScreen: 로그인 시도(이메일, 비밀번호)
EmailLoginScreen->>LoginViewModel: login(email, password)
activate LoginViewModel
LoginViewModel->>UserRepository: login API 호출 (withErrorHandling)
activate UserRepository
UserRepository->>ServerApi: 서버 요청
ServerApi-->>UserRepository: BaseResponse<LoginResult>
UserRepository-->>LoginViewModel: LoginResult(accessToken, refreshToken, userId)
deactivate UserRepository
LoginViewModel->>AuthPreference: saveTokens(accessToken, refreshToken, userId)
LoginViewModel-->>EmailLoginScreen: loginState = Success
deactivate LoginViewModel
EmailLoginScreen->>MainApp: onLoginSuccess() 호출 (네비게이션)
MainApp->>ServerApi: pending-share 소비 등 (withAuth)
MainApp->>AuthPreference: 토큰 확인/사용
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested Reviewers
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
주석에 오타가 있습니다.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ 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
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
구현 난이도가 있어서 고려해보겠습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rate Limit Exceeded
@KateteDeveloper have exceeded the limit for the number of chat messages per hour. Please wait 2 minutes and 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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()})") }
| // 닉네임만 수정할 때 -> TODO : 지현이에게 전달 | ||
| suspend fun updateNickname(nickname: String) { | ||
| val current = session.first() | ||
| context.dataStore.edit { p -> p[Keys.USER_NICK] = nickname } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사용되지 않는 변수 current
updateNickname에서 session.first()를 호출하지만 반환값을 사용하지 않습니다. 불필요한 I/O 호출입니다.
🔧 수정 제안
// 닉네임만 수정할 때 -> TODO : 지현이에게 전달
suspend fun updateNickname(nickname: String) {
- val current = session.first()
context.dataStore.edit { p -> p[Keys.USER_NICK] = nickname }
}📝 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.
| // 닉네임만 수정할 때 -> TODO : 지현이에게 전달 | |
| suspend fun updateNickname(nickname: String) { | |
| val current = session.first() | |
| context.dataStore.edit { p -> p[Keys.USER_NICK] = nickname } | |
| } | |
| // 닉네임만 수정할 때 -> TODO : 지현이에게 전달 | |
| suspend fun updateNickname(nickname: String) { | |
| context.dataStore.edit { p -> p[Keys.USER_NICK] = nickname } | |
| } |
🤖 Prompt for AI Agents
In `@core/src/main/java/com/example/core/session/SessionStore.kt` around lines 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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
userId = response.userId?.toInt() ?: throw IllegalStateException("로그인 응답에 userId가 누락되었습니다."), 으로 수정하겠습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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로 변경해주세요.
|
|
||
| // 새 토큰 저장 | ||
| authPreference.accessToken = newTokens.accessToken | ||
| authPreference.refreshToken = newTokens.refreshToken | ||
| val userId = authPreference.userId ?: -1L | ||
| val userInfo = userRepository.getUserInfo(userId) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
userId가 null일 때 -1L을 사용하면 API 호출에서 문제가 발생할 수 있습니다.
authPreference.userId가 null인 경우 -1L로 대체하여 getUserInfo(-1L)을 호출하게 됩니다. 이는 서버에서 예상치 못한 에러를 발생시킬 수 있습니다. 명시적으로 early return 처리를 권장합니다.
🛠️ 수정 제안
- val userId = authPreference.userId ?: -1L
- val userInfo = userRepository.getUserInfo(userId)
+ val userId = authPreference.userId
+ if (userId == null) {
+ Log.e(TAG, "자동 로그인 실패: userId가 null")
+ _autoLoginState.value = AutoLoginState.Failed
+ onFail()
+ return@launch
+ }
+ val userInfo = userRepository.getUserInfo(userId)🤖 Prompt for AI Agents
In `@feature/login/src/main/java/com/example/login/viewmodel/LoginViewModel.kt`
around lines 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가 전달되지 않게 수정하세요.
📝 설명
✔️ PR 유형
어떤 변경 사항이 있나요?
📎 관련 이슈 번호
Summary by CodeRabbit
새로운 기능
개선 사항
UI 리소스
버그 수정