8주차 미션 / 안드로이드 2조 사예원#28
Conversation
5주차 실습 및 미션 구현
[feat]: 5주차 실습 & 미션 구현
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the 검토 요약WalkthroughAPI 레이어를 구성하고, 저장소 패턴을 구현하며, 로그인 및 이미지 업로드 기능을 추가하는 데이터-뷰모델 아키텍처 리팩토링입니다. 기존 더미 모델을 실제 네트워크 기반 모델로 교체하고, UI 상태 관리를 도입합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant LoginScreen
participant LoginViewModel
participant LoginRepository
participant ApiService
participant TokenRepository
participant TokenStorage
User->>LoginScreen: 로그인 입력 및 제출
LoginScreen->>LoginViewModel: login(context)
LoginViewModel->>LoginRepository: login(id, password)
LoginRepository->>ApiService: login(LoginRequest)
ApiService-->>LoginRepository: BaseResponse<LoginResponse>
alt 성공
LoginRepository-->>LoginViewModel: Result.Success(LoginResponse)
LoginViewModel->>TokenRepository: saveToken(context, token)
TokenRepository->>TokenStorage: DataStore 저장
TokenStorage-->>TokenRepository: 완료
LoginViewModel->>LoginScreen: uiState 업데이트 (Success)
LoginScreen->>User: 로그인 완료
else 실패
LoginRepository-->>LoginViewModel: Result.Failure(Exception)
LoginViewModel->>LoginScreen: uiState 업데이트 (Error)
LoginScreen->>User: 오류 메시지 표시
end
sequenceDiagram
participant User
participant PostListScreen
participant PostListViewModel
participant PostRepository
participant ApiService
participant AuthInterceptor
participant TokenRepository
User->>PostListScreen: 화면 로드
PostListScreen->>PostListViewModel: refresh()
PostListViewModel->>PostRepository: getPosts()
PostRepository->>ApiService: getPosts() (HTTP 요청)
ApiService->>AuthInterceptor: 요청 가로채기
AuthInterceptor->>TokenRepository: getToken(context)
TokenRepository-->>AuthInterceptor: token
AuthInterceptor->>ApiService: Authorization 헤더 추가 후 요청 진행
ApiService-->>PostRepository: BaseResponse<List<PostResponse>>
alt 성공
PostRepository-->>PostListViewModel: Result.Success(posts)
PostListViewModel->>PostListScreen: uiState = Success(posts)
PostListScreen->>User: 게시물 목록 렌더링
else 실패
PostRepository-->>PostListViewModel: Result.Failure(Exception)
PostListViewModel->>PostListScreen: uiState = Error(message)
PostListScreen->>User: 오류 메시지 표시
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 분 주의가 필요한 영역:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
Tip 📝 Customizable high-level summaries are now available in beta!You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.
Example instruction:
Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later. 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 |
@coderabbitai review |
|
@alswlekk, I'll review the changes in this PR now. ✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
.idea/deploymentTargetSelector.xml (1)
1-17: IDE 설정 파일을 저장소에서 제외하세요.
.idea/deploymentTargetSelector.xml은 Android Studio에서 자동 생성되는 설정 파일이며, 타임스탐프 변경만 포함되어 있습니다. 이 파일을 버전 제어에 포함하면 다음과 같은 문제가 발생합니다:
- 개발자마다 다른 타임스탐프로 인한 불필요한 변경 로그 및 병합 충돌
- 저장소 노이즈 증가
- 실제 소스 코드 변경과 무관한 IDE 상태 추적
이 파일과
.idea/디렉토리 내 다른 IDE 설정 파일들을.gitignore에 추가하고, 이미 커밋된 경우 다음과 같이 제거하세요:git rm --cached .idea/deploymentTargetSelector.xmlapp/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt (1)
295-328:viewModel.createPost()호출 직후 바로onPostCreated()를 호출하므로 생성 성공 여부와 무관하게 화면이 전환됩니다.
createPost()는viewModelScope.launch에서 비동기로 실행되어 API 응답을 기다리지 않고, 즉시onPostCreated()가 실행됩니다. 네트워크 요청이 진행 중이거나 실패해도 사용자가 목록 화면으로 이동하게 됩니다.
uiState가 이미 구독 중이므로, 다음과 같이 수정하여Success상태에서만 화면 전환을 처리하세요:var hasCreateRequested by remember { mutableStateOf(false) } Button( onClick = { hasCreateRequested = true viewModel.createPost(finalAuthor, request) }, ... ) LaunchedEffect(uiState, hasCreateRequested) { if (hasCreateRequested && uiState is PostCreateUiState.Success) { hasCreateRequested = false onPostCreated() } }
🧹 Nitpick comments (20)
app/src/main/java/com/example/kuit6_android_api/data/model/response/LoginResponse.kt (1)
5-10: 일관성을 위해 @SerialName 어노테이션 추가를 권장합니다.다른 응답 모델들(AuthResponse, PostCreateRequest 등)은 모두 @SerialName 어노테이션을 사용하고 있습니다. 일관성과 명시성을 위해 이 클래스에도 추가하는 것이 좋습니다.
다음 diff를 적용하세요:
@Serializable data class LoginResponse( - val token: String, - val userId: Long, - val username: String + @SerialName("token") val token: String, + @SerialName("userId") val userId: Long, + @SerialName("username") val username: String )app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/UriUtils.kt (1)
10-26: URI를 파일로 변환하는 로직이 적절합니다.리소스 관리를 위해
use블록을 사용하고 예외 처리도 포함되어 있습니다. 다만, 캐시 디렉토리에 생성된 임시 파일들이 누적될 수 있으니 주기적인 정리 메커니즘을 고려해보세요.필요시 캐시 정리 로직 추가를 고려하세요:
fun clearCache(context: Context) { context.cacheDir.listFiles()?.forEach { file -> if (file.name.startsWith("image_") && file.lastModified() < System.currentTimeMillis() - 86400000) { file.delete() } } }app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepository.kt (1)
6-20: TokenRepository 시그니처에서 Context 의존성 최소화 제안지금 인터페이스가 대부분
Context를 파라미터로 받는데, 이렇게 되면 Repository 계층이 Android 프레임워크에 강하게 결합됩니다. 가능하다면DataStore나ApplicationContext를 생성자 주입해서, 메서드 시그니처에서Context를 없애는 구조도 한 번 고려해 보시면 좋겠습니다.특히
validateToken(context: Context)는 현재 구현(TokenRepositoryImpl)에서context를 전혀 사용하지 않으므로, 실제로 사용할 계획이 없다면 파라미터를 제거하거나(또는 나중에 토큰 삭제 등에서 사용할 예정이면 주석으로 의도를 남겨두는 것)을 추천드립니다.app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepositoryImpl.kt (2)
14-17: DataStore 확장 프로퍼티/키는 구현 상세이므로 가급적 캡슐화하면 좋습니다
Context.datastore,KEY,AUTO_LOGIN_KEY가 파일 전역 public 으로 열려 있어 다른 레이어에서 직접 접근할 수 있는 상태입니다. 지금 PR 범위에서는 이 파일 안에서만 쓰이는 구현 상세이므로, 다음처럼private로 한정해 두면 추후 구조 변경 시 영향 범위를 줄이기 좋습니다.-private val Context.datastore: DataStore<Preferences> by preferencesDataStore("Token") -val KEY = stringPreferencesKey("token") -// 자동 로그인 정보 키 -val AUTO_LOGIN_KEY = booleanPreferencesKey("auto_login") +private val Context.datastore: DataStore<Preferences> by preferencesDataStore("Token") +private val KEY = stringPreferencesKey("token") +// 자동 로그인 정보 키 +private val AUTO_LOGIN_KEY = booleanPreferencesKey("auto_login")(이미 다른 파일에서
KEY/AUTO_LOGIN_KEY를 직접 참조 중이라면 그 사용처도 함께 조정이 필요합니다.)
41-72: validateToken 구현 단순화 및 Context 활용 여부 정리 제안
validateToken(context: Context)가 내부에서context를 사용하지 않고,Result<BaseResponse<Boolean>>를 다시Result<Boolean>으로 감싸는 로직이 조금 복잡하게 표현되어 있습니다.
Context 파라미터
- 현재 구현에서는
context를 전혀 사용하지 않으므로, 사용 계획이 없다면 시그니처에서 제거하는 편이 더 명확합니다.- 반대로, 토큰이 유효하지 않을 때
deleteToken(context)까지 같이 호출할 의도라면, 그 로직을 이 함수 안에 넣어 두면 호출 측에서 사용하기 더 편해집니다.Result 변환 로직 단순화
fold대신mapCatching을 사용하면 조금 더 읽기 쉬운 형태로 바꿀 수 있습니다. 예를 들면:override suspend fun validateToken(context: Context): Result<Boolean> { return getValidateTokenApi().mapCatching { baseResponse -> if (baseResponse.data == true) { true } else { throw Exception(baseResponse.message ?: "토큰 검증 실패") } } }동작은 동일하면서도 성공/실패 흐름이 한눈에 들어옵니다.
app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt (1)
72-75:"refreshNeeded"문자열을 상수로 추출하면 재사용성과 안전성이 좋아집니다
onPostCreated와onPostUpdated양쪽에서"refreshNeeded"키를 그대로 문자열로 사용하고 있어, 오타나 키 변경 시 추적이 어려울 수 있습니다. 간단히 상수로 빼 두면 관리가 수월합니다.예시:
// 이 파일 상단에 추가 private const val REFRESH_NEEDED = "refreshNeeded"그리고 사용처는 다음처럼 변경할 수 있습니다.
- ?.set("refreshNeeded", true) + ?.set(REFRESH_NEEDED, true)두 군데 모두 동일하게 적용하시면 됩니다.
Also applies to: 94-97
app/src/main/java/com/example/kuit6_android_api/App.kt (1)
6-18: App 싱글톤 및 DI 컨테이너 노출 방식 관련
App.instance는private set으로 보호되어 있는데,container는lateinit var로 외부에서 재할당이 가능합니다. DI 컨테이너는 보통 앱 생명주기 동안 불변으로 두는 편이어서,lateinit var container: AppContainer private set처럼 setter를
private으로 두거나val+ 지연 초기화를 고려해 볼 만합니다.app/build.gradle.kts (1)
23-30: BASE_URL 누락 시 조기 실패(fail-fast) 처리 권장
local.properties에BASE_URL이 없으면baseUrl이null이 되고, 현재 설정대로면BuildConfig.BASE_URL값이"null"문자열이 됩니다. 빌드는 되지만, 런타임에 이상한 URL로 호출될 수 있어 디버깅이 어려워집니다.예를 들어 다음처럼 값이 없을 때는 빌드를 실패시키거나 명시적으로 에러를 내는 걸 고려해 보세요.
val baseUrl = properties.getProperty("BASE_URL") ?: error("BASE_URL is not defined in local.properties") buildConfigField("String", "BASE_URL", "\"$baseUrl\"")이렇게 하면 설정 실수가 바로 드러나서 운영 안정성이 좋아집니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/state/UploadImageUiState.kt (1)
3-13: 이미지 업로드 성공 상태의Map<String, String>타입 개선 여지
Success에서imgUrl: Map<String, String>을 사용하는 건 유연하지만, 키 값 정의가 느슨해서 호출부에서 실수를 하기 쉽습니다.
향후 스펙이 고정적이라면,data class UploadImageResult(val url: String)같은 전용 모델 또는
typealias ImgUrlMap = Map<String, String>처럼 의미를 드러내는 래핑을 고려해 보셔도 좋겠습니다.app/src/main/java/com/example/kuit6_android_api/data/api/AuthInterceptor.kt (1)
9-28: AuthInterceptor에서 runBlocking + Context 사용 방식에 대한 코멘트현재 구현은 기능적으로는 잘 동작할 것으로 보입니다만, 두 가지 정도만 참고하시면 좋겠습니다.
Context 타입 명시
인터셉터는 보통 앱 전체에서 오래 살아 있기 때문에,Activity나Fragment컨텍스트를 넘기면 메모리 릭 위험이 있습니다. 생성 시 반드시applicationContext를 넘기도록 규약을 명시하거나, 생성자에서context.applicationContext로 한 번 감싸 두는 것도 안전합니다.runBlocking 비용
OkHttp 인터셉터는 원래 동기라runBlocking자체는 허용되지만, 매 요청마다 DataStore에서 토큰을 읽게 되면 약간의 오버헤드가 있습니다.
- DataStore가 내부 캐시를 쓰기 때문에 실제 IO 비용은 크지 않겠지만,
- 토큰이 자주 바뀌지 않는다면
TokenRepository쪽에서 메모리 캐시를 두는 식으로 최적화할 여지도 있습니다.현재 PR 범위에서는 크게 막을 정도의 이슈는 아니고, 추후 트래픽/성능 요구가 늘어날 때 고려할 수 있는 리팩토링 포인트로 보입니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/LoginScreen.kt (1)
91-91: 불필요한 trailing comma 제거Kotlin에서는 함수 호출 시 마지막 인자 뒤의 trailing comma가 불필요합니다.
-viewModel.login(context,) +viewModel.login(context) -viewModel.signup(context,) +viewModel.signup(context) -viewModel.getToken(context,) +viewModel.getToken(context)Also applies to: 96-96, 103-103
app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt (1)
7-25: 업로드/작성 API 응답 타입을 전용 모델로 통일하는 것을 고려해보세요.현재
uploadImage가Result<Map<String, String>>를 반환하고,createPost는author: String파라미터를 필수로 받습니다.
- 이미지 업로드는
Map<String, String>대신ImageUploadResponse같은 전용 응답 모델을 사용하면 키 이름 변경 등에 더 안전해지고, 사용처에서 의미를 더 명확히 표현할 수 있습니다.createPost의author는ApiService.createPost에서 기본값("규빈")을 사용하고 있어, 레포지토리/서비스 간에 작성자 처리 전략(항상 명시 전달 vs 서버 기본값 사용)을 통일해 두면 나중에 변경 시 혼란을 줄일 수 있습니다.app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt (2)
66-85: 삭제 결과 처리 시 사용자 피드백을 조금 더 보완하면 좋겠습니다.
LaunchedEffect(deleteUiState)에서 성공 시onNavigateBack()으로 잘 빠져나가지만, 실패(DeletePostUiState.Error)일 때는 로그만 남기고 UI에는 아무 변화가 없습니다. 최소한 스낵바/다이얼로그/텍스트 등으로 “삭제 실패”를 알려 주면 사용자가 재시도 여부를 판단하기 쉬울 것 같습니다.
또한 네트워크 오류 등 일시적인 실패와 서버 측 비즈니스 오류를 구분할 수 있다면, 삭제 재시도 버튼을 제공하는 것도 고려해볼 만합니다.
107-217: 상태별 UI에서 로딩/에러 표현과 Success 분기 캐스팅을 정리하면 가독성과 UX가 좋아집니다.
PostDetailUiState.Loading분기가 비어 있어, 상세 진입 시 잠깐이라도 완전히 빈 화면이 보일 수 있습니다.Box(Modifier.fillMaxSize().padding(paddingValues))안에CircularProgressIndicator()를 배치해 최소한의 로딩 표시를 해주는 편이 좋습니다.PostDetailUiState.Error에서도 현재Text("로딩 실패")만 있고padding(paddingValues)를 사용하지 않아 AppBar 아래에 겹칠 수 있습니다. Success 분기처럼Modifier.padding(paddingValues)를 적용해 레이아웃 일관성을 맞추는 것을 권장합니다.- Success 분기 내부에서
(uiState as PostDetailUiState.Success)캐스트가 여러 번 반복되고 있어 읽기 어렵습니다. 분기 시작부에서val success = uiState as PostDetailUiState.Success로 한 번만 캐스팅한 뒤success.post.xxx로 사용하는 식으로 정리하면, 코드가 더 명확해지고 향후 상태 변경 시에도 안전하게 유지보수할 수 있습니다.when (val state = uiState) { is PostDetailUiState.Success -> { val post = state.post // post.author.profileImageUrl, post.title, post.content 등 사용 } ... }app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt (1)
19-68: Retrofit 선언은 적절하지만, 업로드 응답 타입을 전용 모델로 두는 편이 더 안전합니다.전체적으로 HTTP 메서드/경로/어노테이션 조합은 자연스럽습니다. 다만
uploadImage에서BaseResponse<Map<String, String>>를 사용하는 대신, 도메인 전용 응답 DTO(ImageUploadResponse등)를 도입해 서버 스키마를 명시적으로 캡슐화하는 것을 권장합니다. 이렇게 하면 키 이름 변경이나 필드 추가가 발생했을 때, Map을 직접 다루는 코드 곳곳을 수정하는 대신 DTO를 중심으로 변경 범위를 좁힐 수 있습니다.app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt (1)
72-97: 목록 UI 상태 분기와posts스냅샷 추출 패턴이 적절하게 설계되었습니다.
when (uiState)에서PostListUiState.Success분기 안에서val posts = (uiState as PostListUiState.Success).posts로 한 번만 스냅샷을 추출하고,LazyColumn안에서는posts만 캡처하도록 분리한 방식이 좋습니다. 이전 Composition의uiState를 캡처해서ClassCastException이 나는 패턴을 피하면서도, 코드 가독성도 확보되었습니다.
다만 로딩 상태에서는CircularProgressIndicator()에Modifier.padding(paddingValues)나Box(Modifier.fillMaxSize())를 같이 써서 중앙 정렬을 해주고, 에러 상태에서도 동일하게 패딩을 적용하면 화면 전반의 일관성이 더 좋아질 것 같습니다.app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModel.kt (2)
38-62: 회원가입/로그인 실패 시 UI 상태 업데이트가 없어서 UX 가 빈약해질 수 있습니다.
signup/login에서Result의onSuccess만 처리하고onFailure에서 아무 작업도 하지 않아, 네트워크 오류나 잘못된 아이디/비밀번호 같은 실패 상황이 UI에 전혀 전달되지 않습니다.
LoginUiState에errorMessage나loginResult같은 필드를 추가해 실패 시에도_uiState.update { ... }로 상태를 갱신하고, 화면에서 토스트/다이얼로그/텍스트로 피드백을 보여주는 흐름을 붙여두면 실제 사용 환경에서 디버깅과 UX 모두 훨씬 좋아집니다.
72-131: 토큰 검증 상태 모델링을 조금 더 명확히 하면 추후 확장과 디버깅에 도움이 됩니다.현재
validateToken에서 검증 시작 시isLoading = true와 함께tokenValidationState = TokenValidationState.Initial로 되돌리고, 이후 성공/실패에 따라Success/Failure를 설정하고 있습니다.
Initial이 “앱 최초 상태”와 “검증 중 UI 전환용 임시 상태” 두 역할을 동시에 가져서, 화면 쪽 분기 로직이 복잡해질 여지가 있습니다.TokenValidationState에Loading을 추가하거나, 검증 중에는isLoading만으로 UI를 제어하고tokenValidationState는 그대로 두는 쪽이 더 표현력이 좋습니다.onFailure에서는Failure로만 두고 토큰/자동 로그인을 그대로 유지하는데, 이 경우 UI에서는 “네트워크 오류”와 “실제 토큰 만료/무효”를 동일한 Failure로 보게 됩니다. 필요하다면Failure.Network/Failure.InvalidToken처럼 세분화하거나, 최소한 에러 메시지를 함께 상태에 올려 화면에서 구분해 줄 수 있도록 하는 것도 고려해볼 만합니다.현재 로직 자체는 요구사항(유효하지 않은 토큰 시 삭제 + 자동 로그인 해제)을 잘 충족하고 있어, 위 제안은 상태 표현을 더 명확히 하려는 차원의 개선 사항입니다.
app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt (1)
9-91: 에러 로그에 throwable을 함께 넘기면 디버깅이 더 쉬워집니다.현재는
Log.e("PostRepository", error.message.toString())로 메시지만 남기고 있어 스택트레이스를 잃습니다. 아래처럼 throwable을 함께 넘기면 추후 서버/파싱 이슈 분석이 더 수월합니다.}.onFailure { error -> Log.e("PostRepository", "getPosts 실패", error) }다른 메서드(
createPost,getPostDetail,updatePost,deletePost,uploadImage)에도 동일 패턴을 적용하는 것을 고려해 보시면 좋겠습니다.app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt (1)
76-81:isLoaded를postId에 종속시키면 재사용 시 안정성이 좋아집니다.현재
var isLoaded by remember { mutableStateOf(false) }로 선언되어 있어, 같은 컴포저블 인스턴스를 다른postId로 재사용하게 되면 이전 게시글의title/content가 남아 있을 수 있습니다.아래처럼
postId에 의존하도록 기억 범위를 조정하면 더 안전합니다.var isLoaded by remember(postId) { mutableStateOf(false) }이렇게 하면 다른
postId로 진입할 때 자동으로 초기화됩니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (51)
.coderabbit.yaml(1 hunks).idea/deploymentTargetSelector.xml(1 hunks).idea/gradle.xml(1 hunks)CLAUDE.md(0 hunks)app/build.gradle.kts(4 hunks)app/src/main/AndroidManifest.xml(1 hunks)app/src/main/java/com/example/kuit6_android_api/App.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/api/ApiService.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/api/AuthInterceptor.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/model/Post.kt(0 hunks)app/src/main/java/com/example/kuit6_android_api/data/model/request/LoginRequest.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/model/request/PostCreateRequest.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/model/response/AuthResponse.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/model/response/BaseResponse.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/model/response/ImageUploadResponse.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/model/response/LoginResponse.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/model/response/PostResponse.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepository.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepositoryImpl.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepository.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/repository/PostRepositoryImpl.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepository.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/data/repository/TokenRepositoryImpl.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt(5 hunks)app/src/main/java/com/example/kuit6_android_api/ui/navigation/Routes.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/component/PostItem.kt(2 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/screen/LoginScreen.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt(5 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostDetailScreen.kt(6 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt(3 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostListScreen.kt(3 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/state/DeletePostUiState.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/state/LoginUiState.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostCreateUiState.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostDetailUiState.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostEditUiState.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostListUiState.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/state/TokenValidationState.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/state/UploadImageUiState.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModel.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModelFactory.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt(0 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModelFactory.kt(1 hunks)app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/UriUtils.kt(1 hunks)gradle/libs.versions.toml(3 hunks)
💤 Files with no reviewable changes (3)
- app/src/main/java/com/example/kuit6_android_api/data/model/Post.kt
- app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModel.kt
- CLAUDE.md
🔇 Additional comments (36)
app/src/main/java/com/example/kuit6_android_api/data/model/response/AuthResponse.kt (1)
6-11: LGTM!직렬화 어노테이션이 올바르게 적용되어 있고, 필드 타입도 적절합니다.
app/src/main/java/com/example/kuit6_android_api/data/model/request/PostCreateRequest.kt (1)
6-11: LGTM!요청 모델이 올바르게 구성되어 있으며, 선택적 imageUrl 필드도 적절합니다.
app/src/main/java/com/example/kuit6_android_api/data/model/request/LoginRequest.kt (1)
6-10: LGTM!로그인 요청 모델이 올바르게 정의되어 있습니다.
app/src/main/java/com/example/kuit6_android_api/data/model/response/ImageUploadResponse.kt (1)
6-9: LGTM!이미지 업로드 응답 모델이 올바르게 구성되어 있습니다.
.idea/gradle.xml (1)
12-13: LGTM!Gradle 모듈 경로가 프로젝트 루트를 기준으로 올바르게 설정되었습니다.
app/src/main/java/com/example/kuit6_android_api/data/model/response/BaseResponse.kt (1)
6-12: LGTM!제네릭 타입을 사용한 API 응답 래퍼가 잘 설계되었습니다. 모든 필드에 적절한 직렬화 어노테이션이 적용되어 있으며, timestamp의 기본값 처리도 적절합니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/state/TokenValidationState.kt (1)
3-7: 깔끔한 상태 패턴 구현입니다.토큰 검증의 세 가지 상태를 명확하게 표현하고 있으며, sealed class와 object를 적절히 사용했습니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/state/LoginUiState.kt (1)
3-11: 로그인 UI 상태가 잘 구성되어 있습니다.토큰 검증 상태와 로딩 상태를 포함하여 자동 로그인 기능에 필요한 모든 필드가 적절하게 정의되었습니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostEditUiState.kt (1)
5-15: 일관된 UI 상태 패턴이 잘 적용되었습니다.Loading, Success, Error 상태가 명확하게 정의되었고, Success 상태에서 PostResponse를 반환하는 것이 적절합니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/UriUtils.kt (1)
28-40: 파일명 추출 로직이 안전하게 구현되었습니다.ContentResolver 쿼리와 커서 처리가 적절하며, nameIndex 검증도 포함되어 있습니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostDetailUiState.kt (1)
5-15: 포스트 상세 화면의 UI 상태가 잘 정의되었습니다.다른 UI 상태 클래스들과 일관된 패턴을 따르고 있어 코드베이스 전체의 일관성이 유지됩니다.
app/src/main/java/com/example/kuit6_android_api/data/model/response/PostResponse.kt (1)
6-15: API 응답 모델이 올바르게 정의되었습니다.모든 필드에
@SerialName어노테이션이 적용되었고, nullable 필드(imageUrl)도 적절하게 처리되었습니다.app/src/main/java/com/example/kuit6_android_api/ui/post/component/PostItem.kt (2)
32-33: 응답 모델 타입으로의 마이그레이션이 깔끔하게 완료되었습니다.
Post에서PostResponse로의 타입 변경이 일관되게 적용되었고, 컴포넌트의 동작에는 영향이 없습니다.Also applies to: 38-38
156-164: 프리뷰가 새로운 응답 모델에 맞게 업데이트되었습니다.
PostResponse와AuthorResponse를 사용하여 프리뷰 데이터를 생성하고 있으며, 모든 필수 필드가 포함되어 있습니다.app/src/main/AndroidManifest.xml (1)
16-27: 커스텀 Application 등록 확인
android:name=".App"으로 커스텀Application을 등록한 부분은 구성 상 자연해 보입니다. 다만 다음 두 가지는 한 번만 확인해 주세요.
App클래스가android.app.Application(또는 그 하위 타입, 예: Hilt 사용 시HiltAndroidApp기반 클래스) 를 상속하고 있는지AndroidManifest.xml의package와App클래스의 패키지(com.example.kuit6_android_api.App) 가 일치하는지문제 없다면 이 변경 자체는 그대로 사용하셔도 될 것 같습니다.
app/src/main/java/com/example/kuit6_android_api/ui/navigation/NavGraph.kt (1)
4-20: 네비게이션 + ViewModel 팩토리 연결 구조 괜찮습니다
postViewModelFactory/loginViewModelFactory를 이용해 각 Route 별로 필요한 Repository 를 주입하는 패턴이 잘 맞춰져 있고,onLoginClick으로LoginRoute로 네비게이션하는 흐름도 자연스럽습니다. 현재 구조 기준으로는 특별히 막히는 부분은 없어 보입니다.Also applies to: 31-44
app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostListUiState.kt (1)
5-15: PostListUiState 설계는 깔끔합니다 (data object 사용 시 Kotlin 버전만 확인)
Loading / Success / Error로 나눈sealed class구조가 UI 상태 표현에 잘 맞고,Loading을data object로 선언한 것도 일관성 면에서 좋습니다.다만
data object는 비교적 최신 Kotlin 버전(1.9+ 이후)에서 지원되는 기능이므로, 프로젝트의 Kotlin 플러그인 버전이 이를 지원하는지 한 번만 확인해 주세요. 만약 호환성 문제가 있다면data object Loading을 그냥object Loading으로 바꾸는 것만으로도 동일한 용도로 사용 가능합니다.app/src/main/java/com/example/kuit6_android_api/ui/navigation/Routes.kt (1)
5-18: 타입 세이프 네비게이션용 Route 정의 적절
PostEditRoute와LoginRoute모두@Serializable로 처리되어 있고, 기존 패턴(object/data class)과 일관성 있게 잘 맞습니다. 네이밍과 구조도 직관적이라 유지보수에 무리 없을 것 같습니다.app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostViewModelFactory.kt (1)
10-19: ViewModelFactory에서 App 캐스팅 시 매니페스트 설정 확인 필요
this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as App캐스팅은AndroidManifest.xml에android:name=".App"이 제대로 등록되어 있을 때만 안전합니다.
해당 설정만 확인되면,postRepository를 DI하는 이 패턴은 재사용성과 테스트 용이성 측면에서 잘 잡힌 것 같습니다.app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostCreateUiState.kt (1)
5-15: PostCreate UI 상태 모델링 깔끔함
Loading / Success(PostResponse) / Error(message)로 나눈sealed class구조가 직관적이고, ViewModel–UI 간 상태 전달에도 무리가 없어 보입니다. 이후 에러 타입이 늘어나면Error를 서브타입으로 확장하는 것도 자연스러울 것 같습니다.app/build.gradle.kts (1)
81-93: DataStore / Retrofit / OkHttp 의존성 추가 방향 적절토큰 저장/로딩(DataStore)과 API 통신(Retrofit + OkHttp + logging interceptor)을 위해 필요한 의존성 구성이 잘 되어 있습니다. 나중에 네트워크 레벨 테스트를 위해서는 OkHttp mockWebServer 같은 테스트 의존성 추가도 고려해 볼 수 있을 것 같습니다.
app/src/main/java/com/example/kuit6_android_api/data/di/AppContainer.kt (1)
12-27: 간단한 수동 DI 컨테이너 구성 잘 되어 있음
RetrofitClient.apiService를 기준으로PostRepository,LoginRepository,TokenRepository를 lazy로 주입하는 구조가 명료하고,App에서 한 번만 생성해 앱 전역으로 쓰기에도 적절해 보입니다.
향후 의존성이 더 늘어나면 모듈별로 분리하거나 DI 프레임워크(Hilt 등)로 이전하는 것도 자연스러운 다음 단계일 것 같습니다.app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepository.kt (1)
5-15: 인터페이스 정의가 깔끔합니다.저장소 패턴을 잘 따르고 있으며,
Result타입을 사용한 에러 핸들링과 suspend 함수 사용이 적절합니다.app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepositoryImpl.kt (1)
15-26: Result 타입을 사용한 에러 핸들링이 적절합니다.
runCatching과Result타입을 사용한 에러 핸들링 패턴이 좋습니다. 응답 검증 로직도 명확합니다.app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt (1)
34-37: Kotlinx Serialization 설정이 적절합니다.
ignoreUnknownKeys와coerceInputValues설정으로 서버 응답의 유연한 처리가 가능합니다.app/src/main/java/com/example/kuit6_android_api/ui/post/screen/LoginScreen.kt (2)
45-47: 자동 로그인 초기화 로직이 적절합니다.
LaunchedEffect(Unit)을 사용하여 화면 진입 시 한 번만 자동 로그인을 초기화하는 패턴이 올바릅니다.
38-42: 토큰 검증 상태에 따른 버튼 텍스트 업데이트가 잘 구현되었습니다.
when표현식을 사용한 상태별 텍스트 분기 처리가 명확합니다.app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/LoginViewModelFactory.kt (1)
11-22: ViewModel Factory 구현이 우수합니다.
reified타입 파라미터와inline함수를 사용한 제네릭 팩토리 패턴이 매우 깔끔합니다. AppContainer에서 의존성을 주입받는 구조도 적절합니다.app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostCreateViewModel.kt (3)
15-24: UI 상태 관리가 잘 구조화되어 있습니다.게시글 작성과 이미지 업로드를 별도의 StateFlow로 분리하여 관리하는 것이 좋은 설계입니다.
asStateFlow()를 통한 읽기 전용 노출도 적절합니다.
27-45: 게시글 작성 로직이 견고합니다.로딩 상태 설정, 저장소 호출, 성공/실패 처리가 모두 적절하게 구현되어 있습니다.
48-64: 이미지 업로드 에러 핸들링이 적절합니다.
MultipartBody.Part를 사용한 파일 업로드와 에러 메시지 전달이 잘 구현되어 있습니다.app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostListViewModel.kt (3)
12-21: ViewModel 초기화 패턴이 우수합니다.
init블록에서 데이터를 로드하는 패턴이 적절하며, StateFlow를 사용한 상태 관리가 깔끔합니다.
23-36: 게시글 목록 로드 로직이 견고합니다.로딩 상태 관리와 에러 핸들링이 PostCreateViewModel과 일관된 패턴을 따르고 있어 좋습니다.
38-40: refresh 기능 제공이 좋습니다.사용자가 수동으로 목록을 새로고침할 수 있는 기능을 제공하는 것이 좋은 UX입니다.
app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostDetailViewModel.kt (1)
13-40: Post 상세 로딩/삭제 StateFlow 설계가 명확합니다.
Result→PostDetailUiState/DeletePostUiState로 바로 매핑하고,viewModelScope로 생명주기도 잘 맞춰서 관리하고 있어 특별한 문제는 없어 보입니다.app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt (1)
15-82: 수정/조회/이미지 업로드용 UiState 분리가 잘 되어 있습니다.
editPost,getPostDetail,uploadImage각각을 코루틴 +Result→ UiState로 매핑하는 구조가 일관적이고, ViewModel 책임 범위도 잘 맞춰져 있어 그대로 사용해도 무방해 보입니다.
| version: 1 | ||
|
|
||
| reviews: | ||
| auto_review: true # 자동 리뷰 활성화 |
There was a problem hiding this comment.
설정 구문 오류를 수정하세요.
auto_review는 boolean 값이 아닌 객체여야 합니다. 현재 설정으로는 파싱 오류가 발생하여 CodeRabbit이 작동하지 않습니다.
다음 diff를 적용하여 수정하세요:
reviews:
- auto_review: true # 자동 리뷰 활성화
+ auto_review:
+ enabled: true # 자동 리뷰 활성화
+ auto_incremental_review: true # 증분 리뷰 활성화
review_status: true # 체크 메시지 표시📝 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.
| auto_review: true # 자동 리뷰 활성화 | |
| reviews: | |
| auto_review: | |
| enabled: true # 자동 리뷰 활성화 | |
| auto_incremental_review: true # 증분 리뷰 활성화 | |
| review_status: true # 체크 메시지 표시 |
🤖 Prompt for AI Agents
In .coderabbit.yaml around line 4, the auto_review key is currently a bare
boolean which causes a parsing error; change it to an object structure (for
example include an "enabled" key and any other expected options) instead of
true/false, and ensure proper YAML indentation and formatting so the file parses
(replace "auto_review: true" with an object mapping like auto_review: { enabled:
true, ... } or equivalent multi-line mapping).
| triggers: | ||
| pull_request: true # PR 생성 시 자동 리뷰 | ||
| pull_request_branches: # 어떤 브랜치든지 허용 | ||
| include: | ||
| - "*" # 모든 브랜치 | ||
| push: false | ||
| pull_request_review: false |
There was a problem hiding this comment.
triggers 설정은 auto_review 객체 내부로 이동해야 합니다.
스키마에 따르면 triggers 관련 설정(pull_request, push 등)은 더 이상 최상위 속성이 아닙니다. 대신 auto_review 객체 내의 base_branches와 drafts 속성을 사용하거나 제거해야 합니다.
다음과 같이 수정하세요:
auto_review:
enabled: true # 자동 리뷰 활성화
auto_incremental_review: true
+ base_branches:
+ - ".*" # 모든 브랜치에서 자동 리뷰
+ drafts: false # 드래프트 PR은 제외
review_status: true
max_review_comments: 50
-
- # 모든 브랜치 PR에 대해 자동으로 리뷰
- triggers:
- pull_request: true # PR 생성 시 자동 리뷰
- pull_request_branches: # 어떤 브랜치든지 허용
- include:
- - "*" # 모든 브랜치
- push: false
- pull_request_review: false📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| triggers: | |
| pull_request: true # PR 생성 시 자동 리뷰 | |
| pull_request_branches: # 어떤 브랜치든지 허용 | |
| include: | |
| - "*" # 모든 브랜치 | |
| push: false | |
| pull_request_review: false | |
| auto_review: | |
| enabled: true # 자동 리뷰 활성화 | |
| auto_incremental_review: true | |
| base_branches: | |
| - ".*" # 모든 브랜치에서 자동 리뷰 | |
| drafts: false # 드래프트 PR은 제외 | |
| review_status: true | |
| max_review_comments: 50 |
🤖 Prompt for AI Agents
.coderabbit.yaml around lines 9 to 15: the current top-level triggers block must
be moved into the auto_review object per schema; remove the top-level triggers
and instead add equivalent settings under auto_review using base_branches (to
represent branch inclusion patterns) and drafts (if draft PR handling is
required), or drop unsupported keys; ensure the resulting auto_review object
contains base_branches: ["*"] (or the intended branch list) and any
draft-related flags, and validate the YAML against the schema to confirm no
remaining top-level trigger keys.
| @SuppressLint("StaticFieldLeak") | ||
| private val authInterceptor = AuthInterceptor( | ||
| App.instance.applicationContext, | ||
| TokenRepositoryImpl() | ||
| ) |
There was a problem hiding this comment.
아키텍처: TokenRepositoryImpl의 직접 인스턴스화 문제
AuthInterceptor에서 TokenRepositoryImpl()을 직접 생성하면 다음 문제가 발생합니다:
- AppContainer의 tokenRepository와 다른 인스턴스가 생성됨
- 싱글톤이 보장되지 않아 상태 불일치 가능
- 의존성 주입 원칙 위반
AppContainer에서 공유 인스턴스를 주입받도록 수정하세요:
+// RetrofitClient를 object에서 class로 변경하고 AppContainer에서 초기화
-object RetrofitClient {
+class RetrofitClient(tokenRepository: TokenRepository) {
private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
- @SuppressLint("StaticFieldLeak")
- private val authInterceptor = AuthInterceptor(
- App.instance.applicationContext,
- TokenRepositoryImpl()
- )
+ private val authInterceptor = AuthInterceptor(
+ App.instance.applicationContext,
+ tokenRepository
+ )그리고 AppContainer에서:
val tokenRepository: TokenRepository by lazy { TokenRepositoryImpl() }
val retrofitClient by lazy { RetrofitClient(tokenRepository) }
val apiService: ApiService by lazy { retrofitClient.apiService }🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/data/api/RetrofitClient.kt
around lines 20 to 24, you're directly instantiating TokenRepositoryImpl for the
AuthInterceptor which causes multiple instances and breaks DI/singleton
guarantees; change RetrofitClient to accept a TokenRepository (interface) via
constructor and use that instance to create AuthInterceptor instead of new
TokenRepositoryImpl(), then update AppContainer to provide a single
TokenRepository by lazy and to construct RetrofitClient(tokenRepository) so the
shared AppContainer instance is injected everywhere.
| }.onFailure { error -> | ||
| Log.e("LoginRepository", error.message.toString()) | ||
| } |
There was a problem hiding this comment.
보안: 에러 메시지 로깅 시 민감 정보 노출 위험
error.message를 직접 로깅하면 사용자 ID, 비밀번호 등의 민감한 정보가 로그에 포함될 수 있습니다. 프로덕션 빌드에서는 민감 정보를 제거하거나 로깅을 비활성화해야 합니다.
다음과 같이 수정하는 것을 권장합니다:
}.onFailure { error ->
- Log.e("LoginRepository", error.message.toString())
+ Log.e("LoginRepository", "Signup failed", error)
}📝 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.
| }.onFailure { error -> | |
| Log.e("LoginRepository", error.message.toString()) | |
| } | |
| }.onFailure { error -> | |
| Log.e("LoginRepository", "Signup failed", error) | |
| } |
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepositoryImpl.kt
around lines 23 to 25, the current onFailure handler logs error.message directly
which can leak sensitive data; replace this by not logging raw error messages in
production: log a sanitized/generic message (e.g., "Login failed") or the error
type only, and conditionally emit detailed logs only for debug builds
(BuildConfig.DEBUG) or route full error details to a secure internal
error-collection system instead of standard logs; ensure no user identifiers,
passwords, or full exception messages are written to logcat in release builds.
| }.onFailure { error -> | ||
| Log.e("LoginRepository", error.message.toString()) | ||
| } |
There was a problem hiding this comment.
보안: 에러 메시지 로깅 시 민감 정보 노출 위험
signup 함수와 동일한 보안 이슈가 있습니다. 에러 메시지에 민감 정보가 포함될 수 있습니다.
다음과 같이 수정하세요:
}.onFailure { error ->
- Log.e("LoginRepository", error.message.toString())
+ Log.e("LoginRepository", "Login failed", error)
}📝 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.
| }.onFailure { error -> | |
| Log.e("LoginRepository", error.message.toString()) | |
| } | |
| }.onFailure { error -> | |
| Log.e("LoginRepository", "Login failed", error) | |
| } |
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/data/repository/LoginRepositoryImpl.kt
around lines 40 to 42, the current onFailure block logs error.message directly
which may expose sensitive information; replace direct error.message logging
with a generic log entry (e.g., "Login failed") and, if you need the full error
for diagnostics, send it to a secure telemetry/crash-reporting system instead of
standard logs; ensure you handle null safely (avoid toString() on a potentially
null message) and remove any sensitive fields from what you report.
| Text("회원가입") | ||
| } | ||
| } | ||
| Text("토큰 : ${uiState.token}") |
There was a problem hiding this comment.
보안: UI에 토큰 값 노출
액세스 토큰을 UI에 직접 표시하면 보안 위험이 있습니다:
- 스크린샷/화면 녹화로 토큰 유출 가능
- 어깨 너머 공격(shoulder surfing)에 취약
- 디버그 목적이라면 프로덕션 빌드에서 제거 필요
프로덕션에서는 이 줄을 제거하거나 디버그 빌드에서만 표시하도록 수정하세요:
-Text("토큰 : ${uiState.token}")
+// Debug only - remove in production
+if (BuildConfig.DEBUG) {
+ Text("토큰 : ${uiState.token?.take(10)}...") // 일부만 표시
+}📝 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.
| Text("토큰 : ${uiState.token}") | |
| // Debug only - remove in production | |
| if (BuildConfig.DEBUG) { | |
| Text("토큰 : ${uiState.token?.take(10)}...") // 일부만 표시 | |
| } |
🤖 Prompt for AI Agents
In app/src/main/java/com/example/kuit6_android_api/ui/post/screen/LoginScreen.kt
around line 101, the UI is directly displaying the access token which exposes
sensitive credentials; remove this Text line from production and instead
conditionally show it only in debug builds (e.g., guard with BuildConfig.DEBUG)
or replace it with a non-sensitive placeholder/masked value for UI display and
log sensitive details only to secure debug logs not shipped in release APKs.
| // 업로드된 이미지 미리보기 | ||
| if (selectedImageUri != null && imgUiState !is UploadImageUiState.Loading) { | ||
| Spacer(modifier = Modifier.height(12.dp)) | ||
| Box( | ||
| modifier = Modifier.fillMaxWidth() | ||
| ) { | ||
| AsyncImage( | ||
| model = selectedImageUri, | ||
| contentDescription = "선택된 이미지", | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .height(200.dp) | ||
| .clip(RoundedCornerShape(12.dp)), | ||
| contentScale = ContentScale.Crop | ||
| ) | ||
|
|
||
| IconButton( | ||
| onClick = { | ||
| selectedImageUri = null | ||
| viewModel.clearUploadedImageUrl() | ||
| }, | ||
| modifier = Modifier | ||
| .align(Alignment.TopEnd) | ||
| .padding(8.dp) | ||
| ) { | ||
| Icon( | ||
| Icons.Default.Close, | ||
| contentDescription = "이미지 제거", | ||
| tint = MaterialTheme.colorScheme.error | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
이미지를 제거해도 uploadedImageUrl이 유지되어 서버로 전송될 수 있습니다.
이미지 미리보기의 닫기 버튼에서:
onClick = {
selectedImageUri = null
viewModel.clearUploadedImageUrl()
}만 호출하고 있어, uploadedImageUrl(로컬 상태)은 여전히 이전 업로드 결과 값을 유지합니다. 이후 사용자가 이미지를 제거한 상태에서 "작성하기"를 누르면, 의도와 달리 제거된 이미지 URL이 서버로 함께 전송됩니다.
아래처럼 로컬 상태도 함께 초기화해 주는 것이 안전합니다.
- onClick = {
- selectedImageUri = null
- viewModel.clearUploadedImageUrl()
- },
+ onClick = {
+ selectedImageUri = null
+ uploadedImageUrl = null
+ viewModel.clearUploadedImageUrl()
+ },이렇게 하면 UI/서버 양쪽에서 "이미지 제거" 동작이 일치하게 됩니다.
🏁 Script executed:
#!/bin/bash
# Find PostCreateScreen.kt and examine uploadedImageUrl state management
fd -e kt -name "PostCreateScreen.kt" | head -5Length of output: 230
🏁 Script executed:
#!/bin/bash
# Search for uploadedImageUrl variable in PostCreateScreen
rg -n "uploadedImageUrl" --type=kt -C5 app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.ktLength of output: 26
🏁 Script executed:
#!/bin/bash
# Find createPost function and understand the request payload
rg -n "createPost|uploadedImageUrl" --type=kt app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt -A3 -B3Length of output: 26
🏁 Script executed:
#!/bin/bash
# Check clearUploadedImageUrl implementation in the ViewModel
rg -n "clearUploadedImageUrl" --type=kt -A5 -B2 app/src/main/java/com/example/kuit6_android_api/Length of output: 26
🏁 Script executed:
#!/bin/bash
# Look at the complete IconButton onClick handler and surrounding context
rg -n "Icons.Default.Close|contentDescription.*이미지 제거" --type=kt -A10 -B10 app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.ktLength of output: 26
🏁 Script executed:
#!/bin/bash
# Find where onPostCreated is called and understand the flow after createPost
rg -n "onPostCreated|createPost\(" --type=kt -C7 app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.ktLength of output: 26
🏁 Script executed:
#!/bin/bash
# Compare with PostEditScreen to see if similar pattern exists there
fd -e kt -name "PostEditScreen.kt" | xargs rg -n "uploadedImageUrl|clearUploadedImageUrl" -C5Length of output: 18474
이미지를 제거해도 uploadedImageUrl이 유지되어 서버로 전송될 수 있습니다.
이미지 미리보기의 닫기 버튼에서 uploadedImageUrl(로컬 상태)을 초기화하지 않고 있습니다. PostEditScreen과 달리 selectedImageUri = null만 호출하고 있어, 사용자가 이미지를 제거한 상태에서 "작성하기"를 누르면 의도와 달리 제거된 이미지 URL이 서버로 함께 전송됩니다(라인 302에서 imageUrl = uploadedImageUrl 사용).
아래처럼 로컬 상태도 함께 초기화해 주는 것이 필요합니다.
IconButton(
onClick = {
selectedImageUri = null
+ uploadedImageUrl = null
viewModel.clearUploadedImageUrl()
},이렇게 하면 PostEditScreen(라인 220)과 일치하게 되며, UI/서버 양쪽에서 "이미지 제거" 동작이 일치하게 됩니다.
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostCreateScreen.kt
around lines 257 to 288, the image close button only clears selectedImageUri but
does not clear the uploaded image URL state, so a removed image can still be
submitted; update the IconButton onClick to also clear the uploaded image URL
(call the ViewModel clearUploadedImageUrl() and/or set any local
uploadedImageUrl variable to null) so the local UI and ViewModel state match
PostEditScreen and a removed image is not sent to the server.
| // 수정 버튼 | ||
| Button( | ||
| onClick = { | ||
| val finalImageUrl = uploadedImageUrl | ||
| val request = PostCreateRequest( | ||
| title = title, | ||
| content = content, | ||
| imageUrl = finalImageUrl | ||
| ) | ||
| // 수정 버튼 누를 시 PostEditViewModel의 editPost() 호출 | ||
| viewModel.editPost(postId, request) | ||
| onPostUpdated() | ||
| }, | ||
| modifier = Modifier | ||
| .fillMaxWidth() | ||
| .height(56.dp), | ||
| enabled = title.isNotBlank() && content.isNotBlank() && imgUiState !is UploadImageUiState.Loading, | ||
| shape = RoundedCornerShape(12.dp), | ||
| colors = ButtonDefaults.outlinedButtonColors( | ||
| contentColor = MaterialTheme.colorScheme.primary | ||
| colors = ButtonDefaults.buttonColors( | ||
| containerColor = MaterialTheme.colorScheme.primary | ||
| ) | ||
| ) { | ||
| Text("갤러리에서 이미지 선택") | ||
| if (imgUiState is UploadImageUiState.Loading) { | ||
| Row( | ||
| verticalAlignment = Alignment.CenterVertically, | ||
| horizontalArrangement = Arrangement.Center | ||
| ) { | ||
| CircularProgressIndicator( | ||
| modifier = Modifier.size(20.dp), | ||
| color = MaterialTheme.colorScheme.onPrimary | ||
| ) | ||
| Spacer(modifier = Modifier.width(8.dp)) | ||
| Text("업로드 중...") | ||
| } | ||
| } else { | ||
| Text("수정하기", fontWeight = FontWeight.Bold) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
수정 버튼이 API 성공 여부와 무관하게 바로 onPostUpdated()를 호출합니다.
현재는 viewModel.editPost(...) 직후 곧바로 onPostUpdated()를 호출하고 있어,
- 수정 API가 실패해도 이전 화면으로 돌아가 버리고,
- 네트워크가 느린 경우에도 성공 여부를 확인하기 전에 화면이 닫힐 수 있습니다.
수정 결과가 Success일 때만 내비게이션/콜백을 트리거하도록 UiState를 관찰하는 쪽이 더 안전합니다. 예를 들어(개념 예시):
var hasEditRequested by remember { mutableStateOf(false) }
Button(
onClick = {
hasEditRequested = true
viewModel.editPost(postId, request)
},
...
)
// editPost 호출 이후에만 Success를 감지해서 콜백 실행
LaunchedEffect(uiState, hasEditRequested) {
if (hasEditRequested && uiState is PostEditUiState.Success) {
hasEditRequested = false
onPostUpdated()
}
}이렇게 하면 최초 상세 조회용 Success와 실제 수정 완료 Success를 구분할 수 있고, 실패 시에는 화면을 유지한 채 에러 UI를 보여줄 여지도 생깁니다.
검증을 위해 코드베이스를 탐색하겠습니다.
---
🏁 Script executed:
```shell
#!/bin/bash
# Find PostEditViewModel file
fd -type f -name "*PostEditViewModel*"
Length of output: 170
🏁 Script executed:
#!/bin/bash
# Examine PostEditScreen.kt to understand the complete context around the button
cat app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.ktLength of output: 12221
🏁 Script executed:
#!/bin/bash
# Search for PostEditUiState definition
fd -type f -name "*.kt" | xargs rg -l "PostEditUiState" -A 5 -B 5Length of output: 419
---
🏁 Script executed:
```shell
#!/bin/bash
# Examine PostEditViewModel.kt
cat app/src/main/java/com/example/kuit6_android_api/ui/post/viewmodel/PostEditViewModel.kt
Length of output: 3055
🏁 Script executed:
#!/bin/bash
# Examine PostEditUiState.kt
cat app/src/main/java/com/example/kuit6_android_api/ui/post/state/PostEditUiState.ktLength of output: 359
수정 버튼이 API 성공 여부와 무관하게 바로 onPostUpdated()를 호출합니다.
viewModel.editPost(postId, request)는 비동기 코루틴이지만, 다음 줄의 onPostUpdated()가 즉시 실행되고 있습니다.
실제 API 응답을 기다리지 않으므로:
- 수정 API가 실패해도 이전 화면으로 돌아가 버리고,
- 네트워크가 느린 경우에도 성공 여부를 확인하기 전에 화면이 닫힐 수 있습니다.
수정 결과가 Success일 때만 내비게이션/콜백을 트리거하는 것이 필요합니다. 예를 들어:
var hasEditRequested by remember { mutableStateOf(false) }
Button(
onClick = {
hasEditRequested = true
viewModel.editPost(postId, request)
},
...
)
LaunchedEffect(uiState, hasEditRequested) {
if (hasEditRequested && uiState is PostEditUiState.Success) {
hasEditRequested = false
onPostUpdated()
}
}추가로 isLoaded 플래그도 postId 변경 시 초기화되도록 개선하세요:
- 현재:
remember { mutableStateOf(false) } - 개선:
remember(postId) { mutableStateOf(false) }
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/screen/PostEditScreen.kt
around lines 248-285, the Button currently calls onPostUpdated() immediately
after viewModel.editPost(...), which triggers navigation regardless of the async
API result; change the flow so the button only requests the edit and sets a
local "edit requested" flag (e.g., hasEditRequested = true) then call
viewModel.editPost(postId, request) without invoking onPostUpdated(); add a
LaunchedEffect (or observe uiState) that watches hasEditRequested and the
ViewModel's uiState and calls onPostUpdated() only when hasEditRequested is true
and uiState indicates Success (resetting hasEditRequested afterward on both
success and error as appropriate); also update the isLoaded state initialization
to remember(postId) { mutableStateOf(false) } so it resets when postId changes.
| data class Success( | ||
| val deletePost : Unit | ||
| ) : DeletePostUiState() |
There was a problem hiding this comment.
Success 상태에서 Unit 타입을 저장하는 것은 불필요합니다.
삭제 작업의 성공은 의미 있는 데이터를 반환하지 않으므로, data class 대신 data object를 사용하는 것이 더 적절합니다.
다음과 같이 수정하세요:
- data class Success(
- val deletePost : Unit
- ) : DeletePostUiState()
+ data object Success : DeletePostUiState()📝 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.
| data class Success( | |
| val deletePost : Unit | |
| ) : DeletePostUiState() | |
| data object Success : DeletePostUiState() |
🤖 Prompt for AI Agents
In
app/src/main/java/com/example/kuit6_android_api/ui/post/state/DeletePostUiState.kt
around lines 6 to 8, the Success variant currently stores a Unit property
unnecessarily; change it from a data class with a Unit field to a singleton
object (remove the deletePost property and declare Success as an object) so that
success is represented without carrying meaningless data.
|
|
||
| # Coroutines | ||
| coroutines = "1.9.0" | ||
| datastorePreferences = "1.2.0-rc01" |
There was a problem hiding this comment.
🧩 Analysis chain
DataStore Preferences의 RC 버전 사용을 확인하세요.
1.2.0-rc01은 릴리스 후보(RC) 버전입니다. 프로덕션 환경에서는 안정(stable) 버전 사용을 권장합니다.
최신 안정 버전을 확인하려면 다음을 검색해 주세요:
🌐 Web query:
What is the latest stable version of androidx.datastore datastore-preferences?
💡 Result:
The latest stable release is androidx.datastore:datastore-preferences (stable) version 1.1.7. [1]
Sources:
[1] Android Developers — Jetpack DataStore release notes (shows stable release 1.1.7).
DataStore Preferences RC 버전을 안정 버전으로 업데이트하세요.
최신 안정 버전은 1.1.7입니다. 현재 설정된 1.2.0-rc01은 릴리스 후보 버전으로, datastorePreferences = "1.1.7"로 변경하여 프로덕션 환경에서 안정성을 확보하시기 바랍니다.
🤖 Prompt for AI Agents
In gradle/libs.versions.toml around line 32, the datastorePreferences entry is
set to the release-candidate version "1.2.0-rc01"; update it to the stable
production version by changing the value to "1.1.7" so the file reads
datastorePreferences = "1.1.7".
📝 미션
미션: 자동 로그인 구현 및 토큰 검증
🙋 구현에 대한 설명
미션 구현 설명
[Interceptor]
[TokenRepositoryImpl]
[LoginUiState]
[LoginViewModel]
[LoginScreen]
📷 스크린샷 & 실행영상
8.mp4
🎸 기타
Summary by CodeRabbit
Release Notes
✏️ Tip: You can customize this high-level summary in your review settings.