-
Notifications
You must be signed in to change notification settings - Fork 0
[Refactor] elderId 내부 스토리지 저장 방식으로 수정 #177 #244
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
…factor/elderinfo-#206
[Refactor] ElderInfo 관련 시간설정, 결제 화면, 뷰모델 등 수정 #206
[Fix] elderId 관련 온보딩 플로우 수정
…all-Android into refactor/elderid-#177
…all-Android into refactor/elderid-#177
WalkthroughElderIdRepository를 메모리 기반에서 Android DataStore 기반 저장소로 리팩토링하고, 동기 메서드를 suspend 함수와 Flow로 전환했습니다. 이에 따라 관련된 ViewModels과 UI 화면들도 새로운 비동기 API에 맞춰 업데이트되었습니다. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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 (2)
app/src/main/java/com/konkuk/medicarecall/domain/usecase/CheckLoginStatusUseCase.kt (1)
43-64:HttpException이 아닌 예외가 무시됩니다.
when (exception)블록에서HttpException만 처리하고, 네트워크 타임아웃이나IOException같은 다른 예외는 무시됩니다. 이 경우 해당 elder의 시간 설정 확인이 실패해도 다음 elder로 넘어가서 잘못된 상태로 홈 화면에 도달할 수 있습니다.🛠️ 수정 제안
.onFailure { exception -> - when (exception) { - is HttpException -> { - val code = exception.code() - val errorBody = exception.response()?.errorBody()?.toString() - - Log.e("httplog", "HTTP 에러 발생 - 코드: $code, 메시지: $errorBody") - - when (code) { - 404 -> { - // 404 Not Found 에러 처리 - Log.d("httplog", "시간 설정 정보 없음, 시간 등록 화면으로") - return@runCatching NavigationDestination.GoToTimeSetting - } - - else -> { - return@runCatching NavigationDestination.GoToLogin - } - } + if (exception is HttpException) { + val code = exception.code() + val errorBody = exception.response()?.errorBody()?.toString() + + Log.e("httplog", "HTTP 에러 발생 - 코드: $code, 메시지: $errorBody") + + when (code) { + 404 -> { + Log.d("httplog", "시간 설정 정보 없음, 시간 등록 화면으로") + return@runCatching NavigationDestination.GoToTimeSetting + } + else -> { + return@runCatching NavigationDestination.GoToLogin + } } + } else { + Log.e("httplog", "시간 설정 확인 중 예외 발생", exception) + return@runCatching NavigationDestination.GoToLogin } }app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderViewModel.kt (1)
260-287:postElderBulk()성공 후 elderId가 DataStore에 저장되지 않습니다.서버에서 받은
elderId가 로컬 UI 상태(eldersList,elderHealthList)에만 저장되고,ElderIdRepository를 통한 DataStore 저장이 이루어지지 않습니다.등록 완료 후
navigateToMainAfterLogin()을 통해 Home 화면으로 직접 이동하는데, 이 경로는 Splash 화면을 거치지 않으므로CheckLoginStatusUseCase(DataStore 저장을 담당)가 실행되지 않습니다. 그 결과HomeViewModel.fetchElderList()가 DataStore에서 빈 맵을 읽게 되어 첫 진입 시 어르신 목록이 표시되지 않을 수 있습니다.
postElderBulk().onSuccess블록에서elderIdRepository.updateElderIds()를 호출하거나, 다른 방식으로 DataStore 저장을 보장해야 합니다.
🤖 Fix all issues with AI agents
In `@app/src/main/java/com/konkuk/medicarecall/data/model/ElderIds.kt`:
- Around line 3-5: The data class ElderIds is missing the kotlinx.serialization
annotation required by ElderIdsSerializer (which calls
Json.decodeFromString/encodeToString); add the `@Serializable` annotation to the
ElderIds data class and import kotlinx.serialization.Serializable so
kotlinx.serialization can generate the serializer used by ElderIdsSerializer.
In
`@app/src/main/java/com/konkuk/medicarecall/data/repositoryimpl/ElderIdRepositoryImpl.kt`:
- Around line 27-31: The current updateElderId implementation reads state with
getElderIds().first() then calls context.elderIdsDataStore.updateData, risking
lost updates; change updateElderId to perform the read-and-modify inside the
DataStore's atomic updateData block (use context.elderIdsDataStore.updateData {
current -> val m = current.elderIds.toMutableMap(); m[elderId] = name;
current.copy(elderIds = m) }) so the mutation is done atomically; reference
updateElderId, getElderIds, and context.elderIdsDataStore.updateData to locate
where to apply this change.
In `@app/src/main/java/com/konkuk/medicarecall/data/util/ElderIdsSerializer.kt`:
- Around line 20-38: The readFrom implementation closes the provided InputStream
via input.use { ... }, which conflicts with DataStore's stream management;
remove the input.use wrapper and instead read bytes without closing the stream
(e.g., call input.readBytes() inside withContext(Dispatchers.IO) directly),
leaving stream lifecycle to DataStore; keep existing Base64 decoding and the
exception fallback to defaultValue and adjust the read logic inside the readFrom
function accordingly.
- Around line 40-49: ElderIdsSerializer.writeTo currently closes the provided
OutputStream via output.use { ... }, which can conflict with DataStore's
lifecycle management; remove the use/closing and instead write to the stream
without closing it (e.g., perform output.write(encodedBytesBase64) inside the
withContext(Dispatchers.IO) block) so the OutputStream remains open for
DataStore to manage. Ensure you do not call close() on the OutputStream and keep
the Base64 encoding and IO dispatcher usage intact.
In
`@app/src/main/java/com/konkuk/medicarecall/ui/feature/home/viewmodel/HomeViewModel.kt`:
- Around line 122-137: fetchElderList currently calls
elderIdRepository.getElderIds().first() without handling exceptions, which can
leave _homeUiState.isLoading true and collapse the UI; wrap the DataStore read
in a try-catch around the call to elderIdRepository.getElderIds().first() inside
fetchElderList, on success continue populating _elderInfoList and selecting
restoredId via savedStateHandle/KEY_SELECTED_ELDER_ID as before, and on failure
set _homeUiState.update { it.copy(isLoading = false, error = /* appropriate
error or flag */) }, clear or leave _elderInfoList empty as appropriate, and log
the exception so the coroutine doesn't silently fail and the loading state is
always cleared.
In
`@app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/screen/CallTimeScreen.kt`:
- Around line 69-92: The code crashes when elderMap.keys.first() is called on an
empty map; update the initialization of selectedId and related state to guard
against an empty elderMap (e.g., use firstOrNull or check
elderMap.isNotEmpty()), set a safe default (such as -1 or null) for selectedId,
and adjust usage of callTimeViewModel.timeMap[selectedId] (and saved) and UI
rendering to handle the empty/invalid selectedId by showing an empty-state or
early return; locate and change the variables referenced (elderMap, selectedId,
saved, selectedTabIndex) in CallTimeScreen to implement this guard and
downstream null/empty handling.
- Around line 201-219: The code calls listState.animateScrollToItem(id) passing
an elder ID (from elderMap keys) instead of the list index; change the LazyRow
items(...) usage to itemsIndexed(...) so you receive the positional index and
use that index in animateScrollToItem, update the click handler to set
selectedId using the elder ID (value from the item) while calling
listState.animateScrollToItem(index), and ensure references to selectedId,
elderMap and listState remain consistent (functions/methods:
animateScrollToItem, items -> itemsIndexed, selectedId, elderMap, listState).
In
`@app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/viewmodel/CallTimeViewModel.kt`:
- Around line 29-58: The code maintains two separate elder ID states
(_elderIdMap / elderIds) causing staleness; unify them by using a single source
of truth or synchronizing updates: remove the one-off update that uses
_elderIdMap.update { elderIdRepository.getElderIds().first() } and instead
update _elderIdMap inside the same collect flow used by observeElderIds (or
collapse to only _elderIdMap and expose it as a State/StateFlow), ensuring all
updates come from elderIdRepository.getElderIds() in viewModelScope.launch
within observeElderIds so _elderIdMap, _elderIds (or the single retained state)
remain consistent.
🧹 Nitpick comments (2)
app/src/main/java/com/konkuk/medicarecall/ui/feature/settings/screen/ElderInfoScreen.kt (1)
53-58: Composable 내 로깅은 recomposition마다 실행됩니다.디버깅 목적의 로그 호출이 Composable 함수 본문에 있어서 매 recomposition마다 실행됩니다. 프로덕션 빌드에서는 로그 스팸이 발생할 수 있으니 디버깅 후 제거하거나
LaunchedEffect로 이동하는 것을 고려해 주세요.app/src/main/java/com/konkuk/medicarecall/data/util/ElderIdsSerializer.kt (1)
31-33: Base64 인코딩이 불필요해 보입니다.DataStore 파일은 이미 앱 내부 저장소에 저장되므로 JSON만으로 충분합니다. Base64 인코딩은 추가적인 CPU 오버헤드와 파일 크기 증가(약 33%)를 발생시킵니다. 특별한 이유가 없다면 JSON 직접 저장을 고려해 주세요.
| data class ElderIds( | ||
| val elderIds: Map<Int, String>, | ||
| ) |
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.
@Serializable 어노테이션이 누락되어 런타임 크래시가 발생합니다.
ElderIdsSerializer에서 Json.decodeFromString()과 Json.encodeToString()을 사용하는데, 이 클래스에 @Serializable 어노테이션이 없으면 kotlinx.serialization이 동작하지 않아 런타임에 SerializationException이 발생합니다.
🐛 수정 제안
package com.konkuk.medicarecall.data.model
+import kotlinx.serialization.Serializable
+
+@Serializable
data class ElderIds(
val elderIds: Map<Int, String>,
)🤖 Prompt for AI Agents
In `@app/src/main/java/com/konkuk/medicarecall/data/model/ElderIds.kt` around
lines 3 - 5, The data class ElderIds is missing the kotlinx.serialization
annotation required by ElderIdsSerializer (which calls
Json.decodeFromString/encodeToString); add the `@Serializable` annotation to the
ElderIds data class and import kotlinx.serialization.Serializable so
kotlinx.serialization can generate the serializer used by ElderIdsSerializer.
| override suspend fun updateElderId(elderId: Int, name: String) { | ||
| val elderIds = getElderIds().first().toMutableMap() | ||
| elderIds[elderId] = name | ||
| context.elderIdsDataStore.updateData { it.copy(elderIds = elderIds) } | ||
| } |
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 "ElderIdRepositoryImpl.kt" -type f | head -20Repository: Medicare-Call/Medicare-Call-Android
Length of output: 167
🏁 Script executed:
git ls-files | grep -i repository | head -20Repository: Medicare-Call/Medicare-Call-Android
Length of output: 1689
🏁 Script executed:
cat -n ./app/src/main/java/com/konkuk/medicarecall/data/repositoryimpl/ElderIdRepositoryImpl.ktRepository: Medicare-Call/Medicare-Call-Android
Length of output: 1816
🏁 Script executed:
cat -n ./app/src/main/java/com/konkuk/medicarecall/data/repository/ElderIdRepository.ktRepository: Medicare-Call/Medicare-Call-Android
Length of output: 463
🏁 Script executed:
rg "updateData" app/src/main/java/com/konkuk/medicarecall/data/ -A 2 -B 1Repository: Medicare-Call/Medicare-Call-Android
Length of output: 3980
🏁 Script executed:
cat -n ./app/src/main/java/com/konkuk/medicarecall/data/model/ElderIds.ktRepository: Medicare-Call/Medicare-Call-Android
Length of output: 216
updateElderId 메서드의 동시성 문제로 인한 데이터 유실 위험
getElderIds().first()로 읽은 후 별도로 updateData를 호출하면, 그 사이에 다른 업데이트가 발생했을 때 변경사항이 덮어써질 수 있어요. DataStore의 updateData 블록 내에서 읽고 수정하면 이런 경쟁 상태를 원자적으로 처리할 수 있습니다.
✅ 권장 수정
- override suspend fun updateElderId(elderId: Int, name: String) {
- val elderIds = getElderIds().first().toMutableMap()
- elderIds[elderId] = name
- context.elderIdsDataStore.updateData { it.copy(elderIds = elderIds) }
- }
+ override suspend fun updateElderId(elderId: Int, name: String) {
+ context.elderIdsDataStore.updateData { current ->
+ current.copy(elderIds = current.elderIds + (elderId to name))
+ }
+ }🤖 Prompt for AI Agents
In
`@app/src/main/java/com/konkuk/medicarecall/data/repositoryimpl/ElderIdRepositoryImpl.kt`
around lines 27 - 31, The current updateElderId implementation reads state with
getElderIds().first() then calls context.elderIdsDataStore.updateData, risking
lost updates; change updateElderId to perform the read-and-modify inside the
DataStore's atomic updateData block (use context.elderIdsDataStore.updateData {
current -> val m = current.elderIds.toMutableMap(); m[elderId] = name;
current.copy(elderIds = m) }) so the mutation is done atomically; reference
updateElderId, getElderIds, and context.elderIdsDataStore.updateData to locate
where to apply this change.
| override suspend fun readFrom(input: InputStream): ElderIds { | ||
| return try { | ||
| val encodedBytes = withContext(Dispatchers.IO) { | ||
| input.use { it.readBytes() } | ||
| } | ||
|
|
||
| // 빈 파일인 경우 기본값 반환 | ||
| if (encodedBytes.isEmpty()) { | ||
| return defaultValue | ||
| } | ||
|
|
||
| val decodedBytesBase64 = Base64.getDecoder().decode(encodedBytes) | ||
| val decodedJsonString = decodedBytesBase64.decodeToString() | ||
| Json.decodeFromString(decodedJsonString) | ||
| } catch (e: Exception) { | ||
| Log.d("ElderIdsSerializer", "Failed to read ElderIds: ${e.message}") | ||
| defaultValue | ||
| } | ||
| } |
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.
input.use {} 도 동일하게 스트림 관리 충돌 가능성이 있습니다.
readFrom에서도 input.use {}로 스트림을 닫고 있습니다. DataStore가 스트림을 관리하므로 직접 닫지 않는 것이 좋습니다.
🛠️ 수정 제안
override suspend fun readFrom(input: InputStream): ElderIds {
return try {
val encodedBytes = withContext(Dispatchers.IO) {
- input.use { it.readBytes() }
+ input.readBytes()
}
// 빈 파일인 경우 기본값 반환
if (encodedBytes.isEmpty()) {
return defaultValue
}
val decodedBytesBase64 = Base64.getDecoder().decode(encodedBytes)
val decodedJsonString = decodedBytesBase64.decodeToString()
Json.decodeFromString(decodedJsonString)
} catch (e: Exception) {
Log.d("ElderIdsSerializer", "Failed to read ElderIds: ${e.message}")
defaultValue
}
}📝 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.
| override suspend fun readFrom(input: InputStream): ElderIds { | |
| return try { | |
| val encodedBytes = withContext(Dispatchers.IO) { | |
| input.use { it.readBytes() } | |
| } | |
| // 빈 파일인 경우 기본값 반환 | |
| if (encodedBytes.isEmpty()) { | |
| return defaultValue | |
| } | |
| val decodedBytesBase64 = Base64.getDecoder().decode(encodedBytes) | |
| val decodedJsonString = decodedBytesBase64.decodeToString() | |
| Json.decodeFromString(decodedJsonString) | |
| } catch (e: Exception) { | |
| Log.d("ElderIdsSerializer", "Failed to read ElderIds: ${e.message}") | |
| defaultValue | |
| } | |
| } | |
| override suspend fun readFrom(input: InputStream): ElderIds { | |
| return try { | |
| val encodedBytes = withContext(Dispatchers.IO) { | |
| input.readBytes() | |
| } | |
| // 빈 파일인 경우 기본값 반환 | |
| if (encodedBytes.isEmpty()) { | |
| return defaultValue | |
| } | |
| val decodedBytesBase64 = Base64.getDecoder().decode(encodedBytes) | |
| val decodedJsonString = decodedBytesBase64.decodeToString() | |
| Json.decodeFromString(decodedJsonString) | |
| } catch (e: Exception) { | |
| Log.d("ElderIdsSerializer", "Failed to read ElderIds: ${e.message}") | |
| defaultValue | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@app/src/main/java/com/konkuk/medicarecall/data/util/ElderIdsSerializer.kt`
around lines 20 - 38, The readFrom implementation closes the provided
InputStream via input.use { ... }, which conflicts with DataStore's stream
management; remove the input.use wrapper and instead read bytes without closing
the stream (e.g., call input.readBytes() inside withContext(Dispatchers.IO)
directly), leaving stream lifecycle to DataStore; keep existing Base64 decoding
and the exception fallback to defaultValue and adjust the read logic inside the
readFrom function accordingly.
| override suspend fun writeTo(t: ElderIds, output: OutputStream) { | ||
| val json = Json.Default.encodeToString(t) | ||
| val bytes = json.toByteArray() | ||
| val encodedBytesBase64 = Base64.getEncoder().encode(bytes) | ||
| withContext(Dispatchers.IO) { | ||
| output.use { | ||
| it.write(encodedBytesBase64) | ||
| } | ||
| } | ||
| } |
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.
output.use {} 사용 시 DataStore와 충돌 가능성이 있습니다.
DataStore는 writeTo에 전달된 OutputStream의 생명주기를 내부적으로 관리합니다. use {} 블록으로 스트림을 닫으면 DataStore가 후속 작업(예: flush, 트랜잭션 커밋)을 수행할 때 문제가 발생할 수 있습니다.
🛠️ 수정 제안
override suspend fun writeTo(t: ElderIds, output: OutputStream) {
val json = Json.Default.encodeToString(t)
val bytes = json.toByteArray()
val encodedBytesBase64 = Base64.getEncoder().encode(bytes)
withContext(Dispatchers.IO) {
- output.use {
- it.write(encodedBytesBase64)
- }
+ output.write(encodedBytesBase64)
}
}🤖 Prompt for AI Agents
In `@app/src/main/java/com/konkuk/medicarecall/data/util/ElderIdsSerializer.kt`
around lines 40 - 49, ElderIdsSerializer.writeTo currently closes the provided
OutputStream via output.use { ... }, which can conflict with DataStore's
lifecycle management; remove the use/closing and instead write to the stream
without closing it (e.g., perform output.write(encodedBytesBase64) inside the
withContext(Dispatchers.IO) block) so the OutputStream remains open for
DataStore to manage. Ensure you do not call close() on the OutputStream and keep
the Base64 encoding and IO dispatcher usage intact.
| // 로컬에서 어르신 전체 목록을 불러옴 | ||
| fun fetchElderList() { | ||
| viewModelScope.launch { | ||
| if (_elderInfoList.value.isEmpty()) { | ||
| _homeUiState.update { it.copy(isLoading = true) } | ||
| } | ||
| eldersInfoRepository.getElders() | ||
| .onSuccess { elders -> | ||
| _elderInfoList.value = elders.map { | ||
| ElderInfo(id = it.elderId, name = it.name, phone = it.phone) | ||
| } | ||
| val restoredId = savedStateHandle.get<Int?>(KEY_SELECTED_ELDER_ID) | ||
| if (restoredId != null && _elderInfoList.value.any { it.id == restoredId }) { | ||
| _selectedElderId.value = restoredId | ||
| } else if (_selectedElderId.value == null && _elderInfoList.value.isNotEmpty()) { | ||
| _selectedElderId.value = _elderInfoList.value.first().id | ||
| } | ||
| } | ||
| .onFailure { error -> | ||
| Log.e(TAG, "어르신 목록 로딩 실패", error) | ||
| _homeUiState.update { it.copy(isLoading = false) } | ||
| } | ||
| val elderIdMap = elderIdRepository.getElderIds().first() | ||
| _elderInfoList.value = elderIdMap.map { | ||
| ElderInfo(id = it.key, name = it.value, phone = null) | ||
| } | ||
| val restoredId = savedStateHandle.get<Int?>(KEY_SELECTED_ELDER_ID) | ||
| if (restoredId != null && _elderInfoList.value.any { it.id == restoredId }) { | ||
| _selectedElderId.value = restoredId | ||
| } else if (_selectedElderId.value == null && _elderInfoList.value.isNotEmpty()) { | ||
| _selectedElderId.value = _elderInfoList.value.first().id | ||
| } |
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:
cd app/src/main/java/com/konkuk/medicarecall/ui/feature/home/viewmodel && \
wc -l HomeViewModel.kt && \
cat -n HomeViewModel.kt | sed -n '110,150p'Repository: Medicare-Call/Medicare-Call-Android
Length of output: 2005
🏁 Script executed:
# Find elderIdRepository definition to understand getElderIds() return type
fd -t f "*.kt" | xargs rg "elderIdRepository\s*[:=]" -A 2 -B 2 | head -50Repository: Medicare-Call/Medicare-Call-Android
Length of output: 3004
🏁 Script executed:
# Search for ElderIdRepository definition
fd -t f "ElderIdRepository.kt" -o "*Repository*.kt" | xargs cat -n | head -100Repository: Medicare-Call/Medicare-Call-Android
Length of output: 228
🏁 Script executed:
# Search for ElderIdRepository interface/class definition
fd -type f -name "*.kt" | xargs rg "class ElderIdRepository|interface ElderIdRepository" -A 10Repository: Medicare-Call/Medicare-Call-Android
Length of output: 2330
🏁 Script executed:
# Search for getElderIds() method definition
fd -type f -name "*.kt" | xargs rg "fun getElderIds" -A 5Repository: Medicare-Call/Medicare-Call-Android
Length of output: 1276
🏁 Script executed:
# Check if there's DataStore usage or Flow involved
fd -type f -name "*.kt" | xargs rg "DataStore|Flow" | grep -i "elderid" | head -20Repository: Medicare-Call/Medicare-Call-Android
Length of output: 2622
🏁 Script executed:
# Check the complete ElderIdRepositoryImpl to see DataStore setup and error handling
cat -n app/src/main/java/com/konkuk/medicarecall/data/repositoryimpl/ElderIdRepositoryImpl.ktRepository: Medicare-Call/Medicare-Call-Android
Length of output: 1816
🏁 Script executed:
# Look for error handling patterns in HomeViewModel
cat -n app/src/main/java/com/konkuk/medicarecall/ui/feature/home/viewmodel/HomeViewModel.kt | sed -n '140,200p'Repository: Medicare-Call/Medicare-Call-Android
Length of output: 3106
🏁 Script executed:
# Check if there's a global error handler or CoroutineExceptionHandler in HomeViewModel
rg "CoroutineExceptionHandler|try\s*\{|catch|CoroutineContext" app/src/main/java/com/konkuk/medicarecall/ui/feature/home/viewmodel/HomeViewModel.kt -A 3Repository: Medicare-Call/Medicare-Call-Android
Length of output: 636
DataStore 읽기 실패 시 예외 처리 필요
getElderIds().first()에서 예외 발생 시 코루틴이 조용히 실패하고, 로딩 상태가 true로 남아 UI가 멈춥니다. 같은 ViewModel의 다른 메서드들(fetchHomeSummaryForToday, forceRefreshHomeData)은 이미 try-catch를 적용하고 있으므로, 이 메서드도 일관성 있게 예외 처리해야 합니다.
💡 제안 수정
- val elderIdMap = elderIdRepository.getElderIds().first()
+ val elderIdMap = try {
+ elderIdRepository.getElderIds().first()
+ } catch (e: Exception) {
+ Log.e(TAG, "elderIds load failed", e)
+ _homeUiState.update { it.copy(isLoading = false) }
+ return@launch
+ }🤖 Prompt for AI Agents
In
`@app/src/main/java/com/konkuk/medicarecall/ui/feature/home/viewmodel/HomeViewModel.kt`
around lines 122 - 137, fetchElderList currently calls
elderIdRepository.getElderIds().first() without handling exceptions, which can
leave _homeUiState.isLoading true and collapse the UI; wrap the DataStore read
in a try-catch around the call to elderIdRepository.getElderIds().first() inside
fetchElderList, on success continue populating _elderInfoList and selecting
restoredId via savedStateHandle/KEY_SELECTED_ELDER_ID as before, and on failure
set _homeUiState.update { it.copy(isLoading = false, error = /* appropriate
error or flag */) }, clear or leave _elderInfoList empty as appropriate, and log
the exception so the coroutine doesn't silently fail and the loading state is
always cleared.
| val elderMap = callTimeViewModel.elderIds | ||
| val isLoading = callTimeViewModel.isLoading.value | ||
|
|
||
| val isLoading by eldersInfoViewModel.isLoading.collectAsStateWithLifecycle() | ||
| val error by eldersInfoViewModel.error.collectAsStateWithLifecycle() | ||
| val nameIdList by eldersInfoViewModel.elderNameIdMapList.collectAsStateWithLifecycle() | ||
|
|
||
| when { | ||
| isLoading -> { | ||
| Box( | ||
| Modifier | ||
| .fillMaxSize() | ||
| .background(MediCareCallTheme.colors.bg) | ||
| .systemBarsPadding(), | ||
| contentAlignment = Alignment.Center, | ||
| ) { | ||
| CircularProgressIndicator( | ||
| color = MediCareCallTheme.colors.main, | ||
| modifier = Modifier.align(Alignment.Center), | ||
| ) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| error != null -> { | ||
| Column( | ||
| Modifier.fillMaxSize(), | ||
| horizontalAlignment = Alignment.CenterHorizontally, | ||
| verticalArrangement = Arrangement.Center, | ||
| ) { | ||
| Text("어르신 정보를 불러오지 못했어요.\n잠시 후 다시 시도해 주세요.") | ||
| Spacer(Modifier.height(12.dp)) | ||
| CTAButton( | ||
| type = CTAButtonType.GREEN, | ||
| text = "다시 시도", | ||
| onClick = { eldersInfoViewModel.refresh() }, | ||
| ) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| nameIdList.isEmpty() -> { | ||
| Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { | ||
| Text("등록된 어르신이 없습니다.") | ||
| } | ||
| return | ||
| if (isLoading) { | ||
| Box( | ||
| Modifier | ||
| .fillMaxSize() | ||
| .background(MediCareCallTheme.colors.bg), | ||
| contentAlignment = Alignment.Center, | ||
| ) { | ||
| CircularProgressIndicator( | ||
| color = MediCareCallTheme.colors.main, | ||
| ) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| val scrollState = rememberScrollState() // 스크롤 상태 | ||
| val elderNames = nameIdList.map { it.keys.first() } // 어르신 이름 리스트 | ||
| val elderIds = nameIdList.map { it.values.first() } // 어르신 아이디 리스트 | ||
|
|
||
| // ViewModel 상태 구독 | ||
| val showBottomSheet by callTimeViewModel.showBottomSheet.collectAsStateWithLifecycle() | ||
| val selectedIndex by callTimeViewModel.selectedIndex.collectAsStateWithLifecycle() | ||
| val selectedTabIndex by callTimeViewModel.selectedTabIndex.collectAsStateWithLifecycle() | ||
| val timeMap by callTimeViewModel.timeMap.collectAsStateWithLifecycle() | ||
| var showBottomSheet by remember { mutableStateOf(false) } // 하단 시트 제어 | ||
|
|
||
| val selectedId = elderIds.getOrNull(selectedIndex) ?: 0 // 선택된 어르신 아이디 | ||
| val saved = timeMap[selectedId] ?: CallTimes() | ||
| // var selectedIndex by remember { mutableIntStateOf(0) } // 선택된 어르신 인덱스 | ||
| var selectedId by remember { mutableIntStateOf(elderMap.keys.first()) } // 선택된 어르신 아이디 | ||
| val saved = callTimeViewModel.timeMap[selectedId] ?: CallTimes() | ||
| var selectedTabIndex by remember { mutableIntStateOf(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 "app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/screen/CallTimeScreen.kt"Repository: Medicare-Call/Medicare-Call-Android
Length of output: 18126
🏁 Script executed:
find . -name "CallTimeViewModel.kt" -type fRepository: Medicare-Call/Medicare-Call-Android
Length of output: 179
🏁 Script executed:
cat -n "./app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/viewmodel/CallTimeViewModel.kt"Repository: Medicare-Call/Medicare-Call-Android
Length of output: 4997
🏁 Script executed:
# Check if there are any other safeguards in the UI layer
rg "isLoading|isCompleteFor|isAllComplete" app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/screen/CallTimeScreen.kt -A 2 -B 2Repository: Medicare-Call/Medicare-Call-Android
Length of output: 386
🏁 Script executed:
# Check if isAllComplete is called on an empty set anywhere
rg "isAllComplete" app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/ -B 2 -A 2Repository: Medicare-Call/Medicare-Call-Android
Length of output: 1300
빈 elderMap에서 first() 호출로 크래시 가능
로딩이 끝났지만 elderMap이 비어있는 경우 first()가 터집니다. isLoading 플래그는 초기 데이터 로드 중에는 false이고, Flow 구독이 비동기로 진행되기 때문에 화면이 렌더링될 때 map이 아직 비어있을 수 있습니다.
🛠️ 제안 수정 (빈 목록 가드)
- var selectedId by remember { mutableIntStateOf(elderMap.keys.first()) } // 선택된 어르신 아이디
+ val firstId = elderMap.keys.firstOrNull()
+ if (firstId == null) {
+ // TODO: empty state UI
+ return
+ }
+ var selectedId by remember(firstId) { mutableIntStateOf(firstId) } // 선택된 어르신 아이디🤖 Prompt for AI Agents
In
`@app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/screen/CallTimeScreen.kt`
around lines 69 - 92, The code crashes when elderMap.keys.first() is called on
an empty map; update the initialization of selectedId and related state to guard
against an empty elderMap (e.g., use firstOrNull or check
elderMap.isNotEmpty()), set a safe default (such as -1 or null) for selectedId,
and adjust usage of callTimeViewModel.timeMap[selectedId] (and saved) and UI
rendering to handle the empty/invalid selectedId by showing an empty-state or
early return; locate and change the variables referenced (elderMap, selectedId,
saved, selectedTabIndex) in CallTimeScreen to implement this guard and
downstream null/empty handling.
| items(elderMap.keys.toList()) { id -> | ||
| Text( | ||
| text = name, | ||
| text = elderMap[id] ?: "", | ||
| modifier = Modifier | ||
| .clip(CircleShape) | ||
| .border( | ||
| width = if (idx == selectedIndex) 0.dp else (1.2).dp, | ||
| color = if (idx == selectedIndex) MediCareCallTheme.colors.main else MediCareCallTheme.colors.gray2, | ||
| width = if (id == selectedId) 0.dp else (1.2).dp, | ||
| color = if (id == selectedId) MediCareCallTheme.colors.main else MediCareCallTheme.colors.gray2, | ||
| shape = RoundedCornerShape(100.dp), | ||
| ) | ||
| .background( | ||
| color = if (idx == selectedIndex) MediCareCallTheme.colors.main else Color.Transparent, | ||
| color = if (id == selectedId) MediCareCallTheme.colors.main else Color.Transparent, | ||
| shape = RoundedCornerShape(100.dp), | ||
| ) | ||
| .clickable { | ||
| callTimeViewModel.setSelectedIndex(idx) | ||
| selectedId = id | ||
| scope.launch { | ||
| listState.animateScrollToItem(idx) | ||
| listState.animateScrollToItem(id) | ||
| } |
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:
fd "CallTimeScreen.kt" --type fRepository: Medicare-Call/Medicare-Call-Android
Length of output: 171
🏁 Script executed:
wc -l "app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/screen/CallTimeScreen.kt"Repository: Medicare-Call/Medicare-Call-Android
Length of output: 175
🏁 Script executed:
cat -n "app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/screen/CallTimeScreen.kt" | sed -n '180,230p'Repository: Medicare-Call/Medicare-Call-Android
Length of output: 2751
🏁 Script executed:
cat -n "app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/screen/CallTimeScreen.kt" | sed -n '1,100p'Repository: Medicare-Call/Medicare-Call-Android
Length of output: 5050
🏁 Script executed:
cat -n "app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/screen/CallTimeScreen.kt" | sed -n '55,110p'Repository: Medicare-Call/Medicare-Call-Android
Length of output: 2349
🏁 Script executed:
rg "class CallTimeViewModel" -A 30 --type ktRepository: Medicare-Call/Medicare-Call-Android
Length of output: 105
🏁 Script executed:
rg "class CallTimeViewModel" -A 30Repository: Medicare-Call/Medicare-Call-Android
Length of output: 4175
🏁 Script executed:
rg "animateScrollToItem" --context 3Repository: Medicare-Call/Medicare-Call-Android
Length of output: 1074
🏁 Script executed:
cat -n "app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/viewmodel/CallTimeViewModel.kt" | head -50Repository: Medicare-Call/Medicare-Call-Android
Length of output: 2216
🏁 Script executed:
cat -n "app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/viewmodel/CallTimeViewModel.kt" | sed -n '50,100p'Repository: Medicare-Call/Medicare-Call-Android
Length of output: 2105
🌐 Web query:
Jetpack Compose animateScrollToItem LazyListState index parameter
💡 Result:
In LazyListState.animateScrollToItem(index, scrollOffset):
indexis the 0-based position in the lazy list (the same index you get initems(...) { index -> ... }). It must be non‑negative. [1]- It’s the global list index across everything you emit into the
LazyColumn/LazyRow—including headers likestickyHeader(those also have a “header index” which is the item’s position in the total list). [2] scrollOffsetis an extra pixel offset applied after positioning the item; in a normal top-to-bottom list, positive values push the item further up (partly offscreen). [1]
If you’re using stable keys, note that animateScrollToItem still scrolls by index, not key—you must find the item’s current index yourself before calling it. [2]
Sources: Android Developers API for LazyListState.animateScrollToItem [1], LazyListScope (global index/key behavior) [2].
animateScrollToItem()에 인덱스가 아닌 엘더 ID를 넘기는 문제
animateScrollToItem()은 LazyRow의 **위치 인덱스(0, 1, 2...)**를 기대하는데, 현재 코드는 elderMap의 키(엘더 ID)를 직접 넘기고 있습니다. 엘더 ID가 임의의 정수값(예: 101, 202, 303)이므로 스크롤 실패나 크래시가 발생할 수 있습니다.
itemsIndexed를 사용해서 인덱스를 얻어 전달하세요:
수정 방법
-import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
...
- items(elderMap.keys.toList()) { id ->
+ itemsIndexed(elderMap.keys.toList()) { index, id ->
...
.clickable {
selectedId = id
scope.launch {
- listState.animateScrollToItem(id)
+ listState.animateScrollToItem(index)
}📝 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.
| items(elderMap.keys.toList()) { id -> | |
| Text( | |
| text = name, | |
| text = elderMap[id] ?: "", | |
| modifier = Modifier | |
| .clip(CircleShape) | |
| .border( | |
| width = if (idx == selectedIndex) 0.dp else (1.2).dp, | |
| color = if (idx == selectedIndex) MediCareCallTheme.colors.main else MediCareCallTheme.colors.gray2, | |
| width = if (id == selectedId) 0.dp else (1.2).dp, | |
| color = if (id == selectedId) MediCareCallTheme.colors.main else MediCareCallTheme.colors.gray2, | |
| shape = RoundedCornerShape(100.dp), | |
| ) | |
| .background( | |
| color = if (idx == selectedIndex) MediCareCallTheme.colors.main else Color.Transparent, | |
| color = if (id == selectedId) MediCareCallTheme.colors.main else Color.Transparent, | |
| shape = RoundedCornerShape(100.dp), | |
| ) | |
| .clickable { | |
| callTimeViewModel.setSelectedIndex(idx) | |
| selectedId = id | |
| scope.launch { | |
| listState.animateScrollToItem(idx) | |
| listState.animateScrollToItem(id) | |
| } | |
| itemsIndexed(elderMap.keys.toList()) { index, id -> | |
| Text( | |
| text = elderMap[id] ?: "", | |
| modifier = Modifier | |
| .clip(CircleShape) | |
| .border( | |
| width = if (id == selectedId) 0.dp else (1.2).dp, | |
| color = if (id == selectedId) MediCareCallTheme.colors.main else MediCareCallTheme.colors.gray2, | |
| shape = RoundedCornerShape(100.dp), | |
| ) | |
| .background( | |
| color = if (id == selectedId) MediCareCallTheme.colors.main else Color.Transparent, | |
| shape = RoundedCornerShape(100.dp), | |
| ) | |
| .clickable { | |
| selectedId = id | |
| scope.launch { | |
| listState.animateScrollToItem(index) | |
| } |
🤖 Prompt for AI Agents
In
`@app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/screen/CallTimeScreen.kt`
around lines 201 - 219, The code calls listState.animateScrollToItem(id) passing
an elder ID (from elderMap keys) instead of the list index; change the LazyRow
items(...) usage to itemsIndexed(...) so you receive the positional index and
use that index in animateScrollToItem, update the click handler to set
selectedId using the elder ID (value from the item) while calling
listState.animateScrollToItem(index), and ensure references to selectedId,
elderMap and listState remain consistent (functions/methods:
animateScrollToItem, items -> itemsIndexed, selectedId, elderMap, listState).
| private val _elderIdMap = MutableStateFlow(emptyMap<Int, String>()) | ||
| val elderIdMap = _elderIdMap.asStateFlow() | ||
|
|
||
| // Time data | ||
| private val _timeMap = MutableStateFlow<Map<Int, CallTimes>>(emptyMap()) | ||
| val timeMap: StateFlow<Map<Int, CallTimes>> = _timeMap.asStateFlow() | ||
| private val _showBottomSheet = mutableStateOf(false) | ||
| private val _selectedIndex = mutableIntStateOf(0) | ||
| private val _selectedTabIndex = mutableIntStateOf(0) | ||
|
|
||
| // UI state | ||
| private val _showBottomSheet = MutableStateFlow(false) | ||
| val showBottomSheet: StateFlow<Boolean> = _showBottomSheet.asStateFlow() | ||
|
|
||
| private val _selectedIndex = MutableStateFlow(0) | ||
| val selectedIndex: StateFlow<Int> = _selectedIndex.asStateFlow() | ||
| init { | ||
| viewModelScope.launch { | ||
| _elderIdMap.update { elderIdRepository.getElderIds().first(); } | ||
| } | ||
| } | ||
|
|
||
| private val _selectedTabIndex = MutableStateFlow(0) | ||
| val selectedTabIndex: StateFlow<Int> = _selectedTabIndex.asStateFlow() | ||
| // Flow -> State 로 뱐환해서 보관 | ||
| private val _elderIds = mutableStateOf<Map<Int, String>>(emptyMap()) | ||
| val elderIds get() = _elderIds.value // UI에서 접근할 값 | ||
|
|
||
| // Async state | ||
| private val _isLoading = MutableStateFlow(false) | ||
| val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow() | ||
| init { | ||
| observeElderIds() | ||
| } | ||
|
|
||
| private val _lastError = MutableStateFlow<Throwable?>(null) | ||
| val lastError: StateFlow<Throwable?> = _lastError.asStateFlow() | ||
| // suspend + Flow 안전하게 처리하는 함수 | ||
| private fun observeElderIds() { | ||
| viewModelScope.launch { | ||
| try { | ||
| elderIdRepository.getElderIds() | ||
| .collect { result -> | ||
| _elderIds.value = result | ||
| Log.d("CallTimeViewModel", "elderIds 업데이트: $result") | ||
| } |
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 . -type f -name "CallTimeViewModel.kt" | head -5Repository: Medicare-Call/Medicare-Call-Android
Length of output: 179
🏁 Script executed:
cat -n app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/viewmodel/CallTimeViewModel.ktRepository: Medicare-Call/Medicare-Call-Android
Length of output: 4997
🏁 Script executed:
rg "_elderIdMap|_elderIds" --type kotlin -B 2 -A 2Repository: Medicare-Call/Medicare-Call-Android
Length of output: 3074
🏁 Script executed:
rg "elderIdMap|elderIds" --type kotlin -B 1 -A 1 | grep -v "CallTimeViewModel.kt"Repository: Medicare-Call/Medicare-Call-Android
Length of output: 6249
🏁 Script executed:
find . -type kotlin -name "*.kt" -path "*/ui/*" | xargs grep -l "elderIdMap\|elderIds" 2>/dev/nullRepository: Medicare-Call/Medicare-Call-Android
Length of output: 115
🏁 Script executed:
rg "\.elderIdMap" --type kotlin -B 2 -A 2Repository: Medicare-Call/Medicare-Call-Android
Length of output: 61
🏁 Script executed:
grep -n "elderIdMap\|elderIds" app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/screen/CallTimeScreen.ktRepository: Medicare-Call/Medicare-Call-Android
Length of output: 190
🏁 Script executed:
find . -type f -name "CallTimeScreen.kt" | head -1 | xargs cat -nRepository: Medicare-Call/Medicare-Call-Android
Length of output: 18126
중복된 elder ID 상태로 인한 staleness 위험
_elderIdMap은 .first()로 한 번만 업데이트되어 이후 변경사항을 놓치지만, _elderIds는 계속 collect되므로 둘이 불일치할 수 있습니다. 특히 elderIdMap이 공개 API인데 stale 상태가 되는 것은 위험해요.
한 가지 source of truth로 통일하거나, 둘 다 유지한다면 같은 업데이트 흐름에서 동기화해주세요.
🧩 제안 수정
- init {
- viewModelScope.launch {
- _elderIdMap.update { elderIdRepository.getElderIds().first(); }
- }
- }
...
elderIdRepository.getElderIds()
.collect { result ->
+ _elderIdMap.update { result }
_elderIds.value = result
Log.d("CallTimeViewModel", "elderIds 업데이트: $result")
}📝 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 _elderIdMap = MutableStateFlow(emptyMap<Int, String>()) | |
| val elderIdMap = _elderIdMap.asStateFlow() | |
| // Time data | |
| private val _timeMap = MutableStateFlow<Map<Int, CallTimes>>(emptyMap()) | |
| val timeMap: StateFlow<Map<Int, CallTimes>> = _timeMap.asStateFlow() | |
| private val _showBottomSheet = mutableStateOf(false) | |
| private val _selectedIndex = mutableIntStateOf(0) | |
| private val _selectedTabIndex = mutableIntStateOf(0) | |
| // UI state | |
| private val _showBottomSheet = MutableStateFlow(false) | |
| val showBottomSheet: StateFlow<Boolean> = _showBottomSheet.asStateFlow() | |
| private val _selectedIndex = MutableStateFlow(0) | |
| val selectedIndex: StateFlow<Int> = _selectedIndex.asStateFlow() | |
| init { | |
| viewModelScope.launch { | |
| _elderIdMap.update { elderIdRepository.getElderIds().first(); } | |
| } | |
| } | |
| private val _selectedTabIndex = MutableStateFlow(0) | |
| val selectedTabIndex: StateFlow<Int> = _selectedTabIndex.asStateFlow() | |
| // Flow -> State 로 뱐환해서 보관 | |
| private val _elderIds = mutableStateOf<Map<Int, String>>(emptyMap()) | |
| val elderIds get() = _elderIds.value // UI에서 접근할 값 | |
| // Async state | |
| private val _isLoading = MutableStateFlow(false) | |
| val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow() | |
| init { | |
| observeElderIds() | |
| } | |
| private val _lastError = MutableStateFlow<Throwable?>(null) | |
| val lastError: StateFlow<Throwable?> = _lastError.asStateFlow() | |
| // suspend + Flow 안전하게 처리하는 함수 | |
| private fun observeElderIds() { | |
| viewModelScope.launch { | |
| try { | |
| elderIdRepository.getElderIds() | |
| .collect { result -> | |
| _elderIds.value = result | |
| Log.d("CallTimeViewModel", "elderIds 업데이트: $result") | |
| } | |
| private val _elderIdMap = MutableStateFlow(emptyMap<Int, String>()) | |
| val elderIdMap = _elderIdMap.asStateFlow() | |
| private val _showBottomSheet = mutableStateOf(false) | |
| private val _selectedIndex = mutableIntStateOf(0) | |
| private val _selectedTabIndex = mutableIntStateOf(0) | |
| // Flow -> State 로 뱐환해서 보관 | |
| private val _elderIds = mutableStateOf<Map<Int, String>>(emptyMap()) | |
| val elderIds get() = _elderIds.value // UI에서 접근할 값 | |
| init { | |
| observeElderIds() | |
| } | |
| // suspend + Flow 안전하게 처리하는 함수 | |
| private fun observeElderIds() { | |
| viewModelScope.launch { | |
| try { | |
| elderIdRepository.getElderIds() | |
| .collect { result -> | |
| _elderIdMap.update { result } | |
| _elderIds.value = result | |
| Log.d("CallTimeViewModel", "elderIds 업데이트: $result") | |
| } |
🤖 Prompt for AI Agents
In
`@app/src/main/java/com/konkuk/medicarecall/ui/feature/login/carecall/viewmodel/CallTimeViewModel.kt`
around lines 29 - 58, The code maintains two separate elder ID states
(_elderIdMap / elderIds) causing staleness; unify them by using a single source
of truth or synchronizing updates: remove the one-off update that uses
_elderIdMap.update { elderIdRepository.getElderIds().first() } and instead
update _elderIdMap inside the same collect flow used by observeElderIds (or
collapse to only _elderIdMap and expose it as a State/StateFlow), ensuring all
updates come from elderIdRepository.getElderIds() in viewModelScope.launch
within observeElderIds so _elderIdMap, _elderIds (or the single retained state)
remain consistent.
🔗 관련 이슈
Summary by CodeRabbit
릴리스 노트
✏️ Tip: You can customize this high-level summary in your review settings.