From a59259ef21b6dcbb5a210d6bc9eb1e171710624f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 18 Oct 2026 18:54:30 +0900 Subject: [PATCH] =?UTF-8?q?fix(444=20B06=20=EA=B4=80=20=EA=B5=AC=EA=B0=84?= =?UTF-8?q?=EA=B0=92):=20=EC=84=B8=EC=9B=94=EA=B5=90=20=C2=B7=20BOX=20?= =?UTF-8?q?=ED=8F=BC=20=EA=B0=92=20=3D=20=EA=B4=80=20=EC=B4=88=EC=95=88=20?= =?UTF-8?q?=ED=95=9C=20=EB=B2=8C=20=C2=B7=20=EC=98=9B=20=EC=98=88=EC=95=BD?= =?UTF-8?q?=EC=9D=80=20=EA=B4=80=20=EC=98=AE=EA=B9=80=20=EB=94=B0=EB=9D=BC?= =?UTF-8?q?=EA=B0=90=20=C2=B7=20=EB=AA=BB=20=EB=A7=9E=EC=B6=98=20=EA=B0=92?= =?UTF-8?q?=20=EB=B2=84=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 세월교 · BOX 조정 제어가 폼 값을 조정 예약(culvertopt)에 또 담지 않음 — 관 초안(가게 pipes)이 관 id 로 옮김 · 노선 바뀜을 따라감(H4a) - 측점 갈아끼움 옮김 목록에 culvertopt 추가 — 옛 세션 잔값도 관 새 자리로 - 내보내기 주석 — 못 맞춘 값은 다른 관에 안 얹고 버림 - 시험 test_444_culvertopt_single_draft — 옮김 · 두 번째 [저장] 값 그대로(H4b) · 못 맞춘 값 버림 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn --- .../B06_Section_Api_Culvert_Options.ts | 2 + B06_Section/B06_Section_Station_Keys.ts | 3 +- .../B06_Section_UI_Page_Ford_Controls.ts | 10 +- .../B06_Section_UI_Page_Station_Controls.ts | 1 - resources/tester/helper_444_culvertopt.mjs | 145 ++++++++++++++++++ resources/tester/helper_444_entry.ts | 3 + .../test_444_culvertopt_single_draft.py | 88 +++++++++++ 7 files changed, 242 insertions(+), 10 deletions(-) create mode 100644 resources/tester/helper_444_culvertopt.mjs create mode 100644 resources/tester/helper_444_entry.ts create mode 100644 resources/tester/test_444_culvertopt_single_draft.py diff --git a/B06_Section/B06_Section_Api_Culvert_Options.ts b/B06_Section/B06_Section_Api_Culvert_Options.ts index 89bd7be8c..ac80fd1dd 100644 --- a/B06_Section/B06_Section_Api_Culvert_Options.ts +++ b/B06_Section/B06_Section_Api_Culvert_Options.ts @@ -67,6 +67,8 @@ function writePending(sessionKey: string | null, pending: PendingMap): void { * 세션에 쌓인 구간값을 `pipe_points.json`에 한 번에 쓴다. 예약분이 없으면 아무것도 * 하지 않는다. 성공하면 세션을 비우고, 실패하면 **그대로 두어** 다음 [저장]에서 * 다시 시도한다 — 조용히 잃으면 화면과 저장분이 갈린다. + * 지금 관에 못 맞춘 값(옮긴 · 지운 관 옛 자리)은 버림 — 남기면 그 자리에 온 다른 관에 얹힘(444 · + * 세월교 · BOX 폼 값은 관 초안 한 벌로 나가므로 잃지 않음). * 옛 「관 이동 예약」(culvertmove)은 읽지 않음 — 관 위치는 B05·B06 이 같은 관 목록 스냅샷으로만 * 옮김. 옛 세션 잔값을 저장 때 적용하면 관이 이웃 측점으로 튀어 시설 속성이 넘어감. */ diff --git a/B06_Section/B06_Section_Station_Keys.ts b/B06_Section/B06_Section_Station_Keys.ts index e63b1f823..123cd4017 100644 --- a/B06_Section/B06_Section_Station_Keys.ts +++ b/B06_Section/B06_Section_Station_Keys.ts @@ -491,7 +491,8 @@ function moveIdleStates(moves: ReadonlyMap, owned: ReadonlySet) => Object.fromEntries(renameChainageKeys(new Map(Object.entries(cells)), moves)); - for (const state of Object.keys(ROUTE_CARRY_STATES)) { + // 관 구간값 예약(culvertopt · 설계 칸 아님 = 옮김 목록 짝 밖)도 관 새 자리로(444) + for (const state of [...Object.keys(ROUTE_CARRY_STATES), "culvertopt"]) { if ((KEYED_STATES as readonly string[]).includes(state)) continue; const name = state as StateName; const key = stateKey(name, projectId, routeId); diff --git a/B06_Section/B06_Section_UI_Page_Ford_Controls.ts b/B06_Section/B06_Section_UI_Page_Ford_Controls.ts index 8b5c997b4..495de821e 100644 --- a/B06_Section/B06_Section_UI_Page_Ford_Controls.ts +++ b/B06_Section/B06_Section_UI_Page_Ford_Controls.ts @@ -4,8 +4,8 @@ * (`_UI_Page_Station_Controls.ts`)에서 700줄 제한으로 분리했다(2026-08-25). * * 데이터 흐름은 집수정과 같다: 세션 사본 + 캐시 `design.ford_adjust`에 함께 실어 - * 3D(코리도)가 같은 값으로 다시 그리게 한다. 관경·수량은 B05 정본(`pipe_points`)이 - * 원천이라 배수관 구간값과 같은 저장기로 되돌려 쓴다. + * 3D(코리도)가 같은 값으로 다시 그리게 한다. 관경·수량·날개벽은 폼이 관 초안(가게 pipes) + * 한 벌로 냄 — 여기는 캐시만 고침(444 · 조정 예약 culvertopt 에 또 담으면 옮긴 관 옛 자리에 남음). * ========================================================================== */ import { @@ -106,8 +106,6 @@ export interface FordControlDeps { sectionAt: (chainageM: number) => CrossSection | undefined; patchCachedDesign: (chainageM: number, patch: Partial) => void; refreshCard: (chainageM: number) => void; - /** 관경·수량·날개벽 제원을 B05 정본으로 되돌려 쓴다(묶어서 늦게 저장). */ - queuePipeOptions: (chainageM: number, patch: Record) => void; round1: (value: number) => number; clampMove: (value: number) => number; /** 같은 측점 벽 고름을 풂 — 측벽 · 흙막이 동시 고름 금지(107-1). */ @@ -203,7 +201,6 @@ export function createFordControls(deps: FordControlDeps): FordControls { spec.overflow_depth_m = Math.max(patch.ford_height_m, 0); } dropDefaulted(spec, patch); - deps.queuePipeOptions(chainageM, patch); deps.refreshCard(chainageM); }, setWing: (chainageM, role, patch) => { @@ -218,7 +215,6 @@ export function createFordControls(deps: FordControlDeps): FordControls { wing.slab_extend_m = wingSlabExtendM(wing.installed, wing.length_m, wing.angle_deg); } dropDefaulted(spec, wingOptions(role, patch)); - deps.queuePipeOptions(chainageM, wingOptions(role, patch)); deps.refreshCard(chainageM); }, selectedFor: (chainageM) => fordSelections.get(chainageM.toFixed(2)) ?? null, @@ -400,7 +396,6 @@ export function createBoxControls(deps: FordControlDeps): { if (patch.body_height_m) spec.inner_height_m = patch.body_height_m; } dropDefaulted(spec, patch); - deps.queuePipeOptions(chainageM, patch as Record); deps.refreshCard(chainageM); }, setWing: (chainageM, role, patch) => { @@ -416,7 +411,6 @@ export function createBoxControls(deps: FordControlDeps): { : 0; } dropDefaulted(spec, wingOptions(role, patch)); - deps.queuePipeOptions(chainageM, wingOptions(role, patch)); deps.refreshCard(chainageM); }, selectedFor: (chainageM) => selections.get(chainageM.toFixed(2)) ?? null, diff --git a/B06_Section/B06_Section_UI_Page_Station_Controls.ts b/B06_Section/B06_Section_UI_Page_Station_Controls.ts index 7a6ebe3c7..53d7bc8d3 100644 --- a/B06_Section/B06_Section_UI_Page_Station_Controls.ts +++ b/B06_Section/B06_Section_UI_Page_Station_Controls.ts @@ -580,7 +580,6 @@ export function createStationControls(deps: StationControlDeps): StationControls sectionAt, patchCachedDesign, refreshCard: deps.refreshCard, - queuePipeOptions: culvertOptions.queue, round1, clampMove, releaseWall: (chainageM) => { diff --git a/resources/tester/helper_444_culvertopt.mjs b/resources/tester/helper_444_culvertopt.mjs new file mode 100644 index 000000000..271337537 --- /dev/null +++ b/resources/tester/helper_444_culvertopt.mjs @@ -0,0 +1,145 @@ +/* 444 헬퍼 — 관 구간값 예약(culvertopt)이 관 옮김을 따라가고 · 못 맞춘 값은 버리고 · 두 번째 [저장]이 안 덮음. + * node helper_444_culvertopt.mjs <묶음.mjs> + * 묶음 = 444 진입점(`helper_444_entry.ts` · 346-2 묶음 + 내보내기 · vite ssr 진짜 모듈) · 표준출력 JSON 한 벌. + * 짝 = test_444_culvertopt_single_draft.py · 틀 = helper_415_swap.mjs(415-3). + */ +import { pathToFileURL } from "node:url"; + +const store = () => { + const m = new Map(); + return { + getItem: (k) => (m.has(k) ? m.get(k) : null), + setItem: (k, v) => m.set(k, String(v)), + removeItem: (k) => m.delete(k), + key: (i) => [...m.keys()][i] ?? null, + get length() { + return m.size; + }, + clear: () => m.clear(), + }; +}; +globalThis.window = globalThis; +globalThis.sessionStorage = store(); +globalThis.localStorage = store(); +const any = new Proxy(function () {}, { + get: (_t, k) => (k === Symbol.toPrimitive ? () => 0 : any), + apply: () => any, + construct: () => any, +}); +globalThis.document = any; +globalThis.requestAnimationFrame = (run) => setTimeout(run, 0); +globalThis.__toasts = []; + +/* 관 정본(서버) — GET 은 지금 관 · PUT 은 받은 몸통으로 바꿈 */ +let saved = []; +const puts = []; +globalThis.fetch = async (url, init = {}) => { + if (String(url).includes("/drainage/pipe-points") && init.method === "PUT") { + const points = JSON.parse(init.body).points; + puts.push(points); + saved = points.map((point) => ({ ...point, lonlat: [0, 0] })); + } + return { ok: true, status: 200, json: async () => ({ pipe_points: saved, basins: [] }) }; +}; + +const m = await import(pathToFileURL(process.argv[2]).href); +const P = "p444"; +const R = 3; +const out = {}; + +const section = (chainage_m, kind, extra = {}) => ({ + station_id: `station_${String(Math.round(chainage_m * 1000)).padStart(12, "0")}`, + chainage_m, + label: "", + kind, + center_z: 100, + azimuth_deg: 90, + center_x: chainage_m, + center_y: 0, + frame: { left_xy: [0, 1] }, + samples: [{ offset_m: 0, elevation_m: 100, valid: true }], + design: { design_elevation_m: 101 }, + ...extra, +}); +const detail = () => ({ + longitudinal: { + length_m: 100, + stations: [], + profile_alignment: { policy: { station_interval_m: 20 } }, + }, + cross_sections: [ + section(0, "bp"), + section(20, "regular"), + section(40, "regular"), + section(45.054, "irregular", { structure: "세월교", culvert: {} }), + section(60, "regular"), + section(80, "regular"), + section(100, "ep"), + ], +}); +const station = (key, chainageM, kind, id = null) => ({ + key, + chainageM, + x: chainageM, + y: 0, + azimuthDeg: 90, + left: [0, 1], + kind, + id, + ground: { centerZ: 99, left: [{ offsetM: 0.5, z: 99.1 }], right: [{ offsetM: 0.5, z: 98.9 }] }, + planRadiusM: null, + curveOuterSide: null, + curveWideningM: null, +}); +const list = (pipeAt) => + [ + station("end:BP", 0, "end"), + station("grid:1", 20, "grid"), + station("grid:2", 40, "grid"), + station("pipe:pipe-s1", pipeAt, "pipe", "pipe-s1"), + station("grid:3", 60, "grid"), + station("grid:4", 80, "grid"), + station("end:EP", 100, "end"), + ].sort((a, b) => a.chainageM - b.chainageM); +const key = `aislo:draft:culvertopt:${P}:${R}`; + +/* 1. 관 5 m 옮김 — 옛 세션 구간값 예약도 관 새 자리로(조정창 맵 없이 · 415-3 과 같은 길) */ +m.writeState("section-detail", detail(), P, R); +m.writeState("pipe-ids", { next: 2, ids: [{ id: "pipe-s1", at: 45.054 }] }, P); +await m.loadSectionDetail(P, R); +m.writeState("culvertopt", { 45.05: { ford_width_m: 4 } }, P, R); +const stop = m.followSwaps( + P, + () => R, + { visible: () => true }, + async () => undefined, +); +m.writeState("pipe-ids", { next: 2, ids: [{ id: "pipe-s1", at: 50 }] }, P); +m.useDesignStore(P, null).set("stations", list(50)); +await new Promise((resolve) => setTimeout(resolve, 30)); +out.moved = m.readState("culvertopt", P, R); +stop(); + +/* 2. [저장] 두 번 — 첫 번째는 관 새 자리에 얹고 세션 비움 · 두 번째는 안 보냄(값 그대로) */ +saved = [ + { id: "p5000", chainage_m: 50, source: "stream", facility: "ford", options: { ford_width_m: 3 } }, +]; +await m.flushCulvertOptions(P, key); +out.firstSave = { + puts: puts.length, + width: saved[0].options.ford_width_m, + left: sessionStorage.getItem(key), +}; +await m.flushCulvertOptions(P, key); +out.secondSave = { puts: puts.length, width: saved[0].options.ford_width_m }; + +/* 3. 못 맞춘 값(지운 · 옮긴 관 옛 자리) — 다른 관에 안 얹고 버림 */ +m.writeState("culvertopt", { "30.00": { ford_width_m: 9 } }, P, R); +await m.flushCulvertOptions(P, key); +out.unmatched = { + puts: puts.length, + width: saved[0].options.ford_width_m, + left: sessionStorage.getItem(key), +}; + +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/helper_444_entry.ts b/resources/tester/helper_444_entry.ts new file mode 100644 index 000000000..f943accb4 --- /dev/null +++ b/resources/tester/helper_444_entry.ts @@ -0,0 +1,3 @@ +/* 444 시험 묶음 진입점 — 346-2 갈아끼움 묶음 + 관 구간값 예약 내보내기(진짜 모듈 · vite ssr). */ +export * from "./helper_346_2_swap_entry"; +export { flushCulvertOptions } from "../../B06_Section/B06_Section_Api_Culvert_Options"; diff --git a/resources/tester/test_444_culvertopt_single_draft.py b/resources/tester/test_444_culvertopt_single_draft.py new file mode 100644 index 000000000..778c1230e --- /dev/null +++ b/resources/tester/test_444_culvertopt_single_draft.py @@ -0,0 +1,88 @@ +"""444 관 구간값(H4a · H4b) — 세월교 · BOX 폼 값은 관 초안 한 벌 · 옛 예약은 관을 따라가고 못 맞추면 버림. + +- 폼이 낸 세월교 · BOX 값(관경 · 수량 · 월류 폭 · 날개벽)은 관 초안(가게 pipes)으로만 나감 — 조정 예약 + (culvertopt)에 또 담지 않음(test_b06_adjust_single_form 규칙 · 관 id 로 옮김 · 노선 바뀜을 따라감). +- 옛 세션 예약(culvertopt)은 관 옮김(측점 갈아끼움)을 따라 새 자리로 · [저장] 때 못 맞춘 값은 버림(지운 관 + 자리에 온 다른 관에 안 얹힘) · 두 번째 [저장]은 다시 안 보냄(값 그대로). + +짝: `helper_444_culvertopt.mjs`(444 진입점 묶음 · 진짜 상세 가게 · 내보내기). +""" + +import json +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +import test_365_page_pipeline as t365 # noqa: E402 + +pytestmark = t365.pytestmark + + +@pytest.fixture(scope="module") +def out(tmp_path_factory) -> dict: + work = tmp_path_factory.mktemp("b444") + build = subprocess.run( # noqa: S603 — 고정 실행 파일 + [ + "node", + str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"), + "build", + "--configLoader", + "runner", + "--ssr", + "../resources/tester/helper_444_entry.ts", + "--outDir", + str(work), + "--logLevel", + "error", + ], + cwd=ROOT, + capture_output=True, + text=True, + encoding="utf-8", + timeout=300, + ) + assert build.returncode == 0, build.stderr + bundle = work / "bundle.mjs" + shutil.copy(work / "helper_444_entry.js", bundle) + proc = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", str(HERE / "helper_444_culvertopt.mjs"), str(bundle)], + cwd=ROOT, + capture_output=True, + text=True, + encoding="utf-8", + timeout=120, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_세월교_BOX_폼_값은_조정_예약에_안_쌓음() -> None: + ford = (ROOT / "B06_Section" / "B06_Section_UI_Page_Ford_Controls.ts").read_text( + encoding="utf-8" + ) + controls = (ROOT / "B06_Section" / "B06_Section_UI_Page_Station_Controls.ts").read_text( + encoding="utf-8" + ) + assert "queuePipeOptions" not in ford + assert "queuePipeOptions" not in controls + + +def test_관_옮김_예약도_새_자리로(out): + assert out["moved"] == {"50.00": {"ford_width_m": 4}} + + +def test_두_번째_저장은_값_그대로(out): + assert out["firstSave"] == {"puts": 1, "width": 4, "left": None} + assert out["secondSave"] == {"puts": 1, "width": 4} + + +def test_못_맞춘_값은_다른_관에_안_얹고_버림(out): + assert out["unmatched"] == {"puts": 1, "width": 4, "left": None}