[FEAT/#10] 앱에 서버 연결하기 - #13
Conversation
📝 WalkthroughWalkthrough앱 시작 시 Changes서버 연동과 화면 전환
Sequence Diagram(s)sequenceDiagram
participant MainActivity
participant GundamdexHomeViewModel
participant GundamRepositoryImpl
participant GundamApi
MainActivity->>GundamdexHomeViewModel: viewModels<GundamdexHomeViewModel>()
GundamdexHomeViewModel->>GundamRepositoryImpl: getGundamList()
GundamRepositoryImpl->>GundamApi: getGundams()
GundamApi-->>GundamRepositoryImpl: Response<List<GundamDto>>
GundamRepositoryImpl-->>GundamdexHomeViewModel: Result<List<GundamInfo>>
sequenceDiagram
participant DetailRoute
participant GundamdexDetailViewModel
participant GundamRepositoryImpl
participant GundamApi
DetailRoute->>GundamdexDetailViewModel: Factory(id)
GundamdexDetailViewModel->>GundamRepositoryImpl: getGundamDetail(id)
GundamRepositoryImpl->>GundamApi: getGundamDetail(idFilter)
GundamApi-->>GundamRepositoryImpl: Response<List<GundamDto>>
GundamRepositoryImpl-->>GundamdexDetailViewModel: Result<List<GundamInfo>>
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 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.
Actionable comments posted: 6
🧹 Nitpick comments (6)
app/src/main/java/com/example/gundamdexapp/MainActivity.kt (1)
18-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value사용되지 않는
gundamRepository변수입니다.
application.appContainer.gundamRepository를 조회하지만 이후 코드에서 전혀 사용되지 않습니다.GundamdexHomeViewModel.Factory가APPLICATION_KEY를 통해 저장소를 직접 획득하므로 이 두 줄은 죽은 코드입니다. 제거를 권장합니다.♻️ 제안 수정
- val application = application as GundamApplication - val gundamRepository = application.appContainer.gundamRepository - val gundamdexHomeViewModel by viewModels<GundamdexHomeViewModel> { GundamdexHomeViewModel.Factory }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/com/example/gundamdexapp/MainActivity.kt` around lines 18 - 19, The `MainActivity` setup contains dead code: `application.appContainer.gundamRepository` is retrieved into `gundamRepository` but never used because `GundamdexHomeViewModel.Factory` already resolves the repository via `APPLICATION_KEY`. Remove the unused `gundamRepository` assignment from `MainActivity` and keep the factory initialization focused on the application reference only.app/src/main/java/com/example/gundamdexapp/feature/navigation/GundamdexNavigation.kt (1)
29-29: 🚀 Performance & Scalability | 🔵 Trivial
collectAsState대신collectAsStateWithLifecycle를 사용하세요.
libs.versions.toml에서androidx-lifecycle-runtime-compose의존성이 이미 존재합니다.collectAsStateWithLifecycle를 사용하면 Composable 이 생명주기STARTED상태가 아닐 때 불필요한 상태 수집을 중단하여 성능을 최적화할 수 있습니다.- val uiState by gundamdexHomeViewModel.uiState.collectAsState() + val uiState by gundamdexHomeViewModel.uiState.collectAsStateWithLifecycle()🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/com/example/gundamdexapp/feature/navigation/GundamdexNavigation.kt` at line 29, The state collection in GundamdexNavigation should use lifecycle-aware collection instead of collectAsState. Update the uiState collection on GundamdexHomeViewModel to use collectAsStateWithLifecycle so the Composable only collects while the lifecycle is STARTED. Make sure the existing androidx.lifecycle.runtime.compose import/dependency is used and the change is applied at the uiState collection site in GundamdexNavigation.app/src/main/java/com/example/gundamdexapp/data/network/repositoryImpl/GundamRepositoryImpl.kt (1)
10-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win중복된 응답 처리 로직 추출 권장
getGundamList와getGundamDetail의Response→Result변환 로직(성공 시toDomain()매핑, 실패 시 코드 포함 예외,catch처리)이 거의 동일합니다. 공통 헬퍼로 추출하면 중복이 제거되고 에러 메시지 일관성도 유지됩니다.♻️ 공통 헬퍼 추출 예시
+ private inline fun <T, R> Response<List<T>>.toDomainResult( + transform: (T) -> R, + ): Result<List<R>> = + if (isSuccessful) { + Result.success((body() ?: emptyList()).map(transform)) + } else { + Result.failure(Exception("서버 통신 실패 : ${code()}")) + } + - override suspend fun getGundamList(): Result<List<GundamInfo>> = try { - val response = api.getGundams() - - if (response.isSuccessful) { - val dtoList = response.body() ?: emptyList() - - val domainList = dtoList.map { it.toDomain() } - Result.success(domainList) - } else { - Result.failure(Exception("서버 통신 실패 : ${response.code()}")) - } - } catch (e: Exception) { - Result.failure(e) - } + override suspend fun getGundamList(): Result<List<GundamInfo>> = try { + api.getGundams().toDomainResult { it.toDomain() } + } catch (e: Exception) { + Result.failure(e) + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/com/example/gundamdexapp/data/network/repositoryImpl/GundamRepositoryImpl.kt` around lines 10 - 38, `GundamRepositoryImpl`의 `getGundamList`와 `getGundamDetail`에서 `Response`를 `Result`로 바꾸는 로직이 중복되어 있습니다. 공통 처리용 private 헬퍼를 추출해 `api.getGundams()`와 `api.getGundamDetail()`의 성공 시 `toDomain()` 매핑, 실패 시 코드 포함 예외 생성, `catch`에서 `Result.failure` 반환을 한곳에서 처리하도록 정리하세요. 이렇게 하면 두 메서드는 API 호출과 파라미터(idFilter)만 남기고, 에러 메시지 형식도 `GundamRepositoryImpl` 전체에서 일관되게 유지할 수 있습니다.gradle/libs.versions.toml (1)
30-52: 📐 Maintainability & Code Quality | 🔵 TrivialSupabase BOM 의존성명 구체화 권장
postgrest-kt는platform(libs.bom)을 통해 버전이 관리되므로 빌드 오류는 없습니다. 다만,libs.versions.toml내bom별칭이 모호하여supabase-bom등으로 구체화하면 가독성이 향상됩니다.BOM 설정 확인
app/build.gradle.kts implementation(platform(libs.bom)) implementation(libs.postgrest.kt)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@gradle/libs.versions.toml` around lines 30 - 52, The `bom` alias in `libs.versions.toml` is too generic and should be renamed to a more descriptive Supabase-specific name. Update the version catalog entry for the Supabase BOM and any references that use `libs.bom` so the identifier clearly indicates it is the Supabase BOM, improving readability without changing dependency management behavior.app/src/main/java/com/example/gundamdexapp/domain/repository/GundamRepository.kt (1)
7-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win상세 조회가 단건임에도
List를 반환하여 도메인 추상화가 전송 계층 형태에 종속됩니다.
getGundamDetail은 단일 항목 조회인데Result<List<GundamInfo>>를 반환하고, 소비자(GundamdexDetailViewModel)는getOrNull()?.firstOrNull()로 매번 첫 요소만 꺼내 씁니다. Supabase가 배열을 돌려주는 것은 구현 세부사항이므로, 도메인 인터페이스는Result<GundamInfo>(또는 nullable)로 단건 의미를 표현해 변환 책임을 구현체에 두는 편이 클린 아키텍처에 부합합니다.♻️ 제안 수정
interface GundamRepository { suspend fun getGundamList(): Result<List<GundamInfo>> - suspend fun getGundamDetail(id: String): Result<List<GundamInfo>> + suspend fun getGundamDetail(id: String): Result<GundamInfo> }
GundamRepositoryImpl.getGundamDetail와GundamdexDetailViewModel도 함께 수정해야 합니다. 도메인 로직과 UI 로직 분리를 강조하는 경로 지침에 근거합니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/com/example/gundamdexapp/domain/repository/GundamRepository.kt` at line 7, `GundamRepository.getGundamDetail` is modeling a single-detail lookup as `Result<List<GundamInfo>>`, which leaks the transport shape into the domain API. Change the repository contract to return a single `GundamInfo` result (or nullable) and move any list-to-single conversion into `GundamRepositoryImpl.getGundamDetail`. Then update `GundamdexDetailViewModel` to consume the single item directly instead of calling `getOrNull()?.firstOrNull()`.Source: Path instructions
app/src/main/java/com/example/gundamdexapp/data/network/dto/GundamDto.kt (1)
65-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
MISSING_URLfallback에 외부 CDN URL을 하드코딩하고 있습니다.앱과 무관한 외부 서비스(BladeNSoul) CDN URL을 placeholder로 사용하면 해당 리소스가 변경/삭제될 경우 이미지가 깨질 수 있습니다. 앱 번들 내 로컬 placeholder 리소스나 자체 호스팅 자산을 사용하는 편이 안정적입니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/com/example/gundamdexapp/data/network/dto/GundamDto.kt` around lines 65 - 66, The MISSING_URL fallback in GundamDto should not hardcode an external BladeNSoul CDN URL. Replace it with a stable app-owned placeholder source, such as a bundled drawable/resource or a self-hosted asset, and update the constant in GundamDto so any missing image paths resolve to that local placeholder instead of the external URL.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/main/java/com/example/gundamdexapp/data/network/dto/ArmamentDto.kt`:
- Around line 13-19: `ArmamentDto`의 `indicatorColor`가 non-null이라
`indicator_color`가 누락되거나 null일 때 `toDomain()` 전에 역직렬화가 실패합니다. `GundamDto`와
`details` 처리처럼 `indicatorColor`를 nullable로 바꾸고 `toDomain()`에서 기본값으로 fallback하도록
수정해, `ArmamentDto` 및 `ArmamentDto.toDomain()` 경로가 서버 응답 결측에 안전하도록 만드세요.
In
`@app/src/main/java/com/example/gundamdexapp/data/network/repositoryImpl/GundamRepositoryImpl.kt`:
- Around line 25-35: `GundamRepositoryImpl.getGundamDetail` is returning an
empty list as a successful result, but the current flow later turns that into
`null` and triggers `IllegalArgumentException` in `setUiState`. Update the
repository/UI contract so empty detail responses are handled explicitly as a
normal state (for example, a failure result or a dedicated empty-content UI
state) instead of relying on `throw` in the UI layer. Check the
`getGundamDetail` repository method and the `setUiState` logic that consumes its
result, and make the state transition consistent with the intended behavior.
In `@app/src/main/java/com/example/gundamdexapp/data/network/SupabaseNetwork.kt`:
- Around line 14-20: The current HttpLoggingInterceptor setup in SupabaseNetwork
is logging full headers at BODY level, which can expose apikey and Authorization
values in release builds. Update the logging configuration in the
loggingInterceptor/okHttpClient setup so BODY logging is enabled only for debug
builds and disabled or removed for release builds, using the build-type check at
the point where the OkHttpClient.Builder is configured.
In
`@app/src/main/java/com/example/gundamdexapp/feature/detail/GundamdexDetailViewModel.kt`:
- Around line 29-34: The GundamdexDetailViewModel init flow currently lets a
null detail result from getGundamDetail(id) reach setUiState, which throws and
can crash the app. Update GundamdexDetailViewModel so setUiState and the init
launch path handle missing data gracefully by mapping null or failed fetches
into GundamdexDetailUiState error/empty state instead of throwing
IllegalArgumentException. Keep the behavior localized around getGundamDetail,
viewModelScope.launch, and setUiState so the UI can render a fallback state when
the detail is unavailable.
In
`@app/src/main/java/com/example/gundamdexapp/feature/detail/uimodel/IndicatorColor.kt`:
- Around line 10-15: `IndicatorColor.dataToIndicatorColor` is throwing on any
server value outside the current hardcoded set, which can crash
`GundamdexDetailViewModel` during DTO-to-UI mapping. Update
`dataToIndicatorColor` to normalize the input (for example, case-insensitive
handling and accepting `"gray"` as well as `"grey"`), and replace the exception
path with a safe fallback color in `IndicatorColor` for unknown or new server
values.
In
`@app/src/main/java/com/example/gundamdexapp/feature/home/GundamdexHomeViewModel.kt`:
- Around line 26-43: `GundamdexHomeViewModel.getGundamList()` currently unwraps
the repository `Result` with `getOrThrow()` inside `viewModelScope.launch`, so
any Supabase/network failure will crash the coroutine and app. Update the flow
to handle failures explicitly in `getGundamList()` by branching with
`onSuccess`/`onFailure` (or equivalent try/catch), and use
`GundamdexHomeUiState`/`GundamdexHomeUiModel` to represent loading and error
states so the UI can react safely instead of propagating the exception.
---
Nitpick comments:
In `@app/src/main/java/com/example/gundamdexapp/data/network/dto/GundamDto.kt`:
- Around line 65-66: The MISSING_URL fallback in GundamDto should not hardcode
an external BladeNSoul CDN URL. Replace it with a stable app-owned placeholder
source, such as a bundled drawable/resource or a self-hosted asset, and update
the constant in GundamDto so any missing image paths resolve to that local
placeholder instead of the external URL.
In
`@app/src/main/java/com/example/gundamdexapp/data/network/repositoryImpl/GundamRepositoryImpl.kt`:
- Around line 10-38: `GundamRepositoryImpl`의 `getGundamList`와
`getGundamDetail`에서 `Response`를 `Result`로 바꾸는 로직이 중복되어 있습니다. 공통 처리용 private 헬퍼를
추출해 `api.getGundams()`와 `api.getGundamDetail()`의 성공 시 `toDomain()` 매핑, 실패 시 코드
포함 예외 생성, `catch`에서 `Result.failure` 반환을 한곳에서 처리하도록 정리하세요. 이렇게 하면 두 메서드는 API 호출과
파라미터(idFilter)만 남기고, 에러 메시지 형식도 `GundamRepositoryImpl` 전체에서 일관되게 유지할 수 있습니다.
In
`@app/src/main/java/com/example/gundamdexapp/domain/repository/GundamRepository.kt`:
- Line 7: `GundamRepository.getGundamDetail` is modeling a single-detail lookup
as `Result<List<GundamInfo>>`, which leaks the transport shape into the domain
API. Change the repository contract to return a single `GundamInfo` result (or
nullable) and move any list-to-single conversion into
`GundamRepositoryImpl.getGundamDetail`. Then update `GundamdexDetailViewModel`
to consume the single item directly instead of calling
`getOrNull()?.firstOrNull()`.
In
`@app/src/main/java/com/example/gundamdexapp/feature/navigation/GundamdexNavigation.kt`:
- Line 29: The state collection in GundamdexNavigation should use
lifecycle-aware collection instead of collectAsState. Update the uiState
collection on GundamdexHomeViewModel to use collectAsStateWithLifecycle so the
Composable only collects while the lifecycle is STARTED. Make sure the existing
androidx.lifecycle.runtime.compose import/dependency is used and the change is
applied at the uiState collection site in GundamdexNavigation.
In `@app/src/main/java/com/example/gundamdexapp/MainActivity.kt`:
- Around line 18-19: The `MainActivity` setup contains dead code:
`application.appContainer.gundamRepository` is retrieved into `gundamRepository`
but never used because `GundamdexHomeViewModel.Factory` already resolves the
repository via `APPLICATION_KEY`. Remove the unused `gundamRepository`
assignment from `MainActivity` and keep the factory initialization focused on
the application reference only.
In `@gradle/libs.versions.toml`:
- Around line 30-52: The `bom` alias in `libs.versions.toml` is too generic and
should be renamed to a more descriptive Supabase-specific name. Update the
version catalog entry for the Supabase BOM and any references that use
`libs.bom` so the identifier clearly indicates it is the Supabase BOM, improving
readability without changing dependency management behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 92b65ed3-e3cd-491e-96f2-b7829ce51299
⛔ Files ignored due to path filters (1)
app/build.gradle.ktsis excluded by!**/*.gradle.kts
📒 Files selected for processing (19)
app/src/main/AndroidManifest.xmlapp/src/main/java/com/example/gundamdexapp/GundamApplication.ktapp/src/main/java/com/example/gundamdexapp/MainActivity.ktapp/src/main/java/com/example/gundamdexapp/data/network/AppContainer.ktapp/src/main/java/com/example/gundamdexapp/data/network/SupabaseNetwork.ktapp/src/main/java/com/example/gundamdexapp/data/network/api/GundamApi.ktapp/src/main/java/com/example/gundamdexapp/data/network/dto/ArmamentDto.ktapp/src/main/java/com/example/gundamdexapp/data/network/dto/GundamDto.ktapp/src/main/java/com/example/gundamdexapp/data/network/repositoryImpl/GundamRepositoryImpl.ktapp/src/main/java/com/example/gundamdexapp/domain/repository/GundamRepository.ktapp/src/main/java/com/example/gundamdexapp/feature/detail/GundamdexDetailStateHolder.ktapp/src/main/java/com/example/gundamdexapp/feature/detail/GundamdexDetailViewModel.ktapp/src/main/java/com/example/gundamdexapp/feature/detail/mapper/IndicatorColorMapper.ktapp/src/main/java/com/example/gundamdexapp/feature/detail/uimodel/IndicatorColor.ktapp/src/main/java/com/example/gundamdexapp/feature/home/GundamdexHomeStatHolder.ktapp/src/main/java/com/example/gundamdexapp/feature/home/GundamdexHomeViewModel.ktapp/src/main/java/com/example/gundamdexapp/feature/navigation/GundamdexNavigation.ktapp/src/main/java/com/example/gundamdexapp/feature/utils/SharedTransitionKey.ktgradle/libs.versions.toml
💤 Files with no reviewable changes (2)
- app/src/main/java/com/example/gundamdexapp/feature/home/GundamdexHomeStatHolder.kt
- app/src/main/java/com/example/gundamdexapp/feature/detail/GundamdexDetailStateHolder.kt
| @SerialName("indicator_color") | ||
| val indicatorColor: String, | ||
| ) { | ||
| fun toDomain(): Armament = Armament( | ||
| name = this.name, | ||
| details = this.details ?: "", | ||
| indicatorColor = this.indicatorColor, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
indicatorColor 누락/null 시 역직렬화가 실패할 수 있습니다.
indicatorColor가 non-null이고 기본값도 없어, 서버 응답에서 indicator_color가 누락되거나 null이면 kotlinx.serialization이 예외를 던져 목록 조회 전체가 실패합니다. GundamDto의 다른 필드(및 details)처럼 nullable + fallback로 방어하는 것이 일관적입니다.
🛡️ 제안 수정
`@SerialName`("indicator_color")
- val indicatorColor: String,
+ val indicatorColor: String? = null,
) {
fun toDomain(): Armament = Armament(
name = this.name,
details = this.details ?: "",
- indicatorColor = this.indicatorColor,
+ indicatorColor = this.indicatorColor ?: "",
)
}📝 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.
| @SerialName("indicator_color") | |
| val indicatorColor: String, | |
| ) { | |
| fun toDomain(): Armament = Armament( | |
| name = this.name, | |
| details = this.details ?: "", | |
| indicatorColor = this.indicatorColor, | |
| `@SerialName`("indicator_color") | |
| val indicatorColor: String? = null, | |
| ) { | |
| fun toDomain(): Armament = Armament( | |
| name = this.name, | |
| details = this.details ?: "", | |
| indicatorColor = this.indicatorColor ?: "", |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/com/example/gundamdexapp/data/network/dto/ArmamentDto.kt`
around lines 13 - 19, `ArmamentDto`의 `indicatorColor`가 non-null이라
`indicator_color`가 누락되거나 null일 때 `toDomain()` 전에 역직렬화가 실패합니다. `GundamDto`와
`details` 처리처럼 `indicatorColor`를 nullable로 바꾸고 `toDomain()`에서 기본값으로 fallback하도록
수정해, `ArmamentDto` 및 `ArmamentDto.toDomain()` 경로가 서버 응답 결측에 안전하도록 만드세요.
| override suspend fun getGundamDetail(id: String): Result<List<GundamInfo>> = try { | ||
| val response = api.getGundamDetail(idFilter = "eq.$id") | ||
|
|
||
| if (response.isSuccessful) { | ||
| val gundamDto = response.body() ?: emptyList() | ||
|
|
||
| val domainList = gundamDto.map { it.toDomain() } | ||
| Result.success(domainList) | ||
| } else { | ||
| Result.failure(Exception("서버 통신 실패 : ${response.code()}")) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 상세 ViewModel에서 getGundamDetail 결과 처리 방식 확인
rg -nP 'getGundamDetail|\.first\(|\[0\]|getOrNull' --type=kotlin -C3 app/src/main/java/com/example/gundamdexapp/feature/detailRepository: noeyhoj/Gundamdex
Length of output: 1572
빈 결과 처리 로직 상세 설명
getGundamDetail 구현은 빈 리스트에 대해 firstOrNull() 을 사용하므로 NoSuchElementException 은 발생하지 않습니다. 다만, 빈 데이터를 null 로 전달받아 setUiState 내부에서 IllegalArgumentException 를 던지는 방식이 적용되어 있습니다.
상세 조회 API 가 존재하지 않는 경우 빈 리스트를 성공 응답으로 반환할 때, 이를 IllegalArgumentException 로 처리하는 것이 의도한 동작인지 확인해 주세요. UI 레이어에서 throw 를 사용하여 제어 흐름을 처리하는 것보다 정상적인 UiState 상태 전환 (예: 에러 상태 또는 빈 콘텐츠) 으로 처리하는 것이 클린 아키텍처 관점에서 권장됩니다.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app/src/main/java/com/example/gundamdexapp/data/network/repositoryImpl/GundamRepositoryImpl.kt`
around lines 25 - 35, `GundamRepositoryImpl.getGundamDetail` is returning an
empty list as a successful result, but the current flow later turns that into
`null` and triggers `IllegalArgumentException` in `setUiState`. Update the
repository/UI contract so empty detail responses are handled explicitly as a
normal state (for example, a failure result or a dedicated empty-content UI
state) instead of relying on `throw` in the UI layer. Check the
`getGundamDetail` repository method and the `setUiState` logic that consumes its
result, and make the state transition consistent with the intended behavior.
| private val loggingInterceptor = HttpLoggingInterceptor().apply { | ||
| level = HttpLoggingInterceptor.Level.BODY | ||
| } | ||
|
|
||
| private val okHttpClient = OkHttpClient.Builder() | ||
| .addInterceptor(loggingInterceptor) | ||
| .build() |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major
🧩 Analysis chain
🌐 Web query:
Does OkHttp HttpLoggingInterceptor Level.BODY log request headers like Authorization?
💡 Result:
Yes, the OkHttp HttpLoggingInterceptor at Level.BODY will log request headers, including sensitive ones like Authorization, by default [1][2][3]. Because the BODY and HEADERS levels log all request and response headers, they pose a risk of leaking sensitive information such as authentication tokens, cookies, or other private data [2][4][5]. OkHttp explicitly warns that these levels should only be used in controlled or non-production environments to avoid accidental exposure [2][4]. To prevent sensitive headers from being logged, you must explicitly redact them using the redactHeader method [2][4][5]: logging.redactHeader("Authorization"); logging.redactHeader("Cookie"); Once redacted, the interceptor will replace the header's value in the logs with a placeholder (typically "██") rather than printing the actual sensitive data [2][5].
Citations:
- 1: HttpLoggingInterceptor leaks Authorization Header lysine-dev/okhttp#3826
- 2: https://github.com/square/okhttp/blob/master/okhttp-logging-interceptor/README.md
- 3: https://square.github.io/okhttp/3.x/logging-interceptor/okhttp3/logging/HttpLoggingInterceptor.Level.html
- 4: https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor
- 5: https://blog.mindorks.com/how-to-enable-logging-in-okhttp
릴리스 빌드에서 인증 헤더가 Logcat에 노출될 위험이 있습니다.
HttpLoggingInterceptor.Level.BODY는 요청 및 응답 헤더 전체를 기록합니다. 코드에서 전송하는 apikey나 Authorization 헤더가 로그에 그대로 노출되며, 이는 보안 위험입니다.
해당 로깅 레벨을 디버그 빌드에서만 적용하고 릴리스 빌드에서는 logs 를 끄도록 수정해야 합니다.
🔒 제안 수정
+import com.example.gundamdexapp.BuildConfig
import okhttp3.logging.HttpLoggingInterceptor
import okhttp3.OkHttpClient
object SupabaseNetwork {
private val loggingInterceptor = HttpLoggingInterceptor().apply {
- level = HttpLoggingInterceptor.Level.BODY
+ level = if (BuildConfig.DEBUG) {
+ HttpLoggingInterceptor.Level.BODY
+ } else {
+ HttpLoggingInterceptor.Level.NONE
+ }
+
+ // 디버그 모드에서도 민감 헤더는 암호화하여 노출 방지 (선택적)
+ redactHeader("Authorization")
+ redactHeader("apikey")
}
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)📝 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.
| private val loggingInterceptor = HttpLoggingInterceptor().apply { | |
| level = HttpLoggingInterceptor.Level.BODY | |
| } | |
| private val okHttpClient = OkHttpClient.Builder() | |
| .addInterceptor(loggingInterceptor) | |
| .build() | |
| import com.example.gundamdexapp.BuildConfig | |
| import okhttp3.logging.HttpLoggingInterceptor | |
| import okhttp3.OkHttpClient | |
| object SupabaseNetwork { | |
| private val loggingInterceptor = HttpLoggingInterceptor().apply { | |
| level = if (BuildConfig.DEBUG) { | |
| HttpLoggingInterceptor.Level.BODY | |
| } else { | |
| HttpLoggingInterceptor.Level.NONE | |
| } | |
| // 디버그 모드에서도 민감 헤더는 암호화하여 노출 방지 (선택적) | |
| redactHeader("Authorization") | |
| redactHeader("apikey") | |
| } | |
| private val okHttpClient = OkHttpClient.Builder() | |
| .addInterceptor(loggingInterceptor) | |
| .build() | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/main/java/com/example/gundamdexapp/data/network/SupabaseNetwork.kt`
around lines 14 - 20, The current HttpLoggingInterceptor setup in
SupabaseNetwork is logging full headers at BODY level, which can expose apikey
and Authorization values in release builds. Update the logging configuration in
the loggingInterceptor/okHttpClient setup so BODY logging is enabled only for
debug builds and disabled or removed for release builds, using the build-type
check at the point where the OkHttpClient.Builder is configured.
| init { | ||
| viewModelScope.launch { | ||
| val gundamInfo = getGundamDetail(id) | ||
| setUiState(gundamInfo) | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
상세 정보가 없을 때 예외를 던져 앱이 크래시됩니다.
init에서 viewModelScope.launch로 getGundamDetail을 호출하고, 결과가 null이면 setUiState가 IllegalArgumentException을 던집니다. 이 예외는 코루틴에서 처리되지 않아 앱이 종료됩니다. 실제 서버에서 해당 id의 데이터가 없거나 호출이 실패(getOrNull()이 null 반환)하는 경우는 정상적인 시나리오이므로, 예외 대신 GundamdexDetailUiState에 에러/빈 상태를 두어 UI에서 처리하도록 권장합니다.
Also applies to: 79-79
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app/src/main/java/com/example/gundamdexapp/feature/detail/GundamdexDetailViewModel.kt`
around lines 29 - 34, The GundamdexDetailViewModel init flow currently lets a
null detail result from getGundamDetail(id) reach setUiState, which throws and
can crash the app. Update GundamdexDetailViewModel so setUiState and the init
launch path handle missing data gracefully by mapping null or failed fetches
into GundamdexDetailUiState error/empty state instead of throwing
IllegalArgumentException. Keep the behavior localized around getGundamDetail,
viewModelScope.launch, and setUiState so the UI can render a fallback state when
the detail is unavailable.
| fun dataToIndicatorColor(value: String): IndicatorColor = when (value) { | ||
| "red" -> RED | ||
| "blue" -> BLUE | ||
| "gray" -> GRAY | ||
| "grey" -> GREY | ||
| else -> throw IllegalArgumentException("잘못된 indicator color 입력 값을 전달받았습니다.") | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
알 수 없는 서버 값에 대해 예외를 던져 크래시될 수 있습니다.
dataToIndicatorColor는 "red"/"blue"/"grey" 외의 값에 대해 IllegalArgumentException을 던집니다. 이번 PR로 실제 서버 응답을 매핑하게 되는데(ArmamentDto.indicatorColor는 자유 형식 String), 서버가 "gray"(미국식 표기)나 대소문자가 다른 값, 신규 색상을 보내면 GundamdexDetailViewModel의 매핑 시점에서 예외가 전파되어 앱이 종료됩니다.
대소문자 정규화 및 기본값(fallback) 처리를 권장합니다.
🛡️ 예시 방향
- fun dataToIndicatorColor(value: String): IndicatorColor = when (value) {
- "red" -> RED
- "blue" -> BLUE
- "grey" -> GREY
- else -> throw IllegalArgumentException("잘못된 indicator color 입력 값을 전달받았습니다.")
- }
+ fun dataToIndicatorColor(value: String): IndicatorColor = when (value.trim().lowercase()) {
+ "red" -> RED
+ "blue" -> BLUE
+ "grey", "gray" -> GREY
+ else -> GREY // 알 수 없는 값은 기본 색상으로 처리
+ }📝 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 dataToIndicatorColor(value: String): IndicatorColor = when (value) { | |
| "red" -> RED | |
| "blue" -> BLUE | |
| "gray" -> GRAY | |
| "grey" -> GREY | |
| else -> throw IllegalArgumentException("잘못된 indicator color 입력 값을 전달받았습니다.") | |
| } | |
| fun dataToIndicatorColor(value: String): IndicatorColor = when (value.trim().lowercase()) { | |
| "red" -> RED | |
| "blue" -> BLUE | |
| "grey", "gray" -> GREY | |
| else -> GREY // 알 수 없는 값은 기본 색상으로 처리 | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app/src/main/java/com/example/gundamdexapp/feature/detail/uimodel/IndicatorColor.kt`
around lines 10 - 15, `IndicatorColor.dataToIndicatorColor` is throwing on any
server value outside the current hardcoded set, which can crash
`GundamdexDetailViewModel` during DTO-to-UI mapping. Update
`dataToIndicatorColor` to normalize the input (for example, case-insensitive
handling and accepting `"gray"` as well as `"grey"`), and replace the exception
path with a safe fallback color in `IndicatorColor` for unknown or new server
values.
| fun getGundamList() { | ||
| viewModelScope.launch { | ||
| _uiState.update { | ||
| it.copy( | ||
| gundamInfoList = gundamRepository.getGundamList() | ||
| .getOrThrow().map { gundamInfo -> | ||
| GundamdexHomeUiModel( | ||
| id = gundamInfo.id, | ||
| modelNumber = gundamInfo.modelNumber, | ||
| name = gundamInfo.name, | ||
| series = gundamInfo.series, | ||
| imageUrl = gundamInfo.imageUrl, | ||
| ) | ||
| }, | ||
| ) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
서버 호출 실패에 대한 예외 처리가 없어 앱이 크래시됩니다.
getGundamList()는 viewModelScope.launch 내부에서 Result를 .getOrThrow()로 풀고 있습니다. 이번 PR로 실제 Supabase 서버와 통신하게 되므로 네트워크 오류·타임아웃·역직렬화 실패가 흔하게 발생할 수 있는데, 이때 예외가 코루틴에서 처리되지 않고 전파되어 앱이 종료됩니다.
Result를 onSuccess/onFailure로 분기하거나, GundamdexHomeUiState에 로딩/에러 상태를 추가해 사용자에게 노출하는 방식을 권장합니다.
🛡️ 예시 방향
fun getGundamList() {
viewModelScope.launch {
- _uiState.update {
- it.copy(
- gundamInfoList = gundamRepository.getGundamList()
- .getOrThrow().map { gundamInfo ->
- GundamdexHomeUiModel(
- id = gundamInfo.id,
- modelNumber = gundamInfo.modelNumber,
- name = gundamInfo.name,
- series = gundamInfo.series,
- imageUrl = gundamInfo.imageUrl,
- )
- },
- )
- }
+ gundamRepository.getGundamList()
+ .onSuccess { list ->
+ _uiState.update { state ->
+ state.copy(
+ gundamInfoList = list.map { gundamInfo ->
+ GundamdexHomeUiModel(
+ id = gundamInfo.id,
+ modelNumber = gundamInfo.modelNumber,
+ name = gundamInfo.name,
+ series = gundamInfo.series,
+ imageUrl = gundamInfo.imageUrl,
+ )
+ },
+ )
+ }
+ }
+ .onFailure { /* 에러 상태로 갱신 */ }
}
}📝 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 getGundamList() { | |
| viewModelScope.launch { | |
| _uiState.update { | |
| it.copy( | |
| gundamInfoList = gundamRepository.getGundamList() | |
| .getOrThrow().map { gundamInfo -> | |
| GundamdexHomeUiModel( | |
| id = gundamInfo.id, | |
| modelNumber = gundamInfo.modelNumber, | |
| name = gundamInfo.name, | |
| series = gundamInfo.series, | |
| imageUrl = gundamInfo.imageUrl, | |
| ) | |
| }, | |
| ) | |
| } | |
| } | |
| } | |
| fun getGundamList() { | |
| viewModelScope.launch { | |
| gundamRepository.getGundamList() | |
| .onSuccess { list -> | |
| _uiState.update { state -> | |
| state.copy( | |
| gundamInfoList = list.map { gundamInfo -> | |
| GundamdexHomeUiModel( | |
| id = gundamInfo.id, | |
| modelNumber = gundamInfo.modelNumber, | |
| name = gundamInfo.name, | |
| series = gundamInfo.series, | |
| imageUrl = gundamInfo.imageUrl, | |
| ) | |
| }, | |
| ) | |
| } | |
| } | |
| .onFailure { /* 에러 상태로 갱신 */ } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@app/src/main/java/com/example/gundamdexapp/feature/home/GundamdexHomeViewModel.kt`
around lines 26 - 43, `GundamdexHomeViewModel.getGundamList()` currently unwraps
the repository `Result` with `getOrThrow()` inside `viewModelScope.launch`, so
any Supabase/network failure will crash the coroutine and app. Update the flow
to handle failures explicitly in `getGundamList()` by branching with
`onSuccess`/`onFailure` (or equivalent try/catch), and use
`GundamdexHomeUiState`/`GundamdexHomeUiModel` to represent loading and error
states so the UI can react safely instead of propagating the exception.
📍 관련 이슈
📝 구현한 기능 목록
1. Retrofit을 사용해 supabase 서버와 연결함
Retrofit 라이브러리를 사용해 서버와 연결했다. 이 때 Network라는 것을 먼저 만들었다. Network에서는 어떤 url에 연결할 것인지, json 역직렬화를 어떻게 할지 설정한다.
그리고 API 코드를 생성했다. API는 서버와 통신할 때 CRUD 명령에 대한 설정을 하는 것이다. 내 코드에서는 GET()을 사용해 서버에서 정보를 불러오도록 하였다.
서버에서 받아온 json 데이터를 역직렬화하기 위해 DTO라는 것을 만들었다. DTO에는 Serializable 어노테이션을 붙여준다. 또한 파라미터에 SerialName 어노테이션을 붙여줌으로써 테이블 컬럼명과 코틀린 객체의 이름이 일치하지 않는 문제를 해결할 수 있다.
2. StateHolder를 ViewModel로 변환함
StateHolder를 ViewModel로 바꾼 이유는 서버와의 통신을 쉽게 하기 위함이다. 서버와 통신을 하기 위해서는 비동기 코드를 작성해야 한다. 이 때 코루틴을 사용한다.
하지만 StateHolder의 경우 비동기 처리를 위해 직접 코루틴 스코프를 관리해야한다. 반면 ViewModel은 내부에서 viewModelScope를 지원해주며, 관리까지 알아서 해준다는 이점이 있다.
3. Application 코드 생성함
Application은 앱에서 단 하나만 존재하며 전역 싱글톤으로 선언된다. 또한 Application은 앱이 시작할 때 실행되며, 앱이 종료되기 전까지 살아있다. 그래서 Application 코드 안에서 서버 연결과 같은 무겁고 초기에만 실행하면 되는 작업을 넣어서 실행한다는 것을 배웠다.
이 때 Activity에서 초기화하면 안되는 것인가? 라는 의문이 들었다. 되긴 하지만 Activity에서 초기화하면 안되는 이유가 명확했다. 첫 번째로 Activity는 화면을 그리는 단위다. 여기서 무거운 작업에 대한 실행을 하게 되면 UI가 그려지는게 늦어질 수 있다. 또한 Activity를 변경할 경우, 구성 변경이 일어날 경우 등 Activity가 초기화하고 다시 생성되면서 서버 연결을 재실행하게 된다. 이는 메모리, 배터리를 불필요하게 사용하게 되는 문제로 이어지게 된다.
📸 스크린샷 (UI 변경 시)
before.mp4
server_connect.mp4
✅ 체크리스트
Summary by CodeRabbit
New Features
Bug Fixes
Refactor