fix(labdb): 정렬 확인 측정이 업로드에서 빠져 있던 것 · phase 로 구분
전수 탐색(0~4cm)이 끝나면 고른 위치를 한 번 더 재고, 그 결과는
`align_{n}cm_confirm.csv` 로 따로 쓰인다(판정에 쓰인 데이터를 덮지 않기 위해).
그런데 페이로드의 파일 정규식이 `^align_(-?\d+)cm\.csv$` 로 끝나 **확인 측정
파일이 하나도 잡히지 않았다** — 개발자가 "왜 이 위치인가"를 보려면 고른 자리의
실제 파형이 필요한데 그것이 서버에 없었다.
정규식에 `(_confirm)?` 를 선택 그룹으로 넣고, 레코드에 `phase`("sweep" |
"confirm")를 추가했다. 같은 `align_cm` 으로 두 벌이 올라가므로 phase 가 없으면
받는 쪽이 같은 위치를 두 번 잰 것을 두 위치로 읽는다.
정렬 순서도 (cm 오름차순, 같은 cm 이면 sweep → confirm) 으로 바꿨다. 확인 측정이
실제로 나중에 일어나므로, 받는 쪽이 파일 순서를 시간 순서로 읽어도 어긋나지 않는다.
이 변경은 작업 트리에 이미 있던 것이고(2026-09-18 자 주석), 이번 세션에서 작성한
것이 아니다. `AlignLabdbPayloadTest` 12건을 재실행해 통과를 확인하고 커밋했다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,9 +39,22 @@ import java.util.Locale
|
||||
* record = 한 위치의 한 cycle
|
||||
* rowIndex : 0..N 연속 (labdb 규약)
|
||||
* align_cm : 그 cycle 을 잰 위치
|
||||
* phase : "sweep" | "confirm" ← 2026-09-18 추가
|
||||
* cycle_idx : 그 위치 안에서의 순번
|
||||
* channels : 6 × {ch, peak, peakIdx, data[~100]}
|
||||
* ```
|
||||
*
|
||||
* ## phase 를 나눠 담는 이유
|
||||
* 전수 탐색(0~4cm)이 끝나면 고른 위치를 **한 번 더 잰다**. 그 확인 측정은
|
||||
* `align_{n}cm_confirm.csv` 로 따로 쓰인다 — 같은 이름에 쓰면 판정에 쓰인 데이터가
|
||||
* 덮이기 때문이다([HospitalRunStore.writeAlignCycles]).
|
||||
*
|
||||
* 그런데 예전 정규식이 `align_{n}cm.csv` 만 잡아서 **확인 측정이 업로드에서 통째로
|
||||
* 빠져 있었다**(2026-09-18 발견). 개발자가 "왜 이 위치인가"를 보려면 고른 자리에서
|
||||
* 실제로 어떻게 나왔는지가 필요한데, 그 파형이 서버에 없었다.
|
||||
*
|
||||
* 같은 `align_cm` 으로 두 벌이 올라가므로 `phase` 없이는 구분할 수 없다. 받는 쪽이
|
||||
* 섞어 보면 같은 위치를 두 번 잰 것이 두 위치처럼 읽힌다.
|
||||
*/
|
||||
object AlignLabdbPayload {
|
||||
|
||||
@@ -55,8 +68,14 @@ object AlignLabdbPayload {
|
||||
|
||||
private const val PROTOCOL = "hospital_align_2026"
|
||||
|
||||
/** `align_3cm.csv` 에서 3 을 뽑는다. */
|
||||
private val NAME_RE = Regex("""^align_(-?\d+)cm\.csv$""", RegexOption.IGNORE_CASE)
|
||||
/**
|
||||
* `align_3cm.csv` → (3, sweep) · `align_3cm_confirm.csv` → (3, confirm).
|
||||
*
|
||||
* 2026-09-18: `_confirm` 을 선택 그룹으로 받는다. 예전에는 `cm\.csv$` 로 끝나
|
||||
* 확인 측정 파일이 하나도 안 잡혔다.
|
||||
*/
|
||||
private val NAME_RE =
|
||||
Regex("""^align_(-?\d+)cm(_confirm)?\.csv$""", RegexOption.IGNORE_CASE)
|
||||
|
||||
private val isoOut: SimpleDateFormat
|
||||
get() = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US)
|
||||
@@ -68,9 +87,16 @@ object AlignLabdbPayload {
|
||||
* @return 올릴 페이로드. 읽을 cycle 이 없으면 null.
|
||||
*/
|
||||
fun build(alignDir: File, memo: String = "", dataType: String = DATA_TYPE): JSONObject? {
|
||||
// cm 오름차순, 같은 cm 이면 sweep 먼저 · confirm 나중. 받는 쪽이 파일 순서를
|
||||
// 그대로 시간 순서로 읽어도 어긋나지 않게 한다(확인 측정이 나중에 일어난다).
|
||||
val files = alignDir.listFiles()
|
||||
?.filter { it.isFile && NAME_RE.matches(it.name) }
|
||||
?.sortedBy { NAME_RE.find(it.name)!!.groupValues[1].toInt() }
|
||||
?.sortedWith(
|
||||
compareBy(
|
||||
{ NAME_RE.find(it.name)!!.groupValues[1].toInt() },
|
||||
{ if (NAME_RE.find(it.name)!!.groupValues[2].isEmpty()) 0 else 1 },
|
||||
)
|
||||
)
|
||||
?: return null
|
||||
if (files.isEmpty()) return null
|
||||
|
||||
@@ -81,7 +107,10 @@ object AlignLabdbPayload {
|
||||
val records = JSONArray()
|
||||
var row = 0
|
||||
for (f in files) {
|
||||
val cm = NAME_RE.find(f.name)!!.groupValues[1].toInt()
|
||||
val m = NAME_RE.find(f.name)!!
|
||||
val cm = m.groupValues[1].toInt()
|
||||
// 같은 align_cm 으로 두 벌이 올라간다 — 이 값이 없으면 받는 쪽이 구분 못 한다.
|
||||
val phase = if (m.groupValues[2].isEmpty()) "sweep" else "confirm"
|
||||
// `cycleIdx,ch,s0,s1,...` — 헤더 없음(HospitalRunStore.writeAlignCycles).
|
||||
val byCycle = linkedMapOf<Int, MutableMap<Int, IntArray>>()
|
||||
runCatching { f.readLines(Charsets.UTF_8) }.getOrNull()?.forEach { line ->
|
||||
@@ -110,6 +139,7 @@ object AlignLabdbPayload {
|
||||
records.put(JSONObject().apply {
|
||||
put("rowIndex", row++)
|
||||
put("align_cm", cm)
|
||||
put("phase", phase)
|
||||
put("cycle_idx", ci)
|
||||
put("commandType", "MTB")
|
||||
put("channels", chans)
|
||||
@@ -127,7 +157,10 @@ object AlignLabdbPayload {
|
||||
put("protocol", PROTOCOL)
|
||||
put("subject", subject)
|
||||
put("save_name", saveName)
|
||||
put("positions_measured", files.size)
|
||||
// 2026-09-18: `files.size` 를 그대로 쓰면 확인 측정이 위치 하나로 더해져
|
||||
// "5군데 쟀다"가 6이 된다. 실제로 **서로 다른 위치**를 센다.
|
||||
put("positions_measured", files.count { NAME_RE.find(it.name)!!.groupValues[2].isEmpty() })
|
||||
put("confirm_positions", files.count { NAME_RE.find(it.name)!!.groupValues[2].isNotEmpty() })
|
||||
put("cycles_total", records.length())
|
||||
// 판정 근거를 **통째로** 넣는다. 개발자가 답해야 할 질문이 "왜 이 위치인가"라,
|
||||
// 지표(nch·ch3·cap_frac)와 선택 결과가 파형과 같이 있어야 한다.
|
||||
|
||||
Reference in New Issue
Block a user