Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1

This commit is contained in:
2026-10-19 09:40:17 +09:00
18 changed files with 420 additions and 166 deletions
@@ -24,6 +24,7 @@ globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStora
const REAL = new Set(
[
"A00_Common/b_page_state.ts",
"A00_Common/design/design_store.ts", // 페이지 설정 값 주인 = 가게 칸(472-5)
"B05_Profile/B05_Profile_Api_Fetch.ts",
"B05_Profile/B05_Profile_Api_PageSettings.ts",
"B05_Profile/B05_Profile_UI_Page_Latest.ts",
@@ -56,18 +57,20 @@ const settings = load(path.join(ROOT, "B05_Profile", "B05_Profile_Api_PageSettin
const state = load(path.join(ROOT, "A00_Common", "b_page_state.ts"));
const P = "P463";
/** 가짜 종단도 — 받은 기준을 적음. */
/** 가짜 종단도 — 받은 기준을 적음. 최소고 강제는 편집 가드가 페이지 설정 한 칸을 바로 읽음(472-5) —
* 화면은 늘 캐시에 둔 노선을 넘기므로(`writeRouteLatestCache` 뒤) 받은 노선을 캐시에 두고 읽음. */
function apply(latest) {
const got = {};
if (latest) state.writeState("latest", latest, P);
latestApi.applyPageSettings(
{
setGradeLimit: (pct) => (got.grade = pct),
setEnforceMinCover: (on) => (got.enforce = on),
setStationDisplay: (next) => (got.display = next),
},
P,
latest,
);
if (Object.keys(got).length) got.enforce = settings.enforceMinCoverOf(P);
return got;
}
const latestOf = (params) => ({ project_id: P, surface_params: {}, route_params: params });
@@ -78,6 +78,18 @@ async function run() {
route_params: { options: { grade_class: "trunk", design_speed_kph: 20 }, max_grade_pct: 15 },
};
const api = load(path.join(ROOT, "B05_Profile", "B05_Profile_Api_PageSettings.ts"), {
// 값 주인 = 설계 가게 칸(472-5) — 같은 세션 흉내 위에
"../A00_Common/design/design_store": {
useDesignStore: (p) => ({
get: () => (session.has(`pagesettings:${p}`) ? session.get(`pagesettings:${p}`) : null),
set: (_key, value) =>
value === null
? session.delete(`pagesettings:${p}`)
: session.set(`pagesettings:${p}`, value),
}),
draftProjectId: () => "p1",
},
"./B05_Profile_UI_Profile_History": { onPageHistory: () => () => undefined },
"../A00_Common/b_page_state": {
readState: (name, p) => (session.has(`${name}:${p}`) ? session.get(`${name}:${p}`) : null),
writeState: (name, value, p) => session.set(`${name}:${p}`, value),
@@ -0,0 +1,119 @@
/* 472-5 5-C 헬퍼 — 종단 편집 · 페이지 설정 값 주인 = 설계 가게 칸 하나. 진짜 `b_page_state` · `design_store` ·
* `_Profile_Edit` · `_Api_PageSettings` 를 Node 로(세션 = 메모리 · 되돌리기 = 세션 칸 되쓰기 + 훅 부름 흉내).
* node helper_472_5_value_owner.cjs (짝 = `test_472_5_value_owner.py`)
*/
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..", "..");
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
function memoryStorage() {
const map = new Map();
return {
getItem: (k) => (map.has(k) ? map.get(k) : null),
setItem: (k, v) => map.set(k, String(v)),
removeItem: (k) => map.delete(k),
key: (i) => [...map.keys()][i] ?? null,
get length() {
return map.size;
},
};
}
globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage() };
const REAL = new Set(
[
"A00_Common/b_page_state.ts",
"A00_Common/design/design_store.ts",
"B05_Profile/B05_Profile_UI_Profile_Edit.ts",
"B05_Profile/B05_Profile_Api_PageSettings.ts",
"B05_Profile/logic/B05_Profile_UI_Profile_Alignment.ts",
].map((rel) => path.join(ROOT, rel)),
);
const historyHooks = [];
const STUBS = {
B05_Profile_UI_Profile_History: { onPageHistory: (entry) => historyHooks.push(entry) },
B05_Profile_Api_Fetch: {
readRouteLatestCache: (p) => state().readState("latest", p),
writeRouteLatestCache: (p, value) => state().writeState("latest", value, p),
savePageSettings: async () => ({}),
},
};
const anything = () =>
new Proxy({}, { get: (_target, key) => (key === "__esModule" ? false : () => undefined) });
const cache = new Map();
function load(file) {
if (cache.has(file)) return cache.get(file);
const box = { exports: {} };
cache.set(file, box.exports);
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const req = (request) => {
const stub = STUBS[path.basename(request)];
if (stub) return stub;
if (request === "@ui/ui_template_locale") return { ui_locales: {}, currentLanguageIndex: 0 };
if (!request.startsWith(".")) return anything();
const target = path.resolve(path.dirname(file), request);
const full = fs.existsSync(`${target}.ts`) ? `${target}.ts` : target;
return REAL.has(full) ? load(full) : anything();
};
new Function("exports", "module", "require", js)(box.exports, box, req);
return box.exports;
}
const state = () => load(path.join(ROOT, "A00_Common", "b_page_state.ts"));
const P = "P4725";
const ROUTE = 7;
const store = load(path.join(ROOT, "A00_Common", "design", "design_store.ts"));
store.pinDraftProject(P);
const edit = load(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Edit.ts"));
const settings = load(path.join(ROOT, "B05_Profile", "B05_Profile_Api_PageSettings.ts"));
const out = {};
// ① 두 보기 = 창구 둘 · 같은 노선 — ↶ 가 세션 칸을 되쓰면 둘 다 그 값(모듈 사본이 옛 값을 쥐지 않음)
const saved = { station_offsets: {}, curve_radii: { "40.000": 30 } };
const a = edit.createProfileEditStore(ROUTE, saved, () => undefined);
const b = edit.createProfileEditStore(ROUTE, saved, () => undefined);
const x = { station_offsets: { "20.000": 0.5 }, curve_radii: { "40.000": 30 } };
const y = { station_offsets: { "20.000": 0.2 }, curve_radii: { "40.000": 30 } };
a.replace(x);
state().writeState("alignment-draft", y, P, ROUTE); // 되돌리기 = 세션 되쓰기
out.undo = {
a: a.edits(),
b: b.edits(),
aDirty: a.dirty(),
resync: a.resync?.() ?? "없음",
again: a.resync?.() ?? "없음",
};
// ② [저장] 뒤 ↶ 로 초안 칸이 비면 = 저장한 값(저장 전 서버 값으로 안 돌아감)
a.replace(x);
a.markSaved();
state().writeState("alignment-draft", null, P, ROUTE);
out.afterSave = { edits: b.edits(), dirty: b.dirty(), canRestore: b.canRestoreSaved() };
// ③ 페이지 설정 — ↶ 훅이 가게로 다시 알림(폼 · 기울기 한도 · 시작 측점이 들음 · N6)
const heard = [];
store.useDesignStore(P, null).subscribe("pageSettings", (value) => heard.push(value));
const draft = {
design_speed_kph: 30,
terrain_type: "normal",
max_grade_pct: 12,
min_vertical_radius_m: null,
min_tangent_length_m: null,
enforce_pipe_clearance: true,
station_display: { station: 0, cumulative: 0 },
};
settings.writePageSettings(P, draft);
state().writeState("pagesettings", null, P); // 되돌리기 = 세션 되쓰기
historyHooks.forEach((entry) => entry.restore?.("undo"));
out.settingsHeard = [...heard];
// ④ 최소고 강제 = 페이지 설정 한 칸(초안 ?? 저장 노선) · 사본 세션 칸 없음(N7)
state().writeState("latest", { project_id: P, route_params: { enforce_pipe_clearance: true } }, P);
out.enforceSaved = settings.enforceMinCoverOf?.(P) ?? "없음"; // 옛 코드 = 창구 없음
settings.writePageSettings(P, { ...draft, enforce_pipe_clearance: false });
out.enforceDraft = settings.enforceMinCoverOf?.(P) ?? "없음";
// 등록부에 없으면 열쇠가 안 섬(null) — 옛 코드는 결과 통 열쇠가 섬
out.copyKey = state().stateKey("profile-enforce-min-cover", P) ?? "none";
console.log(JSON.stringify(out));
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
"""472-5 5-C — 종단 편집 · 페이지 설정 값 주인 = 설계 가게 칸 하나.
옛 흠: 종단 편집은 모듈 사본(EditState)이 지금 값을 쥐어 ↶ 뒤 다른 창구가 옛 값 · [저장] 뒤 ↶ 가 저장 전
서버 값으로 돌아감 · 페이지 설정 ↶ 는 세션 칸만 돌아오고 폼 · 기울기 한도는 안 들음(N6) · 최소고 강제는
결과 통 사본 칸(N7).
"""
from __future__ import annotations
import json
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
HELPER = ROOT / "resources" / "tester" / "helper_472_5_value_owner.cjs"
pytestmark = pytest.mark.skipif(
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
reason="프론트엔드 의존성 미설치",
)
@pytest.fixture(scope="module")
def got() -> dict:
run = subprocess.run(
["node", str(HELPER)], capture_output=True, text=True, encoding="utf-8", timeout=120
)
assert run.returncode == 0, run.stderr
return json.loads(run.stdout)
Y = {"station_offsets": {"20.000": 0.2}, "curve_radii": {"40.000": 30}}
X = {"station_offsets": {"20.000": 0.5}, "curve_radii": {"40.000": 30}}
def test_되돌리면_두_창구가_같은_값(got):
assert got["undo"]["a"] == Y
assert got["undo"]["b"] == Y
assert got["undo"]["aDirty"] is True
assert got["undo"]["resync"] is True # 다시 셈
assert got["undo"]["again"] is False # 같은 값 = 다시 셈 예약 없음
def test_저장_뒤_되돌려도_저장한_값(got):
assert got["afterSave"] == {"edits": X, "dirty": False, "canRestore": False}
def test_페이지_설정_되돌리면_가게가_알림(got):
assert got["settingsHeard"][-1] is None # ↶ 로 초안이 빔 → 듣는 쪽이 저장 설정으로 다시 얹음
def test_최소고_강제는_페이지_설정_한_칸(got):
assert got["enforceSaved"] is True
assert got["enforceDraft"] is False
assert got["copyKey"] == "none"
+3 -4
View File
@@ -76,10 +76,9 @@ def test_source_guard_sits_on_the_single_apply_path():
방향키가 그냥 지나갔다. 판정 로직을 `_Profile_MinCover.blocksMinCover` 로 옮기고
`_Profile_Panel.applyEdits` 한 곳에서만 부른다 — 화면 쪽에는 사본을 두지 않는다.
"""
assert (
"if (blocksMinCover(base, alignment, candidate, enforceMinCover, minCoverTargets)) return false;"
in PANEL
)
# 강제 여부 = 페이지 설정 한 칸을 그 자리에서 읽음(472-5 · 보기마다 사본 없음)
assert "const enforce = enforceMinCoverOf(projectId);" in PANEL
assert "if (blocksMinCover(base, alignment, candidate, enforce, minCoverTargets)) return false;" in PANEL
assert PANEL.count("blocksMinCover(") == 1 # 부르는 자리는 한 곳뿐.
assert "export function blocksMinCover(" in GUARD
assert "blocksMinCover" not in RENDER # 화면 모듈에 사본이 남지 않았다.