Skip to content

Conversation

@ProtossManse
Copy link
Collaborator

@ProtossManse ProtossManse commented Oct 3, 2025

🔗 관련 이슈

📙 작업 설명

  • 기존에는 각 텍스트 필드의 값을 리스트로 관리한 뒤, 화면 전환 시 그 리스트를 어르신 데이터 클래스에 담아 전달했지만, 이제는 처음부터 어르신 및 건강정보 객체를 만들어 값을 바로 저장하도록 변경하여, 화면 이동 시 별도의 변환 과정 없이 그대로 전달할 수 있도록 했습니다.
  • UiState를 새로 만들어서 UiState에서 상태를 관리하는 방식으로 변경했습니다.
  • 기존에는 컴포넌트로 분리된 컴포저블 함수에 Mutable한 값을 넘겨 상태를 컴포넌트 내부에서 수정했다면, 이제는 이벤트만을 전달해서 모든 상태 변화는 viewmodel에서 처리하도록 수정했습니다.
  • 추가로, 상태 관리가 변경되면서 일부 UI의 변경이 있었습니다. 어르신 정보 등록 화면에서 어르신을 여러 명 등록했을 때의 UI가 Chip을 사용한 변경된 디자인으로 바뀌었습니다.

Summary by CodeRabbit

  • 신기능
    • 시니어 로그인에 ElderChip 추가 및 ElderInputForm 개편(미리보기 포함).
    • 새 ViewModel과 UI 상태(LoginElderViewModel, LoginElderUiState, LoginElderHealthUiState) 도입으로 다중 보호자/건강정보 입력, 선택, 제거, 콜백 기반 추가/삭제 지원.
  • 리팩터링
    • DiseaseNamesItem, MedicationItem을 불변 데이터+콜백 기반의 무상태 컴포저블로 변경.
    • 시니어 정보/건강정보 화면을 상태 수집 기반으로 전환, LazyRow 칩 UI·포커스 제어·검증 흐름 개선.
    • 성별(Boolean) 및 모델 기본값 정리(Nullable 제거, 기본값 제공).
    • 기존 LoginElderViewModel 제거 및 네비게이션/임포트 경로 정리.

- 오로지 이벤트만 전달하는 것으로 수정.
- viewmodel: 각 텍스트필드별 함수 추가
- screen: UiState의 값 참조하도록 변경, 어르신 목록 UI 구현
- UiState: 어르신 리스트에 기본 객체 추가, Index 정보 가지고 있게 구현
- ElderMedInfoScreen으로 이동하기 전 어르신 수 만큼 어르신 건강정보 리스트 초기화
- `uiState.value =` 으로 대입. -> `uiState.update`로 변경
@coderabbitai
Copy link

coderabbitai bot commented Oct 3, 2025

Walkthrough

로그인(시니어) 플로우의 상태 관리를 전면 개편. 뷰모델을 패키지 이동·재구성하고, UiState(개인/건강) 도입. 여러 컴포넌트와 스크린을 콜백 기반·불변 데이터로 리팩터링. 성별(Boolean)과 모델 기본값 정리. 일부 파일은 import 경로만 수정.

Changes

Cohort / File(s) Summary
Import 경로 정리
app/src/main/java/com/konkuk/medicarecall/MainActivity.kt, app/src/main/java/com/konkuk/medicarecall/navigation/NavGraph.kt
LoginElderViewModel import를 ...senior.viewmodel.*로 변경. 기능 변화 없음.
UI 컴포넌트 무상태화(Stateless)·콜백화
app/src/main/java/com/konkuk/medicarecall/ui/common/component/DiseaseNamesItem.kt, .../MedicationItem.kt, .../GenderToggleButton.kt
- 입력/리스트를 불변 타입(String, List)으로 변경하고 콜백(onTextChanged, onAdd..., onRemove...) 추가.
- GenderToggleButtonisMale를 비널(Boolean)로 단순화.
시니어 로그인 VM 재구성
.../ui/feature/login/senior/LoginElderViewModel.kt(removed), .../ui/feature/login/senior/viewmodel/LoginElderViewModel.kt, .../viewmodel/LoginElderUiState.kt, .../viewmodel/LoginElderHealthUiState.kt
- 기존 VM 파일 제거 및 viewmodel 패키지로 신규 VM 추가.
- LoginElderUiState/LoginElderHealthUiState 도입(StateFlow 공개).
- 엘더/건강 데이터 업데이트·선택·등록/갱신 로직 정리.
시니어 로그인 스크린 상태 연동
.../senior/screen/LoginElderInfoScreen.kt, .../senior/screen/LoginElderMedInfoScreen.kt
- collectAsState 기반으로 UiState 구독.
- Elder 선택/추가/삭제 및 건강 항목(Disease/Medication/Notes) 연동을 콜백으로 교체.
- CTA 플로우를 신규 VM 메서드 시퀀스로 업데이트.
시니어 UI 컴포넌트 추가/갱신
.../senior/component/ElderChip.kt, .../senior/component/ElderInputForm.kt
- ElderChip 신규 추가(선택/삭제 지원).
- ElderInputForm을 단일 ElderData + 콜백 API로 변경, Preview 추가.
도메인 모델 기본값 정리
app/src/main/java/com/konkuk/medicarecall/ui/model/ElderData.kt, .../ElderHealthData.kt
- ElderData.gender 기본값 true 설정.
- ElderHealthData 컬렉션 필드에 기본값 추가(빈 컬렉션).
기타 스크린 성별 상태 단순화
.../feature/settings/screen/ElderDetailScreen.kt, .../MyDetailScreen.kt
- 성별 상태를 비널(Boolean)로 변경. 관련 널 체크 제거 가능.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant Info as LoginElderInfoScreen
  participant Med as LoginElderMedInfoScreen
  participant VM as LoginElderViewModel
  participant EldersRepo as EldersInfoRepository
  participant IdRepo as ElderIdRepository
  participant RegRepo as ElderRegisterRepository

  U->>Info: 입력/추가/선택/삭제
  Info->>VM: updateElder*/addElder/removeElder/selectElder
  VM-->>Info: elderUiState(StateFlow)

  U->>Med: 질병/복약/노트 편집
  Med->>VM: updateDiseasesText/addDisease/removeDisease
  Med->>VM: selectMedicationTime/updateMedicationText/addMedication/removeMedication
  Med->>VM: addHealthNote/removeHealthNote
  VM-->>Med: elderHealthUiState(StateFlow)

  U->>Med: 완료(제출)
  Med->>VM: updateAllElders
  VM->>IdRepo: getElderIds()
  VM->>EldersRepo: updateElders(data with ids)
  VM-->>Med: 업데이트 결과 로그

  Med->>VM: updateAllEldersHealthInfo
  VM->>IdRepo: getElderIds()
  VM->>RegRepo: updateHealthInfo(per elder)
  VM-->>Med: 결과 로그

  Med->>VM: postElderAndHealth
  VM->>RegRepo: postElderAndHealth(data)
  RegRepo-->>VM: success/failure
  VM-->>Med: 결과 로그/콜백
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested labels

refactor

Suggested reviewers

  • alswlekk

Poem

칩 톡톡, 이름은 반짝, 성별은 true/false 딱!
리스트는 고정, 콜백은 즉각, 상태는 플로우로 착착.
옛 VM은 인사하고, 새 UiState가 막 출항. ⛵
약과 질병, 노트도 정리—로그인 여정, 더 깔끔한 길로 쓱!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.88% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 “[Refactor] 온보딩 상태 관리 방식 변경 #115”은 온보딩(로그인) 과정의 상태 관리 방식을 리팩토링하는 핵심 내용을 명확하게 반영하고 있어 변경의 주된 목적을 이해하기에 충분합니다
Linked Issues Check ✅ Passed 이 PR은 리스트 기반의 상태 관리 대신 ElderData와 ElderHealthData 객체를 도입하고 LoginElderUiState 및 LoginElderHealthUiState를 구현했으며 ViewModel 내부 상태를 StateFlow로 변경해 모든 UI 상태를 관리하도록 리팩토링하고 객체 생성 코드를 수정해 화면 간 변환 없이 인스턴스를 전달하도록 요구사항을 모두 충족합니다
Out of Scope Changes Check ✅ Passed 이 PR에서 수행된 모든 변경은 로그인/온보딩 흐름의 UiState 도입, ViewModel 상태 이전, 컴포저블 이벤트 기반 처리 등 링크된 이슈의 범위에 부합하며 공통 컴포넌트 API 변경에 따른 호출 지점 업데이트 외에 별도 기능 변경은 없습니다
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/onboarding-state-#115

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ProtossManse ProtossManse self-assigned this Oct 3, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/screen/LoginElderMedInfoScreen.kt (1)

190-206: 비동기 작업을 suspend 파이프라인으로 묶고 완료 후 네비게이션

  • ViewModel에 submitAll() 같은 suspend 함수로 3개 작업을 순차/병렬 제어하고 예외 처리
  • UI에서는 coroutineScope.launch { if (viewModel.submitAll().isSuccess) navigate() else showError() } 형태로 호출
🧹 Nitpick comments (11)
app/src/main/java/com/konkuk/medicarecall/ui/feature/settings/screen/MyDetailScreen.kt (2)

113-113: 불필요한 null 체크 제거 권장

isMale이 이제 non-nullable Boolean 타입이므로 isMale != null 체크는 항상 true이고 의미가 없습니다. 이 조건을 제거하는 것이 좋겠습니다.

                 type = if (name.matches(Regex("^[가-힣a-zA-Z]*$"))
                     && birth.length == 8
                     && birth.isValidDate()
-                    && isMale != null
                 ) CTAButtonType.GREEN else CTAButtonType.DISABLED,

117-117: Boolean 체크 단순화 가능

isMale == true는 그냥 isMale로 단순화할 수 있습니다. 가독성이 약간 개선됩니다.

-                    val gender = if (isMale == true) GenderType.MALE else GenderType.FEMALE
+                    val gender = if (isMale) GenderType.MALE else GenderType.FEMALE
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/component/ElderChip.kt (1)

41-45: 선택 컴포넌트는 clickable 대신 selectable 사용 + InteractionSource null 지양

  • 선택 상태를 보조공학에 노출하려면 Modifier.selectable을 쓰는 게 맞습니다. 현재 clickable은 선택 여부를 스크린리더가 알기 어렵습니다.
  • interactionSource = null 대신 remember { MutableInteractionSource() } 사용 권장.

예시 diff:

-            .clickable(
-                indication = null,
-                interactionSource = null,
-                onClick = onClick,
-            ),
+            .selectable(
+                selected = selected,
+                onClick = onClick,
+                role = androidx.compose.ui.semantics.Role.Tab,
+                interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() },
+                indication = null,
+            ),

참고: selectable 사용 시 import androidx.compose.foundation.selection.selectable 필요.

app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderHealthUiState.kt (1)

6-12: selectedIndex 범위 불변식 보장 필요

elderHealthList 변경(추가/삭제) 시 selectedIndex가 항상 0..lastIndex를 유지하도록 ViewModel에서 강제해 주세요. 삭제 시 특히 index < selectedIndex인 경우 selectedIndex를 -1 보정해야 선택 항목이 바뀌지 않습니다.

app/src/main/java/com/konkuk/medicarecall/ui/model/ElderHealthData.kt (1)

6-10: 내부 가변 프로퍼티(var id) 제거 권장 (UiState 일관성/성능)

UiState 계열 모델에서 가변 필드는 재구성/동등성 비교를 어렵게 합니다. 가능하면 불변으로 두고 copy로 갱신하세요. Compose 최적화를 위해 @immutable 또는 @stable도 고려해 주세요.

예시 diff:

-    var id: Int? = null,
+    val id: Int? = null,

(필요 시 copy(id = …)로 갱신)

app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/screen/LoginElderInfoScreen.kt (2)

68-69: collectAsStateWithLifecycle 사용 권장

수명 주기 인지 수집으로 누수/불필요 리컴포지션을 줄이세요.

예시:

val uiState by loginElderViewModel.elderUiState.collectAsStateWithLifecycle()

(의존성: androidx.lifecycle:lifecycle-runtime-compose)


243-244: 내부 위젯에 외부 modifier 전달 지양

CTAButton의 modifier에 화면의 modifier를 재사용하면 예기치 못한 결합이 생깁니다. 로컬 Modifier를 사용하세요.

예시 diff:

-                    modifier.padding(bottom = 20.dp),
+                    Modifier.padding(bottom = 20.dp),
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderViewModel.kt (2)

95-99: 선택 인덱스 범위 가드 추가 권장

외부에서 잘못된 index가 들어오면 이후 접근에서 IOB 위험이 있습니다.

 fun selectElder(index: Int) {
-    _elderUiState.update { state ->
-        state.copy(selectedIndex = index)
-    }
+    _elderUiState.update { state ->
+        val bounded = index.coerceIn(0, (state.eldersList.lastIndex).coerceAtLeast(0))
+        state.copy(selectedIndex = bounded)
+    }
 }

118-126: 입력 검증 강화 제안(숫자 형식)

길이만 체크하면 형식 오류가 섞여 들어올 수 있습니다. 숫자만 허용하도록 보강하세요.

 return elderUiState.value.eldersList.all {
-    it.name.isNotBlank() &&
-        it.birthDate.length == 8 &&
-        it.phoneNumber.length == 11 &&
+    it.name.isNotBlank() &&
+        it.birthDate.length == 8 && it.birthDate.all(Char::isDigit) &&
+        it.phoneNumber.length == 11 && it.phoneNumber.all(Char::isDigit) &&
         it.relationship.isNotBlank() &&
         it.livingType.isNotBlank()
 }
app/src/main/java/com/konkuk/medicarecall/ui/common/component/MedicationItem.kt (1)

143-151: 약 추가 후 선택된 시간대도 해제하면 UX가 더 자연스럽습니다

주석상 “입력 필드와 선택된 시간 초기화”지만 현재는 입력만 초기화됩니다. stateless 패턴을 유지하며 토글 콜백으로 해제할 수 있어요.

 clickPlus = {
     if (inputText.isNotBlank() && selectedList.isNotEmpty()) {
         selectedList.forEach { time ->
             onAddMedication(time, inputText)
         }
-        // 사용성 개선: 약 추가 후 입력 필드와 선택된 시간 초기화
-        onTextChange("")
+        // 사용성 개선: 약 추가 후 입력/선택 초기화
+        onTextChange("")
+        selectedList.forEach { time -> onSelectTime(time) } // 선택 해제
     }
 },
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/screen/LoginElderMedInfoScreen.kt (1)

91-94: scrollState 중복 선언(섀도잉)

바깥 scrollState와 동일 이름을 내부에서 다시 선언해 섀도잉되고 있습니다. 재사용 또는 변수명 정리로 혼동을 줄이는 편이 좋습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28817aa and 63510e9.

📒 Files selected for processing (18)
  • app/src/main/java/com/konkuk/medicarecall/MainActivity.kt (1 hunks)
  • app/src/main/java/com/konkuk/medicarecall/navigation/NavGraph.kt (1 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/common/component/DiseaseNamesItem.kt (1 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/common/component/GenderToggleButton.kt (2 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/common/component/MedicationItem.kt (5 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/feature/login/info/viewmodel/LoginViewModel.kt (6 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/LoginElderViewModel.kt (0 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/component/ElderChip.kt (1 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/component/ElderInputForm.kt (1 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/screen/LoginElderInfoScreen.kt (6 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/screen/LoginElderMedInfoScreen.kt (4 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderHealthUiState.kt (1 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderUiState.kt (1 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderViewModel.kt (1 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/feature/settings/screen/ElderDetailScreen.kt (1 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/feature/settings/screen/MyDetailScreen.kt (1 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/model/ElderData.kt (1 hunks)
  • app/src/main/java/com/konkuk/medicarecall/ui/model/ElderHealthData.kt (1 hunks)
💤 Files with no reviewable changes (1)
  • app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/LoginElderViewModel.kt
🧰 Additional context used
🧬 Code graph analysis (6)
app/src/main/java/com/konkuk/medicarecall/ui/common/component/DiseaseNamesItem.kt (2)
app/src/main/java/com/konkuk/medicarecall/ui/common/component/ChipItem.kt (1)
  • ChipItem (22-52)
app/src/main/java/com/konkuk/medicarecall/ui/common/component/AddTextField.kt (1)
  • AddTextField (20-66)
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/component/ElderInputForm.kt (3)
app/src/main/java/com/konkuk/medicarecall/ui/common/component/DefaultTextField.kt (1)
  • DefaultTextField (18-64)
app/src/main/java/com/konkuk/medicarecall/ui/common/component/GenderToggleButton.kt (1)
  • GenderToggleButton (21-87)
app/src/main/java/com/konkuk/medicarecall/ui/common/component/DefaultDropDown.kt (1)
  • DefaultDropdown (41-175)
app/src/main/java/com/konkuk/medicarecall/ui/common/component/MedicationItem.kt (1)
app/src/main/java/com/konkuk/medicarecall/ui/common/component/AddTextField.kt (1)
  • AddTextField (20-66)
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/screen/LoginElderMedInfoScreen.kt (5)
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/info/component/LoginBackButton.kt (1)
  • LoginBackButton (17-37)
app/src/main/java/com/konkuk/medicarecall/ui/common/component/DiseaseNamesItem.kt (1)
  • DiseaseNamesItem (17-69)
app/src/main/java/com/konkuk/medicarecall/ui/common/component/MedicationItem.kt (1)
  • MedicationItem (60-155)
app/src/main/java/com/konkuk/medicarecall/ui/common/component/ChipItem.kt (1)
  • ChipItem (22-52)
app/src/main/java/com/konkuk/medicarecall/ui/common/component/DefaultSnackBar.kt (1)
  • DefaultSnackBar (17-37)
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/screen/LoginElderInfoScreen.kt (4)
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/info/component/LoginBackButton.kt (1)
  • LoginBackButton (17-37)
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/component/ElderChip.kt (1)
  • ElderChip (24-77)
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/component/ElderInputForm.kt (1)
  • ElderInputForm (27-120)
app/src/main/java/com/konkuk/medicarecall/ui/common/component/DefaultSnackBar.kt (1)
  • DefaultSnackBar (17-37)
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/component/ElderChip.kt (1)
app/src/main/java/com/konkuk/medicarecall/ui/theme/Theme.kt (1)
  • MediCareCallTheme (38-60)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ci-build
🔇 Additional comments (10)
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/info/viewmodel/LoginViewModel.kt (2)

50-50: 성별 기본값이 남성(true)으로 설정되었는지 확인해 주세요.

이제 isMale이 non-nullable Boolean으로 변경되고 기본값이 true로 설정되었습니다. 이것이 의도된 비즈니스 로직인지 확인이 필요합니다. 성별을 명시적으로 선택하도록 유도하는 것이 더 나을 수도 있습니다.


70-72: LGTM!

onGenderChanged가 non-nullable Boolean을 받도록 변경되어 null 처리가 필요 없어졌습니다. GenderToggleButton의 변경사항과 일관성 있게 정리되었네요.

app/src/main/java/com/konkuk/medicarecall/ui/common/component/GenderToggleButton.kt (1)

22-87: LGTM!

GenderToggleButton이 nullable Boolean 대신 non-nullable Boolean을 사용하도록 깔끔하게 리팩터링되었습니다. 기본값 true를 제공하여 사용처에서 초기화가 간편해졌고, 콜백도 non-nullable로 통일되어 일관성이 좋습니다.

app/src/main/java/com/konkuk/medicarecall/ui/common/component/DiseaseNamesItem.kt (1)

17-69: LGTM!

DiseaseNamesItem이 stateless 컴포넌트로 훌륭하게 리팩터링되었습니다. 이제 내부에서 상태를 직접 변경하지 않고 콜백을 통해 상위로 이벤트를 전달하는 구조로, 단방향 데이터 플로우와 ViewModel 중심의 상태 관리 원칙에 부합합니다.

app/src/main/java/com/konkuk/medicarecall/navigation/NavGraph.kt (1)

43-43: LGTM!

LoginElderViewModel의 import 경로가 viewmodel 패키지로 이동되었습니다. 패키지 구조 개선 작업의 일환으로 보이며, 로직 변경 없이 경로만 업데이트되었습니다.

app/src/main/java/com/konkuk/medicarecall/MainActivity.kt (1)

43-43: LGTM!

MainActivity에서도 LoginElderViewModel import 경로가 업데이트되었습니다. 패키지 재구성 작업과 일관성 있게 적용되었네요.

app/src/main/java/com/konkuk/medicarecall/ui/feature/settings/screen/ElderDetailScreen.kt (1)

65-65: LGTM!

isMale 상태가 non-nullable Boolean으로 변경되었습니다. GenderToggleButton의 API 변경과 일관성 있게 적용되었고, 초기값도 enum에서 Boolean으로 매핑되어 안전합니다.

app/src/main/java/com/konkuk/medicarecall/ui/model/ElderData.kt (1)

6-6: 성별 기본값 설정을 확인해 주세요.

gender 필드에 기본값 true(남성)가 추가되어 ElderData() 생성 시 성별을 명시하지 않아도 됩니다. 이것이 의도된 것인지, 아니면 성별 선택을 필수로 하는 것이 더 나은지 검토가 필요할 수 있습니다.

app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderUiState.kt (1)

1-8: LGTM!

새로운 LoginElderUiState 클래스가 추가되어 어르신 로그인 플로우의 UI 상태를 중앙 집중식으로 관리할 수 있게 되었습니다. eldersListselectedIndex로 상태를 깔끔하게 표현하고 있으며, 기본값 제공으로 초기화가 간편합니다. ViewModel 기반 상태 관리 리팩터링의 핵심 부분이네요.

app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/screen/LoginElderMedInfoScreen.kt (1)

94-116: 선택 인덱스 분리 사용(elders vs health)이 의도인지 확인 필요

상단 칩 하이라이트는 uiState.selectedIndex(health) 기준이며, elderUiState의 selectedIndex와는 별개로 움직입니다. 의도된 분리라면 OK, 아니라면 동기화/단일 소스화를 고려해 주세요.

Comment on lines +61 to +74
Icon(
painter = painterResource(id = R.drawable.ic_close),
contentDescription = "remove",
modifier = Modifier
.size(16.dp)
.clickable(
indication = null,
interactionSource = null,
onClick = { onRemove() },
),
tint =
if (selected) MediCareCallTheme.colors.main
else MediCareCallTheme.colors.gray3,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

삭제 아이콘 탭 영역 48dp 이상 확보 + IconButton 사용 권장

  • 현재 size(16.dp) + clickable은 터치 타깃이 너무 작아 접근성/사용성 문제를 유발합니다.
  • IconButton은 최소 터치 영역을 보장합니다. 또한 contentDescription는 stringResource로 i18n 권장.

예시 diff:

-            Icon(
-                painter = painterResource(id = R.drawable.ic_close),
-                contentDescription = "remove",
-                modifier = Modifier
-                    .size(16.dp)
-                    .clickable(
-                        indication = null,
-                        interactionSource = null,
-                        onClick = { onRemove() },
-                    ),
-                tint =
-                    if (selected) MediCareCallTheme.colors.main
-                    else MediCareCallTheme.colors.gray3,
-            )
+            androidx.compose.material3.IconButton(
+                onClick = onRemove,
+                modifier = Modifier.size(40.dp) // 최소 터치 타깃 확보
+            ) {
+                Icon(
+                    painter = painterResource(id = R.drawable.ic_close),
+                    contentDescription = stringResource(id = R.string.accessibility_remove_chip),
+                    tint = if (selected) MediCareCallTheme.colors.main else MediCareCallTheme.colors.gray3,
+                    modifier = Modifier.size(16.dp)
+                )
+            }

주의: R.string.accessibility_remove_chip 추가 필요.

🤖 Prompt for AI Agents
In
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/component/ElderChip.kt
around lines 61 to 74, the delete icon uses Icon with size(16.dp) + clickable
which yields a touch target smaller than the recommended 48dp and a hardcoded
contentDescription; replace the Icon+clickable with an IconButton (or a Box with
minimum touch size) to guarantee at least a 48.dp touch target, preserve the
tint logic based on selected, call onRemove from the IconButton onClick, and
change contentDescription to stringResource(R.string.accessibility_remove_chip)
(add that string resource if missing) to support i18n/accessibility.

Comment on lines +40 to +43
Column(
verticalArrangement = Arrangement.Center,
) {
Column(
verticalArrangement = Arrangement.Center
) {
DefaultTextField(
value = loginElderViewModel.nameList[index],
onValueChange = { loginElderViewModel.onNameChanged(index, it) },
category = "이름",
placeHolder = "이름"
)
Spacer(Modifier.height(20.dp))
DefaultTextField(
loginElderViewModel.dateOfBirthList[index],
{ input ->
val filtered = input.filter { it.isDigit() }.take(8)
loginElderViewModel.onDOBChanged(index, filtered)
},
category = "생년월일",
placeHolder = "YYYY / MM / DD",
keyboardType = KeyboardType.Number,
visualTransformation = DateOfBirthVisualTransformation(),
maxLength = 8
DefaultTextField(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

modifier 파라미터 미사용

호출처에서 전달한 modifier가 반영되지 않습니다. Column에 전달하세요.

예시 diff:

-    Column(
-        verticalArrangement = Arrangement.Center,
-    ) {
+    Column(
+        modifier = modifier,
+        verticalArrangement = Arrangement.Center,
+    ) {
📝 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.

Suggested change
Column(
verticalArrangement = Arrangement.Center,
) {
Column(
verticalArrangement = Arrangement.Center
) {
DefaultTextField(
value = loginElderViewModel.nameList[index],
onValueChange = { loginElderViewModel.onNameChanged(index, it) },
category = "이름",
placeHolder = "이름"
)
Spacer(Modifier.height(20.dp))
DefaultTextField(
loginElderViewModel.dateOfBirthList[index],
{ input ->
val filtered = input.filter { it.isDigit() }.take(8)
loginElderViewModel.onDOBChanged(index, filtered)
},
category = "생년월일",
placeHolder = "YYYY / MM / DD",
keyboardType = KeyboardType.Number,
visualTransformation = DateOfBirthVisualTransformation(),
maxLength = 8
DefaultTextField(
Column(
modifier = modifier,
verticalArrangement = Arrangement.Center,
) {
DefaultTextField(
🤖 Prompt for AI Agents
In
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/component/ElderInputForm.kt
around lines 40 to 43, the passed modifier parameter is not applied because
Column is created without it; update the Column call to accept and forward the
modifier argument (e.g., Column(modifier = modifier, verticalArrangement =
Arrangement.Center) { ... }) so the caller's modifier is honored.

Comment on lines +110 to +120
itemsIndexed(uiState.eldersList) { index, it ->
ElderChip(
name = it.name,
selected = index == selectedIndex,
onRemove = {
if (selectedIndex == uiState.eldersList.size - 1)
loginElderViewModel.selectElder(selectedIndex - 1)
loginElderViewModel.removeElder(index)
},
onClick = { loginElderViewModel.selectElder(index) },
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

어르신 삭제 시 selectedIndex 보정 로직 보강 필요

현재는 “선택된 항목이 마지막인 경우”만 보정합니다. 삭제 대상이 선택 인덱스보다 앞이면 선택 항목이 한 칸 밀려 의도와 달라집니다. 삭제 후 인덱스를 일관되게 보정하세요.

예시 diff:

-                                onRemove = {
-                                    if (selectedIndex == uiState.eldersList.size - 1)
-                                        loginElderViewModel.selectElder(selectedIndex - 1)
-                                    loginElderViewModel.removeElder(index)
-                                },
+                                onRemove = {
+                                    val lastIndex = uiState.eldersList.lastIndex
+                                    val newSelected = when {
+                                        index == selectedIndex && selectedIndex == lastIndex -> selectedIndex - 1
+                                        index < selectedIndex -> selectedIndex - 1
+                                        else -> selectedIndex
+                                    }.coerceAtLeast(0)
+                                    loginElderViewModel.removeElder(index)
+                                    loginElderViewModel.selectElder(newSelected)
+                                },
📝 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.

Suggested change
itemsIndexed(uiState.eldersList) { index, it ->
ElderChip(
name = it.name,
selected = index == selectedIndex,
onRemove = {
if (selectedIndex == uiState.eldersList.size - 1)
loginElderViewModel.selectElder(selectedIndex - 1)
loginElderViewModel.removeElder(index)
},
onClick = { loginElderViewModel.selectElder(index) },
)
itemsIndexed(uiState.eldersList) { index, it ->
ElderChip(
name = it.name,
selected = index == selectedIndex,
onRemove = {
val lastIndex = uiState.eldersList.lastIndex
val newSelected = when {
// removed the selected item at the end → move selection back
index == selectedIndex && selectedIndex == lastIndex ->
selectedIndex - 1
// removed an item before the selected one → shift selection back
index < selectedIndex ->
selectedIndex - 1
// in all other cases, keep selection
else ->
selectedIndex
}.coerceAtLeast(0)
loginElderViewModel.removeElder(index)
loginElderViewModel.selectElder(newSelected)
},
onClick = { loginElderViewModel.selectElder(index) },
)
🤖 Prompt for AI Agents
In
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/screen/LoginElderInfoScreen.kt
around lines 110 to 120, the selectedIndex correction only handles the case when
the selected item is the last one; you must also adjust the selected index when
a removed item is before the current selection. Compute the newSelectedIndex
based on the removed index: if removedIndex < selectedIndex then decrement
selectedIndex by 1, else if selectedIndex == uiState.eldersList.size - 1 and
removing that last item then decrement, otherwise keep it; ensure
newSelectedIndex is bounded >= 0; call
loginElderViewModel.selectElder(newSelectedIndex) (or none if list becomes
empty) and then remove the elder (or compute then update viewModel in the
correct order) so selection stays consistent after removal.

Comment on lines +230 to 236
else if (!uiState.eldersList.filter { it.phoneNumber.isNotEmpty() }
.all { it.phoneNumber.startsWith("010") })
coroutineScope.launch {
snackBarState.showSnackbar(
"휴대폰 번호를 다시 확인해주세요",
duration = SnackbarDuration.Short
duration = SnackbarDuration.Short,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

휴대폰 번호 검증 강화 필요

현재는 "010" 시작만 확인하여 길이 11 미만도 통과합니다. 정규식으로 11자리 보장하세요.

예시 diff:

-                        else if (!uiState.eldersList.filter { it.phoneNumber.isNotEmpty() }
-                                .all { it.phoneNumber.startsWith("010") })
+                        else if (!uiState.eldersList.filter { it.phoneNumber.isNotEmpty() }
+                                .all { it.phoneNumber.matches(Regex("^010\\d{8}$")) })
📝 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.

Suggested change
else if (!uiState.eldersList.filter { it.phoneNumber.isNotEmpty() }
.all { it.phoneNumber.startsWith("010") })
coroutineScope.launch {
snackBarState.showSnackbar(
"휴대폰 번호를 다시 확인해주세요",
duration = SnackbarDuration.Short
duration = SnackbarDuration.Short,
)
else if (!uiState.eldersList
.filter { it.phoneNumber.isNotEmpty() }
.all { it.phoneNumber.matches(Regex("^010\\d{8}$")) })
coroutineScope.launch {
snackBarState.showSnackbar(
"휴대폰 번호를 다시 확인해주세요",
duration = SnackbarDuration.Short,
)
🤖 Prompt for AI Agents
In
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/screen/LoginElderInfoScreen.kt
around lines 230 to 236, the current check only ensures phone numbers start with
"010" and allows shorter than 11 digits; replace that predicate with a regex
check that non-empty phone numbers must match exactly 11 digits starting with
010 (e.g. use pattern "^010\\d{8}$") so only valid 11-digit numbers pass, and
keep the snackbar behavior the same when the validation fails.

Comment on lines +59 to +63
val elderUiState by loginElderViewModel.elderUiState.collectAsState()

val uiState by loginElderViewModel.elderHealthUiState.collectAsState()
val selectedIndex = uiState.selectedIndex

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

elderHealthList 직접 인덱싱 범위 체크 없음 → IOB 위험

selectedIndex가 리스트 범위를 벗어나면 크래시 납니다. 안전 바인딩으로 치환을 권장합니다.

-    val uiState by loginElderViewModel.elderHealthUiState.collectAsState()
-    val selectedIndex = uiState.selectedIndex
+    val uiState by loginElderViewModel.elderHealthUiState.collectAsState()
+    val selectedIndex = uiState.selectedIndex
+    val currentHealth = uiState.elderHealthList.getOrNull(selectedIndex) ?: return
@@
-    DiseaseNamesItem(
-        inputText = uiState.diseaseInputText,
-        diseaseList = uiState.elderHealthList[selectedIndex].diseaseNames,
+    DiseaseNamesItem(
+        inputText = uiState.diseaseInputText,
+        diseaseList = currentHealth.diseaseNames,
@@
-    MedicationItem(
-        medicationSchedule = uiState.elderHealthList[selectedIndex].medicationMap,
+    MedicationItem(
+        medicationSchedule = currentHealth.medicationMap,
@@
-    if (uiState.elderHealthList[selectedIndex].notes.isNotEmpty()) {
+    if (currentHealth.notes.isNotEmpty()) {
@@
-        uiState.elderHealthList[selectedIndex].notes.forEach { note ->
+        currentHealth.notes.forEach { note ->

Also applies to: 136-144, 147-155, 165-173

🤖 Prompt for AI Agents
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/screen/LoginElderMedInfoScreen.kt
lines 59-63 (also fix similar usages at 136-144, 147-155, 165-173): the code
reads uiState.selectedIndex and directly indexes elderHealthList which can crash
if selectedIndex is out of bounds; replace direct indexing with safe access
(check that selectedIndex is within 0 until elderHealthList.size or use
elderHealthList.getOrNull(selectedIndex) ?: /* fallback */) and provide a
sensible fallback UI/state (e.g., null-handling, default selectedIndex, or early
return) so out-of-range indices do not cause IOOB exceptions; apply the same
safe-access pattern to the other referenced line ranges.

Comment on lines +101 to +108
fun addElder() {
_elderUiState.update { state ->
state.copy(
eldersList = state.eldersList + ElderData(),
selectedIndex = state.selectedIndex + 1,
)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

어르신 추가/삭제 시 건강정보 상태와 동기화되지 않아 인덱스 불일치/IOB 위험

  • addElder/removeElder가 elderHealthUiState를 함께 갱신하지 않습니다. 이후 화면에서 elderHealthList[selectedIndex] 접근 시 크기 불일치로 IOB가 납니다.
  • 삭제 시 selectedIndex 보정도 필요합니다.

다음과 같이 두 UiState를 함께 업데이트해 주세요.

 fun addElder() {
-    _elderUiState.update { state ->
-        state.copy(
-            eldersList = state.eldersList + ElderData(),
-            selectedIndex = state.selectedIndex + 1,
-        )
-    }
+    _elderUiState.update { state ->
+        val newSelected = state.eldersList.size // 추가 후 마지막 인덱스
+        state.copy(
+            eldersList = state.eldersList + ElderData(),
+            selectedIndex = newSelected,
+        )
+    }
+    _elderHealthUiState.update { state ->
+        val newList = state.elderHealthList + ElderHealthData()
+        state.copy(
+            elderHealthList = newList,
+            selectedIndex = newList.lastIndex,
+        )
+    }
 }

 fun removeElder(index: Int) {
-    _elderUiState.update { state ->
-        state.copy(
-            eldersList = state.eldersList.filterIndexed { i, _ -> i != index },
-        )
-    }
+    _elderUiState.update { state ->
+        val newList = state.eldersList.filterIndexed { i, _ -> i != index }
+        val newSelected = when {
+            newList.isEmpty() -> 0
+            state.selectedIndex > newList.lastIndex -> newList.lastIndex
+            else -> state.selectedIndex
+        }
+        state.copy(eldersList = newList, selectedIndex = newSelected)
+    }
+    _elderHealthUiState.update { state ->
+        val newList = state.elderHealthList.filterIndexed { i, _ -> i != index }
+        val newSelected = when {
+            newList.isEmpty() -> 0
+            state.selectedIndex > newList.lastIndex -> newList.lastIndex
+            else -> state.selectedIndex
+        }
+        state.copy(elderHealthList = newList, selectedIndex = newSelected)
+    }
 }

Also applies to: 110-116

🤖 Prompt for AI Agents
In
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderViewModel.kt
around lines 101-108 (and similarly 110-116), addElder/removeElder currently
only updates elderUiState causing elderHealthUiState length to get out of sync
and leading to index-out-of-bounds when accessing
elderHealthList[selectedIndex]; update both UiStates together: when adding,
append a new default ElderHealthData to elderHealthUiState and increment
selectedIndex to point to the new item; when removing, remove the corresponding
element from elderHealthUiState and adjust selectedIndex to remain within
0..(newList.size-1) (and avoid negative values); perform the two updates in
sequence/atomically so both lists remain synchronized.

Comment on lines +135 to +139
fun initElderHealthData() {
_elderHealthUiState.update { state ->
state.copy(elderHealthList = List(elderUiState.value.eldersList.size) { ElderHealthData() })
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

initElderHealthData 호출 시점 보강 필요

지금은 수동 호출 전제입니다. 초기화/리스트 변경(add/remove) 시점에 자동 동기화가 안전합니다.

 // flows 선언 아래에 추가
+init {
+    initElderHealthData()
+}

또는 add/remove에서 직접 동기화(위 코멘트의 diff 참고).


#!/bin/bash
rg -n 'initElderHealthData' -C2 app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderViewModel.kt || true
rg -n 'init\s*{' -C2 app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderViewModel.kt || true


🏁 Script executed:

#!/bin/bash
rg -n 'initElderHealthData' -C2 app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderViewModel.kt || true
rg -n 'init\s*{' -C2 app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderViewModel.kt || true

Length of output: 314


#!/bin/bash
rg -nP '^[[:space:]]*init\s*\{' -C2 app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderViewModel.kt || true
rg -n 'init{' -C2 app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderViewModel.kt || true

🏁 Script executed:

#!/bin/bash
rg -nP '^[[:space:]]*init\s*\{' -C2 app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderViewModel.kt || true
rg -nP '\binitElderHealthData\s*\(' -C2 app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderViewModel.kt || true

Length of output: 234


initElderHealthData 자동 호출 추가
ViewModel 초기화(init 블록)에 initElderHealthData() 호출을 추가하고, eldersList 변경(add/remove) 시에도 동일 로직을 호출하세요.

Comment on lines +218 to +238
fun addMedication(time: MedicationTimeType?, medicine: String) {
if (time == null) return

_elderHealthUiState.update { state ->
state.copy(
elderHealthList = state.elderHealthList.mapIndexed { index, elder ->
if (index == state.selectedIndex) {
val currentList = elder.medicationMap[time] ?: emptyList()

val updatedMap =
elder.medicationMap + if (medicine !in (elder.medicationMap[time]
?: emptyList())
) (time to (currentList + medicine)) else return
elder.copy(medicationMap = updatedMap)
} else {
elder
}
},
)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

addMedication의 비지역 return으로 상태 업데이트가 중단될 수 있음

mapIndexed 내부 else return은 비지역 return으로 함수 전체를 빠져나가 update 자체가 실행되지 않을 수 있어 가독성과 안정성 모두 떨어집니다. 중복이면 현재 elder만 그대로 반환하도록 재구성하세요.

 fun addMedication(time: MedicationTimeType?, medicine: String) {
     if (time == null) return
-
-    _elderHealthUiState.update { state ->
-        state.copy(
-            elderHealthList = state.elderHealthList.mapIndexed { index, elder ->
-                if (index == state.selectedIndex) {
-                    val currentList = elder.medicationMap[time] ?: emptyList()
-
-                    val updatedMap =
-                        elder.medicationMap + if (medicine !in (elder.medicationMap[time]
-                                ?: emptyList())
-                        ) (time to (currentList + medicine)) else return
-                    elder.copy(medicationMap = updatedMap)
-                } else {
-                    elder
-                }
-            },
-        )
-    }
+    val trimmed = medicine.trim()
+    if (trimmed.isEmpty()) return
+    _elderHealthUiState.update { state ->
+        state.copy(
+            elderHealthList = state.elderHealthList.mapIndexed { index, elder ->
+                if (index == state.selectedIndex) {
+                    val currentList = elder.medicationMap[time].orEmpty()
+                    if (trimmed in currentList) elder
+                    else elder.copy(
+                        medicationMap = elder.medicationMap + (time to (currentList + trimmed))
+                    )
+                } else elder
+            }
+        )
+    }
 }
📝 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.

Suggested change
fun addMedication(time: MedicationTimeType?, medicine: String) {
if (time == null) return
_elderHealthUiState.update { state ->
state.copy(
elderHealthList = state.elderHealthList.mapIndexed { index, elder ->
if (index == state.selectedIndex) {
val currentList = elder.medicationMap[time] ?: emptyList()
val updatedMap =
elder.medicationMap + if (medicine !in (elder.medicationMap[time]
?: emptyList())
) (time to (currentList + medicine)) else return
elder.copy(medicationMap = updatedMap)
} else {
elder
}
},
)
}
}
fun addMedication(time: MedicationTimeType?, medicine: String) {
if (time == null) return
val trimmed = medicine.trim()
if (trimmed.isEmpty()) return
_elderHealthUiState.update { state ->
state.copy(
elderHealthList = state.elderHealthList.mapIndexed { index, elder ->
if (index == state.selectedIndex) {
val currentList = elder.medicationMap[time].orEmpty()
if (trimmed in currentList) elder
else elder.copy(
medicationMap = elder.medicationMap + (time to (currentList + trimmed))
)
} else elder
}
)
}
}

Comment on lines +292 to +307
fun updateAllElders() { // getElderIds.isNotEmpty == true
viewModelScope.launch {
val elderIds = elderIdRepository.getElderIds()
elderIds.filterIndexed { index, it ->
it.values.first() == elderUiState.value.eldersList[index].id
}.forEachIndexed { index, it ->
eldersInfoRepository.updateElder(
it.values.first(), elderUiState.value.eldersList[index],
).onSuccess {
Log.d("httplog", "어르신 재등록(수정) 성공")
}.onFailure { exception ->
Log.e("httplog", "어르신 정보 등록 실패: ${exception.message}")


}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

updateAllElders: filterIndexed → forEachIndexed로 인덱스가 재시작하여 elder 매칭이 틀어집니다 (OOB 위험 포함)

  • filterIndexed { index, it -> ... }.forEachIndexed { index, it -> ... }에서 두 번째 index는 필터된 리스트 기준으로 0부터 다시 시작합니다. 결과적으로 잘못된 elder를 업데이트하거나 IOB가 날 수 있습니다.
  • eldersList[index] 접근도 getOrNull로 안전하게.
 fun updateAllElders() { // getElderIds.isNotEmpty == true
     viewModelScope.launch {
         val elderIds = elderIdRepository.getElderIds()
-        elderIds.filterIndexed { index, it ->
-            it.values.first() == elderUiState.value.eldersList[index].id
-        }.forEachIndexed { index, it ->
-            eldersInfoRepository.updateElder(
-                it.values.first(), elderUiState.value.eldersList[index],
-            ).onSuccess {
+        val elders = elderUiState.value.eldersList
+        elderIds.forEachIndexed { idx, entry ->
+            val id = entry.values.first()
+            val elder = elders.getOrNull(idx) ?: return@forEachIndexed
+            eldersInfoRepository.updateElder(id, elder).onSuccess {
                 Log.d("httplog", "어르신 재등록(수정) 성공")
             }.onFailure { exception ->
                 Log.e("httplog", "어르신 정보 등록 실패: ${exception.message}")
 
             }
-        }
+        }
 
     }
 }
📝 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.

Suggested change
fun updateAllElders() { // getElderIds.isNotEmpty == true
viewModelScope.launch {
val elderIds = elderIdRepository.getElderIds()
elderIds.filterIndexed { index, it ->
it.values.first() == elderUiState.value.eldersList[index].id
}.forEachIndexed { index, it ->
eldersInfoRepository.updateElder(
it.values.first(), elderUiState.value.eldersList[index],
).onSuccess {
Log.d("httplog", "어르신 재등록(수정) 성공")
}.onFailure { exception ->
Log.e("httplog", "어르신 정보 등록 실패: ${exception.message}")
}
}
fun updateAllElders() { // getElderIds.isNotEmpty == true
viewModelScope.launch {
val elderIds = elderIdRepository.getElderIds()
val elders = elderUiState.value.eldersList
elderIds.forEachIndexed { idx, entry ->
val id = entry.values.first()
val elder = elders.getOrNull(idx) ?: return@forEachIndexed
eldersInfoRepository.updateElder(id, elder).onSuccess {
Log.d("httplog", "어르신 재등록(수정) 성공")
}.onFailure { exception ->
Log.e("httplog", "어르신 정보 등록 실패: ${exception.message}")
}
}
}
}

Comment on lines +314 to +330
fun updateAllEldersHealthInfo() {
viewModelScope.launch {
val elderIds = elderIdRepository.getElderIds()
elderIds.filterIndexed { index, it ->
it.values.first() == elderHealthUiState.value.elderHealthList[index].id
}.forEachIndexed { index, it ->
runCatching {
elderRegisterRepository.postElderHealthInfo(
it.values.first(),
elderHealthUiState.value.elderHealthList[index],
)
}.onSuccess {
Log.d("httplog", "어르신 건강정보 재등록(수정) 성공")
}
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

updateAllEldersHealthInfo도 동일한 인덱스 드리프트/에러 누락

  • 같은 패턴의 인덱스 드리프트가 있어 잘못된 매칭 혹은 IOB가 발생할 수 있습니다.
  • 실패 로그가 없습니다.
 fun updateAllEldersHealthInfo() {
     viewModelScope.launch {
         val elderIds = elderIdRepository.getElderIds()
-        elderIds.filterIndexed { index, it ->
-            it.values.first() == elderHealthUiState.value.elderHealthList[index].id
-        }.forEachIndexed { index, it ->
-            runCatching {
-                elderRegisterRepository.postElderHealthInfo(
-                    it.values.first(),
-                    elderHealthUiState.value.elderHealthList[index],
-                )
-            }.onSuccess {
-                Log.d("httplog", "어르신 건강정보 재등록(수정) 성공")
-            }
-        }
+        val healthList = elderHealthUiState.value.elderHealthList
+        elderIds.forEachIndexed { idx, entry ->
+            val id = entry.values.first()
+            val health = healthList.getOrNull(idx) ?: return@forEachIndexed
+            runCatching {
+                elderRegisterRepository.postElderHealthInfo(id, health)
+            }.onSuccess {
+                Log.d("httplog", "어르신 건강정보 재등록(수정) 성공")
+            }.onFailure { e ->
+                Log.e("httplog", "어르신 건강정보 등록 실패: ${e.message}")
+            }
+        }
     }
 }
📝 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.

Suggested change
fun updateAllEldersHealthInfo() {
viewModelScope.launch {
val elderIds = elderIdRepository.getElderIds()
elderIds.filterIndexed { index, it ->
it.values.first() == elderHealthUiState.value.elderHealthList[index].id
}.forEachIndexed { index, it ->
runCatching {
elderRegisterRepository.postElderHealthInfo(
it.values.first(),
elderHealthUiState.value.elderHealthList[index],
)
}.onSuccess {
Log.d("httplog", "어르신 건강정보 재등록(수정) 성공")
}
}
}
}
fun updateAllEldersHealthInfo() {
viewModelScope.launch {
val elderIds = elderIdRepository.getElderIds()
val healthList = elderHealthUiState.value.elderHealthList
elderIds.forEachIndexed { idx, entry ->
val id = entry.values.first()
val health = healthList.getOrNull(idx) ?: return@forEachIndexed
runCatching {
elderRegisterRepository.postElderHealthInfo(id, health)
}
.onSuccess {
Log.d("httplog", "어르신 건강정보 재등록(수정) 성공")
}
.onFailure { e ->
Log.e("httplog", "어르신 건강정보 등록 실패: ${e.message}")
}
}
}
}
🤖 Prompt for AI Agents
In
app/src/main/java/com/konkuk/medicarecall/ui/feature/login/senior/viewmodel/LoginElderViewModel.kt
around lines 314 to 330, the code matches elderIds to elderHealthUiState by
relying on the same index (risking index drift/IOB) and does not log failures;
instead, build a lookup (map) from elderHealthUiState.value.elderHealthList by
id, then iterate elderIds and for each id find the corresponding UI state via
that map (skip or log if not found), call
elderRegisterRepository.postElderHealthInfo with the matched id and state, and
add onFailure/exception handling to log the error details so failures are
visible.

Copy link
Collaborator

@librawish808 librawish808 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 고생하셨습니다!

@alswlekk alswlekk merged commit ac36653 into develop Oct 8, 2025
2 checks passed
@alswlekk alswlekk deleted the refactor/onboarding-state-#115 branch October 8, 2025 03:18
Copy link
Contributor

@ikseong00 ikseong00 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다!

Comment on lines +28 to +32
private val _elderUiState = MutableStateFlow(LoginElderUiState())
val elderUiState: StateFlow<LoginElderUiState> = _elderUiState.asStateFlow()

private val _elderHealthUiState = MutableStateFlow(LoginElderHealthUiState())
val elderHealthUiState: StateFlow<LoginElderHealthUiState> = _elderHealthUiState.asStateFlow()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 두 개를 합쳐서 그냥 LoginElderUiState() 로 묶는 건 어떨까요??
각자 다른 뷰모델에서 사용된다면 분리하는 게 맞겠지만,

하나의 뷰모델에서 선언되고 뷰모델의 생명주기와 동일하게 간다면, 굳이 따로 가져가기보다 합치는 게 해당 UiState 를 수정하거나 사용할 때 편리할 것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

viewModel 함수 주석에 달아놨었는데, 이후 viewmodel을 분리하는 게 나을 것 같아서 별도의 UiState로 구현해두었습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] 로그인 플로우 상태 관리 방식 변경

5 participants