fix(clinical): 조건 완료 판정을 실행 단위 → 조합 단위 합산으로
화면에서 6조합 중 일부만 골라 돌릴 수 있게 되면서(다음 커밋) 판정이 깨진다. 종전에는 실행 하나만 보고 "6조합이 다 있나"를 따졌는데, 나눠 재면 어느 실행도 6개를 못 채워 **영원히 PARTIAL** 로 남는다. 그러면 "덜 됐다"는 표시가 의미를 잃는다. 같은 자세·충만도의 여러 실행을 조합 단위로 합산한다. 조합 하나가 여러 실행에 나오면 한 번이라도 계획을 채운 실행이 있으면 그 조합은 끝난 것으로 본다 — 20회 중 3회에서 끊긴 뒤 다시 20회를 채웠다면 완료다. 판정 전부를 progressFrom(manifests) 순수 함수로 떼어 시험으로 고정했다. 이 판정이 틀리면 간호사가 다 됐다고 믿고 방광을 비우는데 데이터는 못 쓰고, 그 단계는 그날 다시 못 잰다. 실기기 없이도 고정해 둬야 한다. 나눠 재기 · 재측정 · 계획 미달 · planned 없는 옛 매니페스트 · 깨진 JSON · 조건 간 혼선 8건. org.json 은 안드로이드 단위테스트에서 모든 메서드가 스텁이라(isReturnDefaultValues 로도 안 된다 — 반환값이 아니라 동작이 없다) 실제 구현을 테스트 의존성으로 넣었다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -220,31 +220,59 @@ object HospitalRunStore {
|
||||
* 매니페스트는 `finally` 에서 쓰므로 중단된 실행에도 남는다. 그 경우 조합이
|
||||
* 모자라거나 saved < planned 라 PARTIAL 로 잡힌다.
|
||||
*
|
||||
* 같은 조건을 여러 번 실행했으면(정상적인 재측정) **가장 좋은 결과**를 취한다.
|
||||
* 한 번이라도 제대로 받았으면 그 조건은 끝난 것이다.
|
||||
* 같은 조건의 여러 실행은 **조합 단위로 합산한다.**
|
||||
*
|
||||
* 실행 하나만 보고 판정하면 안 된다 — 화면에서 조합을 골라 나눠 잴 수 있기 때문이다
|
||||
* (2.3MHz·c5 만 먼저 재고 나머지를 나중에). 실행별로 보면 어느 것도 6조합을 채우지
|
||||
* 못해 영원히 PARTIAL 로 남고, 그러면 "덜 됐다"는 표시가 의미를 잃는다.
|
||||
*
|
||||
* 조합 하나가 여러 실행에 나오면(재측정) **한 번이라도 계획을 채운 실행**이 있으면
|
||||
* 그 조합은 끝난 것으로 본다. 20회 중 3회에서 끊긴 뒤 다시 20회를 채웠다면 그
|
||||
* 조합은 완료다.
|
||||
*
|
||||
* @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 texts = dir.listFiles { f ->
|
||||
f.isFile && f.name.startsWith("run_") && f.name.endsWith(".json")
|
||||
}?.mapNotNull { runCatching { it.readText(Charsets.UTF_8) }.getOrNull() } ?: emptyList()
|
||||
return progressFrom(texts)
|
||||
}
|
||||
|
||||
/**
|
||||
* 매니페스트 본문들 → 조건별 진행 상태. **판정 전부가 여기 있다.**
|
||||
*
|
||||
* 파일 읽기와 분리해 둔 이유는 시험 때문이다. 이 판정이 틀리면 간호사가 "다 됐다"고
|
||||
* 믿고 방광을 비우는데 데이터는 못 쓰는 상황이 되므로, 실기기 없이도 고정해 둬야 한다.
|
||||
*/
|
||||
internal fun progressFrom(manifests: List<String>): Map<Pair<String, Int>, RunProgress> {
|
||||
// (자세, 충만도) → 계획을 채운 조합들. 조합 식별자는 "주파수옵션/cycle" 이다.
|
||||
val filled = HashMap<Pair<String, Int>, MutableSet<String>>()
|
||||
// 매니페스트는 있는데 채운 조합이 하나도 없는 경우도 PARTIAL 로 남겨야 한다.
|
||||
val seen = HashSet<Pair<String, Int>>()
|
||||
manifests.forEach { text ->
|
||||
runCatching {
|
||||
val o = JSONObject(text)
|
||||
val key = o.getString("posture") to o.getInt("fill_pct")
|
||||
seen.add(key)
|
||||
val combos = o.getJSONArray("combinations")
|
||||
for (i in 0 until combos.length()) {
|
||||
val c = combos.getJSONObject(i)
|
||||
// planned 가 없으면 판정할 수 없다 — 채운 것으로 보지 않는다.
|
||||
val planned = c.optInt("planned", Int.MAX_VALUE)
|
||||
if (c.optInt("saved") >= planned) {
|
||||
filled.getOrPut(key) { HashSet() }
|
||||
.add("${c.optInt("freq_option")}/${c.optInt("cycles")}")
|
||||
}
|
||||
val st = if (done) RunProgress.DONE else RunProgress.PARTIAL
|
||||
// 재측정 — 더 좋은 쪽으로만 올린다.
|
||||
if (out[key] != RunProgress.DONE) out[key] = st
|
||||
}
|
||||
}
|
||||
}
|
||||
val out = HashMap<Pair<String, Int>, RunProgress>()
|
||||
for (key in seen) {
|
||||
val n = filled[key]?.size ?: 0
|
||||
out[key] = if (n >= EXPECTED_COMBINATIONS) RunProgress.DONE else RunProgress.PARTIAL
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user