From df8d33f3498137165591b9d5f8a9a2778c648aa9 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 19 Oct 2026 09:42:27 +0900 Subject: [PATCH] =?UTF-8?q?fix(473=20=EC=83=88=EB=A1=9C=EA=B3=A0=EC=B9=A8?= =?UTF-8?q?=20=EB=88=84=EA=B0=80=ED=86=A0=EB=9F=89=20=ED=9D=94=EB=93=A4?= =?UTF-8?q?=EB=A6=BC):=20=EC=8B=9C=EC=84=A4=20=EC=A4=84=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5(=EC=A2=85=EB=A5=98=20=ED=91=9C=20=C2=B7=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=AC=BC=20=C2=B7=20=EA=B4=80=20=EC=95=8C=EB=A6=BC)?= =?UTF-8?q?=EC=9D=B4=20=EB=8B=A4=20=EC=98=A8=20=EB=92=A4=EB=A7=8C=20?= =?UTF-8?q?=EC=B8=A1=EC=A0=90=20=EC=8B=AC=EC=9D=8C=20=E2=80=94=20=EA=B7=B8?= =?UTF-8?q?=20=EC=A0=84=20=EC=A2=85=EB=8B=A8=EB=8F=84=20=3D=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B8=B0=EC=A4=80=20=C2=B7=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EC=B8=A1=EC=A0=90=20=EA=B7=B8=EB=8C=80=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 원인: 관 알림 전 「모름」을 빈 목록으로 심어 종단도 선이 저장 관 측점(389) 꼭짓점을 뺌 → 첫 cross 가 380 · 389 · 400 을 그 선으로 셈 → 배지 −211.6 ㎥ 1 초 남짓 뒤 −204.2(새로고침 20 번 중 1 ~ 2) - design_facility_lane known() · ready · design_profile planted · listedBase null = 저장 기준 · Render deriveProfile 서버 측점 그대로 · design_view_profile_sections stationsReady - 시험 test_473_lane_ready(옛 코드 빨강) · test_346_2 가짜 다리에 stationsReady · 위키 rules 한 줄 Co-Authored-By: Claude Opus 5.5 --- A00_Common/design/design_facility_lane.ts | 13 +- A00_Common/design/design_profile.ts | 7 +- A00_Common/design/design_profile_base.ts | 6 +- .../design/design_view_profile_sections.ts | 4 +- B05_Profile/B05_Profile_UI_Page_Structures.ts | 2 + B05_Profile/B05_Profile_UI_Profile_Render.ts | 5 +- docs/wiki/concepts/design_page/rules.md | 1 + .../tester/helper_346_2_station_swap.mjs | 2 +- resources/tester/helper_473_lane_ready.cjs | 140 ++++++++++++++++++ resources/tester/test_473_lane_ready.py | 68 +++++++++ 10 files changed, 238 insertions(+), 10 deletions(-) create mode 100644 resources/tester/helper_473_lane_ready.cjs create mode 100644 resources/tester/test_473_lane_ready.py diff --git a/A00_Common/design/design_facility_lane.ts b/A00_Common/design/design_facility_lane.ts index 1db49cdf5..f9faa06d1 100644 --- a/A00_Common/design/design_facility_lane.ts +++ b/A00_Common/design/design_facility_lane.ts @@ -74,6 +74,8 @@ export interface FacilityLane { /** 알약 = 구조물 + 관(가상 구조물 · 관 세션 id) — 평면 마커도 이것. */ marks: () => StructureInstance[]; stations: () => IrregularStation[]; + /** 종류 표 · 구조물 · 관 알림을 다 받음 = 측점 계획이 섬(그 전 측점은 「모름」 · 473). */ + ready: () => boolean; intervalM: () => number; onChange: (fn: () => void) => () => void; dispose: () => void; @@ -165,15 +167,19 @@ export function createFacilityLane( .catch(() => undefined); } + /** 측점 계획 입력이 다 옴 — 모름(관 알림 전 · 구조물 받기 전)을 빈 목록으로 셈하면 종단도가 저장 시설 측점 + * (관 등)을 뺀 선으로 횡단을 다시 셈 · 유토 배지가 잠깐 틀림(473 · 요구 272). */ + const known = (): boolean => types.length > 0 && reported !== null && structuresKnown; + function paint(force = false): void { const to = profile(); if (!to) return; to.setStructureTypes([...types]); to.setStructures(marks()); const signature = JSON.stringify(stations); - // 종류 표를 받기 전 = 측점 계획 없음(빈 목록을 먼저 쓰면 종단도가 측점을 지웠다 다시 셈 · 467) · + // 입력이 다 오기 전 = 측점 계획 없음(빈 목록을 먼저 쓰면 종단도가 측점을 지웠다 다시 셈 · 467 · 473) · // 같은 측점이면 안 씀 — 측점을 쓰면 종단도가 계산 묶음을 다시 돌림 - if (types.length && (force || painted?.to !== to || painted.stations !== signature)) { + if (known() && (force || painted?.to !== to || painted.stations !== signature)) { to.setIrregularStations(stations); painted = { to, stations: signature }; } @@ -183,7 +189,7 @@ export function createFacilityLane( /** 다시 셈 — 종류 표를 받기 전이면 측점은 미룸(받으면 다시 맞춤). */ function refresh(carryEdits: boolean, force = false): void { pipes = reported ? shownPipes(reported) : []; - if (types.length) { + if (known()) { const next = plannedIrregular( planStations(withPipeIds(pipes, projectId), structures, types), intervalM, @@ -241,6 +247,7 @@ export function createFacilityLane( types: () => types, marks, stations: () => stations, + ready: known, intervalM: () => intervalM, onChange(fn) { listeners.add(fn); diff --git a/A00_Common/design/design_profile.ts b/A00_Common/design/design_profile.ts index 4937ff064..d3322adf8 100644 --- a/A00_Common/design/design_profile.ts +++ b/A00_Common/design/design_profile.ts @@ -161,6 +161,8 @@ export function createDesignProfile(ctx: DesignProfileContext) { let stationInterval: number | undefined; let routeId: number | null = null; let irregularStations: IrregularStation[] = []; + // 시설 줄이 측점을 처음 심기 전 = 저장 측점 그대로(빈 목록 = 저장 관 측점을 뺀 선 · 473) + let planted = false; // 구조물 정본(structures.json) 목록과 타입 레지스트리 — 그래프 서클마크·벌룬에 쓴다. let structures: StructureInstance[] = []; let structureTypes: StructureType[] = []; @@ -298,7 +300,8 @@ export function createDesignProfile(ctx: DesignProfileContext) { } /** 계획선 측점 줄 = 측점 한 벌 — 셈은 `design_profile_base` 몫(계산 묶음 planline 도 같은 기준). */ - const listed = (): AlignmentBase | null => listedBase(storedBase, detail, irregularStations); + const listed = (): AlignmentBase | null => + listedBase(storedBase, detail, planted ? irregularStations : null); /** 편집 후 다시 그린다 — 길게 누르기(초당 10회)도 한 프레임에 한 번만 그림. */ function rebuild(): void { @@ -375,6 +378,7 @@ export function createDesignProfile(ctx: DesignProfileContext) { base: () => base, stationInterval: () => stationInterval, irregularStations: () => irregularStations, + stationsPlanted: () => planted, minCoverTargets: () => minCoverTargets, structures: () => structures, structureTypes: () => structureTypes, @@ -534,6 +538,7 @@ export function createDesignProfile(ctx: DesignProfileContext) { /** 비정규 측점 목록을 반영해 그래프(세로선+라벨)·테이블(주석)을 다시 그린다. */ setIrregularStations(stations: IrregularStation[]) { irregularStations = stations; + planted = true; if (store !== preview) base = listed(); if (base) alignment = buildAlignment(base, store.edits()); pagePipeline.restartPagePipeline(projectId); diff --git a/A00_Common/design/design_profile_base.ts b/A00_Common/design/design_profile_base.ts index ddad9ab42..4bf4bcfa7 100644 --- a/A00_Common/design/design_profile_base.ts +++ b/A00_Common/design/design_profile_base.ts @@ -17,13 +17,13 @@ import { seatBornNodes } from "../../B05_Profile/logic/B05_Profile_Grade_Seat"; /** 계획선 측점 줄 = 측점 한 벌(서버 일반 + 지금 구조물 측점 · 272) — 표 · ▲▼ · 그래프 · 3D · 횡단 다시 셈(342-1)이 * 같은 목록 · 사라진 구조물 측점의 꼭짓점은 서버와 같게 옮기거나 지우고 새 측점엔 세움(`followMovedNodes` · `seatBornNodes` · 427) · - * 늘 저장분에서 셈. */ + * 늘 저장분에서 셈. 심은 측점이 아직 없으면(null · 시설 줄 입력 전) 저장분 그대로(473). */ export function listedBase( storedBase: AlignmentBase | null, detail: SectionDetailResponse | null, - irregularStations: IrregularStation[], + irregularStations: IrregularStation[] | null, ): AlignmentBase | null { - if (!storedBase || !detail) return storedBase; + if (!storedBase || !detail || !irregularStations) return storedBase; const server = detail.longitudinal.stations; const planted = irregularGraphStations(irregularStations, maxChainageOf(detail.longitudinal)); const list = oneStationList(server, planted); diff --git a/A00_Common/design/design_view_profile_sections.ts b/A00_Common/design/design_view_profile_sections.ts index 884223b1f..7dbb41b62 100644 --- a/A00_Common/design/design_view_profile_sections.ts +++ b/A00_Common/design/design_view_profile_sections.ts @@ -87,7 +87,9 @@ export function createProfileSections(deps: ProfileSectionsDeps) { // 저장된 계획선의 정책은 만들 당시 기준이라 지금 설정(임도 종류·설계속도·지형)과 // 다를 수 있다 — 그릴 때마다 페이지 설정 한 칸(세션 초안 ?? 저장 설정 · 횡단 보기와 같은 길 · 463)으로 맞춘다. applyPageSettings(profilePanel, projectId, state.latest ?? undefined); - profilePanel.setIrregularStations(deps.bridge().irregularStations()); + // 시설 줄 입력이 다 온 뒤만 — 그 전 빈 목록은 저장 관 측점을 지운 선(473 · 다 오면 시설 줄이 심음) + if (deps.bridge().stationsReady()) + profilePanel.setIrregularStations(deps.bridge().irregularStations()); renderStationLines(detail); // 예상형상 코리도 — 저장본이 지금 정본과 맞을 때만 올린다. 낡았으면 [3D 업데이트] 대기 // 표시만 켠다(2026-09-06 실측: 받아놓고 버리는 데다 다시 만드느라 전환이 16.7초까지 갔다). diff --git a/B05_Profile/B05_Profile_UI_Page_Structures.ts b/B05_Profile/B05_Profile_UI_Page_Structures.ts index 6d43421d6..667ad1d0c 100644 --- a/B05_Profile/B05_Profile_UI_Page_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Page_Structures.ts @@ -236,6 +236,8 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) { types: (): StructureType[] => deps.lane.types(), /** 그래프·3D가 쓰는 현재 비정규 측점 목록(시설 줄 · 물넘이포장 제외 — 알약 레인 몫). */ irregularStations: () => deps.lane.stations(), + /** 측점 계획 입력(종류 · 구조물 · 관)이 다 옴 — 그 전 목록은 「모름」(473). */ + stationsReady: () => deps.lane.ready(), /** 구조물 종류의 군(A~G · 모르면 undefined) — 측점 고름이 덮어 열 구조물을 가름(216-1). */ typeGroup: (typeId: string) => structureTypeMap.get(typeId)?.group, /** [초기선 복원] — 그래프의 배관 투영만 지운다(관 정본은 배수유역이 맡는다). */ diff --git a/B05_Profile/B05_Profile_UI_Profile_Render.ts b/B05_Profile/B05_Profile_UI_Profile_Render.ts index 499f85633..1e8fca7c4 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Render.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Render.ts @@ -87,6 +87,8 @@ export interface ProfileRenderContext { base: () => AlignmentBase | null; stationInterval: () => number | undefined; irregularStations: () => IrregularStation[]; + /** 시설 줄이 측점을 심었나 — 전 = 서버 측점 그대로(빈 목록으로 저장 관 측점을 지우지 않음 · 473). */ + stationsPlanted?: () => boolean; /** 횡단배수 최소 계획고 대상(시설·제원 반영) — 요약줄 경고 표시에 쓴다. * 편집 차단 가드는 `_Profile_Panel.applyEdits` 한 곳으로 옮겼다(2026-09-02). */ minCoverTargets: () => MinCoverPoint[]; @@ -150,9 +152,10 @@ function deriveProfile( // 측점 한 벌(272) — 서버의 구조물 측점은 사이드바 목록(관을 옮기면 따라옴)이 갈음하고, 같은 m // 일반 측점은 숨김(구조물이 위) · 표 · ▲▼ · 3D 와 같은 목록(`oneStationList`). const injected = irregularGraphStations(irregularStations, maxChainageOf(longitudinal)); + const planted = ctx.stationsPlanted?.() ?? true; const graphLongitudinal = { ...graphData, - stations: oneStationList(graphData.stations, injected), + stations: planted ? oneStationList(graphData.stations, injected) : graphData.stations, }; return { designProfiles, stale, pending, graphData, graphLongitudinal }; } diff --git a/docs/wiki/concepts/design_page/rules.md b/docs/wiki/concepts/design_page/rules.md index ee35c93ad..bce4254f8 100644 --- a/docs/wiki/concepts/design_page/rules.md +++ b/docs/wiki/concepts/design_page/rules.md @@ -22,6 +22,7 @@ source: ["CLAUDE.md §5", "docs/raw/조사/2026-10-19_465_요구272_값안변함 | 소단 사본 쓰는 자리 = 시설 줄 하나(B05 만 열어도) · 같으면 안 씀 · 사본 없으면 저장분 `design.berm` | `design_facility_lane.ts:151-166` · `B06_Section_Cross_Refresh.ts:362-368` | `test_272_berm_source` | | 두 보기가 보는 파생값은 페이지 한 벌이 숨은 동안도 셈 · 보기는 그리기만 · 접힘 = 그리기만 건너뜀 | `design_facility_lane.ts:1-12` · `B05_Profile_UI_Profile_Render.ts:211-226` | `test_272_hidden_summary` · `test_271_facility_lane` | | 되돌리기 걸음 = 사용자 조작으로 값이 실제로 바뀐 때만 · 빈 칸(「""」「{}」「null」) = 칸 없음 · 빈 맵 persist = 칸 지움 | `B05_Profile_UI_Profile_History.ts:50 · 235-236` · `B06_Section_UI_Page_Link_Session.ts:97-105 · 153` | `test_272_history_empty_cells` | +| 시설 줄 측점 = 종류 표 · 구조물 · 관 알림이 다 온 뒤만 심음 · 그 전 종단도 = 저장 측점 · 저장 기준 그대로(모름 ≠ 빈 목록) | `design_facility_lane.ts` known · `design_profile.ts` planted · `design_profile_base.ts` listedBase · `design_view_profile_sections.ts` stationsReady · `B05_Profile_UI_Profile_Render.ts` deriveProfile | `test_473_lane_ready` | | 짝 없는 새 측점(구조물 · 추가 측점 · 새 관) = 계획선 꼭짓점 안 세움 · 짝 있는 옮긴 측점 = 꼭짓점 따라감 | `common_util_structure_station.ts:356` · `B05_Profile_Router_Confirm.py:306-400` | `test_427_1_follow_seat` · `test_459_route_seat_mirror` | | 구조물 측점 다시 뜸([저장] 상단측 차례 · 측점 맞춤) = 같은 자리 옛 줄의 상단측 출처(`user` 면 값도) · 포장 제안 표기 들고 감 · 새 자리 포장 표기 = 지금 계획선 | `B05_Profile_Router_Confirm.py:426-465` | `test_472_station_user_fields` | | [저장] · [확정] = 서버가 입력만으로 정본 다시 셈 · 브라우저 결과(면적 · 표본 · 측점) 받아 적지 않음 | `design_save.ts:58-68` · `B06_Section_Router_Confirm.py:203-215` | `test_b06_save_recompute` | diff --git a/resources/tester/helper_346_2_station_swap.mjs b/resources/tester/helper_346_2_station_swap.mjs index 52373e024..4d60c78a2 100644 --- a/resources/tester/helper_346_2_station_swap.mjs +++ b/resources/tester/helper_346_2_station_swap.mjs @@ -227,7 +227,7 @@ const NEW = (dy) => [ setLoading: () => undefined, clear: () => undefined, }), - bridge: () => ({ irregularStations: () => [] }), + bridge: () => ({ irregularStations: () => [], stationsReady: () => true }), // 시설 줄 입력 다 옴(473) visible: () => shown, }); await sections.restoreSections(R2); diff --git a/resources/tester/helper_473_lane_ready.cjs b/resources/tester/helper_473_lane_ready.cjs new file mode 100644 index 000000000..9a8986bc7 --- /dev/null +++ b/resources/tester/helper_473_lane_ready.cjs @@ -0,0 +1,140 @@ +/* 473 시설 줄 입력 차례(요구 272 · 새로고침 누가토량 흔들림) — 진짜 `design_facility_lane` · `design_profile_base` 를 + * Node 로 돌려 JSON 한 줄(`test_473_lane_ready.py` 가 판정). 종단도는 가짜(받은 것 기록). + * node helper_473_lane_ready.cjs + */ +const fs = require("fs"); +const path = require("path"); +const ROOT = path.join(__dirname, "..", ".."); +const ts = require(path.join(ROOT, "config", "node_modules", "typescript")); +const CATALOG = require(path.join(__dirname, "helper_322_catalog_module.cjs")); + +function memoryStorage() { + const map = new Map(); + return { + getItem: (k) => (map.has(k) ? map.get(k) : null), + setItem: (k, v) => map.set(k, String(v)), + removeItem: (k) => map.delete(k), + key: (i) => [...map.keys()][i] ?? null, + get length() { + return map.size; + }, + }; +} +globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage() }; +globalThis.sessionStorage = globalThis.window.sessionStorage; +globalThis.localStorage = globalThis.window.localStorage; + +const anyModule = (extra = {}) => + new Proxy(extra, { get: (t, key) => (key in t ? t[key] : () => undefined) }); +const STUBS = { + "config_frontend.ts": anyModule({ + CURRENT_PROJECT_ID_KEY: "frd_current_project_id", + API_BASE_URL: "", + API_TIMEOUT_MS: 1000, + PIPE_TYPES: [], + }), + "ui_template_elements.ts": anyModule(), + "B04_PreProcess_Api_Fetch.ts": anyModule(), + // 소단 사본(467) — 끌어오는 줄이 import.meta 를 써 이 로더로 못 읽음 · 소단 몫은 test_272_berm_source(vite 묶음) + "B06_Section_UI_Page_Persist.ts": anyModule({ bermSpansFromStructures: () => [] }), +}; + +const cache = new Map(); +const resolve = (base) => + [base, `${base}.ts`, path.join(base, "index.ts")].find( + (p) => fs.existsSync(p) && fs.statSync(p).isFile(), + ) ?? `${base}.ts`; +function loadTs(full) { + const stub = STUBS[path.basename(full)]; + if (stub) return stub; + if (cache.has(full)) return cache.get(full); + const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2020, + esModuleInterop: true, + }, + }).outputText; + const box = { exports: {} }; + cache.set(full, box.exports); + const near = (request) => { + if (CATALOG.isCatalogRequest(request)) return CATALOG; + if (request.endsWith(".json")) + return JSON.parse(fs.readFileSync(path.join(path.dirname(full), request), "utf8")); + const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" }; + for (const [prefix, dir] of Object.entries(alias)) + if (request.startsWith(prefix)) + return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length)))); + return loadTs(resolve(path.join(path.dirname(full), request))); + }; + new Function("exports", "module", "require", js)(box.exports, box, near); + return box.exports; +} + +const design = path.join(ROOT, "A00_Common", "design"); +const { createFacilityLane } = loadTs(path.join(design, "design_facility_lane.ts")); +const ids = loadTs(path.join(design, "design_pipe_ids.ts")); +const { listedBase } = loadTs(path.join(design, "design_profile_base.ts")); +const TYPES = JSON.parse( + fs.readFileSync(path.join(ROOT, "resources", "master_template", "구조물정의.json"), "utf8"), +).types; + +/** 가짜 종단도 — 측점을 받은 차례 기록. */ +function fakeProfile() { + const writes = []; + return { + writes, + setStructureTypes: () => undefined, + setStructures: () => undefined, + setIrregularStations: (stations) => writes.push(stations.map((s) => s.chainage_m)), + setMinCoverPipes: () => undefined, + resetStationEdit: () => undefined, + }; +} + +const PIPE = { id: "P389", chainage_m: 389, facility: "pipe" }; +/** 새로고침 차례 — 종류 표 · 구조물이 먼저, 관 알림(배수유역 받기)이 늦게. */ +function lateOrder(P) { + ids.usePipeIds(P); + const profile = fakeProfile(); + const lane = createFacilityLane(P, () => profile); + lane.setTypes(TYPES); + lane.setStored([]); + lane.set({ structures: [] }, false); + const beforePipes = { + writes: profile.writes.length, + ready: typeof lane.ready === "function" ? lane.ready() : null, + }; + lane.set({ pipes: [PIPE] }, false); + return { + beforePipes, + after: profile.writes, + ready: typeof lane.ready === "function" ? lane.ready() : null, + }; +} +/** 빠른 차례 — 관 알림이 구조물보다 먼저. */ +function earlyOrder(P) { + ids.usePipeIds(P); + const profile = fakeProfile(); + const lane = createFacilityLane(P, () => profile); + lane.setTypes(TYPES); + lane.set({ pipes: [PIPE] }, false); + const beforeStructures = profile.writes.length; + lane.set({ structures: [] }, false); + return { beforeStructures, after: profile.writes }; +} + +const stored = { stations: [{ station_id: "s", chainage_m: 389 }], basePvi: [] }; +const out = { + late: lateOrder("P473-late"), + early: earlyOrder("P473-early"), + // 심기 전(null) = 저장 기준 그대로 · 같은 객체 + listedNull: (() => { + try { + return listedBase(stored, { longitudinal: { stations: [] } }, null) === stored; + } catch { + return false; // 옛 코드 = null 을 못 받음 + } + })(), +}; +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_473_lane_ready.py b/resources/tester/test_473_lane_ready.py new file mode 100644 index 000000000..419041f3f --- /dev/null +++ b/resources/tester/test_473_lane_ready.py @@ -0,0 +1,68 @@ +"""473 새로고침 누가토량 흔들림(요구 272) — 시설 줄 입력이 다 오기 전 측점을 심지 않음. + +옛 흠 = 관 알림(배수유역 받기)이 종류 표 · 구조물보다 늦으면 시설 줄이 「모름」을 빈 목록으로 +심음 → 종단도가 저장 관 측점(389)을 뺀 선으로 횡단 다시 셈 → 배지 −211.6 ㎥ 1 초 남짓 뒤 −204.2 +(새로고침 20 번 중 1 ~ 2). +① 늦은 차례 — 관 알림 전 측점 안 씀 · 준비 아님 · 알림 뒤 한 번 [389] +② 빠른 차례 — 구조물 받기 전 안 씀 · 받은 뒤 한 번 [389](들어온 차례 무관 같은 결과) +③ 종단도 기준 — 심기 전(null) = 저장 기준 그대로 +④ 종단도 · 측점 다시 그림이 준비 전 빈 목록을 안 심음(자리 글) +TS 셈은 `helper_473_lane_ready.cjs`. + + ./venv/Scripts/python.exe -m pytest resources/tester/test_473_lane_ready.py -q +""" + +import json +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +HELPER = Path(__file__).with_name("helper_473_lane_ready.cjs") + +pytestmark = pytest.mark.skipif( + not (ROOT / "config" / "node_modules" / "typescript").is_dir(), + reason="프론트엔드 의존성 미설치", +) + + +@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: + late = got["late"] + assert late["beforePipes"] == {"writes": 0, "ready": False} # 옛 코드 = [] 를 먼저 심음 + assert late["after"] == [[389]] + assert late["ready"] is True + + +def test_들어온_차례_무관_같은_측점(got) -> None: + early = got["early"] + assert early["beforeStructures"] == 0 + assert early["after"] == got["late"]["after"] + + +def test_심기_전_종단도_기준_저장분(got) -> None: + assert got["listedNull"] is True + + +def test_준비_전_빈_목록_안_심는_자리() -> None: + profile = (ROOT / "A00_Common/design/design_profile.ts").read_text(encoding="utf-8") + assert "listedBase(storedBase, detail, planted ? irregularStations : null)" in profile + sections = (ROOT / "A00_Common/design/design_view_profile_sections.ts").read_text( + encoding="utf-8" + ) + assert "if (deps.bridge().stationsReady())" in sections + render = (ROOT / "B05_Profile/B05_Profile_UI_Profile_Render.ts").read_text(encoding="utf-8") + assert "planted ? oneStationList(graphData.stations, injected) : graphData.stations" in render