fix(472 덧 B05 관 지움 ↶): 관 초안 없는 걸음 = 저장분 — 판이 마지막 저장분 답과 견줘 다르면 조용히 다시 읽음 · 옛 「처음 본 관 목록」(판이 읽기 전 빈 목록 · 관 하나 지움 ↶ 이 같은 목록으로 멈춤 → 배수판 · 시설 줄 · 알약 · 목록 관 없음 · −17,385.4 · B06 보기 배수유역도 관 · 세부유역 빠짐) 걷음 · 종단 ↶ ↷ 켜짐 = 사진첩 change 알림으로 바로(삭제 직후 꺼져 있던 흠) · 시험 test_472_d_b05_pipe_undo(옛 코드 빨강 2) · 옛 단언 215_1 이름 · 219_1 흉내 · 위키 operations_common · _b05 · rules 줄
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,7 @@ const STUBS = {
|
||||
canRedo: () => false,
|
||||
}),
|
||||
setPageHistory: () => undefined,
|
||||
onPageHistory: () => () => undefined, // ↶ ↷ 켜짐 맞춤(472 덧) · 이 흠과 무관
|
||||
},
|
||||
"./B05_Profile_UI_Profile_Tools": {
|
||||
createProfileTools: () => ({ mode: () => "none", clear: () => undefined }),
|
||||
@@ -95,7 +96,7 @@ tools.createPanelTools({
|
||||
irregularStations: () => [pipe],
|
||||
stationIdOf: (s) => `irregular:${s.id}`,
|
||||
moveStation: (s, to) => calls.move.push([s.chainage_m, to]),
|
||||
drainage: { pipeChainages: () => [], setPipeChainages: () => undefined },
|
||||
drainage: { setPipeChainages: () => undefined },
|
||||
restore: () => undefined,
|
||||
restoreSaved: () => undefined,
|
||||
canRestoreSaved: () => false,
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/* 472 덧 헬퍼 — B05 보기 관 지움 ↶ 가 반쪽만 돌아오던 흠 · 종단 ↶ 단추가 다시 그리기 전까지 꺼져 있던 흠.
|
||||
* 실제 TS(`B05_Profile_UI_Profile_Panel_Tools` · `B05_Profile_UI_Profile_History` · 가게)를 그 자리에서
|
||||
* 트랜스파일해 Node 로 돌림 · 배수 판은 흉내(받은 관 목록만 적음) · 결과 = 표준출력 JSON. 짝 = `test_472_d_b05_pipe_undo.py`.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||
const CATALOG = require("./helper_322_catalog_module.cjs");
|
||||
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const cache = new Map();
|
||||
|
||||
const listeners = {};
|
||||
const storage = () => {
|
||||
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.sessionStorage = storage();
|
||||
globalThis.localStorage = storage();
|
||||
globalThis.window = {
|
||||
sessionStorage: globalThis.sessionStorage,
|
||||
localStorage: globalThis.localStorage,
|
||||
addEventListener: (type, fn) => (listeners[type] ??= new Set()).add(fn),
|
||||
removeEventListener: (type, fn) => listeners[type]?.delete(fn),
|
||||
setTimeout: () => 0,
|
||||
clearTimeout: () => undefined,
|
||||
};
|
||||
globalThis.document = { addEventListener: () => undefined, querySelector: () => null };
|
||||
|
||||
// 도구줄 · 자유지정 그림은 흉내 — ↶ ↷ 켜짐 맞춤 부름 수만 셈
|
||||
let synced = 0;
|
||||
const STUBS = {
|
||||
"./B05_Profile_UI_Profile_Tools": {
|
||||
createProfileTools: () => ({
|
||||
mode: () => "none",
|
||||
clear: () => undefined,
|
||||
syncHistory: () => void (synced += 1),
|
||||
}),
|
||||
},
|
||||
"../A00_Common/design/logic/design_profile_free": {
|
||||
createFreeDraw: () => ({ key: () => false, hint: () => "" }),
|
||||
},
|
||||
"../A00_Common/design/design_profile_free_view": {
|
||||
createFreeView: () => ({ mount: () => undefined, redraw: () => undefined }),
|
||||
},
|
||||
};
|
||||
|
||||
function loadTs(file) {
|
||||
const full = path.resolve(file);
|
||||
if (cache.has(full)) return cache.get(full);
|
||||
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(full, box.exports);
|
||||
const localRequire = (request) =>
|
||||
(CATALOG.isCatalogRequest(request) ? CATALOG : null) ??
|
||||
STUBS[request] ??
|
||||
(request.startsWith(".")
|
||||
? loadTs(path.join(path.dirname(full), `${request}.ts`))
|
||||
: require(request));
|
||||
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||
cache.set(full, box.exports);
|
||||
return box.exports;
|
||||
}
|
||||
|
||||
const store = loadTs(path.join(ROOT, "A00_Common", "design", "design_store.ts"));
|
||||
const tools = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Panel_Tools.ts"));
|
||||
|
||||
const P = "p472d";
|
||||
localStorage.setItem("frd_current_project_id", P);
|
||||
store.pinDraftProject(P);
|
||||
const shop = store.useDesignStore(P, null);
|
||||
|
||||
// 배수 판 흉내 — 판이 저장분을 읽기 전(빈 목록)에 사진첩이 섬 · 관 하나(351) 프로젝트
|
||||
let shown = [];
|
||||
const given = [];
|
||||
const panel = tools.createPanelTools({
|
||||
root: { isConnected: true },
|
||||
base: () => ({}),
|
||||
alignment: () => null,
|
||||
edits: () => ({}),
|
||||
savedRadii: () => ({}),
|
||||
applyEdits: () => undefined,
|
||||
selectedStationId: () => null,
|
||||
irregularStations: () => [],
|
||||
stationIdOf: (s) => s.id,
|
||||
moveStation: () => undefined,
|
||||
drainage: {
|
||||
pipeChainages: () => shown, // 옛 「처음 본 목록」 길이 부름
|
||||
setPipeChainages: (chainages) => given.push(chainages),
|
||||
},
|
||||
restore: () => undefined,
|
||||
restoreSaved: () => undefined,
|
||||
canRestoreSaved: () => false,
|
||||
refresh: () => undefined,
|
||||
});
|
||||
shown = [351]; // 판이 저장분을 읽음
|
||||
const touch = () => listeners.keydown?.forEach((fn) => fn({ key: "a", target: null }));
|
||||
|
||||
// 종단 우클릭 [배관 삭제] — 판이 관을 빼고 가게 관 초안을 빈 목록으로 씀
|
||||
touch();
|
||||
const syncedBefore = synced;
|
||||
shown = [];
|
||||
shop.set("pipes", []);
|
||||
panel.history.record();
|
||||
const afterDelete = { canUndo: panel.history.canUndo(), synced: synced - syncedBefore };
|
||||
|
||||
panel.history.undo();
|
||||
const undo = { given: given.at(-1), draft: shop.get("pipes") ?? null };
|
||||
panel.history.redo();
|
||||
const redo = { given: given.at(-1), draft: shop.get("pipes") ?? null };
|
||||
panel.history.undo();
|
||||
const undo2 = { given: given.at(-1) };
|
||||
|
||||
process.stdout.write(JSON.stringify({ afterDelete, undo, redo, undo2 }));
|
||||
@@ -135,8 +135,8 @@ def test_적용_막히면_열린_채(out):
|
||||
|
||||
def test_되돌리기_배선_자유지정_먼저():
|
||||
panel = _read("B05_Profile/B05_Profile_UI_Profile_Panel_Tools.ts")
|
||||
assert 'tools.mode() === "free" ? (free.undo(), freeView.redraw()) : inner.undo()' in panel
|
||||
assert 'tools.mode() === "free" ? free.canUndo() : inner.canUndo()' in panel
|
||||
assert 'tools.mode() === "free" ? (free.undo(), freeView.redraw()) : history.undo()' in panel
|
||||
assert 'tools.mode() === "free" ? free.canUndo() : history.canUndo()' in panel
|
||||
keys = panel[panel.index('isCtrlKey(event, "z") || isCtrlKey(event, "y")') :]
|
||||
free_at = keys.index('if (tools.mode() === "free") {')
|
||||
assert (
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""PLAN 472 덧 (요구 272) — B05 보기 관 지움 ↶ 가 반쪽만 돌아옴 · 배수유역도도 안 돌아옴.
|
||||
|
||||
까닭: ↶ 가 관 초안 없는 걸음(저장분)으로 가면 종단 사진첩이 「처음 본 관 목록」을 배수 판에
|
||||
줬는데, 그 목록은 판이 저장분을 읽기 전에 잡혀 빈 목록 — 관 하나 프로젝트에서 지운 뒤 판 목록
|
||||
(빈 목록)과 같아 판이 「같은 목록」으로 멈춤 → 배수 판 · 시설 줄 · 알약 · 목록은 관 없음 ·
|
||||
묶음은 저장분 관(측점 57) · 누가토량 −17,385.4. B06 보기 ↶ 도 같은 길(배수유역도 관 · 세부유역
|
||||
빠짐). 가게 구독은 빈 초안(null)을 안 받음. 또 종단 ↶ 단추는 다시 그릴 때만 켜짐을 맞춰 삭제
|
||||
직후 꺼져 있었음.
|
||||
고침: 초안 없는 걸음 = null(저장분) → 판이 마지막 저장분 답과 견줘 다르면 조용히 다시 읽음 ·
|
||||
사진첩 쌓임 · 되돌림 알림에 ↶ ↷ 켜짐 맞춤. TS 는 `helper_472_d_b05_pipe_undo.cjs` 가 Node 로 돌림.
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_472_d_b05_pipe_undo.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def _read(*parts: str) -> str:
|
||||
return ROOT.joinpath(*parts).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_472_d_b05_pipe_undo.cjs")],
|
||||
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_초안_없는_걸음은_저장분(out) -> None:
|
||||
"""옛 코드 = 판이 읽기 전 빈 목록([])을 줘 지운 뒤 목록과 같아 멈춤(빨강)."""
|
||||
assert out["undo"] == {"given": None, "draft": None}
|
||||
assert out["undo2"]["given"] is None
|
||||
|
||||
|
||||
def test_다시하기는_그_걸음_초안(out) -> None:
|
||||
assert out["redo"] == {"given": [], "draft": []}
|
||||
|
||||
|
||||
def test_삭제_직후_되돌리기_단추_켜짐(out) -> None:
|
||||
"""옛 코드 = 종단 도구줄은 다시 그릴 때만 ↶ 켜짐을 맞춤(맞춤 0 번 · 빨강)."""
|
||||
assert out["afterDelete"]["canUndo"] is True
|
||||
assert out["afterDelete"]["synced"] >= 1
|
||||
|
||||
|
||||
def test_배수_판_저장분_견줌() -> None:
|
||||
panel = _read("B05_Profile", "B05_Profile_UI_Drainage_Panel.ts")
|
||||
# 저장분 답을 받은 때만 견줄 몸통을 적음 · 초안 없는 걸음은 다르면 조용히 다시 읽음
|
||||
assert "(response) => ((saved = { response, request: null }), response)" in panel
|
||||
assert (
|
||||
"if (saved?.response === response) saved.request = JSON.stringify(pipeRequest());" in panel
|
||||
)
|
||||
assert (
|
||||
"return void (JSON.stringify(pipeRequest()) !== saved?.request && run(loadSaved, true));"
|
||||
in panel
|
||||
)
|
||||
assert "if (projectId !== nextProjectId) saved = null;" in panel
|
||||
tools = _read("B05_Profile", "B05_Profile_UI_Profile_Panel_Tools.ts")
|
||||
assert "initialPipes" not in tools
|
||||
profile = _read("A00_Common", "design", "design_profile.ts")
|
||||
assert "disposeTools();" in profile
|
||||
Reference in New Issue
Block a user