diff --git a/A00_Common/design/design_profile.ts b/A00_Common/design/design_profile.ts index a15802aa9..4a5146951 100644 --- a/A00_Common/design/design_profile.ts +++ b/A00_Common/design/design_profile.ts @@ -233,9 +233,16 @@ export function createDesignProfile(ctx: DesignProfileContext) { /** 한 측점에 알약이 여럿(배수관 · 메쌓기)이면 지금 고른 알약을 그대로 둔다 — 첫 알약으로 * 갈아 끼우면 누른 것과 다른 알약이 켜졌다(계획서 24-6b). */ const structureIdAtStation = (stationId: string | null): string | null => - structureIdKeeping(stationId, selectedStructureId, irregularStations, structures, detail); + structureIdKeeping( + stationId, + selectedStructureId, + irregularStations, + structures, + detail, + structureTypes, + ); const stationIdAtStructure = (structureId: string | null): string | null => - stationIdAtStructureOf(structureId, irregularStations, structures); + stationIdAtStructureOf(structureId, irregularStations, structures, structureTypes); /** * 측점 선택 — 같은 측점 재선택이면 해제한다(2026-08-04 사용자 지시). diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index 530de041a..f0c61f60d 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -350,6 +350,7 @@ export async function mountCrossView( structuresPanel.moveStructureTo(structureId, toChainageM); }, selectStructure: (structureId) => structuresPanel.selectStructure(structureId), + selectPipe: (chainageM) => structuresPanel.selectPipeAt(chainageM), // 측점 없는 관(요구 178) pipesChanged: () => void refreshDetailForStructures(), }); // 폼 → 횡단 캐시 반영은 따로 뗀 모듈이 맡는다(2026-09-02 분리). diff --git a/A00_Common/design/design_view_profile.ts b/A00_Common/design/design_view_profile.ts index 218c5cfdd..be81dae68 100644 --- a/A00_Common/design/design_view_profile.ts +++ b/A00_Common/design/design_view_profile.ts @@ -175,7 +175,9 @@ export async function mountProfileView( // (가상 id `pipe-*`)은 관 정본 소관이라 누가거리로 되돌려 보낸다(2026-08-17). onStructureSelect: (structureId) => { const chainage = bridge.pipeMarkChainage(structureId); - rememberPick(bridge.markChainage(structureId), chainage === null ? structureId : null); + // 측점 없는 관(물넘이포장)은 알약 id 도 남김 — 같은 m 일반 측점선을 안 켬(요구 178) + const keepId = chainage === null || bridge.stationlessPipeMark(structureId); + rememberPick(bridge.markChainage(structureId), keepId ? structureId : null); if (chainage !== null) { panel.structures.selectPipeByChainage(chainage); selectStationOfPipe(chainage); diff --git a/B05_Profile/B05_Profile_UI_Page_Structures.ts b/B05_Profile/B05_Profile_UI_Page_Structures.ts index 93c06881b..57f637776 100644 --- a/B05_Profile/B05_Profile_UI_Page_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Page_Structures.ts @@ -34,6 +34,7 @@ import type { createRouteProfilePanel } from "./B05_Profile_UI_Profile_Panel"; import { FACILITY_NAMES } from "./B05_Profile_UI_Page_Helpers"; import { followedStation } from "./B05_Profile_UI_Profile_Alignment"; import { followSnapped } from "./B05_Profile_UI_Structures_Snap"; +import { plantsStation } from "./B05_Profile_UI_Profile_Structures"; import { pipeStationMarks, SAME_STATION_M, @@ -423,6 +424,11 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) { clearProjectedStations, /** 알약 식별자에서 관 누가거리를 되읽는다(관이 아니면 null). */ pipeMarkChainage, + /** 측점 없는 관 알약(물넘이포장)인가 — 고름 값에 알약 id 를 남길지 가름(요구 178). */ + stationlessPipeMark: (structureId: string | null): boolean => { + const mark = pipeMarks.find((entry) => entry.structure_id === structureId); + return !!mark && !plantsStation(mark, []); + }, /** 알약 식별자의 누가거리(구조물·관 공통). B06 으로 넘길 선택값을 만들 때 쓴다. */ markChainage, /** 사이드 목록이 바뀜 — 화면을 맞추고 서버 정본에 저장한다. */ diff --git a/B05_Profile/B05_Profile_UI_Profile_Structures.ts b/B05_Profile/B05_Profile_UI_Profile_Structures.ts index 5649dbd79..07ceb2d77 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Structures.ts @@ -15,9 +15,17 @@ import { isPipeStation, type IrregularStation, } from "./B05_Profile_UI_IrregularStations"; -import { structureAnchorM, type StructureInstance } from "./B05_Profile_Api_Structures"; +import { + structureAnchorM, + type StructureInstance, + type StructureType, +} from "./B05_Profile_Api_Structures"; import type { PickGroup } from "./B05_Profile_UI_Structures_Panel_Types"; import { PIPE_MATCH_M } from "../A00_Common/design/design_selection"; +import { + pipeStationMarks, + structureStationMarks, +} from "../common_util/common_util_structure_station"; /** 사이드 「구조물 배치」 군 · 종류 드롭다운 목록(`section.pickGroups()`)을 그대로 구조물군 → 종류 * 2단 메뉴 항목으로(129-1 — 종단그래프 · 배수유역도 · 3D · B06 종단 공용 · 가린 군 · 종류는 뺌). @@ -157,6 +165,7 @@ export function structureIdKeeping( detail: { longitudinal: { stations: ReadonlyArray<{ station_id: string; chainage_m: number }> }; } | null, + types: ReadonlyArray = [], ): string | null { const selected = structures.find((item) => item.structure_id === selectedId); const at = detail?.longitudinal.stations.find( @@ -165,20 +174,36 @@ export function structureIdKeeping( const keeps = stationId !== null && !!selected && - (stationIdAtStructure(selectedId, stations, structures) === stationId || + (stationIdAtStructure(selectedId, stations, structures, types) === stationId || (at !== undefined && Math.abs(structureAnchorM(selected) - at) < SAME_CHAINAGE_M)); return keeps ? selectedId : structureIdAtStation(stationId, stations, structures); } -/** 알약(구조물) id → 측점선 id. 세로선이 없는 구조물(A군 외)이면 null. */ +/** 제 측점을 심는 알약인가 — 272 측점 심기 한 벌(A군 · C · D군 구간형 · 관은 물넘이포장 뺌). + * 안 심는 구조물 알약은 같은 자리 · 근처 측점선을 짝으로 켜지 않음(요구 178) · 종류를 모르면 옛 길. */ +export function plantsStation( + structure: StructureInstance, + types: ReadonlyArray, +): boolean { + if (String(structure.structure_id ?? "").startsWith("pipe-")) + return ( + pipeStationMarks({ chainage_m: structureAnchorM(structure), facility: structure.type_id }) + .length > 0 + ); + const type = types.find((entry) => entry.type_id === structure.type_id); + return !type || structureStationMarks(structure, type).length > 0; +} + +/** 알약(구조물) id → 측점선 id. 제 측점을 안 심는 구조물(B군 · 물넘이포장 등)이면 null. */ export function stationIdAtStructure( structureId: string | null, stations: IrregularStation[], structures: StructureInstance[], + types: ReadonlyArray = [], ): string | null { if (structureId === null) return null; const structure = structures.find((item) => item.structure_id === structureId); - if (!structure) return null; + if (!structure || !plantsStation(structure, types)) return null; const anchor = structureAnchorM(structure); const station = stations.find((entry) => Math.abs(entry.chainage_m - anchor) < SAME_CHAINAGE_M); return station ? irregularStationId(station.id) : null; diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index c22c9745a..622aac49f 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -49,6 +49,7 @@ import { } from "../A00_Common/design/design_selection"; import { showToast } from "@ui/ui_template_elements"; import { snapStructureStation } from "../common_util/common_util_structure_station"; +import { plantsStation } from "../B05_Profile/B05_Profile_UI_Profile_Structures"; import { adjustDockRoot, refreshAdjustSlots, @@ -129,6 +130,8 @@ export interface B06StructuresPanel { preferred: () => string | null; /** 구조물 하나를 폼에 올리고 그 카드로 간다 — 알약을 누른 길(목록 클릭과 같은 흐름). */ selectStructure: (structureId: string) => void; + /** 측점 없는 관(물넘이포장) 하나를 폼에 올림 — 카드 안 고름 · 스크롤만 · 알약 · 고름 값에 알약 id(요구 178). */ + selectPipeAt: (chainageM: number) => void; /** 지금 목록의 구조물 정본(미저장분 포함) — 카드 찾기에 쓴다. */ getStructures: () => StructureInstance[]; /** 지금 폼에 올라온 계곡 통과 시설의 누가거리(없으면 null). */ @@ -220,11 +223,12 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc currentChainageM = null; // 폼 = 이 구조물 — 옛 관을 붙든 채 남지 않게(165-2) // 카드 고름이 측점으로 다시 풀면 같은 측점 관이 먼저 열린다 — 누른 구조물을 넘긴다. // 노면공 = 카드 하나 안 고름 · 범위 카드 전부 테두리 · 그 카드로 스크롤만 · 폼 유지(177-4) - const pave = !coverable(structure); + // 측점 없는 구조물(제 측점을 안 심음)도 같은 길 — 근처 카드 측점선을 켜지 않음(요구 178) + const revealOnly = !coverable(structure) || !plantsStation(structure, markTypes); const id = structure.structure_id ?? null; preferredId = id; // 측점 고름(카드 하나 · 다시 세움)이 연 폼 = 누른 카드 그대로 — 구조물 기준 카드로 옮겨 가지 않음(189-1 · 231-1) - if (!openingAtStation) deps.focusChainage(focusOf(structure), pave); + if (!openingAtStation) deps.focusChainage(focusOf(structure), revealOnly); preferredId = null; // 알약은 카드 뒤에 — 카드 측점이 알약을 갈아 끼우거나 끄지 않게(53-2) deps.markStructure?.(id, !openingAtStation); // 측점 고름이 연 것 = 카드 하나 · 범위 테두리 없음(231-1) @@ -584,5 +588,22 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc } return { chainageM, structureId: hit.structureId }; }, + selectPipeAt: (chainageM) => { + const pipe = pipeFacilities.find( + (entry) => Math.abs(entry.chainage_m - chainageM) < PIPE_MATCH_M, + ); + if (!pipe) return; + const id = `pipe-${pipe.chainage_m.toFixed(2)}`; + deps.focusChainage(pipe.chainage_m, true); // 카드 안 고름 · 스크롤만 — 옛 카드 · 측점선 풂(177-4 길) + currentChainageM = pipe.chainage_m; + refreshSpecDefaults(pipe.chainage_m); + section.selectPipeByChainage(pipe.chainage_m); + deps.markStructure?.(id); // 알약은 폼 뒤(165-2) + selectDesign( + deps.projectId, + { chainageM: pipe.chainage_m, structureId: id }, + { from: "b06" }, + ); + }, }; } diff --git a/B06_Section/B06_Section_UI_Section_View_Menu.ts b/B06_Section/B06_Section_UI_Section_View_Menu.ts index 2ef62ef59..fab2266fb 100644 --- a/B06_Section/B06_Section_UI_Section_View_Menu.ts +++ b/B06_Section/B06_Section_UI_Section_View_Menu.ts @@ -33,6 +33,8 @@ export interface SectionStructureEdit { moveStructure: (structureId: string, toChainageM: number) => void; /** 구조물(관 아님) 하나를 폼에 올리고 그 카드로 간다 — 같은 측점 관보다 먼저(52-15). */ selectStructure: (structureId: string) => void; + /** 측점 없는 관(물넘이포장) 하나를 폼에 올림 — 카드 안 고름 · 그 자리로 스크롤만(요구 178). */ + selectPipe?: (chainageM: number) => void; /** 배수 재계산이 관 종류 · 관경을 바꿈 — 카드(미리 뜬 새 관 그림 포함)를 다시 얹음(273-2). */ pipesChanged?: () => void; } diff --git a/B06_Section/B06_Section_UI_Section_View_Profile.ts b/B06_Section/B06_Section_UI_Section_View_Profile.ts index 8dd8478d0..84955effb 100644 --- a/B06_Section/B06_Section_UI_Section_View_Profile.ts +++ b/B06_Section/B06_Section_UI_Section_View_Profile.ts @@ -23,6 +23,8 @@ import { selectDesign, } from "../A00_Common/design/design_selection"; import { paintMapPick } from "../A00_Common/design/design_pick_profile"; +import { ESCAPE_FROM } from "../A00_Common/design/design_pick_escape"; +import { plantsStation } from "../B05_Profile/B05_Profile_UI_Profile_Structures"; import { fetchLatestRoute, type RouteLatestResponse } from "../B05_Profile/B05_Profile_Api_Fetch"; import type { CrossSection, @@ -231,6 +233,11 @@ export function createSectionProfile( // 알약을 누르면 그 자리에 가장 가까운 측점 카드를 고른다 — 목록 클릭과 같은 규칙. // 구조물 알약은 그 구조물을 폼에 올린다 — 카드가 측점으로 풀면 같은 측점 관이 먼저다(52-15). onStructureSelect: (structureId) => { + // 알약 다시 누름 = 풀기 한 길(Esc 와 같은 값 — 카드 · 폼 · 알약 · 범위 테두리 모두 · 요구 178) + if (structureId === null) { + selectDesign(shared.projectId, NO_SELECTION, { from: ESCAPE_FROM }); + return; + } const hit = marks.find((entry) => entry.structure_id === structureId); if (!hit) return; const edit = host.edit(); @@ -238,6 +245,11 @@ export function createSectionProfile( edit.selectStructure(hit.structure_id); return; } + // 측점 없는 관(물넘이포장) = 카드 안 고름 · 그 자리로 스크롤만 · 그 관 폼 · 알약만(요구 178) + if (edit?.selectPipe && !plantsStation(hit, types)) { + edit.selectPipe(structureAnchorM(hit)); + return; + } // 목록에서 풀어도 카드는 남음 — 같은 카드면 「다시 보기」만 돼 관이 안 열렸다(53-2) host.selectCard(null); host.selectCard(nearestCard(structureAnchorM(hit))?.station_id ?? null); diff --git a/resources/tester/helper_165_pavement_flow.cjs b/resources/tester/helper_165_pavement_flow.cjs index f9fc900b3..c914d345f 100644 --- a/resources/tester/helper_165_pavement_flow.cjs +++ b/resources/tester/helper_165_pavement_flow.cjs @@ -159,6 +159,24 @@ const panelModule = loadTs( setAdjustSideSlots() {}, }, "./B06_Section_UI_Page_Structures_Spec": { withSpecDefaults: (o) => o ?? {} }, + // 요구 178 — 알약 측점 짝(진짜 `plantsStation` · 272 측점 심기 한 벌) + "../B05_Profile/B05_Profile_UI_Profile_Structures": loadTs( + path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Structures.ts"), + { + "@ui/ui_template_context_menu": { createMapContextMenu() {} }, + "./B05_Profile_UI_IrregularStations": { + irregularStationId: (id) => `irregular:${id}`, + isPipeStation: () => false, + }, + "./B05_Profile_Api_Structures": { + structureAnchorM: (s) => s.chainage_m ?? s.start_m ?? 0, + }, + "../A00_Common/design/design_selection": selection, + "../common_util/common_util_structure_station": loadTs( + path.join(ROOT, "common_util", "common_util_structure_station.ts"), + ), + }, + ), }, ); diff --git a/resources/tester/helper_231_1_pave_reload.cjs b/resources/tester/helper_231_1_pave_reload.cjs index 0f8153e01..96625385b 100644 --- a/resources/tester/helper_231_1_pave_reload.cjs +++ b/resources/tester/helper_231_1_pave_reload.cjs @@ -42,7 +42,7 @@ const structurePick = loadTs( const PID = "p231"; const TYPES = [ { type_id: "pavement_concrete", group: "G" }, - { type_id: "retaining_wall", group: "C" }, + { type_id: "retaining_wall", group: "C", placement: "interval" }, // 실제 등록부 = C 구간형(측점 심음) { type_id: "hazard_sign", group: "E" }, { type_id: "pipe", group: "A", managed_by: "pipe_points" }, ]; @@ -178,6 +178,24 @@ const panelModule = loadTs( setAdjustSideSlots() {}, }, "./B06_Section_UI_Page_Structures_Spec": { withSpecDefaults: (o) => o ?? {} }, + // 요구 178 — 알약 측점 짝(진짜 `plantsStation` · 272 측점 심기 한 벌) + "../B05_Profile/B05_Profile_UI_Profile_Structures": loadTs( + path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Structures.ts"), + { + "@ui/ui_template_context_menu": { createMapContextMenu() {} }, + "./B05_Profile_UI_IrregularStations": { + irregularStationId: (id) => `irregular:${id}`, + isPipeStation: () => false, + }, + "./B05_Profile_Api_Structures": { + structureAnchorM: (s) => s.chainage_m ?? s.start_m ?? 0, + }, + "../A00_Common/design/design_selection": selection, + "../common_util/common_util_structure_station": loadTs( + path.join(ROOT, "common_util", "common_util_structure_station.ts"), + ), + }, + ), }, ); diff --git a/resources/tester/helper_284_1_pill_pick.cjs b/resources/tester/helper_284_1_pill_pick.cjs new file mode 100644 index 000000000..620fe68de --- /dev/null +++ b/resources/tester/helper_284_1_pill_pick.cjs @@ -0,0 +1,106 @@ +/* 284 알약 고름 헬퍼 — 진짜 `B05_Profile_UI_Profile_Structures.ts`(plantsStation · stationIdAtStructure · + * structureIdKeeping)를 Node 로 돌려 표준출력 JSON 한 벌로(요구 178 · 179-1). + * 등록부 꼴 = 구조물정의.json 의 group · placement · managed_by 그대로. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); + +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 selection = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"), { + "../b_page_state": { readState: () => null, writeState() {} }, +}); +const PREFIX = "irregular:"; +const rule = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Structures.ts"), { + "@ui/ui_template_context_menu": { createMapContextMenu() {} }, + "./B05_Profile_UI_IrregularStations": { + irregularStationId: (id) => `${PREFIX}${id}`, + isPipeStation: (station) => station.origin === "pipe", + }, + "./B05_Profile_Api_Structures": { + structureAnchorM: (s) => s.chainage_m ?? s.start_m ?? 0, + }, + "../A00_Common/design/design_selection": selection, + "../common_util/common_util_structure_station": loadTs( + path.join(ROOT, "common_util", "common_util_structure_station.ts"), + ), +}); + +const TYPES = [ + { type_id: "cross_underdrain", group: "A", placement: "point", managed_by: null }, + { type_id: "retaining_wall", group: "C", placement: "interval", managed_by: null }, + { type_id: "ditch_side", group: "B", placement: "interval", managed_by: null }, + { type_id: "hazard_sign", group: "E", placement: "point", managed_by: null }, + { type_id: "bed_sill", group: "D", placement: "point", managed_by: null }, +]; +const S = (id, type, at, extra = {}) => ({ + structure_id: id, + type_id: type, + chainage_m: at, + ...extra, +}); +const STRUCTURES = [ + S("drain", "cross_underdrain", 40), + S("wall", "retaining_wall", 100, { start_m: 90, end_m: 110 }), + S("ditch", "ditch_side", 80, { start_m: 60, end_m: 100 }), + S("sign", "hazard_sign", 80), // 관 측점(80)과 같은 m — 측점 없는 구조물 + S("sill", "bed_sill", 140), + S("unknown", "legacy_x", 160), + S("pipe-80.00", "pipe", 80), + S("pipe-120.00", "ford_pavement", 120), +]; +// 종단도 구조물 측점(관 투영 + 심은 구조물) — 물넘이포장은 측점 없음 +const STATIONS = [ + { id: "drain", chainage_m: 40, origin: "structure" }, + { id: "pipe-80.00", chainage_m: 80, origin: "pipe" }, + { id: "wall", chainage_m: 100, origin: "structure" }, +]; +const byId = (id) => STRUCTURES.find((s) => s.structure_id === id); + +const plants = Object.fromEntries( + STRUCTURES.map((s) => [s.structure_id, rule.plantsStation(s, TYPES)]), +); +const line = Object.fromEntries( + STRUCTURES.map((s) => [ + s.structure_id, + rule.stationIdAtStructure(s.structure_id, STATIONS, STRUCTURES, TYPES), + ]), +); +// 종류 없이 부르면(옛 부름) 구조물은 옛 길 — 같은 자리 측점선 +const lineNoTypes = rule.stationIdAtStructure("sign", STATIONS, STRUCTURES); +// 관 측점선(80)을 다시 누름 — 고른 것이 측점 없는 표지면 관 알약으로(표지는 선 짝 아님) +const keepSign = rule.structureIdKeeping( + `${PREFIX}pipe-80.00`, + "sign", + STATIONS, + STRUCTURES, + null, + TYPES, +); +// 고른 것이 관이면 그대로 +const keepPipe = rule.structureIdKeeping( + `${PREFIX}pipe-80.00`, + "pipe-80.00", + STATIONS, + STRUCTURES, + null, + TYPES, +); + +process.stdout.write( + JSON.stringify({ plants, line, lineNoTypes, keepSign, keepPipe, sample: byId("sign") }), +); diff --git a/resources/tester/test_165_pavement_flow.py b/resources/tester/test_165_pavement_flow.py index 1afa758f1..f13aa4ab6 100644 --- a/resources/tester/test_165_pavement_flow.py +++ b/resources/tester/test_165_pavement_flow.py @@ -110,7 +110,9 @@ def test_6_겹친_노면공(out): def test_배선(): panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts") - assert "deps.focusChainage(focusOf(structure), pave);" in panel + assert ( + "deps.focusChainage(focusOf(structure), revealOnly);" in panel + ) # 노면공 · 측점 없는 구조물(요구 178) assert "openId" not in panel # 범위 안 빈 카드 = 연 노면공 지킴(165-1) 없앰 · 189-1 # 관 알약은 폼 뒤(폼이 연 구조물을 닫으며 알약을 끔) 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 index 324224a05..fe4782090 100644 --- a/resources/tester/test_177_4_pavement_span_box.py +++ b/resources/tester/test_177_4_pavement_span_box.py @@ -70,6 +70,9 @@ def test_스크롤만_길은_리스너를_안_거침(): 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 + # 노면공 + 측점 없는 구조물(제 측점을 안 심음)도 스크롤만(요구 178) + assert ( + "const revealOnly = !coverable(structure) || !plantsStation(structure, markTypes);" in panel + ) # 목록 고름은 노면공뿐 아니라 늘 고름 기록(231-2) assert 'if (!openingAtStation) selectDesign(deps.projectId, at, { from: "b06" });' in panel diff --git a/resources/tester/test_231_1_pave_reload.py b/resources/tester/test_231_1_pave_reload.py index 898c92503..2267491a9 100644 --- a/resources/tester/test_231_1_pave_reload.py +++ b/resources/tester/test_231_1_pave_reload.py @@ -60,7 +60,7 @@ def test_같은_카드_목록_고름_뒤_오감도_노면공_안_켬(out) -> Non assert after["form"] == "sign" # 목록 고름도 기록 — 오감 뒤 그 폼(231-2) assert after["paveLayer"] is None assert after["span"] != "pave" - assert after["card"] == 120 + assert after["card"] is None # 측점 없는 표지 = 카드 안 고름 · 스크롤만(요구 178) def test_목록_노면공은_오감해도_범위_강조_그대로(out) -> None: @@ -99,7 +99,7 @@ def test_B05_3D_고름은_노면공_id_를_안_적음(out) -> None: def test_배선() -> None: panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts") assert "section.selectAt({ chainageM: picked, structureId }, coverable);" in panel - assert "if (!openingAtStation) deps.focusChainage(focusOf(structure), pave);" in panel + assert "if (!openingAtStation) deps.focusChainage(focusOf(structure), revealOnly);" in panel assert "deps.markStructure?.(id, !openingAtStation);" in panel cross = _read("A00_Common", "design", "design_view_cross.ts") assert ( diff --git a/resources/tester/test_231_2_list_pick_record.py b/resources/tester/test_231_2_list_pick_record.py index a1a41b153..4e8dbe756 100644 --- a/resources/tester/test_231_2_list_pick_record.py +++ b/resources/tester/test_231_2_list_pick_record.py @@ -51,7 +51,8 @@ def test_카드_120_뒤_목록_관_오감_새로고침_관_폼(out) -> None: def test_같은_카드_목록_표지는_기록되고_오감_새로고침_뒤_그대로(out) -> None: - sign = {"form": "sign", "paveLayer": None, "span": "sign", "card": 120, "sid": "sign"} + # 측점 없는 표지 = 카드 안 고름 · 스크롤만 · 폼 · 범위 테두리 · 기록은 그대로(요구 178) + sign = {"form": "sign", "paveLayer": None, "span": "sign", "card": None, "sid": "sign"} assert out["listSign"] == sign assert out["listSignTrip"] == sign assert out["listSignF5"] == sign diff --git a/resources/tester/test_284_1_pill_pick.py b/resources/tester/test_284_1_pill_pick.py new file mode 100644 index 000000000..c63114265 --- /dev/null +++ b/resources/tester/test_284_1_pill_pick.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +"""284 종단도 알약 고름 · 풂(요구 178 · 179-1 · 조사 AM). + +못박는 것 + - 측점 있음 = 제 측점을 심는 알약(272 한 벌 — A군 · C · D 구간형 · 관은 물넘이포장 뺌) · 종류 모르면 옛 길. + - 측점 없는 알약(B군 · 표지 · 바닥막이 · 물넘이포장)은 같은 자리 · 근처 측점선을 짝으로 켜지 않음. + - 관 측점선을 다시 누르면 측점 없는 표지가 아니라 그 관 알약. + - B06 알약 다시 누름 = 풀기 한 길(Esc 값) · 측점 없는 관 = 카드 안 고름 · 스크롤만 · 고름 값에 알약 id. + - B05 물넘이포장 알약 = 고름 값에 알약 id(같은 m 일반 측점선 안 켬). + +Node 헬퍼 `helper_284_1_pill_pick.cjs` 가 진짜 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_284_1_pill_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["plants"] == { + "drain": True, # A군 점 + "wall": True, # C군 구간형 + "ditch": False, # B군 + "sign": False, # E군 표지 + "sill": False, # D군 점 + "unknown": True, # 종류 모름 = 옛 길 + "pipe-80.00": True, # 관 + "pipe-120.00": False, # 물넘이포장 + } + + +def test_측점_없는_알약은_측점선_짝_없음(out): + line = out["line"] + assert line["drain"] == "irregular:drain" and line["wall"] == "irregular:wall" + assert line["pipe-80.00"] == "irregular:pipe-80.00" + # 표지는 관 측점(80)과 같은 m 여도 그 선을 안 켬 · 물넘이포장 · B군 · D군 점도 없음 + assert line["sign"] is None and line["pipe-120.00"] is None + assert line["ditch"] is None and line["sill"] is None + assert out["lineNoTypes"] == "irregular:pipe-80.00" # 종류 없이 부르면 옛 길 + + +def test_관_측점선_다시_누름은_관_알약(out): + assert out["keepSign"] == "pipe-80.00" + assert out["keepPipe"] == "pipe-80.00" + + +def test_종단도는_종류를_넘김(): + profile = _read("A00_Common", "design", "design_profile.ts") + assert ( + "stationIdAtStructureOf(structureId, irregularStations, structures, structureTypes)" + in profile + ) + assert " detail,\n structureTypes,\n );" in profile + + +def test_B06_알약_풂과_측점_없는_관(): + view = _read("B06_Section", "B06_Section_UI_Section_View_Profile.ts") + assert "if (structureId === null) {" in view + assert "selectDesign(shared.projectId, NO_SELECTION, { from: ESCAPE_FROM });" in view + assert "if (edit?.selectPipe && !plantsStation(hit, types)) {" in view + cross = _read("A00_Common", "design", "design_view_cross.ts") + assert "selectPipe: (chainageM) => structuresPanel.selectPipeAt(chainageM)," in cross + panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts") + body = panel[panel.index("selectPipeAt: (chainageM) => {") :][:900] + assert "deps.focusChainage(pipe.chainage_m, true);" in body + assert "deps.markStructure?.(id);" in body + assert "{ chainageM: pipe.chainage_m, structureId: id }," in body and '{ from: "b06" },' in body + + +def test_B05_물넘이포장_알약은_id_를_남김(): + view = _read("A00_Common", "design", "design_view_profile.ts") + assert "const keepId = chainage === null || bridge.stationlessPipeMark(structureId);" in view + assert "rememberPick(bridge.markChainage(structureId), keepId ? structureId : null);" in view + page = _read("B05_Profile", "B05_Profile_UI_Page_Structures.ts") + assert "stationlessPipeMark: (structureId: string | null): boolean => {" in page + assert "return !!mark && !plantsStation(mark, []);" in page diff --git a/resources/tester/test_design_page_single.py b/resources/tester/test_design_page_single.py index 47e57b4d2..8c084bb41 100644 --- a/resources/tester/test_design_page_single.py +++ b/resources/tester/test_design_page_single.py @@ -7,6 +7,7 @@ 화면 수치(문맥 수 · 루프 · 같은 노드 · 전환 시간)는 ORCA 로 잰다 — 여기서는 길이 끊기지 않게 잠근다. """ +import re from pathlib import Path ROOT = Path(__file__).resolve().parent.parent.parent @@ -77,10 +78,10 @@ def test_창_키_듣기를_뗌(): def test_겹친_알약은_누른_것을_그대로(): profile = _read("A00_Common/design/design_profile.ts") - assert "structureIdKeeping(stationId, selectedStructureId" in profile + assert re.search(r"structureIdKeeping\(\s*stationId,\s*selectedStructureId", profile) # 판정은 `_Profile_Structures` 로 옮김 — 비정규 측점 짝 · 규칙 측점(B06 카드) 기준점(53-2) rule = _read("B05_Profile/B05_Profile_UI_Profile_Structures.ts") - assert "stationIdAtStructure(selectedId, stations, structures) === stationId" in rule + assert "stationIdAtStructure(selectedId, stations, structures, types) === stationId" in rule assert "Math.abs(structureAnchorM(selected) - at) < SAME_CHAINAGE_M" in rule