From 4ae7ee25cc915502e0e5bffe86a7b038d0991ecd Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 19:13:08 +0900 Subject: [PATCH] =?UTF-8?q?feat(B06):=20=EB=85=B8=EB=A9=B4=EA=B3=B5=20?= =?UTF-8?q?=EA=B3=A0=EB=A6=84=20=3D=20=EB=B2=94=EC=9C=84=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=ED=85=8C=EB=91=90=EB=A6=AC=20=EB=8C=80=EC=8B=A0=20?= =?UTF-8?q?=ED=8F=AC=EC=9E=A5=EC=B8=B5=20=EA=B0=95=EC=A1=B0=20=C2=B7=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=88=84=EB=A6=84=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=85=B8=EB=A9=B4=EA=B3=B5=20=EC=97=B4=EA=B8=B0=20=EB=81=8A?= =?UTF-8?q?=EC=9D=8C(152-5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 포장층 범위 표식(종류 · 시작 · 끝) · 고른 노면공 층만 is-active(겹침은 하나만) - 다른 구간형은 --span 그대로 - resolveSelection ③ 덮는 구조물에 거르개 · B06 은 G군 뺌 · 덮는 것 없으면 알약 · 강조 풂 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TNMXVCZXMpHpVvqmFkeChL --- A00_Common/design/design_selection.ts | 8 +- A00_Common/design/design_view_cross.ts | 2 +- .../B06_Section_UI_Cross_Design_Surface.ts | 26 +++++ .../B06_Section_UI_Page_Structures_Panel.ts | 14 ++- B06_Section/B06_Section_UI_Section_View.ts | 30 ++++-- .../B06_Section_UI_Style_Cross_Areas.css | 7 ++ .../tester/helper_152_5_pavement_pick.cjs | 102 ++++++++++++++++++ .../tester/test_106_span_highlight_wording.py | 13 ++- resources/tester/test_152_5_pavement_pick.py | 70 ++++++++++++ 9 files changed, 252 insertions(+), 20 deletions(-) create mode 100644 resources/tester/helper_152_5_pavement_pick.cjs create mode 100644 resources/tester/test_152_5_pavement_pick.py diff --git a/A00_Common/design/design_selection.ts b/A00_Common/design/design_selection.ts index d4c7a3d17..de688e943 100644 --- a/A00_Common/design/design_selection.ts +++ b/A00_Common/design/design_selection.ts @@ -78,10 +78,12 @@ export function coversChainage(structure: SelectableStructure, chainageM: number * ① 구조물 id 가 있고 목록에 있으면 그 구조물 ② 측점 근처 관(계곡 통과 시설이 먼저 — * 같은 측점에 구조물이 겹쳐도 횡단도에 그려지는 것은 관이다) ③ 측점을 덮는 구조물. */ -export function resolveSelection( +export function resolveSelection( selection: DesignSelection, pipes: ReadonlyArray<{ chainage_m: number }>, - structures: ReadonlyArray, + structures: ReadonlyArray, + /** ③ 덮는 구조물로 열어도 되나 — B06 은 노면공을 뺌(152-5 · ① id 길은 그대로). */ + coverable: (entry: T) => boolean = () => true, ): ResolvedSelection { if (selection.structureId) { const byId = structures.find((entry) => entry.structure_id === selection.structureId); @@ -91,7 +93,7 @@ export function resolveSelection( if (chainageM === null) return { pipeChainageM: null, structureId: null }; const pipe = nearestWithin(pipes, chainageM, (entry) => entry.chainage_m); if (pipe) return { pipeChainageM: pipe.chainage_m, structureId: null }; - const hit = structures.find((entry) => coversChainage(entry, chainageM)); + const hit = structures.find((entry) => coverable(entry) && coversChainage(entry, chainageM)); return { pipeChainageM: null, structureId: hit?.structure_id ?? null }; } diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index 37bcf7a04..775dcc8fd 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -399,7 +399,7 @@ export async function mountCrossView( // 알약이 꺼지므로 연 것으로 다시 세움(관 알약 id = pipe-측점 · 53-1) const pipeAt = structuresPanel.currentChainage(); const mark = opened.structureId ?? (pipeAt === null ? null : `pipe-${pipeAt.toFixed(2)}`); - if (mark) sectionView.selectMark(mark, false); // 측점 고름 = 카드 하나(137-4) · 목록 길은 뒤에 여럿 + sectionView.selectMark(mark, false); // 측점 고름 = 카드 하나(137-4) · 덮는 것 없으면 알약 · 강조 풂(152-5) structureSelection.syncInletStructure(); // 카드만 고른 경우엔 구조물(벽·구체) 선택을 비운다 — 조정창이 저절로 뜨면 안 된다 // (2026-08-29 사용자). 벽을 찍어 카드가 딸려 선택된 경우는 건드리지 않는다. diff --git a/B06_Section/B06_Section_UI_Cross_Design_Surface.ts b/B06_Section/B06_Section_UI_Cross_Design_Surface.ts index 4355fab5c..275afb5e2 100644 --- a/B06_Section/B06_Section_UI_Cross_Design_Surface.ts +++ b/B06_Section/B06_Section_UI_Cross_Design_Surface.ts @@ -64,6 +64,26 @@ function standardEdges(edges: { left: Edge; right: Edge }, design: CrossDesign) }; } +/** + * 고른 노면공의 포장층만 `is-active`(152-5) — 범위 표식(종류 · 시작 · 끝)이 맞는 층. 겹친 노면공은 하나만. + * `mark` 없음 = 모두 풂. + */ +export function highlightPavement( + root: ParentNode, + mark?: { type_id: string; start_m?: number | null; end_m?: number | null }, +): void { + const [low, high] = [mark?.start_m ?? 0, mark?.end_m ?? 0].sort((a, b) => a - b); + for (const layer of root.querySelectorAll(".b06-chart__pavement")) { + const { paveType, paveStart, paveEnd } = layer.dataset; + const same = + !!mark && + paveType === mark.type_id && + Math.abs(Number(paveStart) - low) < 0.01 && + Math.abs(Number(paveEnd) - high) < 0.01; + layer.classList.toggle("is-active", same); + } +} + /** * 포장층 — 노면선(= 포장 윗면) 아래로 두께만큼. 노면공 범위(`section.pavement`)면 그 두께 · 무늬 · * 확폭 켬/끔(여러 건은 아래로 차례로 쌓음) · 없으면 카드 포장 켬일 때 표준 두께 채움만. @@ -92,6 +112,12 @@ export function appendPavementOverlay( const polygon = document.createElementNS(SVG_NS, "polygon"); polygon.setAttribute("points", points); polygon.setAttribute("class", "b06-chart__pavement"); + if (entry) { + // 범위 표식 — 고른 노면공의 포장층만 강조(152-5 · 겹친 노면공 가름) + polygon.dataset.paveType = entry.type_id; + polygon.dataset.paveStart = String(entry.start_m); + polygon.dataset.paveEnd = String(entry.end_m); + } const spec = entry?.spec; const mark = spec ? PAVE_MARKS[spec] : undefined; if (spec && mark) { diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index f5901eb1d..e0f8b98a1 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -159,6 +159,9 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc ); let markTypes: StructureType[] = []; + /** 카드 누름이 덮어 여는 구조물 — 노면공(G군)은 뺌(152-5 · 목록 · 알약 고름은 id 길로 그대로). */ + const coverable = (entry: StructureInstance): boolean => + markTypes.find((type) => type.type_id === entry.type_id)?.group !== "G"; /** 다음 카드 고름이 열 구조물 — 같은 측점에 관이 있어도 누른 것이 먼저다(52-15). */ let preferredId: string | null = null; /** 종단 알약 레인에 올릴 목록 — 구조물 정본 + 관 정본(가상 구조물). */ @@ -516,7 +519,10 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc }, selectionAt: (chainageM) => { const selection = { chainageM, structureId: null }; - return { chainageM, structureId: section.resolveAt(selection).structureId }; + return { + chainageM, + structureId: resolveSelection(selection, pipeFacilities, structures, coverable).structureId, + }; }, prefer: (structureId) => { preferredId = structureId; @@ -527,13 +533,15 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc const selection: DesignSelection = { chainageM, structureId: preferredId }; // 계곡 통과 시설이 먼저다 — 같은 측점에 구조물 정본 항목이 겹쳐도 횡단도에 // 그려지는 것은 이쪽이다(순서 · 거리는 `resolveSelection` 한 곳). - const hit = resolveSelection(selection, pipeFacilities, structures); + const hit = resolveSelection(selection, pipeFacilities, structures, coverable); if (hit.pipeChainageM !== null) { currentChainageM = hit.pipeChainageM; refreshSpecDefaults(hit.pipeChainageM); deps.markStructure?.(`pipe-${hit.pipeChainageM.toFixed(2)}`); // 측점선 없는 물넘이포장도(53-2) } - section.selectAt(selection); + // 폼은 위에서 푼 값으로 엶 — 공용 `selectAt` 은 노면공을 덮어 엶(152-5) + if (hit.pipeChainageM !== null) section.selectPipeByChainage(hit.pipeChainageM); + else section.selectById(hit.structureId); return { chainageM, structureId: hit.structureId }; }, }; diff --git a/B06_Section/B06_Section_UI_Section_View.ts b/B06_Section/B06_Section_UI_Section_View.ts index c27103ecb..2a584f418 100644 --- a/B06_Section/B06_Section_UI_Section_View.ts +++ b/B06_Section/B06_Section_UI_Section_View.ts @@ -19,7 +19,7 @@ import type { HaulEquipmentLimit, SectionDetailResponse, } from "./B06_Section_Api_Fetch"; -import type { StructureInstance } from "../B05_Profile/B05_Profile_Api_Structures"; +import type { StructureInstance, StructureType } from "../B05_Profile/B05_Profile_Api_Structures"; import type { RockBoundaryControl } from "./B06_Section_UI_Cross_Design"; import type { CrossAreaKey } from "./B06_Section_UI_Cross_Areas"; import type { FordControl } from "./B06_Section_UI_Cross_Ford_Panel"; @@ -65,6 +65,7 @@ import { } from "./B06_Section_UI_Section_Common"; import type { SectionViewController } from "./B06_Section_UI_Section_View_Types"; import { createFillSlopeNotice } from "./B06_Section_UI_Cross_FillSlope_Notice"; +import { highlightPavement } from "./B06_Section_UI_Cross_Design_Surface"; import { createSectionProfile } from "./B06_Section_UI_Section_View_Profile"; import type { DesignPageShared } from "../A00_Common/design/design_shared"; @@ -231,23 +232,33 @@ export function createSectionView( if (revetOffset) revetOffset.onSelectionChange = syncRevetHighlights; /** - * 고른 구간형 구조물(노면공 등)의 시작~종료 안 횡단 카드 전부에 테두리 강조(106-1). + * 고른 구간형 구조물(옹벽 · 독립 흙막이 · 사토장)의 시작~종료 안 횡단 카드 전부에 테두리 강조(106-1). + * 노면공(G군)은 테두리 대신 범위 카드 안 그 노면공의 포장층만 강조(152-5) — 겹친 노면공은 범위 표식으로 하나만. * 가장 가까운 한 장의 `--selected` 는 그대로 — 클래스만 갈아 끼워 줌·팬을 지킨다. */ let spanMarks: ReadonlyArray = []; + let spanTypes: ReadonlyArray = []; let spanMarkId: string | null = null; - const inSpan = (section: CrossSection): boolean => { - const mark = spanMarkId ? spanMarks.find((entry) => entry.structure_id === spanMarkId) : null; + const inSpan = (section: CrossSection, mark: StructureInstance | undefined): boolean => { const { start_m: start, end_m: end } = mark ?? {}; if (start == null || end == null || start === end) return false; const at = section.chainage_m; return at >= Math.min(start, end) - 0.01 && at <= Math.max(start, end) + 0.01; }; + const applySpan = (card: HTMLElement, section: CrossSection): void => { + const mark = spanMarkId + ? spanMarks.find((entry) => entry.structure_id === spanMarkId) + : undefined; + const span = inSpan(section, mark); + const pave = span && spanTypes.find((type) => type.type_id === mark?.type_id)?.group === "G"; + card.classList.toggle("b06-cross-card--span", span && !pave); + highlightPavement(card, pave ? mark : undefined); + }; const syncSpanHighlights = (): void => { - for (const section of currentDetail?.cross_sections ?? []) - document - .getElementById(`cross-${section.station_id}`) - ?.classList.toggle("b06-cross-card--span", inSpan(section)); + for (const section of currentDetail?.cross_sections ?? []) { + const card = document.getElementById(`cross-${section.station_id}`); + if (card) applySpan(card, section); + } }; /** 카드 선택 변경 수신자(좌측 구조물 폼). 페이지가 등록한다. */ @@ -349,7 +360,7 @@ export function createSectionView( plotBase, cutSlope, ); - card.classList.toggle("b06-cross-card--span", inSpan(section)); + applySpan(card, section); return card; }; @@ -543,6 +554,7 @@ export function createSectionView( }, setStructureMarks(structures, types) { spanMarks = structures; + spanTypes = types; syncSpanHighlights(); profile.setMarks(structures, types); // 검증용 훅 — 알약 레인이 무엇을 받았는지 화면 밖에서 수치로 본다 diff --git a/B06_Section/B06_Section_UI_Style_Cross_Areas.css b/B06_Section/B06_Section_UI_Style_Cross_Areas.css index de87ddbcc..de92d2214 100644 --- a/B06_Section/B06_Section_UI_Style_Cross_Areas.css +++ b/B06_Section/B06_Section_UI_Style_Cross_Areas.css @@ -200,6 +200,13 @@ stroke-width: 1; } +/* 고른 노면공의 포장층(152-5) — 범위 카드마다 · 무늬 채움은 inline 이라 무늬 없는 층만 채움이 바뀌고 테두리로 강조 */ +.b06-chart__pavement.is-active { + fill: color-mix(in srgb, var(--color-danger) 30%, transparent); + stroke: var(--color-danger); + stroke-width: 2.2; +} + /* 노면공 포장사양 무늬(88): 포장 박스 채움 = 바탕 + 무늬 — 콘크리트 빗금 · 아스콘 점 · 쇄석 돌 */ .b06-chart__pave-bg { fill: color-mix(in srgb, var(--color-text-secondary) 30%, transparent); diff --git a/resources/tester/helper_152_5_pavement_pick.cjs b/resources/tester/helper_152_5_pavement_pick.cjs new file mode 100644 index 000000000..8c8429b1a --- /dev/null +++ b/resources/tester/helper_152_5_pavement_pick.cjs @@ -0,0 +1,102 @@ +/* 152-5 노면공 강조 헬퍼 — 포장층 범위 표식 · 고른 노면공 포장층만 is-active · 카드 누름(③ 덮는 구조물)에서 노면공 뺌을 + * Node 로 돌려 표준출력 JSON 한 벌로. TS 는 그 자리에서 트랜스파일 · SVG 그릇은 쓰는 만큼만 흉내. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); + +class FakeNode { + constructor(tag) { + this.tagName = tag; + this.children = []; + this.dataset = {}; + this.attrs = {}; + this.style = {}; + this._classes = new Set(); + const self = this; + this.classList = { + contains: (name) => self._classes.has(name), + toggle: (name, on) => (on ? self._classes.add(name) : self._classes.delete(name)), + }; + } + setAttribute(key, value) { + this.attrs[key] = String(value); + if (key === "class") this._classes = new Set(String(value).split(/\s+/).filter(Boolean)); + } + append(...kids) { + this.children.push(...kids); + } + querySelectorAll(selector) { + const name = selector.slice(1); + const all = (node) => node.children.flatMap((kid) => [kid, ...all(kid)]); + return all(this).filter((kid) => kid.classList.contains(name)); + } +} +globalThis.document = { createElementNS: (_, tag) => new FakeNode(tag) }; + +function loadTs(file, stubs = {}) { + const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + const localRequire = (request) => { + if (stubs[request]) return stubs[request]; + throw new Error(`stub 없음: ${request}`); + }; + new Function("exports", "module", "require", js)(box.exports, box, localRequire); + return box.exports; +} + +const surface = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Design_Surface.ts")); +const selection = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"), { + "../b_page_state": { readState: () => null, writeState: () => {} }, +}); + +const out = {}; + +// 포장층 — 같은 측점 노면공 둘(겹침) · 범위 표식 +const svg = new FakeNode("svg"); +const edge = (offset) => ({ offset_m: offset, elevation_m: 100 }); +const design = { carriageway_edges: { left: edge(-2), right: edge(2) }, pavement_thickness_m: 0.2 }; +const entry = (start, end) => ({ + type_id: "pavement_concrete", + spec: null, + thickness_m: 0.2, + start_m: start, + end_m: end, + widening_on: true, +}); +surface.appendPavementOverlay( + svg, + design, + (v) => v, + (v) => v, + [entry(0, 40), entry(20, 60)], +); +const layers = () => svg.querySelectorAll(".b06-chart__pavement"); +const active = () => layers().map((layer) => layer.classList.contains("is-active")); +out.marks = layers().map((layer) => ({ ...layer.dataset })); +surface.highlightPavement(svg, { type_id: "pavement_concrete", start_m: 60, end_m: 20 }); +out.pickSecond = active(); +surface.highlightPavement(svg, { type_id: "gravel_surfacing", start_m: 20, end_m: 60 }); +out.otherType = active(); +surface.highlightPavement(svg, { type_id: "pavement_concrete", start_m: 0, end_m: 40 }); +out.pickFirst = active(); +surface.highlightPavement(svg, undefined); +out.cleared = active(); + +// 카드 누름 — ③ 덮는 구조물에서 노면공 뺌 · ① id 길은 그대로 +const pave = { structure_id: "pave", type_id: "pavement_concrete", start_m: 0, end_m: 40 }; +const wall = { structure_id: "wall", type_id: "retaining_wall", start_m: 10, end_m: 30 }; +const coverable = (s) => s.type_id !== "pavement_concrete"; +const resolve = (sel, list, fn) => selection.resolveSelection(sel, [], list, fn); +out.cover = { + default: resolve({ chainageM: 5, structureId: null }, [pave]).structureId, + skipped: resolve({ chainageM: 5, structureId: null }, [pave], coverable).structureId, + wallFirst: resolve({ chainageM: 20, structureId: null }, [pave, wall], coverable).structureId, + byId: resolve({ chainageM: 5, structureId: "pave" }, [pave], coverable).structureId, +}; + +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_106_span_highlight_wording.py b/resources/tester/test_106_span_highlight_wording.py index 3e9ca6238..4c92bdf6f 100644 --- a/resources/tester/test_106_span_highlight_wording.py +++ b/resources/tester/test_106_span_highlight_wording.py @@ -22,19 +22,24 @@ VIEW = _read("B06_Section", "B06_Section_UI_Section_View.ts") def test_범위_강조_클래스는_고름_목록갱신_카드재생성_세_길에서_칠한다(): - assert VIEW.count("b06-cross-card--span") == 2 # syncSpanHighlights · buildCrossCard + # applySpan 한 곳 — syncSpanHighlights · buildCrossCard 가 부름 + assert VIEW.count("b06-cross-card--span") == 1 assert re.search( r"selectMark: \(structureId, span = true\) => \{\s+(//[^\n]*\s+)?spanMarkId = span \? structureId : null;", VIEW, ) - assert re.search(r"spanMarks = structures;\s+syncSpanHighlights\(\);", VIEW) - assert 'card.classList.toggle("b06-cross-card--span", inSpan(section))' in VIEW + assert re.search( + r"spanMarks = structures;\s+spanTypes = types;\s+syncSpanHighlights\(\);", VIEW + ) + assert VIEW.count("applySpan(card, section);") == 2 + # 152-5 노면공(G군)은 테두리 대신 포장층 + assert 'card.classList.toggle("b06-cross-card--span", span && !pave);' in VIEW def test_측점_고름은_카드_하나_목록_고름은_여럿(): """137-4 — 측점 고름 리스너는 span 끔 · 목록(노면공) 길 · 알약은 기본(여럿) 그대로.""" cross = _read("A00_Common", "design", "design_view_cross.ts") - assert "if (mark) sectionView.selectMark(mark, false);" in cross + assert "sectionView.selectMark(mark, false);" in cross # 152-5 덮는 것 없으면 null = 풂 assert "markStructure: (structureId) => sectionView.selectMark(structureId)," in cross types = _read("B06_Section", "B06_Section_UI_Section_View_Types.ts") assert "selectMark: (structureId: string | null, span?: boolean) => void;" in types diff --git a/resources/tester/test_152_5_pavement_pick.py b/resources/tester/test_152_5_pavement_pick.py new file mode 100644 index 000000000..cb15c32f6 --- /dev/null +++ b/resources/tester/test_152_5_pavement_pick.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +"""152-5 B06 노면공 강조. + +못박는 것 + - 포장층마다 범위 표식(종류 · 시작 · 끝) — 고른 노면공의 층만 is-active · 겹친 노면공은 하나만 · 없음 = 모두 풂. + - 노면공(G군)은 범위 카드 테두리(--span) 대신 포장층 · 다른 구간형은 --span 그대로. + - 카드 누름(③ 덮는 구조물)은 노면공을 안 엶 · 목록 · 알약(① id 길)은 그대로 · 덮는 것 없으면 알약 · 강조 풂. + +Node 헬퍼 `helper_152_5_pavement_pick.cjs` 가 SVG 그릇 흉내로 돌림. 화면은 사용자 확인. +""" + +import json +import os +import subprocess + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(HERE, "..", "..")) + + +def _read(*parts: str) -> str: + with open(os.path.join(ROOT, *parts), encoding="utf-8") as handle: + return handle.read() + + +@pytest.fixture(scope="module") +def out(): + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_152_5_pavement_pick.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_포장층_범위_표식(out): + assert out["marks"] == [ + {"paveType": "pavement_concrete", "paveStart": "0", "paveEnd": "40"}, + {"paveType": "pavement_concrete", "paveStart": "20", "paveEnd": "60"}, + ] + + +def test_고른_노면공_층만_강조(out): + assert out["pickSecond"] == [False, True] # 시작 · 끝 거꾸로 와도 같은 범위 + assert out["otherType"] == [False, False] + assert out["pickFirst"] == [True, False] + assert out["cleared"] == [False, False] + + +def test_카드_누름은_노면공을_안_엶(out): + cover = out["cover"] + assert cover["default"] == "pave" # 거르개 없으면 옛 길 그대로(B05) + assert cover["skipped"] is None + assert cover["wallFirst"] == "wall" + assert cover["byId"] == "pave" # 목록 · 알약 고름 + + +def test_화면_배선(): + view = _read("B06_Section", "B06_Section_UI_Section_View.ts") + assert '?.group === "G"' in view + assert "highlightPavement(card, pave ? mark : undefined);" in view + panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts") + assert panel.count("resolveSelection(selection, pipeFacilities, structures, coverable)") == 2 + assert "section.selectAt(selection)" not in panel # 공용 selectAt 은 노면공을 덮어 엶 + css = _read("B06_Section", "B06_Section_UI_Style_Cross_Areas.css") + assert ".b06-chart__pavement.is-active" in css