From 2d356fac91dcbffefb37f61b6838009bfb870813 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 12 Oct 2026 22:48:29 +0900 Subject: [PATCH] =?UTF-8?q?fix(b06):=20=EC=95=88=20=EA=B3=A8=EB=9D=BC?= =?UTF-8?q?=EC=A7=84=20=EC=B9=B4=EB=93=9C=20=EC=B2=AB=20=EB=88=84=EB=A6=84?= =?UTF-8?q?=20=3D=20=EB=88=8C=EB=A6=B0=20=EB=8C=80=EC=83=81=20=E2=80=94=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EA=B3=A0=EB=A6=84=EC=9D=B4=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EA=B4=80=EC=9C=BC=EB=A1=9C=20=ED=8F=BC=EC=9D=84=20?= =?UTF-8?q?=EC=98=AE=EA=B8=B0=EC=A7=80=20=EC=95=8A=EC=9D=8C=20=C2=B7=20?= =?UTF-8?q?=ED=8F=BC=20=EC=97=AC=EB=8A=94=20=EB=8F=99=EC=95=88=20=EC=84=A0?= =?UTF-8?q?=20=EB=8F=84=EC=88=98=EB=A1=9C=20=EC=83=81=EC=9E=90=EB=A5=BC=20?= =?UTF-8?q?=EC=83=88=20=EC=83=81=EC=9E=90=EB=A1=9C=20=EC=95=88=20=EA=B3=A0?= =?UTF-8?q?=EB=A6=84=20(294-8=20=C2=B7=20295-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL --- A00_Common/design/design_view_cross.ts | 8 +- ...06_Section_UI_Page_Structures_Selection.ts | 30 ++++- .../tester/helper_294_7_ford_slab_pick.cjs | 118 +++++++++++++++++- resources/tester/test_294_7_ford_slab_pick.py | 38 ++++++ 4 files changed, 184 insertions(+), 10 deletions(-) diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index 392fe54b4..0fe92a61e 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -57,6 +57,7 @@ import { import { createB06StructuresPanel } from "../../B06_Section/B06_Section_UI_Page_Structures_Panel"; import { isWallSelecting, + wallOwnerM, wireStructureSelection, } from "../../B06_Section/B06_Section_UI_Page_Structures_Selection"; import "../../B06_Section/B06_Section_UI_Style.css"; @@ -392,9 +393,12 @@ export async function mountCrossView( releaseCross(); // 카드 다시 누름 = 풀기 한 길(범위 테두리 · 형상까지 · 233-2) return; } - const owner = isWallSelecting() ? stationControls.structureSpan.ownerOf(target) : null; + // 찍은 부재가 이미 연 시설이 먼저 — 물넘이 콘크리트가 걸친 연동 관으로 안 감(294-8) + const owner = isWallSelecting() + ? (wallOwnerM() ?? stationControls.structureSpan.ownerOf(target)?.chainage_m) + : undefined; const preferred = structuresPanel.preferred() !== null; - const opened = structuresPanel.showAtChainage(owner?.chainage_m ?? target.chainage_m); + const opened = structuresPanel.showAtChainage(owner ?? target.chainage_m); // 카드 하나 고름 = 그 측점(구조물 id 는 목록 · 알약 · 넘김이 고른 것만 · 189-1) const picked = preferred ? opened : { chainageM: opened.chainageM, structureId: null }; if (!isWallSelecting()) selectDesign(projectId, picked, { from: "b06" }); diff --git a/B06_Section/B06_Section_UI_Page_Structures_Selection.ts b/B06_Section/B06_Section_UI_Page_Structures_Selection.ts index 4dc6f64dd..e742ef5cb 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Selection.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Selection.ts @@ -21,15 +21,23 @@ import { BERM_TYPE_ID } from "./B06_Section_UI_Page_Persist"; * 이 값으로 "카드만 고른 경우"를 가려낸다(2026-08-29 사용자: 측점 선택 때 구조물이 * 자동으로 골라지면 안 된다). 같은 턴에서만 참이다. */ let wallSelecting = false; +/** 이번 턴에 찍은 부재가 폼에 올린 시설 자리 — 안 골라진 카드 첫 누름에 카드 고름이 연동 관으로 다시 옮기지 않게(294-8). */ +let wallOwner: number | null = null; export function isWallSelecting(): boolean { return wallSelecting; } -function markWallSelecting(): void { +export function wallOwnerM(): number | null { + return wallSelecting ? wallOwner : null; +} + +function markWallSelecting(owner: number | null = null): void { wallSelecting = true; + wallOwner = owner; queueMicrotask(() => { wallSelecting = false; + wallOwner = null; }); } @@ -81,12 +89,12 @@ export function wireStructureSelection( stationControls.revetOffset.select = (chainageM, key, ownerM) => { // 다른 카드 부재를 고르는 중에 옛 카드가 제 선택을 푸는 것 — 폼 · 선택 칸은 새 것 몫(53-1) const nested = wallSelecting; - // 푸는 것은 고름이 아님 — 표시를 켜면 뒤이은 카드 고름이 선택 칸에 안 적힘(53-1) - if (key) markWallSelecting(); - origRevet(chainageM, key); // 도수로 띠는 자기 측점 시설 — 이웃 관 연장 안이어도 그 관 폼을 안 엶(152-4) const owner = ownerM ?? (key === "chute" ? chainageM : ownerChainageOf(chainageM, key ?? undefined)); + // 푸는 것은 고름이 아님 — 표시를 켜면 뒤이은 카드 고름이 선택 칸에 안 적힘(53-1) + if (key) markWallSelecting(owner); + origRevet(chainageM, key); if (key) remember?.(owner, key); if (key) panel.showAtChainage(owner); else if (!nested) { @@ -163,6 +171,18 @@ export function wireStructureSelection( if (!pointing) pressAt(event.target as HTMLElement); }); panel.root.addEventListener("change", ({ target: el }) => pickAt(el as HTMLElement)); + // 시설을 폼에 여는 동안의 그림(종류 칸 → 값 채움 두 번)은 새 상자가 아님 — 마지막 상자(도수로)를 + // 고르면 누른 선 · 형상을 덮음(294-8 · 295-4) + let opening = false; + const origShow = panel.showAtChainage; + panel.showAtChainage = (chainageM) => { + opening = true; + try { + return origShow(chainageM); + } finally { + opening = false; + } + }; // 상자가 새로 서면(단 늘림 · [추가]) 그 상자를 고름 — 같은 시설을 다시 그린 때만 let painted: { chainageM: number | null; keys: Set } | null = null; panel.root.addEventListener("specpaint", () => { @@ -171,7 +191,7 @@ export function wireStructureSelection( const before = painted?.chainageM === chainageM ? painted.keys : null; painted = { chainageM, keys: new Set(boxes.map(fieldOf)) }; const born = before ? boxes.filter((box) => !before.has(fieldOf(box))).at(-1) : undefined; - if (!born || chainageM === null) return; + if (!born || chainageM === null || opening) return; revealAt(chainageM, null); pickBox(born); }); diff --git a/resources/tester/helper_294_7_ford_slab_pick.cjs b/resources/tester/helper_294_7_ford_slab_pick.cjs index 9319d1ad1..dc9ca0321 100644 --- a/resources/tester/helper_294_7_ford_slab_pick.cjs +++ b/resources/tester/helper_294_7_ford_slab_pick.cjs @@ -191,8 +191,120 @@ DS.selectDesign(P, { chainageM: 110, structureId: null }, { partKey: "", from: " DS.selectDesign(P, { chainageM: 110, structureId: null }, { from: "b06" }); // 그다음 카드 고름 out.firstLine = { oldKey, newKey: DS.readSelection(P)?.partKey ?? null }; -// 다시 누름 = 풂 — 고름 표시(마이크로태스크)가 풀린 뒤 -queueMicrotask(() => { +/* ── ⑤ 안 골라진 카드 첫 누름(294-8 · 295-4) — 페이지 흐름: 부재 고름 → 카드 고름 리스너 → 폼 엶 ── + * 폼은 열 때 두 번 그림(종류 칸 = 기본값 → 시설 값) — 둘째 그림에 도수로 상자가 섬. */ +const tick = () => new Promise((done) => setTimeout(done, 0)); +const flowSections = [ + { chainage_m: 90, station_id: "s90", ford_pavement: { chainage_m: 90, chute: {} } }, + { chainage_m: 95, station_id: "s95", culvert: { c: 95, chute: {} } }, + { chainage_m: 110, station_id: "s110", ford_pavement: { chainage_m: 110, chute: {} } }, +]; +const flowLog = []; +const box = (id) => { + const el = { + dataset: { specContainer: id, specIndex: "0" }, + closest: (sel) => (sel.startsWith("fieldset") ? el : null), + }; + return el; +}; +let shown = null; +let boxes = []; +const handlers = {}; +let flowPicked = null; +const flowControls = { + revetOffset: { + select: (at, key) => { + flowLog.push(["select", at, key]); + flowPicked = key ? { at, key } : null; + }, + highlightFor: (s) => (flowPicked?.at === s.chainage_m ? flowPicked.key : null), + }, + ford: { select: () => {} }, + box: { select: () => {} }, + structureSpan: { + // 연동 관 95 m 의 유출 흙막이가 4+10(90 m) 카드까지 옴(299) + ownerOf: (s) => (s.culvert ? s : s.chainage_m === 90 ? flowSections[1] : null), + }, + inletStructure: { set: () => {}, valueFor: () => "auto" }, +}; +const paint = (ids) => { + boxes = ids.map(box); + handlers.specpaint?.forEach((fn) => fn()); +}; +const flowPanel = { + root: { + addEventListener: (type, fn) => (handlers[type] ??= []).push(fn), + querySelectorAll: () => boxes, + contains: () => false, + }, + facility: { onInletStructureChange: () => {}, setInletStructure: () => {} }, + currentChainage: () => shown, + showAtChainage: (at) => { + flowLog.push(["form", at]); + shown = at; + if (at === null) return { chainageM: null, structureId: null }; + paint(at === 95 ? ["pipe"] : ["slope"]); // 종류 칸 = 기본값 + paint(at === 95 ? ["pipe", "chute_add"] : ["slope", "chute"]); // 시설 값 = 도수로 상자 섬 + return { chainageM: at, structureId: null }; + }, + releasePipe: () => flowLog.push(["release"]), + getStructures: () => [], + selectStructure: () => {}, +}; +SEL.wireStructureSelection( + flowControls, + () => ({ cross_sections: flowSections }), + flowPanel, + (at, key) => flowLog.push(["remember", at, key]), +); +// 카드 고름 리스너(`design_view_cross` setStationSelectListener) 의 폼 자리 고르기 그대로 +const cardSelected = (target) => { + const owner = SEL.isWallSelecting() + ? (SEL.wallOwnerM() ?? flowControls.structureSpan.ownerOf(target)?.chainage_m) + : undefined; + flowControls.revetOffset.highlightFor(target); + flowPanel.showAtChainage(owner ?? target.chainage_m); +}; +const esc = () => { + flowPicked = null; + shown = null; + boxes = []; +}; +const flow = async (before, press) => { + esc(); + before?.(); + await tick(); + flowLog.length = 0; + press(); + const got = flowLog.map((entry) => [...entry]); + await tick(); + return got; +}; +// 형상 누름 = toggleRevet("pipe", 90) — 안 골라진 카드면 부재 고름 → onSelect +const slabPress = () => { + flowControls.revetOffset.select(90, "pipe", 90); + cardSelected(flowSections[0]); +}; +// 보라 선 첫 누름 = pickLine — 남은 부재 풂 → onSelect +const linePress = () => { + flowControls.revetOffset.select(110, null); + cardSelected(flowSections[2]); +}; + +(async () => { + // 다시 누름 = 풂 — 고름 표시(마이크로태스크)가 풀린 뒤 + await Promise.resolve(); out.owner.release = run(90, null, 90); + out.first = { + slabAfterEsc: await flow(null, slabPress), + slabAfterOther: await flow(() => flowPanel.showAtChainage(95), slabPress), + lineAfterEsc: await flow(null, linePress), + lineAfterOther: await flow(() => flowPanel.showAtChainage(95), linePress), + }; + // 폼 안 [추가] · 단 늘림 = 그 상자 고름 그대로(54-30) + flowPanel.showAtChainage(95); + flowLog.length = 0; + paint(["pipe", "chute_add", "soil_add"]); + out.first.addBox = flowLog.map((entry) => [...entry]); process.stdout.write(JSON.stringify(out)); -}); +})(); diff --git a/resources/tester/test_294_7_ford_slab_pick.py b/resources/tester/test_294_7_ford_slab_pick.py index 3c043e626..8f369ca25 100644 --- a/resources/tester/test_294_7_ford_slab_pick.py +++ b/resources/tester/test_294_7_ford_slab_pick.py @@ -5,6 +5,8 @@ - 콘크리트 누름 = 그 물넘이(자기 id 누가거리) 고름 — 카드에 걸친 연동 관(95 m) · 자기 다른 관으로 안 감 · 자기 관 없는 카드의 「관」 되살림도 물넘이 · 관 몸통 누름은 자기 관 그대로 · 다시 누름 = 풂 - 맹암거 형상 = 알림(목록이 id 로 고름) 그대로 +- 294-8 · 295-4 안 골라진 카드 첫 누름 = 눌린 대상 — 카드 고름이 연동 관으로 폼을 옮기지 않음 · 폼을 열 때 + 두 번 그림(기본값 → 시설 값)에 선 도수로 상자를 「새 상자」로 골라 덮지 않음(Esc 뒤 · 다른 카드 고른 뒤) 실제 TS 를 `helper_294_7_ford_slab_pick.cjs` 가 Node 로 돌린다. """ @@ -84,3 +86,39 @@ def test_배선_경사각도_상자(): # 폼에 물넘이(관 상자 없음) = 「경사각도」 상자(294-2 그대로) dock = _read("B06_Section_UI_Adjust_Dock.ts") assert 'side === "pipe" && (!own || own.closest("[hidden]")) ? visibleBox(SLOPE_BOX)' in dock + + +def test_안_골라진_카드_콘크리트_첫_누름_294_8(out): + f = out["first"] + want = [["select", 90, "pipe"], ["remember", 90, "pipe"], ["form", 90], ["form", 90]] + assert f["slabAfterEsc"] == want # 옛 = 폼 95 · {at:95, key:"chute"} + assert f["slabAfterOther"] == want # 4+15 배관 고른 뒤도 + + +def test_안_골라진_카드_보라_선_첫_누름_295_4(out): + f = out["first"] + want = [["select", 110, None], ["remember", 110, ""], ["release"], ["form", 110]] + assert f["lineAfterEsc"] == want # 옛 = {at:110, key:"chute"} · 선 풀림 + assert f["lineAfterOther"] == want + + +def test_폼_안_새_상자는_그대로_고름(out): + assert out["first"]["addBox"] == [ + ["select", 95, "extra0"], + ["remember", 95, "extra0"], + ["form", 95], + ] + + +def test_배선_카드_고름_리스너(): + with open( + os.path.join(ROOT, "A00_Common", "design", "design_view_cross.ts"), encoding="utf-8" + ) as f: + cross = " ".join(f.read().split()) + assert ( + "const owner = isWallSelecting() ? (wallOwnerM() ?? stationControls.structureSpan.ownerOf(target)?.chainage_m) : undefined;" + in cross + ) + assert "structuresPanel.showAtChainage(owner ?? target.chainage_m)" in cross + sel = _read("B06_Section_UI_Page_Structures_Selection.ts") + assert "if (!born || chainageM === null || opening) return;" in sel