feat(clinical): 정렬이 안 끝나도 임상 측정으로 넘어갈 수 있게

실제 인체에서 정렬 로직이 끝까지 안 맞을 수 있다. CH3 가 어느 위치에서도 안 잡히거나
(REATTACH), 상한까지 올라가도 nch 가 붕괴하지 않아 종료 조건이 안 걸린다. 그런데 진행
버튼이 두 군데서 막혀 있었다.

    탐색 중(!done)           버튼이 **없다** — 측정 버튼만 있다
    REATTACH 판정            버튼이 **비활성** (enabled = action == STOP)

후자가 특히 나쁘다. REATTACH 도 done 이라 2단계 화면으로 넘어가는데, 거기서 진행 버튼이
회색이고 "처음부터 다시 정렬" 밖에 없다. 환자는 누워 있고 방광은 계속 차는데 앱이
다음 화면을 안 내주는 막다른 길이다.

진행 버튼을 단계 밖으로 빼서 **항상 활성**으로 두었다. 막는 조건은 측정·좌우 스캔 중
뿐이다 — 그때 나가면 프로브 설정 복원이 끊겨 그 위치 데이터가 반쪽이 된다. 넘기는
위치는 `last?.anchorCm ?: cm`, 둘 다 "프로브가 지금 있는 자리"라 화면 숫자와 기록이
어긋나지 않는다.

**막지 않는 대신 근거를 같이 들고 간다.** AnchorBasis 를 새로 만들었다:

    algorithm           알고리즘이 best 확정 (STOP)
    reattach_override   재부착 권고를 무시하고 진행
    manual              탐색 도중 사람이 결정

이게 이 커밋에서 제일 중요한 부분이다. 위치(cm)만 들고 가면 사람이 고른 자리와
알고리즘이 고른 자리가 기록에서 한 덩어리가 된다. 그러면 "기존 초음파 측정기 대비
정확도"를 낼 때 사람이 고른 자리의 오차가 알고리즘의 오차로 계산되고, 나중에 둘을
가를 단서가 없어 **데이터 전체가 주장을 받치지 못한다.**

그래서 근거를 세 곳에 남긴다: appState(화면), 600 매니페스트, 601 align_result.json
(`anchor_basis` · `proceed_anchor_cm`). LABDB_DATATYPES.md 에 "정확도 분석에는
algorithm 만 추려야 한다"를 표로 박았다 — 서버 쪽이 세션 목록에서 필터를 걸 수 있다.

화면도 근거에 따라 갈린다. 진행 버튼은 algorithm 일 때만 초록이고 나머지는 주황,
AnchorCard 도 같은 규칙이다. 같은 색으로 두면 간호사가 "정렬 끝났다"로 읽는다.
버튼 아래 한 줄로 무엇을 건너뛰는지 말한다(proceedNote) — 다 맞췄으면 아무 말도 안
한다. 늘 뜨는 경고는 곧 무시당한다.

lateral 요약에 `confirmed` 를 추가했다. 좌우를 맞추는 중에 넘어가면 commit 은 있어도
확인은 없는데, 기존 `done` 하나로는 그 둘이 구분되지 않아 확인한 세션으로 잘못 읽힌다.

테스트 110개 통과(신규 11). AnchorAction 이 늘어나면 조용히 MANUAL 로 떨어지므로
entries.size 를 고정해 그때 이 결정을 다시 보게 했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 13:55:52 +09:00
parent 2853127ff5
commit d66b69506f
7 changed files with 334 additions and 26 deletions
@@ -46,6 +46,16 @@ class AppState(private val context: Context) {
*/
var anchorCm: Int? by mutableStateOf(null)
/**
* [anchorCm] 을 **누가** 정했는지. null = 정렬을 거치지 않았다.
*
* 정렬은 실제 인체에서 끝까지 안 맞을 수 있어 진행을 막지 않는다. 그래서 위치만
* 들고 다니면 알고리즘이 고른 자리와 사람이 고른 자리가 한 덩어리로 쌓이고, 나중에
* 정확도 검증의 모집단을 가려낼 수 없다. **항상 [anchorCm] 과 같이 비우고 같이
* 넣는다** — 하나만 남으면 그 순간부터 둘이 어긋난다.
*/
var anchorBasis: com.medithings.vesiscan.managers.AnchorBasis? by mutableStateOf(null)
/**
* 병원 임상 환자명. 화면 상태가 아니라 여기 둔다.
*
@@ -0,0 +1,59 @@
/*
* AnchorBasis — 임상 측정에 쓰는 부착 위치를 **누가** 정했는지.
*/
package com.medithings.vesiscan.managers
/**
* 부착 위치가 어떤 근거로 정해졌는가.
*
* ## 왜 필요한가
* 정렬 알고리즘은 실제 인체에서 끝까지 안 맞을 수 있다. 체형·방광 충만도·젤 도포에 따라
* CH3 가 끝내 안 잡히거나([AnchorAction.REATTACH]), 탐색 상한까지 가도 nch 가 붕괴하지
* 않아 종료 조건이 안 걸린다. 그때 진행을 막으면 **임상 자체가 멈춘다** — 환자는 누워
* 있고 방광은 계속 차는데 앱이 다음 화면을 안 내주는 상황이다.
*
* 그래서 언제든 진행할 수 있게 뚫어 두되, 그렇게 정한 위치와 알고리즘이 확정한 위치를
* **같은 것으로 쌓지 않는다.** 검증의 주장이 "알고리즘이 고른 위치에서 이런 BV 가
* 나왔다"인데 거기에 사람이 고른 위치가 섞이면, 나중에 둘을 가를 방법이 없어 데이터
* 전체가 주장을 받치지 못한다.
*
* 위치(cm)와 이 근거는 **항상 같이** 다닌다 — 화면 표시, 600 매니페스트, 601 요약.
*/
enum class AnchorBasis {
/** 알고리즘이 best 를 확정했다([AnchorAction.STOP]). */
ALGORITHM,
/** 알고리즘은 재부착을 요구했는데([AnchorAction.REATTACH]) 그대로 진행했다. */
REATTACH_OVERRIDE,
/** 탐색이 끝나기 전에 사람이 이 위치로 결정했다. */
MANUAL;
/** 기록에 쓰는 값. 분석 스크립트가 문자열로 비교하므로 고정이다. */
val wire: String get() = name.lowercase()
/** 사람이 읽는 한 줄. 화면과 기록이 같은 말을 쓴다. */
val label: String get() = when (this) {
ALGORITHM -> "알고리즘 확정"
REATTACH_OVERRIDE -> "재부착 권고 무시"
MANUAL -> "수동 지정 (탐색 미완료)"
}
/**
* 알고리즘이 확정한 위치인가.
*
* 정확도 검증(항목 ①)의 모집단은 **이것이 true 인 세션만**이다. 나머지는 "진행을
* 막지 않아 데이터가 남았다"는 의미이지 알고리즘의 성능 근거가 아니다.
*/
val isAlgorithm: Boolean get() = this == ALGORITHM
companion object {
/** 정렬 상태기의 마지막 판정으로부터. `null` = 한 위치도 재지 않았다. */
fun of(action: AnchorAction?): AnchorBasis = when (action) {
AnchorAction.STOP -> ALGORITHM
AnchorAction.REATTACH -> REATTACH_OVERRIDE
// MOVE_UP·MOVE_DOWN·null — 탐색이 아직 답을 내지 않은 상태다.
else -> MANUAL
}
}
}
@@ -48,6 +48,7 @@ import com.medithings.vesiscan.ble.BleManager
import com.medithings.vesiscan.managers.AlignAction
import com.medithings.vesiscan.managers.AlignmentConstants
import com.medithings.vesiscan.managers.AnchorAction
import com.medithings.vesiscan.managers.AnchorBasis
import com.medithings.vesiscan.managers.AnchorConfig
import com.medithings.vesiscan.managers.AnchorGuide
import com.medithings.vesiscan.managers.AnchorPosRecord
@@ -409,6 +410,7 @@ fun AnchorAlignView(appState: AppState) {
onClick = {
if (isConnected) {
appState.anchorCm = null
appState.anchorBasis = null
bleManager.disconnect()
} else {
appState.connectReturnScreen = AppScreen.ANCHOR_ALIGN
@@ -627,32 +629,70 @@ fun AnchorAlignView(appState: AppState) {
buildAlignSummary(
saveName, appState.hospitalPatient, deviceName,
bleManager.firmwareVersion.value, guide.records, s,
lrCommit,
lrCommit, lateralConfirmed = true,
),
)
}
}
},
)
Spacer(Modifier.height(16.dp))
}
Button(
onClick = {
appState.anchorCm = last?.anchorCm
appState.currentScreen = AppScreen.HOSPITAL_MODE
},
enabled = last?.action == AnchorAction.STOP && !lrRunning,
modifier = Modifier.fillMaxWidth().height(52.dp),
shape = RoundedCornerShape(14.dp),
colors = ButtonDefaults.buttonColors(containerColor = MlSuccess),
) { Text("이 위치로 임상 측정 진행", fontSize = 16.sp, fontWeight = FontWeight.Bold) }
// 막지는 않는다. lateral 이 끝내 안 잡히는 환자도 있는데 그때 진행을
// 못 하게 하면 임상 자체가 멈춘다 — 상태만 분명히 보여 준다.
if (!lrConfirmed) {
Spacer(Modifier.height(6.dp))
Text("좌우 정렬을 아직 확인하지 않았습니다.",
fontSize = 12.sp, color = MlSecondaryText)
}
Spacer(Modifier.height(16.dp))
// -- 임상 측정으로 넘어가기 ------------------------------------------
// **단계와 무관하게 항상 누를 수 있다.** 정렬이 실제 인체에서 끝까지 안 맞는
// 경우가 있다 - CH3 가 어느 위치에서도 안 잡히거나, 상한까지 올라가도 nch 가
// 붕괴하지 않아 종료가 안 걸린다. 그때 막으면 환자를 눕혀 둔 채 임상이 멈춘다.
//
// 대신 **근거를 같이 들고 간다**([AnchorBasis]). 사람이 고른 위치와 알고리즘이
// 확정한 위치가 기록에서 구분되지 않으면, 정확도 검증의 모집단을 나중에
// 가려낼 수 없어 데이터 전체가 주장을 받치지 못한다.
val basis = AnchorBasis.of(last?.action)
// 확정됐으면 그 값(best+offset), 아니면 지금 화면의 위치. 둘 다 "프로브가
// 지금 있는 자리"라는 뜻이라 간호사가 보는 숫자와 기록이 어긋나지 않는다.
val proceedCm = last?.anchorCm ?: cm
Button(
onClick = {
appState.anchorCm = proceedCm
appState.anchorBasis = basis
// 근거를 요약에도 박는다. 화면에만 있으면 나중에 파일을 보는 사람이
// 이 위치가 어떻게 정해졌는지 알 길이 없다.
last?.let { s ->
runCatching {
val dir = HospitalRunStore.alignDir(saveName, runStartedAt)
HospitalRunStore.writeAlignSummary(
dir,
buildAlignSummary(
saveName, appState.hospitalPatient, deviceName,
bleManager.firmwareVersion.value, guide.records, s,
lrCommit, lrConfirmed, basis, proceedCm,
),
)
}
}
appState.currentScreen = AppScreen.HOSPITAL_MODE
},
// 측정·좌우 스캔 중에만 막는다. 그때 나가면 프로브 설정 복원이 끊기고
// 그 위치 데이터가 반쪽이 된다.
enabled = !measuring && !lrRunning && !uploadBusy,
modifier = Modifier.fillMaxWidth().height(52.dp),
shape = RoundedCornerShape(14.dp),
// 알고리즘 확정일 때만 초록이다. 사람이 고른 경우를 같은 색으로 두면
// 간호사가 "정렬이 끝났다"고 읽는다.
colors = ButtonDefaults.buttonColors(
containerColor = if (basis.isAlgorithm) MlSuccess else MlWarning,
),
) {
Text("이 위치(${proceedCm}cm)로 임상 측정 진행",
fontSize = 16.sp, fontWeight = FontWeight.Bold)
}
proceedNote(basis, lrConfirmed)?.let { note ->
Spacer(Modifier.height(6.dp))
Text(note, fontSize = 12.sp, color = MlSecondaryText)
}
if (done) {
Spacer(Modifier.height(8.dp))
TextButton(
onClick = {
@@ -858,6 +898,25 @@ private fun RecordRow(r: AnchorPosRecord, isBest: Boolean) {
Spacer(Modifier.height(4.dp))
}
/**
* 진행 버튼 아래 한 줄 - **무엇을 모르고 넘어가는지** 말한다.
*
* 버튼을 항상 열어 두기로 한 대가다. 막지 않는 대신, 누르는 사람이 자기가 무엇을
* 건너뛰는지는 알아야 한다. 알고리즘이 확정하고 좌우까지 확인했으면 할 말이 없다(null).
*/
internal fun proceedNote(basis: AnchorBasis, lateralConfirmed: Boolean): String? {
val head = when (basis) {
AnchorBasis.ALGORITHM -> null
AnchorBasis.REATTACH_OVERRIDE ->
"알고리즘은 재부착을 권고했습니다 - 그대로 진행하면 기록에 그 사실이 남습니다."
AnchorBasis.MANUAL ->
"정렬 탐색이 끝나지 않았습니다 - 이 위치는 사람이 고른 것으로 기록됩니다."
}
val tail = if (lateralConfirmed) null else "좌우 정렬은 아직 확인하지 않았습니다."
val parts = listOfNotNull(head, tail)
return if (parts.isEmpty()) null else parts.joinToString(" ")
}
/**
* 정렬 결과 요약 JSON. 위치별 지표와 선택을 **판정 근거 그대로** 남긴다.
*
@@ -874,6 +933,15 @@ private fun buildAlignSummary(
step: AnchorStep,
/** 좌우 정렬 결과. null = 아직 돌리지 않았음(= 상하만 맞춘 세션). */
lateral: LateralGuide.Commit? = null,
/** 간호사가 "좌우 확인 완료"를 눌렀는가. [lateral] 이 있는 것과 다른 사실이다. */
lateralConfirmed: Boolean = false,
/**
* 이 위치로 임상을 진행한 근거. null = 이 화면에서 진행하지 않았다(측정 중 갱신본).
* 진행 버튼을 누르는 순간 덮어쓰므로, 남은 null 은 "정렬만 하고 나갔다"는 뜻이다.
*/
basis: AnchorBasis? = null,
/** 실제로 임상에 넘긴 cm. [AnchorStep.anchorCm] 이 null 인 채 진행할 수 있다. */
proceedCm: Int? = null,
): JSONObject = JSONObject().apply {
put("patient", patient)
put("save_name", saveName)
@@ -902,8 +970,15 @@ private fun buildAlignSummary(
// 좌우 정렬 (SPEC §8 "좌우(LR) 정렬" · LateralGuide).
// done=false 는 "안 맞췄다"가 아니라 "확인 단계를 거치지 않았다"는 뜻이다.
// lateral 이 끝내 안 잡히는 환자도 있어 진행을 막지 않기 때문에 둘을 구분해야 한다.
// 이 위치를 누가 정했나. 정확도 검증의 모집단은 algorithm 인 세션만이다 -
// 나머지는 "진행을 막지 않아 데이터가 남았다"는 뜻이지 성능 근거가 아니다.
put("anchor_basis", basis?.wire ?: JSONObject.NULL)
put("proceed_anchor_cm", proceedCm ?: JSONObject.NULL)
put("lateral", JSONObject().apply {
put("done", lateral != null)
// 사람의 확인과 값의 존재는 다른 사실이다. 좌우를 맞추는 중에 임상으로 넘어가면
// commit 은 있어도 확인은 없다 - 둘을 합치면 확인한 세션으로 잘못 읽힌다.
put("confirmed", lateralConfirmed)
put("lat_tol", AlignmentConstants.LAT_TOL)
put("accum_k", LateralGuide.ACCUM_K_DEFAULT)
put("action", lateral?.action?.name ?: JSONObject.NULL)
@@ -37,6 +37,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.medithings.vesiscan.managers.AnchorBasis
import com.medithings.vesiscan.managers.ClinicalBv
import com.medithings.vesiscan.ui.components.ChannelDetachPanel
import com.medithings.vesiscan.ui.components.SixChannelWaveformGrid
@@ -315,6 +316,10 @@ fun HospitalModeView(appState: AppState) {
// 정렬로 정한 부착 위치. 자세·용량이 바뀌어도 고정이므로
// 분석 때 "이 데이터는 몇 cm 에서 받았나" 를 되살릴 수 있어야 한다.
put("anchor_cm", appState.anchorCm ?: JSONObject.NULL)
// 그 위치를 누가 정했나. 정렬이 실제 인체에서 안 맞을 때 사람이 고른
// 자리로도 진행할 수 있는데, 그렇게 받은 데이터를 알고리즘 성능의 근거로
// 쓰면 안 된다 - 분석 때 모집단을 가를 유일한 단서다([AnchorBasis]).
put("anchor_basis", appState.anchorBasis?.wire ?: JSONObject.NULL)
put("combinations", results)
}, startedAt)
pendingUploads = HospitalLabdbUploader.pending(dir).size
@@ -361,6 +366,7 @@ fun HospitalModeView(appState: AppState) {
// 서로 달라(0/-7/-14/-21 vs 0/-9/-18/-27) 같은 자리에서도
// nch·cap_frac 이 달라진다 — 최적 부착 위치 자체가 다르다.
appState.anchorCm = null
appState.anchorBasis = null
bleManager.disconnect()
} else {
appState.connectReturnScreen = AppScreen.HOSPITAL_MODE
@@ -383,7 +389,10 @@ fun HospitalModeView(appState: AppState) {
if (!running) {
// 환자가 바뀌면 정렬도 무효다. 앞 환자의 cm 을 그대로 들고 가면
// 매니페스트에 틀린 위치가 기록된다.
if (it != patient) appState.anchorCm = null
if (it != patient) {
appState.anchorCm = null
appState.anchorBasis = null
}
patient = it
}
},
@@ -396,6 +405,7 @@ fun HospitalModeView(appState: AppState) {
AnchorCard(
anchorCm = appState.anchorCm,
basis = appState.anchorBasis,
enabled = !running && isConnected,
onAlign = { appState.currentScreen = AppScreen.ANCHOR_ALIGN },
)
@@ -839,9 +849,17 @@ private fun ProgressPanel(
* `anchor_cm: null` 로 정직하게 남긴다.
*/
@Composable
private fun AnchorCard(anchorCm: Int?, enabled: Boolean, onAlign: () -> Unit) {
private fun AnchorCard(
anchorCm: Int?,
/** 그 위치를 누가 정했는지. null = 정렬을 거치지 않았다. */
basis: AnchorBasis?,
enabled: Boolean,
onAlign: () -> Unit,
) {
val set = anchorCm != null
val tone = if (set) MlSuccess else MlWarning
// 알고리즘이 확정한 위치만 초록이다. 사람이 고른 위치를 같은 색으로 두면 간호사가
// "정렬 끝났다"로 읽고, 그 세션이 정확도 근거로 쓰여도 아무도 모른다.
val tone = if (set && basis?.isAlgorithm == true) MlSuccess else MlWarning
Column(
Modifier.fillMaxWidth()
.background(tone.copy(alpha = 0.10f), RoundedCornerShape(12.dp))
@@ -850,7 +868,11 @@ private fun AnchorCard(anchorCm: Int?, enabled: Boolean, onAlign: () -> Unit) {
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
if (set) "부착 위치 ${anchorCm}cm" else "부착 위치 미정렬",
when {
!set -> "부착 위치 미정렬"
basis?.isAlgorithm == true -> "부착 위치 ${anchorCm}cm"
else -> "부착 위치 ${anchorCm}cm · ${basis?.label ?: "근거 불명"}"
},
fontSize = 15.sp, fontWeight = FontWeight.Bold, color = tone,
modifier = Modifier.weight(1f),
)
@@ -859,8 +881,15 @@ private fun AnchorCard(anchorCm: Int?, enabled: Boolean, onAlign: () -> Unit) {
}
}
Text(
if (set) "이 환자의 전 자세·전 용량에서 이 위치를 그대로 씁니다."
else "정렬하지 않아도 측정은 되지만, 기록에 부착 위치가 남지 않습니다.",
when {
!set -> "정렬하지 않아도 측정은 되지만, 기록에 부착 위치가 남지 않습니다."
basis?.isAlgorithm == true ->
"이 환자의 전 자세·전 용량에서 이 위치를 그대로 씁니다."
// 막지 않는 대신 무엇이 빠진 상태인지는 말해 준다. 이 세션은 정확도
// 검증(기존 측정기 대조)의 모집단이 아니다.
else -> "이 위치를 그대로 쓰되, 알고리즘이 확정한 자리가 아니라는 사실이 " +
"기록에 남습니다. 정확도 비교에는 쓰지 마세요."
},
fontSize = 12.sp, color = MlSecondaryText,
)
}
@@ -0,0 +1,57 @@
package com.medithings.vesiscan.managers
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* 부착 위치를 누가 정했는지.
*
* 정렬이 실제 인체에서 안 맞을 수 있어 임상 진행을 막지 않는다. 막지 않는 대신 **근거가
* 기록에 남아야** 한다 — 사람이 고른 자리에서 받은 데이터가 알고리즘 성능의 근거로
* 섞여 들어가면, 나중에 둘을 가를 방법이 없어 정확도 검증 전체가 무효가 된다.
*/
class AnchorBasisTest {
@Test fun `STOP 만 알고리즘 확정이다`() {
assertEquals(AnchorBasis.ALGORITHM, AnchorBasis.of(AnchorAction.STOP))
assertTrue(AnchorBasis.of(AnchorAction.STOP).isAlgorithm)
}
@Test fun `REATTACH 로 진행하면 무시한 것으로 남는다`() {
// CH3 가 어느 위치에서도 안 잡힌 경우다. 진행은 되지만 알고리즘이 고른 자리가
// 아니므로 성능 근거가 될 수 없다.
val b = AnchorBasis.of(AnchorAction.REATTACH)
assertEquals(AnchorBasis.REATTACH_OVERRIDE, b)
assertFalse(b.isAlgorithm)
}
@Test fun `탐색 도중과 한 번도 안 잰 것은 모두 수동이다`() {
// 사람이 "지금 이 자리로 간다"고 결정한 것이라는 점에서 같다.
assertEquals(AnchorBasis.MANUAL, AnchorBasis.of(AnchorAction.MOVE_UP))
assertEquals(AnchorBasis.MANUAL, AnchorBasis.of(AnchorAction.MOVE_DOWN))
assertEquals(AnchorBasis.MANUAL, AnchorBasis.of(null))
assertFalse(AnchorBasis.of(null).isAlgorithm)
}
@Test fun `모든 AnchorAction 이 근거로 매핑된다`() {
// action 이 늘어났는데 매핑을 안 고치면 조용히 MANUAL 로 떨어진다. 그 자체는
// 안전한 기본값이지만, 새 action 이 추가되면 이 테스트를 보고 판단하게 만든다.
assertEquals(4, AnchorAction.entries.size)
AnchorAction.entries.forEach { AnchorBasis.of(it) }
}
@Test fun `기록에 쓰는 문자열은 고정이다`() {
// 분석 스크립트가 이 값으로 세션을 가른다. 바뀌면 과거 데이터와 대조가 끊긴다.
assertEquals("algorithm", AnchorBasis.ALGORITHM.wire)
assertEquals("reattach_override", AnchorBasis.REATTACH_OVERRIDE.wire)
assertEquals("manual", AnchorBasis.MANUAL.wire)
}
@Test fun `근거마다 사람이 읽는 말이 다르다`() {
val labels = AnchorBasis.entries.map { it.label }
assertEquals(labels.size, labels.toSet().size)
labels.forEach { assertTrue(it.isNotBlank()) }
}
}
@@ -0,0 +1,54 @@
package com.medithings.vesiscan.ui.views.clinical
import com.medithings.vesiscan.managers.AnchorBasis
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* 진행 버튼 아래 안내 — 버튼을 항상 열어 둔 대가다.
*
* 정렬을 끝내지 않아도 임상으로 넘어갈 수 있게 했다. 실제 인체에서 알고리즘이 끝까지
* 안 맞을 수 있고, 그때 막으면 환자를 눕혀 둔 채 임상이 멈추기 때문이다. 막지 않는
* 대신 **누르는 사람이 무엇을 건너뛰는지는 알아야** 한다.
*/
class ProceedNoteTest {
@Test fun `다 맞췄으면 할 말이 없다`() {
// 알고리즘 확정 + 좌우 확인. 여기서 경고를 띄우면 늘 뜨는 글이 되어 무시당한다.
assertNull(proceedNote(AnchorBasis.ALGORITHM, lateralConfirmed = true))
}
@Test fun `좌우만 안 했으면 그것만 말한다`() {
val n = proceedNote(AnchorBasis.ALGORITHM, lateralConfirmed = false)!!
assertTrue(n.contains("좌우"))
assertTrue("상하는 확정됐으므로 탐색을 탓하면 안 된다", !n.contains("탐색"))
}
@Test fun `탐색 미완료는 사람이 고른 것으로 기록된다고 말한다`() {
val n = proceedNote(AnchorBasis.MANUAL, lateralConfirmed = false)!!
assertTrue(n.contains("탐색"))
assertTrue("기록에 남는다는 사실이 핵심이다", n.contains("기록"))
assertTrue("좌우도 빠졌으면 같이 말한다", n.contains("좌우"))
}
@Test fun `재부착 권고를 무시한 경우를 따로 말한다`() {
// CH3 가 어느 위치에서도 안 잡힌 상태다. 탐색이 덜 끝난 것과 원인이 다르므로
// 같은 문구로 묶으면 간호사가 프로브를 다시 붙여 볼 기회를 놓친다.
val re = proceedNote(AnchorBasis.REATTACH_OVERRIDE, lateralConfirmed = true)!!
val man = proceedNote(AnchorBasis.MANUAL, lateralConfirmed = true)!!
assertTrue(re.contains("재부착"))
assertTrue(re != man)
}
@Test fun `모든 조합에 문구가 있다 - 확정·확인 된 경우만 빼고`() {
AnchorBasis.entries.forEach { b ->
listOf(true, false).forEach { lr ->
val n = proceedNote(b, lr)
if (b == AnchorBasis.ALGORITHM && lr) assertNull(n)
else assertNotNull("$b / lr=$lr 에 할 말이 없으면 안 된다", n)
}
}
}
}