Merge remote-tracking branch 'origin/dev' into sub_laptop_3
This commit is contained in:
@@ -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,5 @@
|
||||
/* 472-4 C6 거울 헬퍼 — 맹암거 얹기 TS 짝(vite ssr) · test_472_4_underdrain_mirror.py 가 JSON 으로 받음. */
|
||||
export {
|
||||
attachUnderdrainSpecs,
|
||||
underdrainSpecsFrom,
|
||||
} from "../../common_util/common_util_structure_underdrain";
|
||||
@@ -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,120 @@
|
||||
"""PLAN 472-4 ② C6 (요구 273) — 맹암거 얹기 거울: TS `common_util_structure_underdrain` ↔ 파이썬
|
||||
`B06_Section_Engine_Tilt.underdrains_from` · `attach_underdrain_specs`.
|
||||
|
||||
못박는 것
|
||||
- 구조물 목록 → 제원(높이 · 경사 · 빈 값 · 0 · 음수 · 글 · 맹암거 아닌 종류) 두 쪽 같음.
|
||||
- 기준 측점(±0.02 m) 얹기 · 얹은 수 같음.
|
||||
- 알려진 차 하나 — 옛 `underdrain` 이 남은 측점: TS 는 걷음 · 파이썬은 둠
|
||||
(서버는 파일에서 새로 읽어 옛 값 없음 · Node 로 옮길 때 한 벌).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_472_4_underdrain_mirror.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance
|
||||
from B06_Section.B06_Section_Engine_Tilt import attach_underdrain_specs, underdrains_from
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "vite").is_dir(),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def _drain(at: float, **options) -> dict:
|
||||
return {
|
||||
"type_id": "cross_underdrain",
|
||||
"placement": "point",
|
||||
"chainage_m": at,
|
||||
"options": options,
|
||||
}
|
||||
|
||||
|
||||
STRUCTURES = [
|
||||
_drain(40.0, culvert_height_m=1.2, culvert_slope_pct=3),
|
||||
_drain(80.0, culvert_height_m="", culvert_slope_pct="2.5"),
|
||||
_drain(120.0, culvert_height_m=0, culvert_slope_pct=-1),
|
||||
_drain(160.0),
|
||||
{"type_id": "berm", "placement": "interval", "start_m": 0.0, "end_m": 50.0, "options": {}},
|
||||
]
|
||||
SECTIONS = [
|
||||
{"chainage_m": 40.01},
|
||||
{"chainage_m": 79.99},
|
||||
{"chainage_m": 120.0},
|
||||
{"chainage_m": 160.03}, # 0.02 밖
|
||||
{"chainage_m": 200.0, "underdrain": {"chainage_m": 200.0}}, # 옛 값 — 알려진 차
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts(tmp_path_factory) -> dict:
|
||||
work = tmp_path_factory.mktemp("u4724")
|
||||
subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
[
|
||||
"node",
|
||||
str(ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
"../resources/tester/helper_472_4_underdrain_entry.ts",
|
||||
"--outDir",
|
||||
str(work),
|
||||
"--logLevel",
|
||||
"error",
|
||||
],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
timeout=300,
|
||||
)
|
||||
bundle = work / "bundle.mjs"
|
||||
shutil.copy(work / "helper_472_4_underdrain_entry.js", bundle)
|
||||
job = json.dumps({"structures": STRUCTURES, "sections": SECTIONS})
|
||||
script = (
|
||||
f"const m = await import({json.dumps(bundle.as_uri())});"
|
||||
f"const job = {job};"
|
||||
"const specs = m.underdrainSpecsFrom(job.structures);"
|
||||
"const count = m.attachUnderdrainSpecs(job.sections, specs);"
|
||||
"console.log(JSON.stringify({specs, count, sections: job.sections}));"
|
||||
)
|
||||
out = subprocess.run( # noqa: S603
|
||||
["node", "--input-type=module", "-e", script],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
def _python() -> tuple[list[dict], int, list[dict]]:
|
||||
specs = underdrains_from([StructureInstance(**item) for item in STRUCTURES])
|
||||
sections = copy.deepcopy(SECTIONS)
|
||||
return specs, attach_underdrain_specs(sections, specs), sections
|
||||
|
||||
|
||||
def test_제원_같음(ts) -> None:
|
||||
specs, _count, _sections = _python()
|
||||
assert ts["specs"] == specs
|
||||
|
||||
|
||||
def test_얹기_같음_알려진_차_하나(ts) -> None:
|
||||
_specs, count, sections = _python()
|
||||
assert ts["count"] == count == 3
|
||||
assert ts["sections"][:4] == sections[:4]
|
||||
# 알려진 차 — 옛 underdrain: TS 걷음 · 파이썬 둠
|
||||
assert "underdrain" not in ts["sections"][4] and "underdrain" in sections[4]
|
||||
@@ -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"
|
||||
@@ -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 # 화면 모듈에 사본이 남지 않았다.
|
||||
|
||||
Reference in New Issue
Block a user