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

This commit is contained in:
2026-09-29 22:39:21 +09:00
40 changed files with 2193 additions and 1699 deletions
+67 -31
View File
@@ -1,6 +1,6 @@
/* 두 보기 좌측 패널 **같음 지킴** 헬퍼(계획서 24-2f) — 공용 등록부(`design_panel` · `design_dock` ·
* `design_sections`)와 종단 보기 칸 정의를 트랜스파일해 가짜 DOM 위에 두 보기를 같은 견본으로 세운다.
* 결과는 표준출력에 JSON 한 벌.
/* 두 보기 좌측 패널 **한 벌 지킴** 헬퍼(계획서 24-2f · 24-3b) — 공용 등록부(`design_panel` · `design_dock` ·
* `design_sections`)와 두 보기 칸 정의를 트랜스파일해 가짜 DOM 위에 **패널 하나**를 세우고
* `setView` 로 보기를 오가며 같은 노드인지 본다(한 벌 `design_shared` 가 하는 그대로). 결과는 JSON 한 벌.
*
* ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_curve_radius_floor.cjs` 머리와 같다.
*/
@@ -141,6 +141,11 @@ const STUBS = {
button.addEventListener("click", () => onClick && onClick());
return button;
},
createInputField: () => ({ root: new FakeNode("div"), input: new FakeNode("input") }),
},
"./B06_Section_UI_Page_Common": {
L: (key) =>
({ B06_View_Title: "횡단 보기 설정", B06_Std_Title: "표준 횡단면 설정" })[key] ?? key,
},
};
@@ -166,8 +171,9 @@ function loadTs(file) {
const panel = loadTs(path.join(ROOT, "A00_Common", "design", "design_panel.ts"));
loadTs(path.join(ROOT, "A00_Common", "design", "design_sections.ts"));
loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Panel_Sections.ts"));
loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Panel.ts"));
// ── 같은 견본 — 두 보기가 **같은 구조물 배치 부품**을 받는다 ─────────────────────
// ── 견본 — 두 보기 부품을 한 `parts` 에 모은다(한 벌 `design_shared` 와 같은 모양) ──────────
function sample() {
const root = new FakeNode("section");
root.className =
@@ -182,9 +188,11 @@ function sample() {
return { root, listRoot };
}
const text = (t) => Object.assign(new FakeNode("div"), { _text: t });
function context(view, calls) {
const run = (name) => () => calls.push(`${view}:${name}`);
return {
const calls = [];
let current = "profile";
const run = (name) => () => calls.push(`${current}:${name}`);
const built = panel.createDesignPanel(
{
projectId: "p",
parts: {
structures: sample(),
@@ -197,44 +205,72 @@ function context(view, calls) {
root.hidden = true;
},
},
crossView: { row: text("cross-row") },
standardSlot: text("std"),
},
dock: {
reset: { run: run("reset") },
save: { run: run("save") },
confirm: { run: run("confirm"), enabled: view === "cross" },
confirm: { run: run("confirm") },
switchView: { run: run("switch") },
},
};
}
},
"profile",
);
const calls = [];
const views = {};
for (const view of ["profile", "cross"]) {
const built = panel.createDesignPanel(context(view, calls), view);
const titleOf = (node) => node.children.find((c) => c.tagName === "h3")?.textContent ?? "";
const nodes = () => {
const dockRoot = built.dock.root;
const buttons = dockRoot.querySelectorAll(".ui-btn");
return {
root: built.root,
dock: dockRoot,
buttons: dockRoot.querySelectorAll(".ui-btn"),
structures: built.root.children.find((c) => c.classList.contains("b05-structure-section")),
};
};
function snapshot() {
const { dock, buttons, structures } = nodes();
const shown = built.root.children.filter((c) => c !== dock && c.style.display !== "none");
buttons.forEach((b) => b.click());
const structures = built.root.children.find((c) => c.classList.contains("b05-structure-section"));
views[view] = {
return {
rootClass: built.root.className,
sectionIds: panel.panelSectionsFor(view).map((d) => d.id),
shownTitles: shown.map(titleOf),
structuresFirst: shown[0] === structures,
structuresHtml: html(structures),
structuresFirst: built.root.children[0] === structures,
dockFirstChild: html(dockRoot.children[0]),
dockClass: dockRoot.className,
dockFirstChild: html(dock.children[0]),
dockIsLast: built.root.children[built.root.children.length - 1] === dock,
dockLabels: buttons.map((b) => b.textContent),
dockActions: buttons.map((b) => b.dataset.dockAction),
dockDisabled: buttons.map((b) => b.disabled),
dockRowClass: dockRoot.children[dockRoot.children.length - 1].className,
dockIsLast: built.root.children[built.root.children.length - 1] === dockRoot,
};
}
// 보기 전환 — 글자가 따라 바뀐다.
const switched = panel.createDesignPanel(context("profile", []), "profile");
switched.setView("cross");
const switchLabelAfter = switched.dock.root
.querySelectorAll(".ui-btn")
.map((b) => b.textContent)
.slice(-1)[0];
process.stdout.write(JSON.stringify({ views, calls, switchLabelAfter }));
const first = nodes();
const views = { profile: snapshot() };
current = "cross";
built.setView("cross");
views.cross = snapshot();
const afterCross = nodes();
current = "profile";
built.setView("profile");
views.back = snapshot();
const afterBack = nodes();
const same = (a, b) =>
a.root === b.root &&
a.dock === b.dock &&
a.structures === b.structures &&
a.buttons.length === b.buttons.length &&
a.buttons.every((button, index) => button === b.buttons[index]);
process.stdout.write(
JSON.stringify({
views,
calls,
sameNodes: { cross: same(first, afterCross), back: same(first, afterBack) },
childCount: built.root.children.length,
sectionIds: {
profile: panel.panelSectionsFor("profile").map((d) => d.id),
cross: panel.panelSectionsFor("cross").map((d) => d.id),
},
}),
);
@@ -57,7 +57,8 @@ def test_거리_정의는_한_곳뿐이고_옛_0_05는_없다():
assert definitions == ["design_selection.ts"]
for name, folder in (
("B05_Profile_UI_Structures_Panel.ts", "B05_Profile"),
("B05_Profile_UI_Page.ts", "B05_Profile"),
("design_view_profile.ts", "A00_Common/design"),
("design_view_cross.ts", "A00_Common/design"),
("B05_Profile_UI_Selection.ts", "B05_Profile"),
("B06_Section_UI_Page_Structures_Panel.ts", "B06_Section"),
("B06_Section_UI_Page_Structure_Pick.ts", "B06_Section"),
@@ -178,14 +179,14 @@ def test_횡단도_벽_구체_눌러도_폼은_같은_길_하나():
assert panel.count("panel.showAtChainage(") == 3 # 기슭막이 · 세월교 · BOX
assert panel.count("panel.releasePipe()") == 3 # 벽 선택을 풀 때는 관 폼만 비움
assert "section.selectAt(" in panel
page = _read("B06_Section", "B06_Section_UI_Page.ts")
page = _read("A00_Common/design", "design_view_cross.ts")
assert "writeStructurePick" not in page
assert "selectDesign(" in page
def test_고름은_selectDesign_한_길로_적는다():
for folder, name in (
("B05_Profile", "B05_Profile_UI_Page.ts"),
("A00_Common/design", "design_view_profile.ts"),
("B05_Profile", "B05_Profile_UI_Structure_Pick_Session.ts"),
("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts"),
):
@@ -23,9 +23,12 @@ B05_PANEL = (PROJECT_ROOT / "A00_Common" / "design" / "design_profile.ts").read_
encoding="utf-8"
)
# 설계 재계산 자리는 700줄 제한으로 `_UI_Page_Design_Sync` 로 떨어져 나갔다(2026-09-13).
B06_PAGE = (PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Page.ts").read_text(encoding="utf-8") + (
PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Page_Design_Sync.ts"
).read_text(encoding="utf-8")
# 횡단 보기 본체는 한 페이지 보기 파일로 옮겼다(계획서 24-3b).
B06_PAGE = (PROJECT_ROOT / "A00_Common" / "design" / "design_view_cross.ts").read_text(
encoding="utf-8"
) + (PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Page_Design_Sync.ts").read_text(
encoding="utf-8"
)
def _stale(profile_samples, sections, tol=1e-3):
+5 -2
View File
@@ -21,7 +21,8 @@ from B05_Profile.B05_Profile_Structures_Schema import load_structure_types # no
_PANEL = PROJECT_ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Panel.ts"
# 옵션 칸 그리기는 700줄 제한으로 이 조각으로 옮겨 감(2026-09-29 · PLAN 21-4)
_PANEL_OPTIONS = PROJECT_ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Panel_Options.ts"
_B06 = PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Page_Structures_Panel.ts"
# 폼은 두 보기가 한 벌을 쓴다 — 상세 칸은 한 벌이 보기에 따라 그린다(계획서 24-3b).
_SHARED = PROJECT_ROOT / "A00_Common" / "design" / "design_shared.ts"
_B05 = PROJECT_ROOT / "B05_Profile" / "B05_Profile_UI_Panel.ts"
@@ -30,7 +31,9 @@ def _type(type_id: str):
def test_B06_은_상세_칸까지_받는다():
assert "includeDetail: true" in _B06.read_text(encoding="utf-8")
assert 'includeDetail: () => shared.view() === "cross"' in _SHARED.read_text(encoding="utf-8")
# 그릴 때마다 지금 보기로 가린다 — 만들 때 한 번 굳히면 한 벌 폼이 보기를 못 따라간다.
assert 'typeof detail === "function" ? detail()' in _PANEL.read_text(encoding="utf-8")
def test_B05_는_종전대로_유무_종류_위치만():
+9 -4
View File
@@ -88,11 +88,16 @@ def test_저장이_창구를_다섯_다_거친다():
def test_두_페이지가_저장_한_줄기를_탄다():
"""B05 [임시저장] · B06 [저장] · [확정]이 창구를 따로 부르지 않는다(계획서 24-2a)."""
"""[저장] · [확정]은 한 페이지 한 벌이 두 보기 몫을 모아 저장 한 줄기 한 번으로 보낸다
(계획서 24-2a · 24-3b) — 보기 몫은 창구를 따로 부르지 않는다."""
shared = (ROOT / "A00_Common" / "design" / "design_shared.ts").read_text(encoding="utf-8")
actions = (ROOT / "B05_Profile" / "B05_Profile_UI_Page_Actions.ts").read_text(encoding="utf-8")
persist = (ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts").read_text(encoding="utf-8")
for name, text in (("B05", actions), ("B06", persist)):
assert "saveDesign(" in text, f"{name} 가 저장 한 줄기를 안 탐"
assert shared.count("await saveDesign(") == 1, "저장 한 줄기를 한 자리에서만 부름"
assert "saveSharedDesign(shared, true)" in shared, "[확정]이 한 줄기를 안 탐"
assert 'await shared.ensureView("cross")' in shared, "종단 보기 [확정]이 횡단 자료를 못 얻음"
for name, text in (("종단 보기", actions), ("횡단 보기", persist), ("한 벌", shared)):
for token in ("flushPendingPipes(", "flushPendingStructures(", "flushUphillOverrides("):
assert token not in text, f"{name} 가 {token} 를 따로 부름 — 차례가 갈림"
assert "{ confirm: true }" in persist, "[확정]이 한 줄기를 안 탐"
assert "export function profileSaveParts(" in actions
assert "export function crossSaveParts(" in persist
+10 -14
View File
@@ -6,10 +6,10 @@
안 만진 측점이 옛 값 그대로 정본이 된다.
(같은 입력을 주면 파이썬·TS 는 같은 값을 낸다 — 거울 문제가 아니라 **입력이 갈린 것**이다.)
⇒ `saveCurrentSections`·`confirmCurrentSections` 가 `collectSectionEdits` **앞에서**
`ctx.reconcileDesigns()` 를 부른다. 순서가 뒤집히면 옛 면적이 그대로 실린다.
(계획서 24-2a — 둘 다 저장 한 줄기 `saveDesign` 을 탄다: B06 창구 `sectionSaveContext` 의
`prepare` 가 재계산, `cross` 가 면적 수집이고, `saveDesign` 이 `prepare` 를 먼저 부른다.)
⇒ [저장]·[확정]이 `collectSectionEdits` **앞에서** `ctx.reconcileDesigns()` 를 부른다. 순서가
뒤집히면 옛 면적이 그대로 실린다. (계획서 24-2a · 24-3b — 한 벌(`design_shared`)이 저장 한 줄기
`saveDesign` 을 부르고, 횡단 보기 몫 `crossSaveParts` 의 `prepare` 가 재계산 · `cross` 가 면적
수집이며, `saveDesign` 이 `prepare` 를 먼저 부른다.)
"""
from __future__ import annotations
@@ -18,7 +18,8 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
PERSIST = ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts"
PAGE = ROOT / "B06_Section" / "B06_Section_UI_Page.ts"
PAGE = ROOT / "A00_Common" / "design" / "design_view_cross.ts"
SHARED = ROOT / "A00_Common" / "design" / "design_shared.ts"
SAVE = ROOT / "A00_Common" / "design" / "design_save.ts"
@@ -29,16 +30,11 @@ def _body(source: str, marker: str) -> str:
def test_저장이_면적을_모으기_전에_다시_계산한다() -> None:
text = PERSIST.read_text(encoding="utf-8")
for marker in (
"export async function saveCurrentSections",
"export async function confirmCurrentSections",
):
assert "saveDesign(sectionSaveContext(" in _body(text, marker)[:400], (
f"{marker} 가 B06 창구로 저장 한 줄기를 안 탄다"
)
body = _body(text, "function sectionSaveContext")
assert "prepare: () => ctx.reconcileDesigns()" in body, "B06 창구가 재계산을 안 넘긴다"
body = _body(text, "export function crossSaveParts")
assert "prepare: () => ctx.reconcileDesigns()" in body, "횡단 보기 몫이 재계산을 안 넘긴다"
assert "collectSectionEdits(ctx)" in body[body.index("cross:") :], "면적 수집이 횡단 차례 밖"
shared = _body(SHARED.read_text(encoding="utf-8"), "export async function saveSharedDesign")
assert "prepare: crossSave?.prepare" in shared, "한 벌이 재계산을 저장 한 줄기에 안 넘긴다"
save = SAVE.read_text(encoding="utf-8")
run = _body(save, "export async function saveDesign")
assert run.index("ctx.prepare?.()") < run.index("ctx.cross(confirm)"), (
+40 -8
View File
@@ -32,24 +32,28 @@ def test_같은_페이지면_셸도_다시_안_그림():
assert "if (within) return;" in main
def test_보기_약속_두_길():
def test_보기_약속_가운데만():
"""끝 모습(24-3b) — 보기는 가운데 위 칸만 채우고 좌측 패널 · 단추 · 종단도는 한 벌을 쓴다."""
page = _read("A00_Common/design/design_page.ts")
assert "export interface DesignPageView" in page
assert "mount?: (root: HTMLElement, host: DesignPageHost)" in page
assert "mountCenter?: (" in page and "shared: DesignPageShared" in page
assert "mount?: (root" not in page # 보기가 틀을 통째로 그리던 중간 길은 걷음
assert "mountCenter: (" in page and "shared: DesignPageShared" in page
assert 'import("./design_view_profile")).mountProfileView' in page
assert 'import("./design_view_cross")).mountCrossView' in page
for hook in ("onShow:", "onHide:", "onDispose:"):
assert hook in page
# 보기는 처음 보일 때 한 번만 만든다 — 있으면 보이기만.
assert "if (slot) slot.show();" in page
assert "if (slot) {\n slot.show();" in page
def test_페이지가_숨김_떠남을_건다():
life = _read("B05_Profile/B05_Profile_UI_Page_Life.ts")
assert "deps.viewer.setActive(false)" in life and "deps.viewer.dispose()" in life
assert "bindProfileViewLife(host," in _read("B05_Profile/B05_Profile_UI_Page.ts")
b06 = _read("B06_Section/B06_Section_UI_Page.ts")
assert "host?.onHide(() => sectionView.setActive(false));" in b06
assert "host?.onDispose(() => sectionView.dispose());" in b06
assert "bindProfileViewLife(host," in _read("A00_Common/design/design_view_profile.ts")
cross = _read("A00_Common/design/design_view_cross.ts")
assert "sectionView.dispose();" in cross[cross.index("host.onDispose(") :]
# 종단도는 한 벌 — 보기가 숨어도 그대로 두고, 페이지를 떠날 때 한 번 정리한다.
assert "shared.profile?.dispose();" in _read("A00_Common/design/design_page.ts")
def test_3D_멈춤과_문맥_놓기():
@@ -74,3 +78,31 @@ def test_창_키_듣기를_뗌():
def test_겹친_알약은_누른_것을_그대로():
profile = _read("A00_Common/design/design_profile.ts")
assert "stationIdAtStructure(selectedStructureId) === stationId" in profile
def test_진행_단계_두_칸이_보기를_따라감():
"""종단설계 · 횡단설계 두 칸은 같은 페이지 — 누르면 주소만 바꿔 보기만 바뀐다(계획서 24-3b)."""
page = _read("A00_Common/design/design_page.ts")
assert "const VIEW_STEP: Record<DesignView, number> = { profile: 2, cross: 3 };" in page
assert "layout?.setActiveStep(VIEW_STEP[view]);" in page
assert "layout?.setTitle(" in page
click = page[page.index("onStepClick: (stepIndex) => {") :][:260]
assert "if (designViewOf(route)) navigateTo(route);" in click
layout = _read("ui_template/ui_template_workflow_layout.ts")
assert "setActiveStep: (stepIndex) =>" in layout and "setTitle: (title) =>" in layout
def test_초기화_계획노선_편집은_다시_세움():
"""같은 페이지 안 이동은 보기만 바꾸므로 [초기화] · 계획노선 편집은 페이지를 다시 세운다."""
router = _read("A00_Common/router.ts")
assert "export function remountRoute(" in router
assert "remountRoute(ROUTES.B05_PROFILE)" in _read("A00_Common/design/design_reset.ts")
assert "remountRoute(ROUTES.B05_PROFILE)" in _read("A00_Common/design/design_view_profile.ts")
def test_종단_보기_확정은_횡단_보기를_숨긴_채_세움():
page = _read("A00_Common/design/design_page.ts")
assert "ensureView: (view) => (slots.get(view) ?? mountView(view, false)).ready," in page
cross = _read("A00_Common/design/design_view_cross.ts")
# 숨긴 채 섰으면 한 벌 폼은 종단 보기 몫 — 보일 때 싣는다.
assert "if (host.visible()) void structuresPanel.load()" in cross
@@ -36,13 +36,16 @@ def results():
def test_두_보기가_같은_종단도와_같은_판():
"""종단도는 한 페이지 한 벌(계획서 24-3b) — 두 보기는 같은 노드에 제 알림만 건다."""
shared = _read("A00_Common/design/design_shared.ts")
assert shared.count("createDesignProfile(") == 1
assert "side: drainageSide" in shared and "sideTool: drainageSideTool" in shared
b05 = _read("B05_Profile/B05_Profile_UI_Profile_Panel.ts")
b06 = _read("B06_Section/B06_Section_UI_Section_View_Profile.ts")
for source in (b05, b06):
assert "createDesignProfile(" in source
assert "side: drainageSide" in source
assert "sideTool: drainageSideTool" in source
assert 'view: "profile"' in b05 and 'view: "cross"' in b06
for source, view in ((b05, "profile"), (b06, "cross")):
assert "createDesignProfile(" not in source
assert "sharedProfile(shared)" in source
assert f'bindDesignView(shared, "{view}"' in source
def test_옛_B06_위쪽_종단이_남지_않음():
+78 -61
View File
@@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
"""B05 · B06 한 페이지 두 보기 — **좌측 패널 같음 지킴**(계획서 24-2f).
"""B05 · B06 한 페이지 두 보기 — **좌측 패널 한 벌 지킴**(계획서 24-2f · 24-3b).
① 두 보기의 좌측 패널을 같은 견본으로 세워 공용 칸(구조물 배치)의 DOM · 아래 고정 영역이 같은지 본다
(Node 헬퍼 `helper_design_view_parity.cjs` 가 등록부를 트랜스파일해 가짜 DOM 위에서 돌린다).
② 페이지 파일(`B05_Profile_UI_Page.ts` · `B06_Section_UI_Page.ts`)이 좌측 칸 · 아래 단추 · 저장 단추를
직접 만들면 빨강 — 칸과 단추는 등록부(`A00_Common/design/`)만 만든다.
① 좌측 패널을 **하나** 세우고 보기를 오가도 뿌리 · 구조물 배치 · 아래 고정 영역 · 단추 넷이 같은 노드인지,
보기마다 제 칸만 보이는지, [보기 전환] 글자가 따라가는지 본다(Node 헬퍼 `helper_design_view_parity.cjs`
가 등록부를 트랜스파일해 가짜 DOM 위에서 돌린다).
② 보기 파일(`design_view_profile.ts` · `design_view_cross.ts`)이 좌측 칸 · 아래 단추 · 저장 단추를
직접 만들면 빨강 — 칸 · 단추 · 저장은 한 벌(`design_shared`)만 만든다.
"""
import json
@@ -18,8 +19,8 @@ import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = Path(HERE).parents[1]
PAGE_FILES = (
ROOT / "B05_Profile" / "B05_Profile_UI_Page.ts",
ROOT / "B06_Section" / "B06_Section_UI_Page.ts",
ROOT / "A00_Common" / "design" / "design_view_profile.ts",
ROOT / "A00_Common" / "design" / "design_view_cross.ts",
)
# 페이지 파일에 나오면 안 되는 것 — 좌측 칸 · 아래 단추 · 저장 단추를 직접 만든다는 표시.
FORBIDDEN_IN_PAGES = (
@@ -48,72 +49,68 @@ def results():
["node", os.path.join(HERE, "helper_design_view_parity.cjs")],
capture_output=True,
text=True,
encoding="utf-8",
timeout=60,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
def test_공용_칸_구조물_배치는_두_보기가_같은_DOM(results):
profile, cross = results["views"]["profile"], results["views"]["cross"]
assert profile["structuresHtml"] == cross["structuresHtml"]
assert profile["structuresFirst"] and cross["structuresFirst"] # 맨 위 칸
def test_보기를_오가도_좌측_패널_단추_구조물_배치는_같은_노드(results):
assert results["sameNodes"] == {"cross": True, "back": True}
# 보기마다 새로 만들지 않는다 — 칸은 처음 보일 때 한 번만 서고 뒤로는 숨김 · 보임만.
assert results["childCount"] == 1 + 5 + 2 # 아래 고정 영역 + 종단 칸 다섯 + 횡단 칸 둘
def test_구조물_배치는_두_보기_모두_맨_위_같은_DOM(results):
views = results["views"]
assert views["profile"]["structuresHtml"] == views["cross"]["structuresHtml"]
assert all(views[name]["structuresFirst"] for name in ("profile", "cross", "back"))
def test_구조물_목록은_두_보기_모두_아래_고정_영역_맨_위(results):
profile, cross = results["views"]["profile"], results["views"]["cross"]
assert profile["dockFirstChild"] == cross["dockFirstChild"]
assert profile["dockClass"] == cross["dockClass"]
assert profile["dockIsLast"] and cross["dockIsLast"]
views = results["views"]
assert views["profile"]["dockFirstChild"] == views["cross"]["dockFirstChild"]
assert all(views[name]["dockIsLast"] for name in ("profile", "cross", "back"))
def test_아래_단추는_네_개_두_보기_같은_차례(results):
profile, cross = results["views"]["profile"], results["views"]["cross"]
assert (
profile["dockActions"]
== cross["dockActions"]
== [
"reset",
"save",
"confirm",
"switchView",
]
)
assert profile["dockLabels"][:3] == cross["dockLabels"][:3] == ["초기화", "저장", "확정"]
def test_보기_전환_단추는_가려는_쪽_이름(results):
assert results["views"]["profile"]["dockLabels"][3] == "횡단 보기"
assert results["views"]["cross"]["dockLabels"][3] == "종단 보기"
assert results["switchLabelAfter"] == "종단 보기" # setView("cross") 뒤
def test_꺼진_단추는_안_눌리고_켜진_단추는_동작을_부름(results):
assert results["views"]["profile"]["dockDisabled"] == [False, False, True, False]
assert results["views"]["cross"]["dockDisabled"] == [False, False, False, False]
assert results["calls"] == [
"profile:reset",
"profile:save",
"profile:switch",
"cross:reset",
"cross:save",
"cross:confirm",
"cross:switch",
def test_보기마다_제_칸만_보인다(results):
views = results["views"]
assert views["profile"]["shownTitles"] == [
"구조물 배치",
"페이지 설정",
"계획노선",
"경로 계산 설정",
"선택 포인트 상세 설정",
]
assert views["cross"]["shownTitles"] == ["구조물 배치", "횡단 보기 설정", "표준 횡단면 설정"]
assert views["back"]["shownTitles"] == views["profile"]["shownTitles"]
assert results["sectionIds"]["cross"] == ["structures", "cross-view", "standard-cross"]
def test_칸_차례는_보기마다_등록부_순서(results):
assert results["views"]["profile"]["sectionIds"] == [
"structures",
"page-settings",
"planned-route",
"route-calc",
"selected-point",
def test_아래_단추는_네_개_보기_전환_글자만_따라감(results):
views = results["views"]
for name in ("profile", "cross", "back"):
assert views[name]["dockActions"] == ["reset", "save", "confirm", "switchView"]
assert views[name]["dockLabels"][:3] == ["초기화", "저장", "확정"]
# [확정]은 종단 보기에서도 켜진다(한 벌 저장이 횡단 자료를 같은 페이지에서 얻음).
assert views[name]["dockDisabled"] == [False, False, False, False]
assert views["profile"]["dockLabels"][3] == "횡단 보기"
assert views["cross"]["dockLabels"][3] == "종단 보기"
assert views["back"]["dockLabels"][3] == "횡단 보기"
def test_단추는_한_벌_동작을_부름(results):
names = ["reset", "save", "confirm", "switch"]
assert results["calls"] == [
*(f"profile:{n}" for n in names),
*(f"cross:{n}" for n in names),
*(f"profile:{n}" for n in names),
]
def test_횡단_보기_칸_정의와_차례():
"""B06 칸 정의는 화면 부품(입력 칸 · 표준 패널)을 물어 Node 에서 못 읽는다 — 소스로 본다."""
"""B06 칸 정의는 소스로도 본다 — 차례가 바뀌면 빨강."""
source = (ROOT / "B06_Section" / "B06_Section_UI_Page_Panel.ts").read_text(encoding="utf-8")
found = re.findall(
r'id: "([\w-]+)",\s*title:[^\n]*\n\s*views: \["cross"\],\s*order: (\d+)', source
@@ -125,14 +122,34 @@ def test_횡단_보기_칸_정의와_차례():
@pytest.mark.parametrize("page", PAGE_FILES, ids=lambda path: path.name)
def test_페이지_파일이_좌측_칸_단추_저장을_직접_만들지_않는다(page):
def test_보기_파일이_좌측_칸_단추_저장을_직접_만들지_않는다(page):
text = page.read_text(encoding="utf-8")
found = [marker for marker in FORBIDDEN_IN_PAGES if marker in text]
assert not found, f"{page.name} 이(가) 좌측 칸 · 단추를 직접 만든다: {found}"
assert "createDesignPanel(" not in text and "saveDesign(" not in text
# 좌측 패널은 한 벌을 쓴다 — 종단 보기는 칸 재료 모듈(`createRoutePanel`)이 대신 부른다.
assert "sharedPanel(shared)" in text or "createRoutePanel(" in text
route_panel = (ROOT / "B05_Profile" / "B05_Profile_UI_Panel.ts").read_text(encoding="utf-8")
assert "sharedPanel(shared)" in route_panel
def test_두_보기_패널은_같은_등록부로_세운다():
b05 = (ROOT / "B05_Profile" / "B05_Profile_UI_Panel.ts").read_text(encoding="utf-8")
b06 = (ROOT / "B06_Section" / "B06_Section_UI_Page.ts").read_text(encoding="utf-8")
assert "createDesignPanel(" in b05 and '"profile"' in b05
assert "createDesignPanel(" in b06 and '"cross"' in b06
def test_한_벌이_패널_종단도_폼을_한_번만_만든다():
shared = (ROOT / "A00_Common" / "design" / "design_shared.ts").read_text(encoding="utf-8")
for maker in ("createDesignPanel(", "createDesignProfile(", "createStructuresSection("):
assert shared.count(maker) == 1, maker
for guard in (
"if (shared.panel) return shared.panel;",
"if (shared.profile) return shared.profile;",
"if (shared.structures) return shared.structures;",
):
assert guard in shared
# 보기 쪽은 만들지 않고 한 벌을 쓴다.
for relative in (
"B05_Profile/B05_Profile_UI_Panel.ts",
"B05_Profile/B05_Profile_UI_Profile_Panel.ts",
"B06_Section/B06_Section_UI_Section_View_Profile.ts",
"B06_Section/B06_Section_UI_Page_Structures_Panel.ts",
):
text = (ROOT / relative).read_text(encoding="utf-8")
for maker in ("createDesignPanel(", "createDesignProfile(", "createStructuresSection("):
assert maker not in text, f"{relative} 가 {maker} 를 따로 부름"
@@ -20,7 +20,7 @@ B06_ROUTER = _read("B06_Section/B06_Section_Router.py")
REFRESH = _read("B06_Section/B06_Section_Cross_Refresh.ts")
B05_PREVIEW = _read("B05_Profile/B05_Profile_UI_Profile_Preview.ts")
# 설계 재계산 자리는 700줄 제한으로 `_UI_Page_Design_Sync` 로 떨어져 나갔다(2026-09-13).
B06_PAGE = _read("B06_Section/B06_Section_UI_Page.ts") + _read(
B06_PAGE = _read("A00_Common/design/design_view_cross.ts") + _read(
"B06_Section/B06_Section_UI_Page_Design_Sync.ts"
)