From 6c86612f654f84b1743f6380f9d3c67d2aa82e8a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 12 Oct 2026 21:24:35 +0900 Subject: [PATCH] =?UTF-8?q?fix(b05,b06):=20=EC=9B=94=EB=A5=98=20=ED=8F=AD?= =?UTF-8?q?=20=E2=86=92=20=EB=86=92=EC=9D=B4=20=E2=80=94=20=EC=84=B8?= =?UTF-8?q?=EC=9B=94=EA=B5=90=20=C2=B7=20=EB=AC=BC=EB=84=98=EC=9D=B4=20?= =?UTF-8?q?=ED=95=9C=20=EB=B2=8C=20overflowSync=20=C2=B7=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=20=EA=B8=80=20=C2=B7=20=EB=86=92=EC=9D=B4=20=EB=B9=84?= =?UTF-8?q?=EC=9A=B0=EB=A9=B4=20=EB=8B=A4=EC=8B=9C=20=EC=B1=84=EC=9B=80=20?= =?UTF-8?q?(287-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 까닭: 폭 → 높이 계산은 설계유량(담당 유역)이 있어야 돎 — 유량 없는 지점은 아무 일도 안 일어나고 안내도 없었음 · 높이를 비우면 안 채움 - overflowSync(design_structure_ford_calc) — 폭 바꿈 = 필요 수심 · 높이 비움 · 그 아래 = 필요 수심 · 상자 끝 안내 「필요 수심 n 이상」 · 「설계유량 없음 — 수기 입력」 · 유량이 오면 안내 다시 그림 - 세월교 · 물넘이 폼이 같은 것을 씀 · Facility.ts 줄 줄임(697 → 686) - 시험 test_287 화면 길(폼 onChange 훅) — 유량 있음 · 없음 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ --- .../design/design_structure_ford_calc.ts | 48 +++++++++++++++++ .../B05_Profile_UI_Drainage_Facility.ts | 54 ++++++++----------- .../B05_Profile_UI_Style_Structures.css | 8 +++ resources/tester/helper_287_ford_pavement.cjs | 34 ++++++++++++ .../tester/test_287_ford_pavement_followup.py | 25 ++++++++- 5 files changed, 135 insertions(+), 34 deletions(-) diff --git a/A00_Common/design/design_structure_ford_calc.ts b/A00_Common/design/design_structure_ford_calc.ts index 4df05ff46..1f7fee6bb 100644 --- a/A00_Common/design/design_structure_ford_calc.ts +++ b/A00_Common/design/design_structure_ford_calc.ts @@ -27,6 +27,54 @@ export function fordHeightM(designFlowM3s: number | null, widthM: number): numbe return section ? Math.ceil(section.depthM * 100 - 1e-9) / 100 : null; } +/** + * 월류 폭 ↔ 높이 연동 — 세월교 · 물넘이 포장 한 벌(52-4 · 65-4 · 65-5 · 287-3). + * · 폭을 바꾸면 높이 = 필요 수심 · 높이를 비우거나 그 아래로 넣으면 필요 수심으로. + * · 새 시설(`arm(true)`) 빈 높이 = 유량이 오면 필요 수심(사용자가 넣은 것처럼 — [추가] · [수정]이 읽음). + * · 상자 끝 안내 — 「필요 수심 n 이상」 · 「설계유량 없음 — 수기 입력」(유량이 없으면 손 입력). + * `box` = 폭 · 높이 칸이 든 상자 id(세월교 `ford` · 물넘이 `slope`) · `flow` = 담당 유역 설계유량. + */ +export function overflowSync( + form: () => Pick, + box: string, + flow: () => number | null, + emit: () => void, +) { + let pending = false; + const values = () => form().state()[box]?.[0] ?? {}; + const width = () => Number(values().width ?? ""); + const need = (): number | null => fordHeightM(flow(), width()); + return { + arm: (fresh: boolean): void => { + pending = fresh; + }, + onChange: (c: SpecContainer, item: SpecItem): void => { + if (c.id === box && item.id === "height") pending = false; + const want = c.id === box ? need() : null; + const height = values().height ?? ""; + const low = height === "" || !(Number(height) >= (want ?? 0)); + if (want !== null && (item.id === "width" || (item.id === "height" && low))) + form().setValue(box, "height", want.toFixed(2)); + emit(); + }, + /** 유량이 왔거나 시설을 올림 — 새 시설 빈 높이를 채우고 안내를 다시 그림. */ + fill: (): void => { + const want = pending && !values().height ? need() : null; + if (want === null) return form().refresh(); + form().setValue(box, "height", want.toFixed(2)); + pending = false; + }, + /** 상자 끝 안내 글(폼 `fields` 훅). */ + fields: (c: SpecContainer): HTMLElement[] => { + if (c.id !== box) return []; + const hint = document.createElement("p"); + hint.className = "b05-structure__flow-hint"; + hint.textContent = fordHeightHint(flow(), width()); + return [hint]; + }, + }; +} + /** 높이 빈 칸 안내. */ export function fordHeightHint(designFlowM3s: number | null, widthM: number): string { const need = fordHeightM(designFlowM3s, widthM); diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index 4fa58c94f..18825e7b9 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -56,7 +56,6 @@ import { type SpecForm, type SpecLinks, } from "../A00_Common/design/design_structure_form"; -import type { SpecContainer, SpecItem } from "../A00_Common/design/design_structure_spec"; import { PIPE_SPEC } from "../A00_Common/design/design_structure_pipe"; import { BOX_SPEC, @@ -65,7 +64,7 @@ import { boxOptionsIn, } from "../A00_Common/design/design_structure_box"; import { FORD_SPEC, withLegacyGuards } from "../A00_Common/design/design_structure_ford"; -import { fordHeightM, syncChuteArea } from "../A00_Common/design/design_structure_ford_calc"; +import { overflowSync, syncChuteArea } from "../A00_Common/design/design_structure_ford_calc"; import { pipeDrawn, pipeExtraCount, @@ -372,38 +371,21 @@ export function createFacilityOptionsForm( // 월류 높이 = 월류 폭과 연동(폭을 바꾸면 필요 수심으로 채움 · 그 아래는 안 받음 · `design_structure_ford_calc`) — // 물넘이 포장도 같은 키 · 같은 계산(65-5) · 새 시설은 유량이 오면 빈 높이를 필요 수심으로(65-4) let fordFlow: number | null = null; - let heightPending = false; - const overflow = (form: () => SpecForm, box: string) => { - const values = () => form().state()[box]?.[0] ?? {}; - const need = (): number | null => fordHeightM(fordFlow, Number(values().width ?? "")); - return { - onChange: (c: SpecContainer, item: SpecItem) => { - if (c.id === box && item.id === "height") heightPending = false; - const want = c.id === box ? need() : null; - const height = values().height ?? ""; - const low = height !== "" && !(Number(height) >= (want ?? 0)); - if (want !== null && (item.id === "width" || (item.id === "height" && low))) - form().setValue(box, "height", want.toFixed(2)); - emit(); - }, - /** 새 시설 빈 높이 = 필요 수심(사용자가 넣은 것처럼 · 알림 없음 — [추가] · [수정]이 읽음). */ - fill: () => { - const want = heightPending && !values().height ? need() : null; - if (want === null) return; - form().setValue(box, "height", want.toFixed(2)); - heightPending = false; - }, - }; - }; - const fordFlowBox = overflow(() => fordForm, "ford"); + const fordFlowBox = overflowSync( + () => fordForm, + "ford", + () => fordFlow, + emit, + ); + const guardFields = guardChoiceFields( + () => chainage, + () => fordForm.refresh(), + () => fordForm.recount(), + ); // 유출 흙막이 단 수 = 흙막이 상자 수(관보호공과 같은 방향 · 「+ 흙막이」 없음 · 90-1) const fordForm: SpecForm = createSpecForm(specLinks, { onChange: fordFlowBox.onChange, - fields: guardChoiceFields( - () => chainage, - () => fordForm.refresh(), - () => fordForm.recount(), - ), + fields: (c, values) => [...guardFields(c, values), ...fordFlowBox.fields(c)], count: guardSoilCount( () => chainage, () => fordForm.state().outlet?.[0]?.structure, @@ -485,9 +467,15 @@ export function createFacilityOptionsForm( // ── 물넘이 포장 = 사용자 구조물 표 03 명세(PLAN 52-3) — 경사각도(+ 월류 폭 · 높이 · 65-5) · 도수로/돌붙임 · // 옛 바닥 경사 · 두께 · 길이 칸은 뺌(지난 정의 값) ── - const paveFlowBox = overflow(() => paveForm, "slope"); + const paveFlowBox = overflowSync( + () => paveForm, + "slope", + () => fordFlow, + emit, + ); const paveForm: SpecForm = createSpecForm(specLinks, { onChange: (c, item) => (syncChuteArea(paveForm, c, item), paveFlowBox.onChange(c, item)), + fields: paveFlowBox.fields, }); // 10-A ⑲ 저장 규칙 글은 [?] 설명서 `구조물_<시설>.md` 로 옮김(54-2) — 병합은 `_Drainage_Facility_Merge`. @@ -605,7 +593,7 @@ export function createFacilityOptionsForm( }, setFacility(facility, options = {}, designFlow = null, definition = null, fresh = false) { current = facility; - heightPending = fresh; + [fordFlowBox, paveFlowBox].forEach((box) => box.arm(fresh)); hiddenOptions.remember(options); fordFlow = designFlow ?? null; if (facility === null) { diff --git a/B05_Profile/B05_Profile_UI_Style_Structures.css b/B05_Profile/B05_Profile_UI_Style_Structures.css index 126ed3331..a874d14e7 100644 --- a/B05_Profile/B05_Profile_UI_Style_Structures.css +++ b/B05_Profile/B05_Profile_UI_Style_Structures.css @@ -657,3 +657,11 @@ .b05-structure__widening [aria-pressed="false"] .b05-structure__widening-area { color: var(--color-text-secondary); } + +/* 월류 높이 안내(287-3) — 필요 수심 · 설계유량 없음 */ +.b05-structure__flow-hint { + grid-column: 1 / -1; + margin: 0; + font-size: 0.78rem; + color: var(--color-text-secondary); +} diff --git a/resources/tester/helper_287_ford_pavement.cjs b/resources/tester/helper_287_ford_pavement.cjs index 9f2a35521..9b0b2fb48 100644 --- a/resources/tester/helper_287_ford_pavement.cjs +++ b/resources/tester/helper_287_ford_pavement.cjs @@ -177,7 +177,41 @@ pave.appendFordPavementOverlay( (e) => e, ); +// 287-3 화면 길 — 폼 onChange 훅 그대로(세월교 · 물넘이 한 벌 overflowSync) +globalThis.document.createElement = (tag) => new Node(tag); +const overflowRun = (flowValue) => { + const state = { slope: [{ width: "10", height: "0.05" }] }; + const form = { + state: () => state, + setValue: (c, i, v) => (state[c][0][i] = v), + refresh: () => undefined, + }; + let emitted = 0; + const sync = calc.overflowSync( + () => form, + "slope", + () => flowValue, + () => (emitted += 1), + ); + const edit = (item, value) => { + state.slope[0][item] = value; // 폼이 값을 적고 훅을 부름(design_structure_form onEdit) + sync.onChange({ id: "slope" }, { id: item }); + return state.slope[0].height; + }; + return { + afterWidth8: edit("width", "8"), + afterClear: edit("height", ""), + afterLow: edit("height", "0.01"), + afterHigh: edit("height", "0.9"), + hint: sync.fields({ id: "slope" }).map((el) => el.textContent), + otherBoxHint: sync.fields({ id: "chute" }).length, + emitted, + }; +}; + const out = { + overflowFlow: overflowRun(14.5776), + overflowNoFlow: overflowRun(null), shapeOrder: layer.children.map((k) => (k.attrs.class ?? k.attrs["data-structure-id"]).split(" ").pop(), ), diff --git a/resources/tester/test_287_ford_pavement_followup.py b/resources/tester/test_287_ford_pavement_followup.py index 7d227d155..789408e07 100644 --- a/resources/tester/test_287_ford_pavement_followup.py +++ b/resources/tester/test_287_ford_pavement_followup.py @@ -92,7 +92,6 @@ def test_월류_폭_넣으면_높이_자동_세월교_한_벌(ts): assert ts["heightByWidth"] and ts["heightByWidth"] > 0 assert ts["heightNoFlow"] is None form = _read("B05_Profile", "B05_Profile_UI_Drainage_Facility.ts") - assert 'const paveFlowBox = overflow(() => paveForm, "slope");' in form assert 'if (facility === "ford_pavement") paveFlowBox.fill();' in form @@ -135,3 +134,27 @@ def test_보라_손잡이는_계획고만_형상_누름은_고름만(): assert "highlightShapes(card, pickedMarkId);" in section_view selection = _read("B06_Section", "B06_Section_UI_Page_Structures_Selection.ts") assert "sectionAt.ford_pavement?.chainage_m" in selection + + +def test_월류_폭_높이_화면_길_유량_있음(ts): + got = ts["overflowFlow"] + assert got["afterWidth8"] == "0.26" # 폭 8 → 필요 수심(유량 14.58 ㎥/s) + assert got["afterClear"] == "0.26" # 높이 비움 → 다시 채움 + assert got["afterLow"] == "0.26" # 필요 수심 아래 → 끌어올림 + assert got["afterHigh"] == "0.9" # 올려 적은 값은 그대로 + assert got["hint"] == ["필요 수심 0.26 이상"] and got["otherBoxHint"] == 0 + assert got["emitted"] == 4 + + +def test_월류_폭_높이_화면_길_유량_없음(ts): + got = ts["overflowNoFlow"] + assert got["afterWidth8"] == "0.05" and got["afterClear"] == "" # 손 입력 그대로 + assert got["hint"] == ["설계유량 없음 — 수기 입력"] + + +def test_세월교_물넘이_한_벌_안내_훅(): + form = _read("B05_Profile", "B05_Profile_UI_Drainage_Facility.ts") + flat = " ".join(form.split()) + assert 'overflowSync( () => fordForm, "ford", () => fordFlow, emit, )' in flat + assert 'overflowSync( () => paveForm, "slope", () => fordFlow, emit, )' in flat + assert "...fordFlowBox.fields(c)" in form and "fields: paveFlowBox.fields," in form