From ffa762438fa1b151d790b5cef709b1346d8e2636 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 29 Oct 2026 07:14:40 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05=C2=B7B06):=20=EC=86=8C=EB=8B=A8=20?= =?UTF-8?q?=EC=82=AC=EB=B3=B8=20berm=20=3D=20=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=ED=86=B5=20=E2=80=94=20=EC=B4=88=EC=95=88=20=EC=B9=B8=20=C2=B7?= =?UTF-8?q?=20=E2=86=B6=20=EC=82=AC=EC=A7=84=20=C2=B7=20=EB=96=A0=EB=82=A8?= =?UTF-8?q?=20=EC=A0=9C=EC=99=B8=20=EC=A4=84=EC=97=90=EC=84=9C=20=EB=B9=A0?= =?UTF-8?q?=EC=A7=90=20(PLAN=20477-4=20=C2=B7=20=EC=9A=94=EA=B5=AC=20433)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - berm 은 구조물(소단)에서 셈한 값 · 손 고침 길 없음(쓰는 곳 = 페이지 시설 줄 하나) → 등록부 결과 통으로 - 떠남 확인 제외 목록의 berm 줄 지움(초안 통이 아니라 필요 없음) - 쓰는 곳 없는 readBermSpans 지움 · 읽는 곳(시설 줄 · 다시 셈) 이름 그대로 - 지킴 시험 test_477_undo_now_berm(즉시 켜짐 · flush · 묶음 · berm 통) Co-Authored-By: Claude Opus 5.5 --- A00_Common/b_page_state.ts | 7 +- A00_Common/design/design_leave.ts | 1 - B06_Section/B06_Section_UI_Page_Persist.ts | 20 +-- resources/tester/test_477_undo_now_berm.py | 142 +++++++++++++++++++++ 4 files changed, 147 insertions(+), 23 deletions(-) create mode 100644 resources/tester/test_477_undo_now_berm.py diff --git a/A00_Common/b_page_state.ts b/A00_Common/b_page_state.ts index 810c7b6f5..38c3a5045 100644 --- a/A00_Common/b_page_state.ts +++ b/A00_Common/b_page_state.ts @@ -145,10 +145,6 @@ export const STATE_REGISTRY = { * 암 경계선(`rockb`)과 같은 자리·같은 꼴이다. 재계산에 **계산 전에** 실어 보내야 * 설계선이 새 경사로 그려진다(값만 베껴 붙이면 그림과 숫자가 어긋난다). */ cutslope: { bucket: "draft", scope: "route" }, - /** 소단 제원(측점키 → {width_m, interval_m, slope_deg}) — 계획서 3-9. - * 사용자가 구간에 놓은 값이라 재계산에 함께 실어 보내야 한다. 안 실으면 계획선을 - * 고치는 순간 계단이 사라진다(암 경계선이 옛 키를 보던 것과 같은 자리). */ - berm: { bucket: "draft", scope: "route" }, /** 표준 횡단면 설정 패널의 편집값. * * 칸이 비었으면 `sections/context` 의 저장분(`stored_standard_cross_section`)으로 채운다 @@ -239,6 +235,9 @@ export const STATE_REGISTRY = { "design-stations": { bucket: "result", scope: "project" }, "design-profile": { bucket: "result", scope: "project" }, "design-footprint": { bucket: "result", scope: "project" }, + /** 소단 사본 — 구조물(소단)에서 셈한 구간 목록(`design_facility_lane` 만 씀 · 고침은 `structures` 칸). + * 다시 셈에 실어 보냄 · 안 실으면 계획선을 고치는 순간 계단이 사라짐(3-9 · 477-4). */ + berm: { bucket: "result", scope: "route" }, } as const satisfies Record; export type StateName = keyof typeof STATE_REGISTRY; diff --git a/A00_Common/design/design_leave.ts b/A00_Common/design/design_leave.ts index 49126e933..9e3cc9ecb 100644 --- a/A00_Common/design/design_leave.ts +++ b/A00_Common/design/design_leave.ts @@ -31,7 +31,6 @@ const NOT_EDITS = new Set([ "structure-pick", "structure-type-cache", "std-cross", - "berm", // 소단 사본 — 열 때 시설 줄이 구조물에서 씀(272 · 고침은 `structures` 칸) ]); const OTHER_PAGE = /^(b0[89]|d0\d|e0\d)-/; diff --git a/B06_Section/B06_Section_UI_Page_Persist.ts b/B06_Section/B06_Section_UI_Page_Persist.ts index f14377a40..5f82ba91b 100644 --- a/B06_Section/B06_Section_UI_Page_Persist.ts +++ b/B06_Section/B06_Section_UI_Page_Persist.ts @@ -16,13 +16,7 @@ import { } from "./B06_Section_Api_Fetch"; import type { DesignViewSave } from "../A00_Common/design/design_shared"; import { buildCrossPatches, type CrossPatchSources } from "./logic/B06_Section_UI_Page_Patches"; -import { - clearState, - readByKey, - readState, - writeByKey, - writeState, -} from "../A00_Common/b_page_state"; +import { clearState, readByKey, writeByKey, writeState } from "../A00_Common/b_page_state"; import { kindChangedChainages } from "./B06_Section_Api_Culvert_Options"; import { crossDesignChoices, @@ -130,17 +124,7 @@ function putCell( } } -/** - * 세션에 쌓인 소단 구간 목록. 없거나 손상되면 빈 목록. - * - * 암 경계선과 같은 성격이다 — 확정 전에는 세션에만 있으므로 계획선 재계산에 **함께 실어 - * 보내야** 한다. 안 실으면 계획선을 고치는 순간 계단이 사라진다(계획서 3-9). - */ -export function readBermSpans(projectId: string, routeId: number): BermSpan[] { - const stored = readState("berm", projectId, routeId); - return Array.isArray(stored) ? stored : []; -} - +/** 소단 사본(결과 통 · 구조물에서 셈 · 477-4) — 쓰는 곳은 페이지 시설 줄 하나. */ export function writeBermSpans(projectId: string, routeId: number, spans: BermSpan[]): void { writeState("berm", spans, projectId, routeId); } diff --git a/resources/tester/test_477_undo_now_berm.py b/resources/tester/test_477_undo_now_berm.py new file mode 100644 index 000000000..b37561579 --- /dev/null +++ b/resources/tester/test_477_undo_now_berm.py @@ -0,0 +1,142 @@ +"""477-3 · 477-4 (요구 433) — ↶ 바로 켜짐 · 소단 사본 = 결과 통. + +못박는 것 + - 477-3 조작 놓은 직후(찍기 기다림 400 ms 안) ↶ 켜짐 · 단추 맞춤 알림 · 바뀐 것 없는 누름은 안 켜짐. + - 기다림 중 ↶ = 먼저 찍고 되돌림(한 번에 조작 전) · ↷ = 다시 · 서버 다시 셈 뒤 쓰기 묶음은 그대로 한 걸음. + - 477-4 `berm` = 결과 통(사진 · 떠남 물음 밖) · 떠남 제외 목록에 사본 줄 없음. + +./venv/Scripts/python.exe -m pytest resources/tester/test_477_undo_now_berm.py -q +""" + +from __future__ import annotations + +import json +import re +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +TS_FILE = ROOT / "B05_Profile" / "B05_Profile_UI_Profile_History.ts" +LOADER = Path(__file__).resolve().parent / "helper_ts_cjs_load.cjs" + +DRIVER = r""" +import { writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +const data = new Map(); +globalThis.sessionStorage = { + get length() { return data.size; }, + key: (i) => [...data.keys()][i] ?? null, + getItem: (k) => (data.has(k) ? data.get(k) : null), + setItem: (k, v) => data.set(k, String(v)), + removeItem: (k) => data.delete(k), +}; +globalThis.window = new EventTarget(); +window.setTimeout = setTimeout; +window.sessionStorage = sessionStorage; +window.localStorage = { getItem: (k) => (k === "frd_current_project_id" ? "p1" : null) }; +const { loadTs } = createRequire(import.meta.url)(process.argv[3]); +const { createProfileHistory, onPageHistory } = loadTs(process.argv[4]); +const wait = (ms) => new Promise((r) => setTimeout(r, ms)); +const PIPES = "aislo:draft:pipes:p1"; +const CROSS = "aislo:draft:crossdesign:p1:7"; +const BERM = "aislo:result:berm:p1:7"; +const out = {}; +let changes = 0; +onPageHistory({ change: () => (changes += 1) }); +const press = () => window.dispatchEvent(new Event("pointerdown")); +const lift = () => window.dispatchEvent(new Event("pointerup")); + +// 1. 관 지움 — 놓은 한 틱 뒤 ↶ 켜짐(기다림 400 ms 안) +sessionStorage.setItem(PIPES, '[{"c":10},{"c":20}]'); +sessionStorage.setItem(BERM, "[]"); +const h = createProfileHistory(() => {}); +out.openCanUndo = h.canUndo(); +press(); +sessionStorage.setItem(PIPES, '[{"c":10}]'); +lift(); +await wait(30); +out.soonCanUndo = h.canUndo(); +out.soonChanges = changes; +// 바로 ↶ 단추(누름 · 놓음 · 누름 처리) — 기다리던 찍기 먼저 · 한 번에 조작 전 +press(); +lift(); +out.undo = h.undo(); +out.afterUndo = sessionStorage.getItem(PIPES); +out.undoLeft = h.canUndo(); +out.canRedo = h.canRedo(); +await wait(450); +out.redo = h.redo(); +out.afterRedo = sessionStorage.getItem(PIPES); +out.redoLeft = h.canRedo(); + +// 2. 서버 다시 셈 뒤 쓰기(200 ms) — 기다림이 끝나면 조작과 한 걸음 +data.clear(); +sessionStorage.setItem(PIPES, "a"); +sessionStorage.setItem(CROSS, "x"); +const h2 = createProfileHistory(() => {}); +press(); +sessionStorage.setItem(PIPES, "b"); +lift(); +await wait(200); +sessionStorage.setItem(CROSS, "y"); +sessionStorage.setItem(BERM, '[{"s":1}]'); // 사본 = 결과 통 — 사진 밖 +await wait(300); +h2.undo(); +out.grouped = [sessionStorage.getItem(PIPES), sessionStorage.getItem(CROSS)]; +out.groupedLeft = h2.canUndo(); +out.bermKept = sessionStorage.getItem(BERM); + +// 3. 바뀐 것 없는 누름 — ↶ 안 켜짐(깜빡임 없음) +data.clear(); +sessionStorage.setItem(PIPES, "a"); +const h3 = createProfileHistory(() => {}); +press(); +lift(); +await wait(30); +out.idleCanUndo = h3.canUndo(); +writeFileSync(process.argv[2], JSON.stringify(out)); +""" + + +@pytest.fixture(scope="module") +def result(tmp_path_factory: pytest.TempPathFactory) -> dict: + out = tmp_path_factory.mktemp("ts_477") + (out / "driver.mjs").write_text(DRIVER, encoding="utf-8") + run = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", str(out / "driver.mjs"), str(out / "out.json"), str(LOADER), str(TS_FILE)], + capture_output=True, text=True, cwd=str(out), + ) # fmt: skip + assert run.returncode == 0, run.stderr + return json.loads((out / "out.json").read_text(encoding="utf-8")) + + +def test_놓은_직후_되돌리기_켜짐(result) -> None: + assert result["openCanUndo"] is False + assert result["soonCanUndo"] is True + assert result["soonChanges"] >= 1 # 단추 맞춤 알림이 기다림 전에 감 + + +def test_기다림_중_되돌리기_먼저_찍고_한_걸음(result) -> None: + assert result["undo"] is True + assert result["afterUndo"] == '[{"c":10},{"c":20}]' + assert result["undoLeft"] is False and result["canRedo"] is True + assert result["redo"] is True and result["afterRedo"] == '[{"c":10}]' + assert result["redoLeft"] is False + + +def test_서버_다시_셈_묶음_그대로(result) -> None: + assert result["grouped"] == ["a", "x"] and result["groupedLeft"] is False + assert result["bermKept"] == '[{"s":1}]' # 결과 통 사본은 되돌리기가 안 건드림 + + +def test_바뀐_것_없는_누름은_안_켜짐(result) -> None: + assert result["idleCanUndo"] is False + + +def test_소단_사본은_결과_통() -> None: + state = (ROOT / "A00_Common" / "b_page_state.ts").read_text(encoding="utf-8") + assert re.search(r'^\s*berm: \{ bucket: "result", scope: "route" \}', state, re.M) + leave = (ROOT / "A00_Common" / "design" / "design_leave.ts").read_text(encoding="utf-8") + assert '"berm"' not in leave