From 476205757fa120b33e9ce460c67f1170d7bfed95 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 18 Oct 2026 18:22:19 +0900 Subject: [PATCH] =?UTF-8?q?fix(440=20B06=20=ED=9A=A1=EB=8B=A8=20=EA=B3=A0?= =?UTF-8?q?=EB=A6=84):=20=EB=85=B8=EC=84=A0=20=EC=B4=88=EC=95=88=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=B2=88=ED=98=B8=20=3D=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=B9=B4=EB=93=9C=20=EB=B2=88=ED=98=B8=20=C2=B7=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20=3D=20=EC=A7=80=EA=B8=88=20=EA=B7=B8?= =?UTF-8?q?=EB=A6=B0=20=EC=83=81=EC=84=B8=20=C2=B7=20=ED=8F=89=EB=A9=B4=20?= =?UTF-8?q?=EB=B9=88=20=EA=B3=B3=20=EB=88=84=EB=A6=84=20=3D=20=EB=A7=88?= =?UTF-8?q?=EC=BB=A4=20=EA=B3=A0=EB=A6=84=EB=A7=8C=20=C2=B7=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=9E=90=EB=A6=AC=20=EB=94=B0=EB=9D=BC=EA=B0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 초안 카드(묶음 새 상세)가 같은 측점 열쇠 저장 카드 번호를 이어 씀(옮긴 관도) — 찾기 상세에 모든 카드 번호 - 카드 고름 · 목록 → 카드 · 넘김 · 풀기 = 지금 그린 상세(drawnDetail) - B06 위 평면 패널 빈 곳 누름 = 평면 마커가 고른 것만 풂(B06 고름 그대로) - 바뀜 신호에서 B06 목록 자리가 가게와 다르면 다시 실음(listStale) - 시험: test_440 새 · 409 마커 고름만 · 298 바뀜 신호 꼴 · 191_1 도우미 stub Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ --- A00_Common/design/design_pipeline_cross.ts | 3 +- A00_Common/design/design_view_cross.ts | 24 +++- ...B05_Profile_UI_RouteEdit_StructureMarks.ts | 9 +- .../logic/B05_Profile_Pipeline_Detail.ts | 4 +- .../B06_Section_UI_Page_Structures_Panel.ts | 23 ++++ .../tester/helper_191_1_box_press_pick.cjs | 6 +- .../helper_399_plan_structure_marks.cjs | 7 + .../tester/helper_440_draft_card_ids.cjs | 122 ++++++++++++++++++ resources/tester/test_298_add_reanalyze.py | 5 +- .../tester/test_409_mark_top_deselect.py | 6 + resources/tester/test_440_cross_draft_pick.py | 79 ++++++++++++ 11 files changed, 273 insertions(+), 15 deletions(-) create mode 100644 resources/tester/helper_440_draft_card_ids.cjs create mode 100644 resources/tester/test_440_cross_draft_pick.py diff --git a/A00_Common/design/design_pipeline_cross.ts b/A00_Common/design/design_pipeline_cross.ts index 8a5f10853..20b203ca0 100644 --- a/A00_Common/design/design_pipeline_cross.ts +++ b/A00_Common/design/design_pipeline_cross.ts @@ -166,7 +166,8 @@ export function pipelineCrossSource( stored?.find((row) => Math.abs(row.chainage_m - chainageM) < SAME_M)?.design; const put = (key: string, section: CrossSection, at = section.chainage_m): void => { byKey[key] = carried(section, storedAt(at)); - const own: Record = {}; + // 카드 번호 = 저장 카드 번호(같은 측점 열쇠 · 옮긴 관도 · 찾기 · 고름이 같은 id · 440) + const own: Record = { station_id: section.station_id }; for (const name of SPEC_KEYS) if (section[name] !== undefined) own[name] = section[name]; specs.set(key, own as SectionSpecs); }; diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index 93ed81554..7a7ad78dc 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -103,6 +103,8 @@ export async function mountCrossView( const projectId = shared.projectId || null; let currentRouteId: number | null = null; let sectionDetail: SectionDetailResponse | null = null; + /** 지금 그린 상세(노선 초안 동안 = 묶음 새 노선 상세) — 카드 찾기 · 고름 · 넘김 · 풀기가 이것으로(440). */ + let drawnDetail: SectionDetailResponse | null = null; let stationInterval: number | undefined; // 표준 횡단면 설정 패널 자리. context 로드 후 config 기본값으로 채운다. @@ -115,7 +117,7 @@ export async function mountCrossView( /** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤 — 가장 가까운 카드로 간다. */ const focusStationAt = (chainageM: number, revealOnly = false): void => { - const sections = sectionDetail?.cross_sections; + const sections = drawnDetail?.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, @@ -379,7 +381,7 @@ export async function mountCrossView( sectionView.setPavementSelectListener((id) => structuresPanel.selectStructure(id)); sectionView.setStationSelectListener((stationId) => { const target = stationId - ? sectionDetail?.cross_sections.find((section) => section.station_id === stationId) + ? drawnDetail?.cross_sections.find((section) => section.station_id === stationId) : null; if (!target) { structuresPanel.showAtChainage(null); @@ -458,7 +460,7 @@ export async function mountCrossView( * 적용한다 — 재렌더마다 다시 돌면 사용자가 옮긴 스크롤이 튄다(2026-09-06). */ let appliedPick: string | null = null; function restoreStructurePick(): void { - if (!sectionDetail) return; + if (!drawnDetail) return; const handoff = readStructurePick(projectId); const signature = handoff ? `${handoff.at}|${handoff.key ?? ""}|${handoff.structureId ?? ""}` @@ -468,7 +470,7 @@ export async function mountCrossView( // `load().then(restoreStructurePick)` 가 다시 부른다. const applied = applyStructurePick( handoff, - sectionDetail, + drawnDetail, { // 넘겨받은 구조물 id 가 먼저(52-15) · 폼은 id 로 직접(53-1) · 측점 없는 구조물은 카드 · 스크롤 없음(307) focusStation: (stationId) => @@ -487,7 +489,7 @@ export async function mountCrossView( // 횡단 보기 강조 = 고름 한 칸 구독 · 다른 곳(Esc 등)이 바꾼 값으로 다시 세움(233-2) const crossPick = bindCrossPick(projectId, host, { - sections: () => sectionDetail?.cross_sections ?? [], + sections: () => drawnDetail?.cross_sections ?? [], controls: stationControls, releaseForm: () => structuresPanel.showAtChainage(null), view: sectionView, @@ -508,8 +510,9 @@ export async function mountCrossView( if (!sectionDetail) return; showSectionView(); // 높이 배율은 항상 1(2026-08-06 사용자 지시 — 옵션 폐지). + drawnDetail = shownDetail(projectId, sectionDetail); // 노선 초안 동안 = 새 노선 횡단(346-2 덧 3) sectionView.render( - shownDetail(projectId, sectionDetail), // 노선 초안 동안 = 새 노선 횡단(346-2 덧 3) + drawnDetail, 1, crossHalfWidth(), stationInterval, @@ -564,7 +567,14 @@ export async function mountCrossView( const reloadPanel = (): void => void structuresPanel.load().then(restoreStructurePick); host.onDispose(onStaleStructureTypes(() => host.visible() && reloadPanel())); // 바뀜 신호 하나(323-6 · `watchDrafts`) — 초안이 어느 길로 바뀌든 보일 때 카드 · 그림 다시(옛 되부름 갈음) - host.onDispose(watchDrafts(projectId, () => host.visible() && void refreshDetailForStructures())); + // 목록도 — 관 · 구조물 자리가 바뀌었으면(노선 초안 따라감 · 평면 마커 옮김) 다시 실음(440) + host.onDispose( + watchDrafts(projectId, () => { + if (!host.visible()) return; + if (structuresPanel.listStale()) reloadPanel(); + void refreshDetailForStructures(); + }), + ); host.onDispose(followSwaps(projectId, () => currentRouteId, host, refreshDetailForStructures)); host.onDispose(() => { window.clearTimeout(pipeReloadTimer); diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts b/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts index 6c82835d8..5d7891c21 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts @@ -266,7 +266,7 @@ export interface StructureMarkInput { release: () => boolean; /** 마커 위인가 — 커서 몫. */ over: (px: number, py: number) => boolean; - /** 고른 마커가 있으면 풂 · true(409 — 마커 아닌 곳 누름 · Esc). */ + /** 이 평면 마커가 고른 것이면 풂 · true(409 — 마커 아닌 곳 누름 · 다른 화면이 고른 것은 그대로 · 440). */ deselect: () => boolean; } @@ -277,6 +277,8 @@ export function bindStructureMarks( ): StructureMarkInput { const { canvas, status } = params; const projectId = draftProjectId(); + /** 이 평면 마커 누름이 고른 것(빈 곳 누름이 풀 것 · 440). */ + let ownPick: string | null = null; const list = redraws.get(projectId) ?? new Set<() => void>(); redraws.set(projectId, list); const redraw = (): void => void (canvas.isConnected ? params.draw() : list.delete(redraw)); @@ -335,6 +337,7 @@ export function bindStructureMarks( const deselect = (): boolean => { if (!planMarks(canvas)?.picked) return false; selectDesign(projectId, NO_SELECTION, { from: PLAN_PICK_FROM }); + ownPick = null; params.draw(); return true; }; @@ -402,6 +405,7 @@ export function bindStructureMarks( if (!mark) return false; release(); planMarks(canvas)?.source.select(mark.id); // 알약 고름과 같은 길(공용 고름) + ownPick = mark.id; start(mark); params.draw(); return true; @@ -422,6 +426,7 @@ export function bindStructureMarks( }, release: () => !holdKey && finish(), over: (px, py) => markAt(px, py) !== null, - deselect, + // 빈 곳 누름 = 이 마커가 고른 것만 풂 — B06 카드 · 목록 · 알약이 고른 것은 그대로(440) · Esc 는 앞 그대로 + deselect: () => planMarks(canvas)?.picked === ownPick && deselect(), }; } diff --git a/B05_Profile/logic/B05_Profile_Pipeline_Detail.ts b/B05_Profile/logic/B05_Profile_Pipeline_Detail.ts index ecd3a6f1b..e3a3500dd 100644 --- a/B05_Profile/logic/B05_Profile_Pipeline_Detail.ts +++ b/B05_Profile/logic/B05_Profile_Pipeline_Detail.ts @@ -28,10 +28,12 @@ import { toDesignProfile } from "./B05_Profile_UI_Profile_Data"; import { stationLabel } from "../../common_util/common_util_station"; import { stationIdOf } from "./B05_Profile_Pipeline_Grade"; -/** 측점에 얹힌 시설 제원(저장 상세에서 측점 열쇠로 옮김) — 낡음 판정 · 그림이 읽음. */ +/** 측점에 얹힌 시설 제원(저장 상세에서 측점 열쇠로 옮김) — 낡음 판정 · 그림이 읽음 · 카드 번호(`station_id` · + * 있으면 저장 카드 번호를 이어 씀 · 440). */ export type SectionSpecs = Partial< Pick< CrossSection, + | "station_id" | "structure" | "culvert" | "ford" diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index ae31e6978..42d898bde 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -117,6 +117,8 @@ export interface B06StructuresPanel { listRoot: HTMLElement; /** 타입 레지스트리·구조물 정본(미저장분 우선)·관 지점을 받아 채운다. */ load: () => Promise; + /** 가게(초안 · 없으면 저장분) 관 · 구조물 자리가 목록과 다른가 — 다시 실을 때(노선 초안 따라감 · 평면 마커 옮김 · 440). */ + listStale: () => boolean; /** 기하가 한계로 자른 실제 값을 폼에 되돌린다 — 입력 숫자만 커지고 그림은 그대로인 * 상태를 막는다(2026-08-29 사용자: 재질 높이 한계에 걸린 값). */ overrideOptions: (chainageM: number, patch: Record) => void; @@ -162,9 +164,19 @@ export interface B06StructuresPanel { }; } +type PlaceOf = { chainage_m?: number | null; start_m?: number | null; end_m?: number | null }; +/** 목록 자리 지문 — 누가거리 · 구간만(옵션 고침은 안 셈 · 440). */ +const placeKey = (list: ReadonlyArray): string => + JSON.stringify( + list.map((entry) => [entry.chainage_m ?? null, entry.start_m ?? null, entry.end_m ?? null]), + ); + export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06StructuresPanel { let structures: StructureInstance[] = []; let pipeFacilities: PipeFacilityItem[] = []; + /** 마지막으로 실은 저장분(초안이 비면 목록이 견줄 자리 · 440). */ + let storedStructures: StructureInstance[] = []; + let storedPipes: ReadonlyArray = []; /** 지금 폼에 올라온 계곡 통과 시설의 누가거리 — 유입측 형식 반영 대상. */ let currentChainageM: number | null = null; @@ -385,6 +397,8 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc section.setTypes(types, listPick("b06")); // 표 목록 both + b06(322-1) markTypes = types; // 저장하지 않고 나갔던 조작분이 있으면 그것으로 화면을 세운다(B05와 같은 규칙). + storedStructures = stored.structures; + storedPipes = pipeResponse.pipe_points; structures = readPendingStructures(projectId) ?? stored.structures; const note = staleDefinitionNotice(projectId, stored.definition_version); // 322-2 if (note) showToast(note); @@ -558,6 +572,15 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc root: section.root, listRoot: section.listRoot, load, + listStale: () => { + if (!deps.projectId) return false; + const shopStructures = readPendingStructures(deps.projectId) ?? storedStructures; + const shopPipes = readPendingPipes(deps.projectId) ?? storedPipes; + return ( + placeKey(shopStructures) !== placeKey(structures) || + placeKey(shopPipes) !== placeKey(pipeFacilities) + ); + }, currentChainage: () => currentChainageM, // 종단 우클릭 → 좌측 폼·목록과 같은 함수. 폼이 열리고 목록·알약도 함께 선다. addStructureAt: (chainageM, typeId) => section.addAt(chainageM, typeId), diff --git a/resources/tester/helper_191_1_box_press_pick.cjs b/resources/tester/helper_191_1_box_press_pick.cjs index 7c1268de8..ca7ea0b21 100644 --- a/resources/tester/helper_191_1_box_press_pick.cjs +++ b/resources/tester/helper_191_1_box_press_pick.cjs @@ -141,7 +141,11 @@ function loadSelection() { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; - const stubs = { "./B06_Section_UI_Adjust_Dock": { refreshAdjustSlots: () => {} } }; + const stubs = { + "./B06_Section_UI_Adjust_Dock": { refreshAdjustSlots: () => {} }, + "./B06_Section_UI_Cross_Design_Pick": { setBermPickHost: () => {} }, // 소단 고름 다리(440 덤) + "./B06_Section_UI_Page_Persist": { isBermType: () => false }, + }; new Function("exports", "module", "require", js)(box.exports, box, (r) => stubs[r]); return box.exports; } diff --git a/resources/tester/helper_399_plan_structure_marks.cjs b/resources/tester/helper_399_plan_structure_marks.cjs index a33ee9a40..0420bb689 100644 --- a/resources/tester/helper_399_plan_structure_marks.cjs +++ b/resources/tester/helper_399_plan_structure_marks.cjs @@ -336,6 +336,8 @@ out.redrawnOnChange = redrawn - was; // ⑥ 고름 풀기(409) — 고른 것 없으면 아무 일 없음 · 있으면 공용 고름을 b05-plan 으로 비움 · Esc 도 selection = null; out.deselectNone = [input.deselect(), written.length]; +input.press(80, 0); // 이 마커로 고름(440 — 빈 곳 누름이 풀 것) +input.release(); selection = { chainageM: 40, structureId: "A" }; out.deselectPicked = [input.deselect(), ...written.slice(-1), selection]; selection = { chainageM: 80.17, structureId: null }; // 관 고름(누가거리만) @@ -345,10 +347,15 @@ out.escapePicked = [...written.slice(-1), selection, prevented]; const writes = written.length; key("keydown", "Escape"); // 고른 것 없음 = 공용 Esc 몫(안 막음) out.escapeNone = [written.length - writes, prevented]; +// 440 — 다른 화면(B06 카드 · 목록 · 알약)이 고른 것 = 빈 곳 누름이 안 풂 · 이 마커로 고른 것만 풂 +selection = { chainageM: 40, structureId: "A" }; +const before440 = written.length; +out.deselectOther = [input.deselect(), written.length - before440, selection !== null]; // 마커 누름 = 고름(공용 고름 길) · 법선 · 창 — 그린 차례는 `_Render` 맨 끝(시험이 원본으로 봄) input.press(80, 0); input.release(); out.pressAfterRelease = selects[selects.length - 1]; +out.deselectOwn = [input.deselect(), ...written.slice(-1), selection]; // 440 — 마커로 고른 것은 풂 // ⑦ 순수 셈 out.logic = { diff --git a/resources/tester/helper_440_draft_card_ids.cjs b/resources/tester/helper_440_draft_card_ids.cjs new file mode 100644 index 000000000..30a3c9fc6 --- /dev/null +++ b/resources/tester/helper_440_draft_card_ids.cjs @@ -0,0 +1,122 @@ +// 440 — 노선 초안 동안 B06 카드(묶음 새 상세) 번호가 찾기 상세(갈아끼운 캐시)에 있음. +// 저장 상세 → cross 입력 모음(`pipelineCrossSource` · 진짜) → 새 상세(`draftSectionDetail` · 진짜) 대 갈아끼움(`swapCrossSections` · 진짜). +const fs = require("fs"); +const path = require("path"); +const ROOT = path.resolve(__dirname, "..", ".."); +const ts = require(path.join(ROOT, "config", "node_modules", "typescript")); +const idOf = (m) => `station_${String(Math.round(m * 1000)).padStart(12, "0")}`; +const REAL = /(B05_Profile_Pipeline_Detail|B06_Section_Station_Swap|design_pipeline_cross)$/; +const STUBS = { + B05_Profile_Pipeline_Grade: { stationIdOf: idOf }, + common_util_station: { stationLabel: (m) => String(m) }, + B05_Profile_UI_Profile_Data: { toDesignProfile: () => ({}) }, + b_page_state: { readState: () => null }, + B06_Section_Section_Canon: { savedDesigns: () => null }, + B06_Section_UI_Standard_Panel: { + effectiveStandardCross: () => ({ road_width_m: 3 }), + readRockBoundaryDefault: () => null, + }, +}; +const cache = new Map(); +function load(full) { + if (cache.has(full)) return cache.get(full).exports; + const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + cache.set(full, box); + const req = (r) => { + const name = r.split("/").pop(); + if (STUBS[name]) return STUBS[name]; + if (REAL.test(r)) return load(path.resolve(path.dirname(full), r + ".ts")); + throw new Error(`stub 없음: ${r}`); + }; + new Function("exports", "module", "require", js)(box.exports, box, req); + return box.exports; +} +const detailMod = load(path.join(ROOT, "B05_Profile/logic/B05_Profile_Pipeline_Detail.ts")); +const swapMod = load(path.join(ROOT, "B06_Section/logic/B06_Section_Station_Swap.ts")); +const crossMod = load(path.join(ROOT, "A00_Common/design/design_pipeline_cross.ts")); + +const sec = (m, kind, extra = {}) => ({ + station_id: idOf(m), + chainage_m: m, + label: "", + kind, + center_x: m, + center_y: 0, + samples: [], + design: { design_elevation_m: 100 }, + ...extra, +}); +// 저장 상세: 일반 340 · 관 351(배수관) · 일반 360 +const saved = { + longitudinal: { stations: [], design_profiles: [], length_m: 400, samples: [] }, + cross_sections: [ + sec(340, "regular"), + sec(351, "irregular", { culvert: { chainage_m: 351, diameter_m: 0.8 } }), + sec(360, "regular"), + ], +}; +const st = (key, m, kind) => ({ + key, + chainageM: m, + kind, + x: m + 0.5, + y: 3, + left: [0, 1], + azimuthDeg: 0, + ground: null, +}); +const savedStations = [ + st("grid:340", 340, "grid"), + st("pipe:p35100", 351, "pipe"), + st("grid:360", 360, "grid"), +]; +// 노선 초안: 교점 끌기 → 관이 좌표 따라 354 로(410) · 일반 측점은 같은 m +const draftStations = [ + st("grid:340", 340, "grid"), + st("pipe:p35100", 354, "pipe"), + st("grid:360", 360, "grid"), +]; +const oldKeys = savedStations.map((s) => s.key); + +const source = crossMod.pipelineCrossSource("p1", saved, savedStations, null); +const draft = detailMod.draftSectionDetail( + saved, + { stations: draftStations, longitudinal: { chainageM: [0, 400], z: [0, 0] } }, + {}, + null, + source.specs, + 20, +); +const swapped = swapMod.swapCrossSections(saved, draftStations, oldKeys, 20).detail; +const ids = (detail) => detail.cross_sections.map((s) => s.station_id); +const swappedIds = new Set(ids(swapped)); +const pipeCard = draft.cross_sections[1]; +// 새 측점(초안에서 처음 생긴 일반 측점) = 누가거리 번호 +const added = detailMod.draftSectionDetail( + saved, + { + stations: [...draftStations, st("grid:380", 380, "grid")], + longitudinal: { chainageM: [0, 400], z: [0, 0] }, + }, + {}, + null, + source.specs, + 20, +); +console.log( + JSON.stringify({ + draftIds: ids(draft), + allFoundInSwapped: ids(draft).every((id) => swappedIds.has(id)), + sameOrder: JSON.stringify(ids(draft)) === JSON.stringify(ids(swapped)), + pipeCard: [ + pipeCard.station_id, + pipeCard.chainage_m, + pipeCard.culvert && pipeCard.culvert.chainage_m, + ], + longIds: draft.longitudinal.stations.map((s) => s.station_id), + addedId: added.cross_sections[3].station_id, + }), +); diff --git a/resources/tester/test_298_add_reanalyze.py b/resources/tester/test_298_add_reanalyze.py index c9887465f..a29f2c90f 100644 --- a/resources/tester/test_298_add_reanalyze.py +++ b/resources/tester/test_298_add_reanalyze.py @@ -151,9 +151,8 @@ def test_추가_뒤_새_측점_횡단_미리_뜸(): body = _body(B06, " onPipeAdd: (chainageM, attributes) => {", " onPipeUpdate:") assert "persistPipes();" in body # 초안 씀 → 구독(`watchDrafts`)이 카드 다시(323-6) # 카드 다시 = 바뀜 신호 하나(323-6 · 관 · 구조물 초안 구독 · 옛 되부름 갈음) - assert ( - "watchDrafts(projectId, () => host.visible() && void refreshDetailForStructures())" in CROSS - ) + watch = CROSS[CROSS.index("watchDrafts(projectId, () => {") :] + assert "void refreshDetailForStructures();" in watch[: watch.index("}),")] # 목록도(440) drafts = _read("B06_Section", "B06_Section_Section_Drafts.ts") stations = _body( drafts, "export async function withDraftStations(", "parked.set(detail, stale);" diff --git a/resources/tester/test_409_mark_top_deselect.py b/resources/tester/test_409_mark_top_deselect.py index d8a4f23a5..5bef3a97b 100644 --- a/resources/tester/test_409_mark_top_deselect.py +++ b/resources/tester/test_409_mark_top_deselect.py @@ -52,6 +52,12 @@ def test_마커_누름은_그대로_고름(got) -> None: assert got["pressAfterRelease"] == "A" +def test_빈_곳_누름은_마커가_고른_것만_풂(got) -> None: + # 440 — B06 카드 · 목록 · 알약이 고른 것은 평면 빈 곳 누름이 안 풂(B06 고름 그대로) + assert got["deselectOther"] == [False, 0, True] + assert got["deselectOwn"] == [True, [None, "b05-plan"], None] + + def test_누름_마커_먼저_교각점_풂() -> None: pointer = _read("B05_Profile", "B05_Profile_UI_RouteEdit_Pointer.ts") down = pointer[pointer.index("if (marks.press(px, py)) {") :] diff --git a/resources/tester/test_440_cross_draft_pick.py b/resources/tester/test_440_cross_draft_pick.py new file mode 100644 index 000000000..d50a9f44a --- /dev/null +++ b/resources/tester/test_440_cross_draft_pick.py @@ -0,0 +1,79 @@ +"""440 구조물 횡단 · 내부 구조물 하이라이트(요구 267). + +① 노선 초안 동안 B06 카드(묶음 새 상세) 번호 = 저장 카드 번호(같은 측점 열쇠 · 옮긴 관도) — 찾기 상세 + (갈아끼운 캐시)에 모든 카드 번호가 있음 · 찾기 넷(카드 고름 · 목록 → 카드 · 넘김 · 풀기)은 지금 그린 상세. +② 평면 패널 빈 곳 누름 = 평면 마커가 고른 것만 풂(`test_409` 의 deselectOther · deselectOwn). +③ 바뀜 신호에서 B06 목록도 자리가 바뀌었으면 다시 실음. +TS 셈은 `helper_440_draft_card_ids.cjs`. + + ./venv/Scripts/python.exe -m pytest resources/tester/test_440_cross_draft_pick.py -q +""" + +import json +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +HELPER = Path(__file__).with_name("helper_440_draft_card_ids.cjs") +SAVED_PIPE = "station_000000351000" + + +def _read(*parts: str) -> str: + return ROOT.joinpath(*parts).read_text(encoding="utf-8") + + +@pytest.fixture(scope="module") +def got() -> dict: + run = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", str(HELPER)], + capture_output=True, + text=True, + encoding="utf-8", + timeout=120, + ) + assert run.returncode == 0, run.stderr + return json.loads(run.stdout) + + +def test_초안_카드_번호가_찾기_상세에_모두_있음(got) -> None: + assert got["allFoundInSwapped"] is True + assert got["sameOrder"] is True + + +def test_옮긴_관_카드는_저장_번호_새_자리(got) -> None: + assert got["pipeCard"] == [SAVED_PIPE, 354, 354] + assert got["longIds"] == got["draftIds"] # 종단 측점 머리도 같은 번호 + + +def test_초안_새_측점은_누가거리_번호(got) -> None: + assert got["addedId"] == "station_000000380000" + + +def test_찾기_넷은_지금_그린_상세() -> None: + cross = _read("A00_Common", "design", "design_view_cross.ts") + assert "drawnDetail = shownDetail(projectId, sectionDetail);" in cross + focus = cross[cross.index("const focusStationAt") : cross.index("const ownerAt")] + assert "drawnDetail?.cross_sections" in focus and "sectionDetail" not in focus + pick = cross[cross.index("sectionView.setStationSelectListener") :] + assert "drawnDetail?.cross_sections.find" in pick[: pick.index("if (!target)")] + restore = cross[cross.index("function restoreStructurePick") : cross.index("// 횡단 보기 강조")] + assert "sectionDetail" not in restore and "drawnDetail," in restore + assert "sections: () => drawnDetail?.cross_sections ?? []," in cross + + +def test_바뀜_신호에서_목록_다시_실음() -> None: + cross = _read("A00_Common", "design", "design_view_cross.ts") + watch = cross[cross.index("watchDrafts(projectId, () => {") :] + watch = watch[: watch.index("}),")] + assert "if (structuresPanel.listStale()) reloadPanel();" in watch + panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts") + assert "readPendingStructures(deps.projectId) ?? storedStructures" in panel + assert "readPendingPipes(deps.projectId) ?? storedPipes" in panel + + +def test_평면_빈_곳_누름은_마커_고름만() -> None: + marks = _read("B05_Profile", "B05_Profile_UI_RouteEdit_StructureMarks.ts") + assert "deselect: () => planMarks(canvas)?.picked === ownPick && deselect()," in marks + assert "ownPick = mark.id;" in marks