From 2d4c65dc7828768661edf79b3d866a5036dd8265 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 11:47:34 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix(b06):=20=ED=9A=A1=EB=8B=A8=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EA=B3=A0=EB=A6=84=20=EB=92=A4=20=EC=A2=85=EB=8B=A8?= =?UTF-8?q?=20=EC=B8=A1=EC=A0=90=20=EC=84=B8=EB=A1=9C=EC=84=A0=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80=20=C2=B7=20=EC=A2=85=EB=8B=A8=20=EB=88=84=EB=A6=84=20?= =?UTF-8?q?=EB=B0=98=EB=8C=80=20=EA=B8=B8=EB=8F=84=20(177-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - setSelectedStructure keepStation — B06 카드가 선 주인 · 선 없는 구조물 · 풂도 카드 선 지킴 - 새 시험 test_177_3_station_line_keep Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015sDZSjFvoiDjdGpgj3muc3 --- A00_Common/design/design_profile.ts | 8 +-- .../B06_Section_UI_Section_View_Profile.ts | 4 +- .../tester/helper_177_3_station_line_keep.cjs | 64 +++++++++++++++++ .../tester/test_177_3_station_line_keep.py | 72 +++++++++++++++++++ 4 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 resources/tester/helper_177_3_station_line_keep.cjs create mode 100644 resources/tester/test_177_3_station_line_keep.py diff --git a/A00_Common/design/design_profile.ts b/A00_Common/design/design_profile.ts index fbc325620..cc0b3335e 100644 --- a/A00_Common/design/design_profile.ts +++ b/A00_Common/design/design_profile.ts @@ -622,12 +622,12 @@ export function createDesignProfile(ctx: DesignProfileContext) { syncDrainageSpans(); draw(); }, - /** 사이드 목록에서 고른 구조물을 그래프 알약·측점 세로선 선택에 함께 맞춘다. */ - setSelectedStructure(structureId: string | null) { - // 지금 측점(B06 카드)이 그 구조물 자리면 측점선도 그대로(53-2) + /** 고른 구조물에 알약 · 측점선을 맞춤 — 그 자리면 선 그대로(53-2) · keepStation(B06 카드 = 선 주인) = 선 없는 구조물 · 풂도 카드 선 지킴(177-3). */ + setSelectedStructure(structureId: string | null, keepStation = false) { selectedStructureId = structureId; + const own = stationIdAtStructure(structureId) ?? (keepStation ? selectedStationId : null); const keep = structureId !== null && structureIdAtStation(selectedStationId) === structureId; - selectedStationId = keep ? selectedStationId : stationIdAtStructure(structureId); + selectedStationId = keep ? selectedStationId : own; draw(); }, /** 이어 공사 시작 기준(측점번호·누가거리 오프셋)을 반영해 측점 라벨·누가거리 표시를 옮긴다. */ diff --git a/B06_Section/B06_Section_UI_Section_View_Profile.ts b/B06_Section/B06_Section_UI_Section_View_Profile.ts index 9ea0b7731..0df0e0f12 100644 --- a/B06_Section/B06_Section_UI_Section_View_Profile.ts +++ b/B06_Section/B06_Section_UI_Section_View_Profile.ts @@ -265,7 +265,7 @@ export function createSectionProfile( }, selectMark(structureId) { markedId = structureId; - profile?.setSelectedStructure(structureId); + profile?.setSelectedStructure(structureId, true); // 카드가 측점선 주인 — 선 없는 구조물 · 풂은 카드 선 지킴(177-3) }, showCard(section) { if (!profile) return; @@ -280,7 +280,7 @@ export function createSectionProfile( // 일반 측점은 측점선 짝 알약이 없어 꺼짐 — 고른 구조물이 이 측점을 덮으면 그 알약을 지킴(53-1) const marked = markedId ? marks.find((entry) => entry.structure_id === markedId) : undefined; if (!pipe && marked && coversChainage(marked, section.chainage_m)) - profile.setSelectedStructure(markedId); + profile.setSelectedStructure(markedId, true); }, }; } diff --git a/resources/tester/helper_177_3_station_line_keep.cjs b/resources/tester/helper_177_3_station_line_keep.cjs new file mode 100644 index 000000000..70673c23d --- /dev/null +++ b/resources/tester/helper_177_3_station_line_keep.cjs @@ -0,0 +1,64 @@ +/* 177-3 횡단 카드 ↔ 종단 측점선 헬퍼 — 진짜 design_profile.ts 의 `setSelectedStation` · `setSelectedStructure` + * 본문을 떼어(TS 는 그 자리 트랜스파일) 측점 · 구조물 짝 흉내에 붙이고, B06 카드 고름 차례 + * (showCard → setSelectedStation · 리스너 → selectMark → setSelectedStructure(mark, true))와 B05 기본 길을 돌려 + * 남은 측점선 id 를 표준출력 JSON 한 벌로. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const src = fs.readFileSync(path.join(ROOT, "A00_Common", "design", "design_profile.ts"), "utf8"); +const method = (name) => { + const pattern = String.raw`\r?\n ${name}\([^\r\n]*\) \{\r?\n[\s\S]*?\r?\n \},`; + const match = src.match(new RegExp(pattern)); + if (!match) throw new Error(`본문 없음: ${name}`); + return match[0]; +}; +const body = ` +let selectedStationId = null; +let selectedStructureId = null; +// 관(pipe-80.17)만 측점선(irr-80)이 있음 — 벽 · 노면공은 선 없음(PST stationIdAtStructure) +const structureIdAtStation = (stationId) => (stationId === "irr-80" ? "pipe-80.17" : null); +const stationIdAtStructure = (structureId) => (structureId === "pipe-80.17" ? "irr-80" : null); +const draw = () => {}; +module.exports = { + line: () => selectedStationId, + ${method("setSelectedStation")} + ${method("setSelectedStructure")} +};`; +const js = ts.transpileModule(body, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, +}).outputText; +const box = { exports: {} }; +new Function("exports", "module", js)(box.exports, box); +const dp = box.exports; + +/** B06 카드 고름 — showCard(카드 · 관 측점이면 관 선) 뒤 리스너가 selectMark(mark) = keepStation true. */ +const b06Card = (lineId, mark) => { + dp.setSelectedStation(lineId); + dp.setSelectedStructure(mark, true); + return dp.line(); +}; +/** B05 목록 고름 — 기본값(keepStation 없음). */ +const b05Pick = (lineId, structureId) => { + dp.setSelectedStation(lineId); + dp.setSelectedStructure(structureId); + return dp.line(); +}; + +const out = { + plainCard: b06Card("s40", null), // 일반 카드 — 덮는 것 없음 + wallCard: b06Card("s100", "wall"), // 벽(선 없는 구조물) 카드 + paveCard: b06Card("s140", "pave"), // 노면공 범위 안 카드 + pipeCard: b06Card("irr-80", "pipe-80.17"), // 관 카드 = 관 선 + pipeFromPlain: b06Card("s60", "pipe-80.17"), // 관 알약 = 관 선으로 옮김 + // 반대 길 — 종단 측점선 누름 → 카드 고름 → 같은 리스너(selectMark null) + profileClick: b06Card("s200", null), + // 카드 풂 = showCard(null) → 선 끔 + release: (dp.setSelectedStation(null), dp.line()), + b05Wall: b05Pick("s100", "wall"), // B05 = 지금 그대로(선 없는 구조물 → 선 끔) + b05Pipe: b05Pick("s40", "pipe-80.17"), + b05Keep: b05Pick("irr-80", "pipe-80.17"), +}; +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_177_3_station_line_keep.py b/resources/tester/test_177_3_station_line_keep.py new file mode 100644 index 000000000..26b2e1f63 --- /dev/null +++ b/resources/tester/test_177_3_station_line_keep.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +"""177-3 B06 횡단 카드 고름 → 종단 측점 세로선 유지(반대 방향도). + +못박는 것 + - 카드 고름 뒤 리스너의 selectMark(mark) 가 측점선을 끄지 않음 — 일반 · 벽 · 노면공 카드 = 그 카드 선. + - 관 카드 · 관 알약 = 관 측점선 · 카드 풂 = 선 끔. + - 종단 측점선 누름(반대 길)도 같은 리스너 — 누른 선 그대로. + - B05(keepStation 기본값) = 지금 그대로(선 없는 구조물 → 선 끔). + +Node 헬퍼 `helper_177_3_station_line_keep.cjs` 가 진짜 design_profile.ts 본문을 돌림. 화면은 사용자 확인. +""" + +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_177_3_station_line_keep.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["plainCard"] == "s40" + assert out["wallCard"] == "s100" + assert out["paveCard"] == "s140" + + +def test_관은_관_측점선(out): + assert out["pipeCard"] == "irr-80" + assert out["pipeFromPlain"] == "irr-80" + + +def test_반대_길_종단_측점선_누름도_유지(out): + assert out["profileClick"] == "s200" + assert out["release"] is None + + +def test_B05_기본_길은_그대로(out): + assert out["b05Wall"] is None + assert out["b05Pipe"] == "irr-80" + assert out["b05Keep"] == "irr-80" + + +def test_배선(): + svp = _read("B06_Section", "B06_Section_UI_Section_View_Profile.ts") + assert "profile?.setSelectedStructure(structureId, true);" in svp + assert "profile.setSelectedStructure(markedId, true);" in svp + cross = _read("A00_Common", "design", "design_view_cross.ts") + assert "sectionView.selectMark(mark, false);" in cross + b05 = _read("A00_Common", "design", "design_view_profile.ts") + assert ", true)" not in "".join( + line for line in b05.splitlines() if "setSelectedStructure(" in line + ) From 1a96188ce09c3d376ade72f926da86dc4ae8ad63 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 11:51:15 +0900 Subject: [PATCH 2/3] =?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=EC=A0=84=EB=B6=80=20=ED=85=8C=EB=91=90=EB=A6=AC=20?= =?UTF-8?q?=C2=B7=20=EC=B9=B4=EB=93=9C=20=ED=95=98=EB=82=98=20=EC=95=88=20?= =?UTF-8?q?=EA=B3=A0=EB=A6=84=20=C2=B7=20=EC=B2=AB=20=EC=B9=B4=EB=93=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=8A=A4=ED=81=AC=EB=A1=A4=EB=A7=8C=20(177-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - applySpan 노면공도 --span · revealStation(리스너 없이 옛 카드 풂 · 폼 유지) - focusChainage revealOnly · 세션 고름은 패널이 직접 - test_165 · test_106 기대값 고침 · 새 시험 test_177_4_pavement_span_box Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015sDZSjFvoiDjdGpgj3muc3 --- A00_Common/design/design_view_cross.ts | 9 ++- .../B06_Section_UI_Page_Structures_Panel.ts | 16 ++-- B06_Section/B06_Section_UI_Section_View.ts | 13 +++- .../B06_Section_UI_Section_View_Types.ts | 2 + resources/tester/helper_165_pavement_flow.cjs | 25 +++++-- .../tester/test_106_span_highlight_wording.py | 4 +- resources/tester/test_165_pavement_flow.py | 14 ++-- .../tester/test_177_4_pavement_span_box.py | 74 +++++++++++++++++++ 8 files changed, 133 insertions(+), 24 deletions(-) create mode 100644 resources/tester/test_177_4_pavement_span_box.py diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index 5dbd9f0c3..e1ae1e94c 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -115,13 +115,14 @@ export async function mountCrossView( const crossHalfWidthField = crossView.field; /** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤 — 가장 가까운 카드로 간다. */ - const focusStationAt = (chainageM: number): void => { + const focusStationAt = (chainageM: number, revealOnly = false): void => { const sections = sectionDetail?.cross_sections; if (!sections?.length) return; const nearest = sections.reduce((best, entry) => Math.abs(entry.chainage_m - chainageM) < Math.abs(best.chainage_m - chainageM) ? entry : best, ); - sectionView.focusStation(nearest.station_id); + if (revealOnly) sectionView.revealStation(nearest.station_id); + else sectionView.focusStation(nearest.station_id); }; /** 그 누가거리를 가진 측점 카드(관 찾는 거리 한 값). */ const ownerAt = (chainageM: number) => @@ -186,8 +187,8 @@ export async function mountCrossView( stationInterval: () => stationInterval ?? 20, // 벽·구체를 누르는 중이면 그 누름이 카드를 고른다 — 폼이 구조물을 열며 카드를 먼저 // 고르면 뒤이은 카드 선택이 「같은 측점 재클릭 = 해제」로 먹힌다. - focusChainage: (chainageM) => { - if (!isWallSelecting()) focusStationAt(chainageM); + focusChainage: (chainageM, revealOnly) => { + if (!isWallSelecting()) focusStationAt(chainageM, revealOnly); }, releaseCard: () => releaseCross(), markStructure: (structureId) => sectionView.selectMark(structureId), diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 375e616db..25c0eaded 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -84,8 +84,8 @@ export interface B06StructuresPanelDeps { wallSideLabels?: (chainageM: number) => { inlet: "좌" | "우"; outlet: "좌" | "우" } | null; /** 측점 간격(m) — 측점번호+잔여거리 환산용. */ stationInterval: () => number; - /** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다. */ - focusChainage: (chainageM: number) => void; + /** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다(revealOnly = 고르지 않고 스크롤만 · 177-4). */ + focusChainage: (chainageM: number, revealOnly?: boolean) => void; /** 목록에서 고른 줄을 다시 눌러 풂 — 횡단 카드 선택도 끔(54-8). */ releaseCard?: () => void; /** 종단 알약 강조를 목록에서 고른 것에 맞춘다(null = 관 · 해제 — 측점선 짝을 따름). */ @@ -216,11 +216,17 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc } currentChainageM = null; // 폼 = 이 구조물 — 옛 관을 붙든 채 남지 않게(165-2) // 카드 고름이 측점으로 다시 풀면 같은 측점 관이 먼저 열린다 — 누른 구조물을 넘긴다. - preferredId = structure.structure_id ?? null; - deps.focusChainage(focusOf(structure)); + // 노면공 = 카드 하나 안 고름 · 범위 카드 전부 테두리 · 그 카드로 스크롤만 · 폼 유지(177-4) + const pave = !coverable(structure); + const id = structure.structure_id ?? null; + preferredId = id; + deps.focusChainage(focusOf(structure), pave); preferredId = null; // 알약은 카드 뒤에 — 카드 측점이 알약을 갈아 끼우거나 끄지 않게(53-2) - deps.markStructure?.(structure.structure_id ?? null); + deps.markStructure?.(id); + // 카드 고름 리스너를 안 거치니 세션 고름은 여기서(B05 로 가도 그 노면공) + const at = { chainageM: focusOf(structure), structureId: id }; + if (pave) selectDesign(deps.projectId, at, { from: "b06" }); }, getInterval: deps.stationInterval, // 관 추가 — 정본은 [저장]·[확정]에서 나간다. 화면 목록·알약은 바로 세운다. diff --git a/B06_Section/B06_Section_UI_Section_View.ts b/B06_Section/B06_Section_UI_Section_View.ts index b4f916667..805ac4c5d 100644 --- a/B06_Section/B06_Section_UI_Section_View.ts +++ b/B06_Section/B06_Section_UI_Section_View.ts @@ -235,7 +235,7 @@ export function createSectionView( /** * 고른 구간형 구조물(옹벽 · 독립 흙막이 · 사토장)의 시작~종료 안 횡단 카드 전부에 테두리 강조(106-1). - * 노면공(G군)은 테두리 대신 범위 카드 안 그 노면공의 포장층만 강조(152-5) — 겹친 노면공은 범위 표식으로 하나만. + * 노면공(G군)도 테두리 + 범위 카드 안 그 노면공의 포장층 강조(152-5 · 177-4) — 겹친 노면공은 범위 표식으로 하나만. * 가장 가까운 한 장의 `--selected` 는 그대로 — 클래스만 갈아 끼워 줌·팬을 지킨다. */ let spanMarks: ReadonlyArray = []; @@ -257,7 +257,7 @@ export function createSectionView( : undefined; const span = inSpan(section, mark); const pave = span && isPavement(spanMarkId); - card.classList.toggle("b06-cross-card--span", span && !pave); + card.classList.toggle("b06-cross-card--span", span); highlightPavement(card, pave ? mark : undefined); }; const syncSpanHighlights = (): void => { @@ -576,6 +576,15 @@ export function createSectionView( ); } }, + revealStation(stationId) { + // 노면공 고름 = 카드 하나 안 고름 · 옛 카드는 리스너 없이 풂(폼 유지) · 그 카드로 스크롤만(177-4) + const previous = selectedStationId; + selectedStationId = null; + activeAreaKey = null; + markCard(previous); + profile.showCard(null); + revealCard(stationId, "smooth"); + }, selectMark: (structureId, span = true) => { // span 끔 = 알약만 · 카드는 고른 측점 하나(137-4 — 측점 고름 리스너) · 노면공 포장층은 테두리가 아니라 늘 켬(165-2) spanMarkId = span || isPavement(structureId) ? structureId : null; diff --git a/B06_Section/B06_Section_UI_Section_View_Types.ts b/B06_Section/B06_Section_UI_Section_View_Types.ts index 1db878a43..0e7439732 100644 --- a/B06_Section/B06_Section_UI_Section_View_Types.ts +++ b/B06_Section/B06_Section_UI_Section_View_Types.ts @@ -42,6 +42,8 @@ export interface SectionViewController { /** 좌측 구조물 목록에서 고른 측점 카드를 선택하고 화면에 드러낸다 — 재클릭 토글 없음 * (2026-08-29 B05/B06 일원화: 목록 클릭 → 해당 카드 스크롤·강조). null = 카드 선택 풂(54-8). */ focusStation: (stationId: string | null) => void; + /** 카드를 고르지 않고 스크롤만 — 고른 카드는 알림 없이 풂(노면공 목록 고름 · 177-4). */ + revealStation: (stationId: string) => void; /** 종단 알약 강조를 그 구조물로(null = 풀기 — 카드 측점선 짝을 따름). * `span` = 구간형 범위 안 카드 전부 테두리(기본) · 끄면 카드는 고른 측점 하나(137-4). */ selectMark: (structureId: string | null, span?: boolean) => void; diff --git a/resources/tester/helper_165_pavement_flow.cjs b/resources/tester/helper_165_pavement_flow.cjs index 407a783ef..75a3f17e1 100644 --- a/resources/tester/helper_165_pavement_flow.cjs +++ b/resources/tester/helper_165_pavement_flow.cjs @@ -202,6 +202,13 @@ const view = { profile.showCard(CARDS.find((card) => card.station_id === this.selected) ?? null); if (this.selected !== previous) this.listener(this.selected); }, + revealed: null, + revealStation(id) { + // 노면공 목록 고름 = 카드 하나 안 고름 · 리스너 없이 풂 · 스크롤만(177-4 · Section_View `revealStation`) + this.selected = null; + profile.showCard(null); + this.revealed = id; + }, focusStation(id) { if (id === null) this.selected = null; else if (this.selected !== id) this.selectStation(id); @@ -214,11 +221,12 @@ const view = { }; let wallSelecting = false; -const focusStationAt = (at) => { +const focusStationAt = (at, revealOnly = false) => { const nearest = CARDS.reduce((best, card) => Math.abs(card.chainage_m - at) < Math.abs(best.chainage_m - at) ? card : best, ); - view.focusStation(nearest.station_id); + if (revealOnly) view.revealStation(nearest.station_id); + else view.focusStation(nearest.station_id); }; const current = { structures: [PAVE] }; @@ -248,6 +256,7 @@ async function build(list) { form.id = null; form.pipe = null; view.selected = null; + view.revealed = null; view.spanMarkId = null; profile.markedId = null; profile.pill = null; @@ -256,8 +265,8 @@ async function build(list) { shared: { view: () => "cross" }, detail: () => ({ cross_sections: CARDS }), stationInterval: () => 20, - focusChainage: (at) => { - if (!wallSelecting) focusStationAt(at); + focusChainage: (at, revealOnly) => { + if (!wallSelecting) focusStationAt(at, revealOnly); }, markStructure: (id) => view.selectMark(id), onMarks: (next, types) => { @@ -308,7 +317,11 @@ const wallPick = (cardAtM, ownerM) => { await build([PAVE, { structure_id: "sign", type_id: "hazard_sign", chainage_m: 127.86 }]); listPick("pave"); out.step1 = snap(); - cardPick(40); // 고름이 이미 연 범위 첫 카드 재누름(브레인 ORCA ②) + out.reveal1 = view.revealed; // 고르지 않고 스크롤만 한 카드(177-4) + out.pick1 = selection.readSelection(PID); + cardPick(40); // 스크롤만 한 범위 첫 카드 누름 + out.step2first = snap(); + cardPick(40); // 고른 범위 카드 재누름(167-3 · 브레인 ORCA ②) out.step2re = snap(); cardPick(140); out.step2 = snap(); @@ -318,6 +331,7 @@ const wallPick = (cardAtM, ownerM) => { out.step3 = snap(); pillPick("pave"); out.step4 = snap(); + out.reveal4 = view.revealed; cardPick(80); // 관 자리 카드 = 관(지금처럼) out.pipeCard = snap(); pillPick("pave"); @@ -339,6 +353,7 @@ const wallPick = (cardAtM, ownerM) => { await build([PAVE, PAVE2]); listPick("pave2"); out.step6 = snap(); + out.reveal6 = view.revealed; cardPick(160); out.step6b = snap(); 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 aeec79260..e0b5f9c97 100644 --- a/resources/tester/test_106_span_highlight_wording.py +++ b/resources/tester/test_106_span_highlight_wording.py @@ -32,8 +32,8 @@ def test_범위_강조_클래스는_고름_목록갱신_카드재생성_세_길 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 + # 152-5 노면공(G군) 포장층 · 177-4 범위 카드 테두리도 함께 + assert 'card.classList.toggle("b06-cross-card--span", span);' in VIEW def test_측점_고름은_카드_하나_목록_고름은_여럿(): diff --git a/resources/tester/test_165_pavement_flow.py b/resources/tester/test_165_pavement_flow.py index 7ab3b105e..e03dfe7c6 100644 --- a/resources/tester/test_165_pavement_flow.py +++ b/resources/tester/test_165_pavement_flow.py @@ -2,7 +2,7 @@ """165 B06 노면공 고름 일관성 — 조사 K 재현 차례 6단계. 못박는 것(단계마다 폼 · 알약 · 포장층 · 고른 카드 · 세션 sid 가 같은 것을 가리킴) - 1. 목록 노면공 = 범위 안 카드 · 포장층 · 알약 · 폼 · sid 모두 노면공(기준점이 범위 경계여도 범위 밖 카드로 안 감). + 1. 목록 노면공 = 포장층 · 알약 · 폼 · sid 모두 노면공 · 카드는 안 고름(스크롤만 · 177-4 · 기준점이 범위 경계여도 범위 밖 카드로 안 감). 2. 범위 안 빈 카드(관 · 덮는 구조물 없음) = 노면공 그대로 · 카드만 옮김(165-1). 3. 고른 카드의 벽 = 관 폼 + 관 알약(알약이 꺼진 채 폼만 남지 않음 · 165-2) · 포장층 풂. 4. 알약 다시 = 1 과 같음. 관 자리 카드 = 관 · 범위 밖 카드 = 모두 풂(지금처럼). @@ -55,10 +55,12 @@ PIPE = {"form": "pipe-80.17", "pill": "pipe-80.17", "paveLayer": None, "paveCard def test_1_목록_노면공(out): - assert out["step1"] == _pave(40) # 기준점 30 = 범위 경계 → 범위 안 첫 카드(20 아님) + assert out["step1"] == _pave(None) # 카드 하나 안 고름(177-4) + assert out["reveal1"] == "s40" # 기준점 30 = 범위 경계 → 범위 안 첫 카드(20 아님) def test_2_범위_안_빈_카드는_노면공_그대로(out): + assert out["step2first"] == _pave(40) assert out["step2re"] == _pave(40) # 이미 고른 카드 재누름도 그대로(167-3) assert out["step2"] == _pave(140) assert out["step2b"] == _pave(100) @@ -69,7 +71,7 @@ def test_3_벽은_관_폼과_관_알약_함께(out): def test_4_알약_다시_관_카드_범위_밖(out): - assert out["step4"] == _pave(40) + assert out["step4"] == _pave(None) assert out["pipeCard"] == {**PIPE, "card": 80, "sid": None} assert out["outside"] == { "form": None, @@ -83,17 +85,17 @@ def test_4_알약_다시_관_카드_범위_밖(out): def test_5_B05_복귀(out): - assert out["step5"] == _pave(40) + assert out["step5"] == _pave(None) def test_6_겹친_노면공(out): - assert out["step6"] == _pave(100, "pave2", 6) + assert out["step6"] == _pave(None, "pave2", 6) assert out["step6b"] == _pave(160, "pave2", 6) def test_배선(): panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts") - assert "deps.focusChainage(focusOf(structure));" in panel + assert "deps.focusChainage(focusOf(structure), pave);" in panel assert "return { chainageM, structureId: openId };" in panel # 관 알약은 폼 뒤(폼이 연 구조물을 닫으며 알약을 끔) at = panel.index("section.selectPipeByChainage(hit.pipeChainageM);") diff --git a/resources/tester/test_177_4_pavement_span_box.py b/resources/tester/test_177_4_pavement_span_box.py new file mode 100644 index 000000000..b604678ee --- /dev/null +++ b/resources/tester/test_177_4_pavement_span_box.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +"""177-4 B06 노면공 목록 · 알약 고름 = 범위 카드 전부 테두리(박스) · 카드 하나 안 고름. + +못박는 것 + - 노면공 고름 = 범위 안 카드 전부 `b06-cross-card--span`(다른 구간형과 같음) + 포장층 강조 그대로. + - 카드 하나 고르기(`--selected`) 없음 · 범위 첫 카드로 스크롤만 · 옛 고른 카드는 리스너 없이 풂(폼 · 알약 유지). + - 세션 고름은 노면공 + 스크롤한 카드 측점(B05 로 가도 그 노면공). + +Node 헬퍼 `helper_165_pavement_flow.cjs`(진짜 SPN) 결과를 같이 씀. 화면은 사용자 확인. +""" + +import json +import os +import re +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_165_pavement_flow.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): + for step, reveal, layer, cards in ( + ("step1", "reveal1", "pave", 12), + ("step4", "reveal4", "pave", 12), + ("step6", "reveal6", "pave2", 6), + ): + snap = out[step] + assert snap["card"] is None # --selected 0 + assert snap["form"] == layer and snap["pill"] == layer # 폼 · 알약 유지 + assert snap["paveCards"] == cards # 범위 카드 전부 + assert out[reveal] in ("s40", "s100") + assert out["pick1"] == {"chainageM": 40, "structureId": "pave"} + + +def test_범위_카드_테두리는_노면공도(): + view = _read("B06_Section", "B06_Section_UI_Section_View.ts") + assert 'card.classList.toggle("b06-cross-card--span", span);' in view + assert "highlightPavement(card, pave ? mark : undefined);" in view + + +def test_스크롤만_길은_리스너를_안_거침(): + view = _read("B06_Section", "B06_Section_UI_Section_View.ts") + body = re.search(r"revealStation\(stationId\) \{(.*?)\n \},", view, re.S) + assert body, "revealStation 없음" + text = body.group(1) + assert "selectedStationId = null;" in text + assert "profile.showCard(null);" in text + assert 'revealCard(stationId, "smooth");' in text + assert "stationSelectListener" not in text # 폼을 닫지 않음 + cross = _read("A00_Common", "design", "design_view_cross.ts") + assert "if (revealOnly) sectionView.revealStation(nearest.station_id);" in cross + panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts") + assert "const pave = !coverable(structure);" in panel + assert 'if (pave) selectDesign(deps.projectId, at, { from: "b06" });' in panel From f60657c8b9b6ea5a2401cee0ed5649b292060f60 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 11:52:28 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(b05):=20=EC=A2=8C=EC=B8=A1=20=ED=8F=BC?= =?UTF-8?q?=20=EC=B8=A1=EC=A0=90=20=EB=B2=94=EC=9C=84=20=EC=A4=84=20?= =?UTF-8?q?=EC=82=AC=EB=A9=B4=EC=95=88=EC=A0=95=EA=B5=B0(C=EA=B5=B0)?= =?UTF-8?q?=EB=A7=8C=20=EC=88=A8=EA=B9=80=20(177-5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - syncRangeDisplay C군 거름 · hasComputedRange 그대로 · 699줄 그대로 - 새 시험 test_177_5_slope_range_hidden Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015sDZSjFvoiDjdGpgj3muc3 --- .../B05_Profile_UI_Structures_Panel.ts | 4 +- .../tester/test_177_5_slope_range_hidden.py | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 resources/tester/test_177_5_slope_range_hidden.py diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index bd0f0800f..6ea28ae39 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -154,9 +154,9 @@ export function createStructuresSection( } /** 측점 범위 표시 갱신 — 시작 = 기준 − 전길이, 종료 = 시작 + 길이(2026-08-19 - * 사용자 지시: 전/후 분할, 후길이 = 길이 − 전길이 자동). 계산 불가면 "—". */ + * 사용자 지시: 전/후 분할, 후길이 = 길이 − 전길이 자동). 계산 불가면 "—" · 사면안정군(C)은 줄 숨김(177-5). */ function syncRangeDisplay(): void { - const computed = hasComputedRange(currentType()); + const computed = hasComputedRange(currentType()) && currentType()?.group !== "C"; rangeWrap.hidden = !computed; if (!computed) return; const step = interval(); diff --git a/resources/tester/test_177_5_slope_range_hidden.py b/resources/tester/test_177_5_slope_range_hidden.py new file mode 100644 index 000000000..f0ead6c2a --- /dev/null +++ b/resources/tester/test_177_5_slope_range_hidden.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +"""177-5 좌측 폼 「측점 범위」 줄 — 사면안정군(C군)만 숨김. + +못박는 것 + - PNL5 `syncRangeDisplay` 가 C군이면 `rangeWrap.hidden` · 다른 구간형(B 소단배수로 · E 대피소 등)은 그대로. + - `hasComputedRange` 자체는 그대로(시작 · 종료 칸 숨김에도 씀). + - C군 = 사면안정 6종(정의 기준) · 그 밖 범위 종류는 C 가 아님. + +화면은 사용자 확인. +""" + +import os + +from B05_Profile.B05_Profile_Structures_Schema import load_structure_types + +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() + + +def test_범위_줄은_C군만_숨김(): + panel = _read("B05_Profile", "B05_Profile_UI_Structures_Panel.ts") + at = panel.index("function syncRangeDisplay(): void {") + body = panel[at : panel.index("\n }", at)] + assert ( + 'const computed = hasComputedRange(currentType()) && currentType()?.group !== "C";' in body + ) + assert "rangeWrap.hidden = !computed;" in body + + +def test_hasComputedRange_는_그대로(): + types = _read("B05_Profile", "B05_Profile_UI_Structures_Panel_Types.ts") + assert "export function hasComputedRange(" in types + assert '"C"' not in types[types.index("export function hasComputedRange(") :][:600] + + +def test_C군은_사면안정_6종_다른_범위_종류는_C_아님(): + groups = {t.type_id: t.group for t in load_structure_types()} + slope = { + "retaining_wall", + "masonry_wet", + "masonry_dry", + "soil_guard", + "boulder_masonry", + "berm", + } + assert {k for k, g in groups.items() if g == "C"} == slope + for kept in ("ditch_berm", "refuge", "work_yard", "guardrail"): + assert groups.get(kept) not in (None, "C"), kept