From afeb3acc5757dfbe5a91c700efa4c029916d47f7 Mon Sep 17 00:00:00 2001 From: jjangddu Date: Tue, 8 Sep 2026 14:12:17 +0900 Subject: [PATCH] =?UTF-8?q?fix(labdb):=20902=20=EA=B0=80=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=EC=9A=94=EC=95=BD=EC=9D=84=20=ED=86=B5=EC=A7=B8?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=B4=EB=82=B4=EA=B2=8C=20=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit params 를 화이트리스트로 채우고 있었는데, 그 목록에 없던 두 필드가 조용히 빠져 있었다: · patient_unnamed — 환자명 없이 잰 데이터인가 · selection_verified_against_reference — 앱 판정이 레퍼런스와 대조 완료인가 요약에 필드를 추가할 때마다 여기도 고쳐야 하는데 안 고쳐도 아무 신호가 없다. 이 세션은 "파형이 이상하니 봐 달라"는 진단 요청이라 **덜 보내는 쪽이 위험**하다 — 개발자가 되물어야 하고 그 왕복이 임상에서는 하루다. align_result.json 을 통째로 옮기고, 이미 다른 이름으로 넣은 것(patient→subject, save_name)만 건너뛴다. 요약의 모든 키가 params 에 도달하는지 테스트로 고정했다. 문서도 함께 고쳤다. positions[] 표에서 ch3_hit·ch3_tot·eligible 이 빠져 있었고 (실제로는 올라가고 있었다), params 표에 위 두 필드를 추가했다. 표에 없는 키가 보여도 정상이라는 설명도 넣었다. Co-Authored-By: Claude Opus 5 --- .../services/labdb/AlignLabdbPayload.kt | 17 +++++++------ .../services/labdb/AlignLabdbPayloadTest.kt | 24 ++++++++++++++++++ docs/LABDB_DATATYPES.md | 25 +++++++++++++------ 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayload.kt b/app/src/main/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayload.kt index cdea850..60ad302 100644 --- a/app/src/main/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayload.kt +++ b/app/src/main/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayload.kt @@ -124,15 +124,18 @@ object AlignLabdbPayload { put("save_name", saveName) put("positions_measured", files.size) put("cycles_total", records.length()) - // 판정 근거를 통째로 넣는다. 개발자가 답해야 할 질문이 "왜 이 위치인가"라, + // 판정 근거를 **통째로** 넣는다. 개발자가 답해야 할 질문이 "왜 이 위치인가"라, // 지표(nch·ch3·cap_frac)와 선택 결과가 파형과 같이 있어야 한다. + // + // 고를 키를 나열하지 않는다. 예전에 화이트리스트로 두었더니 `patient_unnamed` + // 과 `selection_verified_against_reference` 가 조용히 빠졌다 — 요약에 필드를 + // 하나 추가할 때마다 여기도 고쳐야 하는데 그걸 알아챌 방법이 없다. 진단용 + // 페이로드는 **덜 보내는 쪽이 위험**하다. + // + // 이미 다른 이름으로 넣은 것만 건너뛴다. summary?.let { s -> - for (k in listOf( - "device", "firmware_version", "hw_preset", "freq_mhz", "freq_option", - "cycles_per_position", "probe_cycles", "avg", "delay_us", "samples", - "ch3_hit_min", "final_offset_cm", "best_cm", "anchor_cm", "action", - "stop_reason", "max_nch", "positions", "lateral", - )) if (s.has(k)) put(k, s.get(k)) + val mapped = setOf("patient", "save_name") // → subject / save_name + for (k in s.keys()) if (k !in mapped) put(k, s.get(k)) } // 자동이 아니라 사람이 올린 것임을 데이터에 남긴다 — 정상 세션과 섞이면 // "왜 이것만 올라와 있나"를 나중에 설명할 수 없다. diff --git a/app/src/test/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayloadTest.kt b/app/src/test/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayloadTest.kt index 2400471..e5a09fa 100644 --- a/app/src/test/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayloadTest.kt +++ b/app/src/test/java/com/medithings/vesiscan/services/labdb/AlignLabdbPayloadTest.kt @@ -94,6 +94,30 @@ class AlignLabdbPayloadTest { assertEquals("nurse_review", params.getString("upload_reason")) } + @Test fun `요약의 모든 키가 params 로 간다`() { + // 고를 키를 나열하면 요약에 필드를 추가할 때마다 여기도 고쳐야 하는데, 안 고쳐도 + // 아무 신호가 없어서 조용히 빠진다(실제로 patient_unnamed 와 + // selection_verified_against_reference 가 그렇게 빠져 있었다). + // 진단용 페이로드는 덜 보내는 쪽이 위험하다. + val d = tmp.newFolder("align") + writeCycles(d, 0, cycles = 1) + val s = summary().apply { + put("patient_unnamed", false) + put("selection_verified_against_reference", true) + put("나중에_추가될_필드", "값") + } + File(d, "align_result.json").writeText(s.toString(), Charsets.UTF_8) + + val params = AlignLabdbPayload.build(d)!!.getJSONObject("params") + for (k in s.keys()) { + if (k == "patient" || k == "save_name") continue // subject / save_name 으로 매핑 + assertTrue("요약의 '$k' 가 params 에 없다", params.has(k)) + } + assertEquals(false, params.getBoolean("patient_unnamed")) + assertEquals(true, params.getBoolean("selection_verified_against_reference")) + assertEquals("값", params.getString("나중에_추가될_필드")) + } + @Test fun `메모가 들어간다`() { val d = tmp.newFolder("align") writeCycles(d, 0, cycles = 1) diff --git a/docs/LABDB_DATATYPES.md b/docs/LABDB_DATATYPES.md index e2e02ae..72c00d9 100644 --- a/docs/LABDB_DATATYPES.md +++ b/docs/LABDB_DATATYPES.md @@ -140,19 +140,28 @@ | `action` | string | `STOP` / `REATTACH` / … | | `stop_reason` | string / null | 탐색 종료 사유 | | `max_nch` | int | 관측된 최대 검출 채널 수 | +| `patient_unnamed` | bool | 환자명 없이 잰 데이터인가(폴더가 `unnamed_*`) | +| `selection_verified_against_reference` | bool | 앱 판정이 Python 레퍼런스와 대조 완료인가 | | `positions` | array | 위치별 지표 — 아래 | | `lateral` | object | 좌우 정렬 결과 — 아래 | +> `params` 는 `align_result.json` 을 **통째로** 옮깁니다(`patient` → `subject`, +> `save_name` 만 이름이 바뀝니다). 앱에서 요약에 필드를 추가하면 별도 작업 없이 +> 같이 올라갑니다 — 위 표에 없는 키가 보여도 정상입니다. + `params.positions[]` (위치별 판정 근거): -| 필드 | 설명 | -|---|---| -| `align_cm` | 위치 | -| `n_trace` | 슬라이딩 trace 수 (20 cycle → 11) | -| `nch` | CH0~CH3 중 검출 채널 수 | -| `ch3` | `"O"` / `"X"` | -| `ch3_rate` | trace 기준 CH3 검출률 | -| `cap_frac` | 기하 지표 (BV 파이프라인 산출) | +| 필드 | 타입 | 설명 | +|---|---|---| +| `align_cm` | int | 위치(치골 위 cm) | +| `n_trace` | int | 슬라이딩 trace 수 (20 cycle → 11) | +| `nch` | int | CH0~CH3 중 검출 채널 수 | +| `ch3` | `"O"` / `"X"` | mean-scan 기준 CH3 검출 여부 | +| `ch3_hit` | int | CH3 가 검출된 trace 수 | +| `ch3_tot` | int | 전체 trace 수 | +| `ch3_rate` | number | `ch3_hit / ch3_tot` | +| `cap_frac` | number | 기하 지표 (BV 파이프라인 산출) | +| `eligible` | bool | 후보 자격 = `ch3 == "O"` AND `ch3_rate >= ch3_hit_min` | `params.lateral` (좌우 정렬):