feat(clinical): 병원 임상 모드 — 어디까지 측정했는지 칩에 표시
간호사가 자세·방광 단계를 하나씩 올려 가며 재는데, 중간에 자리를 비우거나 순서가 꼬이면 "0% 했던가?"를 기억으로 판단하게 된다는 현장 의견. 그러면 한 단계를 통째로 빠뜨리거나(방광을 다시 채워야 한다) 같은 조건을 두 번 재게 된다. 화면이 대신 기억하게 한다. - 방광 단계 칩: 지금 고른 자세 안에서의 진행. 완료 ✓(초록) / 일부 ·(주황 테두리) - 자세 칩: 그 자세의 6단계가 전부 끝나야 완료 - 실행이 끝날 때마다 다시 읽어 방금 잰 단계가 즉시 채워진다 판정 근거는 **파일 존재가 아니라 실행 매니페스트**다. CSV 는 첫 회차만 저장돼도 만들어지므로, 파일 유무로 보면 20회 중 2회에서 끊긴 것도 "완료"로 보인다. 그게 제일 위험하다 — 다 됐다고 믿고 방광을 비우는데 데이터는 못 쓰고 그 단계는 그날 다시 못 잰다. 매니페스트의 조합별 planned/saved 로 "6조합이 전부 계획한 횟수를 채웠는가"를 본다. 중단된 실행도 매니페스트가 남으므로 PARTIAL 로 잡힌다. 완료/일부를 색뿐 아니라 기호(✓ ·)로도 구분한다 — 병실 조명·화면 반사에서 색만으로는 잘 안 보인다. 검증: 실기기(VBT26080001 연결)에서 오늘 09:40 실측 데이터로 확인. (Supine, 0%) → 0% 칩 ✓ · Supine 칩 · (6단계 중 1단계라 일부). 선택을 20% 로 옮기면 0% 가 초록 채움으로 남는 것까지 확인. 측정 경로는 건드리지 않았다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -149,4 +149,60 @@ object HospitalRunStore {
|
||||
.writeText(json.toString(2), Charsets.UTF_8)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 진행 현황 ────────────────────────────────────────────────────────
|
||||
//
|
||||
// 간호사가 자세·방광 단계를 하나씩 올려 가며 재는데, 중간에 자리를 비우거나
|
||||
// 순서가 꼬이면 "0% 했던가?"를 기억으로 판단하게 된다. 그러면 한 단계를 통째로
|
||||
// 빠뜨리거나(방광을 다시 채워야 한다) 같은 조건을 두 번 재게 된다.
|
||||
// 화면이 대신 기억하게 한다.
|
||||
|
||||
/** 한 조건(자세 × 방광 단계)의 진행 상태. */
|
||||
enum class RunProgress { NONE, PARTIAL, DONE }
|
||||
|
||||
/**
|
||||
* 판정 근거는 **파일 존재가 아니라 실행 매니페스트**다.
|
||||
*
|
||||
* CSV 파일은 첫 회차만 저장돼도 만들어진다. 파일 유무로 판단하면 20회 중 2회에서
|
||||
* 끊긴 것도 "완료"로 보이고, 그게 제일 위험하다 — 간호사는 다 됐다고 믿고 방광을
|
||||
* 비우는데 데이터는 못 쓴다. 매니페스트에는 조합별 planned/saved 가 남으므로
|
||||
* "6조합이 전부 계획한 횟수를 채웠는가"를 정확히 볼 수 있다.
|
||||
*
|
||||
* 매니페스트는 `finally` 에서 쓰므로 중단된 실행에도 남는다. 그 경우 조합이
|
||||
* 모자라거나 saved < planned 라 PARTIAL 로 잡힌다.
|
||||
*
|
||||
* 같은 조건을 여러 번 실행했으면(정상적인 재측정) **가장 좋은 결과**를 취한다.
|
||||
* 한 번이라도 제대로 받았으면 그 조건은 끝난 것이다.
|
||||
*
|
||||
* @return (자세 label, 방광 %) → 진행 상태
|
||||
*/
|
||||
fun scanProgress(patient: String, day: Date): Map<Pair<String, Int>, RunProgress> {
|
||||
val dir = runDirIfExists(patient, day) ?: return emptyMap()
|
||||
val out = HashMap<Pair<String, Int>, RunProgress>()
|
||||
dir.listFiles { f -> f.isFile && f.name.startsWith("run_") && f.name.endsWith(".json") }
|
||||
?.forEach { f ->
|
||||
runCatching {
|
||||
val o = JSONObject(f.readText(Charsets.UTF_8))
|
||||
val key = o.getString("posture") to o.getInt("fill_pct")
|
||||
val combos = o.getJSONArray("combinations")
|
||||
// 6조합이 다 있고, 각 조합이 계획한 횟수를 채웠을 때만 DONE.
|
||||
var done = combos.length() == EXPECTED_COMBINATIONS
|
||||
for (i in 0 until combos.length()) {
|
||||
val c = combos.getJSONObject(i)
|
||||
if (c.optInt("saved") < c.optInt("planned", Int.MAX_VALUE)) done = false
|
||||
}
|
||||
val st = if (done) RunProgress.DONE else RunProgress.PARTIAL
|
||||
// 재측정 — 더 좋은 쪽으로만 올린다.
|
||||
if (out[key] != RunProgress.DONE) out[key] = st
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** 읽기 전용 조회 — [runDir] 과 달리 폴더를 만들지 않는다. */
|
||||
private fun runDirIfExists(patient: String, day: Date): File? =
|
||||
File(File(downloads(), ROOT), "${dayFmt.format(day)}_${safe(patient)}").takeIf { it.isDirectory }
|
||||
|
||||
/** 주파수 2종 × cycle 3종. [HOSPITAL_COMBINATIONS] 와 같은 수여야 한다. */
|
||||
private const val EXPECTED_COMBINATIONS = 6
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user