From 157111a09467a3420a5fc041bd8459f3892ff363 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 8 Oct 2026 19:42:46 +0900 Subject: [PATCH] =?UTF-8?q?feat(B05):=20=EC=9A=B0=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EC=84=B8=EC=9B=94=EA=B5=90=20=C2=B7=20=EB=AC=BC=EB=84=98?= =?UTF-8?q?=EC=9D=B4=ED=8F=AC=EC=9E=A5=EB=8F=84=20=EB=B0=94=EB=A1=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=C2=B7=20=EB=B9=88=20=EC=9B=94=EB=A5=98=20?= =?UTF-8?q?=EB=86=92=EC=9D=B4=EB=8A=94=20=EC=9E=AC=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EC=9C=A0=EB=9F=89=EC=9D=B4=20=EC=98=A4=EB=A9=B4=20=EA=B7=B8=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=EC=97=90=20=EC=B1=84=EC=9B=80(126-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 우클릭 바로 추가에서 세월교 · 물넘이포장을 폼만 갈래에서 뺌 · 추가 뒤 그 측점 관 고름 + 알림 한 줄 - 배수유역 패널 — 바로 놓은 빈 높이 측점을 기다렸다가 응답 유량으로 필요 수심 채움(`fordHeightM` · 서버 `ford_height_m` 짝) · 손 값은 안 덮음 · 유량 전엔 빈칸 · 채우면 세션 초안에 다시 담음 - 세부유역 요청은 최신 응답만 채택(requestSeq) - 시험 test_126_ford_right_click(헬퍼 helper_126_ford_fill) · test_116 기대값 고침 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s --- .../B05_Profile_UI_Drainage_Ford_Fill.ts | 60 ++++++++ B05_Profile/B05_Profile_UI_Drainage_Panel.ts | 22 ++- .../B05_Profile_UI_Structures_Panel_AddAt.ts | 12 +- resources/tester/helper_126_ford_fill.cjs | 139 ++++++++++++++++++ resources/tester/test_116_right_click_add.py | 6 +- resources/tester/test_126_ford_right_click.py | 107 ++++++++++++++ 6 files changed, 336 insertions(+), 10 deletions(-) create mode 100644 B05_Profile/B05_Profile_UI_Drainage_Ford_Fill.ts create mode 100644 resources/tester/helper_126_ford_fill.cjs create mode 100644 resources/tester/test_126_ford_right_click.py diff --git a/B05_Profile/B05_Profile_UI_Drainage_Ford_Fill.ts b/B05_Profile/B05_Profile_UI_Drainage_Ford_Fill.ts new file mode 100644 index 000000000..4e9983648 --- /dev/null +++ b/B05_Profile/B05_Profile_UI_Drainage_Ford_Fill.ts @@ -0,0 +1,60 @@ +/* ============================================================================= + * B05_Profile_UI_Drainage_Ford_Fill.ts + * 우클릭으로 바로 추가한 세월교 · 물넘이포장의 빈 월류 높이 = 배수유역 재계산이 준 설계유량의 + * 필요 수심(PLAN 126-1 · 폼 쪽 65-4 와 같은 풀이 `fordHeightM`). + * + * 기다리는 측점만 채움 · 사용자가 넣은 높이(응답 옵션에 있음)는 안 덮음 · 유량이 아직 없으면 다음 + * 응답까지 기다림. 응답 옵션을 고쳐 두면 보관소(`replaceFromResponse`)가 그 값을 정본으로 받음. + * ========================================================================== */ + +import { + FORD_BRIDGE_DEFAULT_WIDTH_M, + FORD_PAVEMENT_DEFAULT_WIDTH_M, +} from "@config/config_frontend"; +import { fordHeightM } from "../A00_Common/design/design_structure_ford_calc"; + +const FORD_KINDS: Record = { + ford_bridge: FORD_BRIDGE_DEFAULT_WIDTH_M, + ford_pavement: FORD_PAVEMENT_DEFAULT_WIDTH_M, +}; + +/** 새로 놓으면 높이를 기다리는 종류 — 높이 칸이 빈 세월교 · 물넘이포장. */ +export function waitsFordHeight( + facility: string | undefined, + options?: Record, +): boolean { + return !!facility && facility in FORD_KINDS && options?.ford_height_m === undefined; +} + +/** 기다리는 측점의 빈 높이를 채움 — 채운(또는 사용자 값이라 그만 기다릴) 측점은 `pending` 에서 뺌. + * 채운 것이 있으면 true(세션 초안에 다시 담을 것). */ +export function fillFordHeights( + points: Array<{ + chainage_m: number; + facility?: string; + options?: Record; + }>, + basins: ReadonlyArray<{ chainage_m: number; design_flow_m3s?: number | null }>, + pending: Set, +): boolean { + let filled = false; + for (const wait of [...pending]) { + const point = points.find((entry) => Math.abs(entry.chainage_m - wait) < 0.51); + if (!point?.facility || !(point.facility in FORD_KINDS)) { + pending.delete(wait); // 관이 사라졌거나 종류가 바뀜 + continue; + } + if (point.options?.ford_height_m !== undefined && point.options.ford_height_m !== "") { + pending.delete(wait); // 사용자가 넣은 값 — 안 덮음 + continue; + } + const flow = basins.find((entry) => Math.abs(entry.chainage_m - point.chainage_m) < 0.51); + const width = Number(point.options?.ford_width_m) || FORD_KINDS[point.facility]; + const need = fordHeightM(flow?.design_flow_m3s ?? null, width); + if (need === null) continue; // 유량 아직 없음 — 다음 응답까지 빈칸 + point.options = { ...point.options, ford_height_m: need }; + pending.delete(wait); + filled = true; + } + return filled; +} diff --git a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts index a580b5f08..28f7f2d5f 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts @@ -33,6 +33,7 @@ import { createFacilityStore } from "./B05_Profile_UI_Drainage_Facility"; import { mergeFacilityOptions } from "./B05_Profile_UI_Drainage_Facility_Merge"; import { readPendingPipes, writePendingPipes } from "./B05_Profile_Api_Pipes_Draft"; import { buildPipeReports } from "./B05_Profile_UI_Drainage_Report"; +import { fillFordHeights, waitsFordHeight } from "./B05_Profile_UI_Drainage_Ford_Fill"; import { createDrainageChrome } from "./B05_Profile_UI_Drainage_Chrome"; import { bindDrainageInteractions } from "./B05_Profile_UI_Drainage_Interact"; import type { RouteSpanBand } from "./B05_Profile_UI_Drainage_Spans"; @@ -172,6 +173,9 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra let offsetY = 0; let frameHandle = 0; let loadSequence = 0; + let requestSeq = 0; // 세부유역 요청 — 최신 응답만 채택 + /** 우클릭으로 바로 놓은 세월교 · 물넘이포장 — 재계산 유량이 오면 빈 월류 높이를 채울 측점(126-1). */ + const fordWaits = new Set(); let canvasWidth = 0; let canvasHeight = 0; let canvasDpr = 0; @@ -346,8 +350,10 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra reason: pipe.source, })), ); - // 시설 확장 정보(종류·구간·옵션)는 응답이 정본 — 통째로 다시 채운다. + // 시설 확장 정보(종류·구간·옵션)는 응답이 정본 — 통째로 다시 채운다(기다리던 월류 높이를 먼저 얹음). + const fordFilled = fillFordHeights(response.pipe_points, response.basins, fordWaits); facilityStore.replaceFromResponse(response.pipe_points); + if (fordFilled) rememberPipes(); // 재계산 뒤에는 선택을 다시 알려야 한다 — 관을 새로 넣은 직후에는 목록이 아직 // 비어 폼이 열리지 못했고, 중복 억제 가드에 걸려 두 번째 기회를 잃는다. lastNotifiedPipeChainage = null; @@ -367,18 +373,24 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra status.hidden = false; status.textContent = L("B05_Drainage_Status_Analyzing"); showProgress(null, L("B05_Drainage_Status_Analyzing")); + const seq = ++requestSeq; try { - apply(await request()); + const response = await request(); + if (seq !== requestSeq) return; // 뒤에 보낸 요청이 있음 — 그 응답을 기다림 + apply(response); status.hidden = basins.length > 0; if (basins.length === 0) status.textContent = L("B05_Drainage_Status_NoBasin"); scheduleDraw(); } catch (error) { + if (seq !== requestSeq) return; status.hidden = false; status.textContent = error instanceof Error ? error.message : L("B05_Drainage_Status_AnalyzeFailed"); } finally { - analyzeButton.disabled = false; - showProgress(null, null); + if (seq === requestSeq) { + analyzeButton.disabled = false; + showProgress(null, null); + } } } @@ -591,6 +603,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra addPipe(chainageM, attributes) { // 시설 정보를 먼저 보관해야 addAtChainage가 촉발하는 재계산 요청에 실려 간다. facilityStore.set(chainageM, attributes ?? null); + if (waitsFordHeight(attributes?.facility, attributes?.options)) fordWaits.add(chainageM); pipeEditor.addAtChainage(chainageM); }, updatePipeFacility(fromChainageM, toChainageM, attributes) { @@ -598,6 +611,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra const previous = facilityStore.get(fromChainageM); facilityStore.set(fromChainageM, null); facilityStore.set(toChainageM, mergeFacilityOptions(previous, attributes)); + if (fordWaits.delete(fromChainageM)) fordWaits.add(toChainageM); if (Math.abs(fromChainageM - toChainageM) > 0.005) { // 기준점이 옮겨졌다 — 관을 이동시키면 onCommit이 재계산을 돌리고, // attach가 새 위치로 시설 정보를 승계한다. diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_AddAt.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_AddAt.ts index 4e402c01a..22d989a3b 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_AddAt.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_AddAt.ts @@ -2,14 +2,19 @@ * B05_Profile_UI_Structures_Panel_AddAt.ts * 종단 우클릭 → 종류 고름 = 「[추가]까지 끝난 상태」(PLAN 116-1) — 세 길(B05 종단 · B06 종단 · * 3D)이 함께 쓰는 패널 `addAt` 의 끝맺음. 필수 빈칸이 없는 종류는 그 측점에 바로 추가하고 - * 새 항목을 고른 채로 둠 · 남는 종류는 폼만. + * 새 항목을 고른 채로 둠 · 남는 종류는 폼만. 세월교 · 물넘이포장도 바로 추가 — 빈 월류 높이는 + * 배수유역 재계산 유량이 오면 그 항목에 채움(126-1 · `_Drainage_Ford_Fill`). * ========================================================================== */ import type { StructureInstance, StructureType } from "./B05_Profile_Api_Structures"; import type { StructuresSection } from "./B05_Profile_UI_Structures_Panel_Types"; +import { showToast } from "@ui/ui_template_elements"; -/** 바로 추가하지 않는 종류 — 노면공(겹침 확인창) · 세월교 · 물넘이포장(월류 높이가 유량 뒤 채워짐). */ -const FORM_ONLY = new Set(["pavement_concrete", "ford_bridge", "ford_pavement"]); +/** 바로 추가하지 않는 종류 — 노면공(겹침 확인창). */ +const FORM_ONLY = new Set(["pavement_concrete"]); +/** 월류 높이를 유량 뒤에 받는 종류 — 추가 때 알림 한 줄. */ +const FORD_KINDS = new Set(["ford_bridge", "ford_pavement"]); +export const FORD_WAIT_NOTICE = "월류 높이는 배수유역 재계산(설계유량)이 오면 채워집니다."; /** 우클릭 한 번으로 [추가]까지 끝낼 수 있는지 — [추가]가 막는 빈 필수 칸이 없어야 함. */ export function addsAtOnce( @@ -35,6 +40,7 @@ export async function finishAdd(ctx: { // B05 는 재계산 뒤 배수유역 패널이 새 관 고름을 다시 알려 폼이 열림. await ctx.commit(); ctx.pick.selectPipeByChainage(ctx.anchorM); + if (FORD_KINDS.has(ctx.type.type_id)) showToast(FORD_WAIT_NOTICE); return true; } const before = new Set(ctx.structures.map((entry) => entry.structure_id)); diff --git a/resources/tester/helper_126_ford_fill.cjs b/resources/tester/helper_126_ford_fill.cjs new file mode 100644 index 000000000..1fe3f5984 --- /dev/null +++ b/resources/tester/helper_126_ford_fill.cjs @@ -0,0 +1,139 @@ +/* 126-1 우클릭 세월교 · 물넘이포장 바로 추가 헬퍼 — `_Structures_Panel_AddAt`(판정 · 끝맺음 · 알림)과 + * `_Drainage_Ford_Fill`(유량 응답 뒤 빈 월류 높이 채움)을 Node 로 돌려 표준출력 JSON 한 벌로. + * TS 로더는 `helper_116_right_click_add.cjs` 와 같다 · 설정값은 진짜 `config_frontend` 를 읽음. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const cache = new Map(); +const toasts = []; +const STUBS = { "@ui/ui_template_elements": { showToast: (message) => toasts.push(message) } }; + +function loadTs(file) { + const full = path.resolve(file); + 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 }, + }).outputText; + const box = { exports: {} }; + cache.set(full, box.exports); + const localRequire = (request) => { + if (STUBS[request]) return STUBS[request]; + if (request === "@config/config_frontend") + return loadTs(path.join(ROOT, "config", "config_frontend.ts")); + if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`)); + if (request.startsWith("@")) return new Proxy({}, { get: () => () => {} }); + return require(request); + }; + new Function("exports", "module", "require", js)(box.exports, box, localRequire); + cache.set(full, box.exports); + return box.exports; +} + +const B05 = path.join(ROOT, "B05_Profile"); +const { addsAtOnce, finishAdd, FORD_WAIT_NOTICE } = loadTs( + path.join(B05, "B05_Profile_UI_Structures_Panel_AddAt.ts"), +); +const { fillFordHeights, waitsFordHeight } = loadTs( + path.join(B05, "B05_Profile_UI_Drainage_Ford_Fill.ts"), +); +const { fordHeightM } = loadTs( + path.join(ROOT, "A00_Common", "design", "design_structure_ford_calc.ts"), +); + +const type = (type_id) => ({ type_id, group: "A", managed_by: "pipe_points" }); + +/** 우클릭 → 바로 추가 — 관 지점 정본에 한 번 넘기고 그 측점 관을 고름 · 알림 한 줄. */ +async function rightClick(typeId) { + toasts.length = 0; + const picked = []; + let commits = 0; + const done = await finishAdd({ + type: type(typeId), + anchorM: 120, + structures: [], + commit: async () => void commits++, + pick: { + selectById: (id) => picked.push(["id", id]), + selectPipeByChainage: (at) => picked.push(["pipe", at]), + }, + }); + return { done, commits, picked, toasts: [...toasts] }; +} + +/** 재계산 응답 한 번 — 응답 관 목록에 기다리던 높이를 얹은 결과. */ +function respond(points, basins, pending) { + const filled = fillFordHeights(points, basins, pending); + return { + filled, + heights: points.map((p) => p.options?.ford_height_m ?? null), + pending: [...pending], + }; +} + +(async () => { + const flow = 8.0; + // 첫 응답은 유량 없음 → 빈칸 · 둘째 응답에 유량 → 그 항목에 채움(폭 = 항목 값) + const pendingA = new Set([120]); + const pointsA = () => [ + { chainage_m: 120.2, facility: "ford_bridge", options: { ford_width_m: 10 } }, + ]; + const noFlow = respond(pointsA(), [{ chainage_m: 120.2, design_flow_m3s: null }], pendingA); + const withFlow = respond(pointsA(), [{ chainage_m: 120.2, design_flow_m3s: flow }], pendingA); + // 손 값은 안 덮음 + const pendingB = new Set([300]); + const hand = respond( + [ + { + chainage_m: 300, + facility: "ford_pavement", + options: { ford_width_m: 5, ford_height_m: 0.5 }, + }, + ], + [{ chainage_m: 300, design_flow_m3s: flow }], + pendingB, + ); + // 기다리지 않는 측점(옛 세월교)의 빈 높이는 그대로 · 폭 없으면 기본 폭 + const pendingC = new Set([50]); + const other = respond( + [ + { chainage_m: 50, facility: "ford_pavement", options: {} }, + { chainage_m: 80, facility: "ford_bridge", options: { ford_width_m: 10 } }, + ], + [ + { chainage_m: 50, design_flow_m3s: flow }, + { chainage_m: 80, design_flow_m3s: flow }, + ], + pendingC, + ); + // 종류가 바뀌었거나 관이 사라짐 → 그만 기다림 + const pendingD = new Set([10, 20]); + const gone = respond([{ chainage_m: 10, facility: "pipe", options: {} }], [], pendingD); + + const out = { + once: { + fordBridge: addsAtOnce(type("ford_bridge"), []), + fordPavement: addsAtOnce(type("ford_pavement"), []), + pavement: addsAtOnce({ type_id: "pavement_concrete", managed_by: null }, []), + }, + bridge: await rightClick("ford_bridge"), + paveRc: await rightClick("ford_pavement"), + pipe: await rightClick("pipe"), + notice: FORD_WAIT_NOTICE, + waits: { + bridgeEmpty: waitsFordHeight("ford_bridge", { ford_width_m: 10 }), + paveEmpty: waitsFordHeight("ford_pavement", undefined), + bridgeHand: waitsFordHeight("ford_bridge", { ford_height_m: 0.4 }), + pipe: waitsFordHeight("pipe", {}), + }, + noFlow, + withFlow, + hand, + other, + gone, + need: { w10: fordHeightM(flow, 10), w5: fordHeightM(flow, 5) }, + }; + process.stdout.write(JSON.stringify(out)); +})(); diff --git a/resources/tester/test_116_right_click_add.py b/resources/tester/test_116_right_click_add.py index efa21ac6d..c925d89b1 100644 --- a/resources/tester/test_116_right_click_add.py +++ b/resources/tester/test_116_right_click_add.py @@ -3,7 +3,7 @@ 못박는 것 - 필수 빈칸 없는 종류 = 그 측점에 바로 추가(캐시) + 새 항목을 고른 채(구조물 = 임시 id · A군 = 측점). - - 필수 빈칸 남는 종류 · 노면공(확인창) · 세월교 · 물넘이포장(월류 높이 = 유량 뒤) = 폼만. + - 필수 빈칸 남는 종류 · 노면공(확인창) = 폼만 · 세월교 · 물넘이포장은 바로 추가(126-1 · test_126). - [추가]가 거절하면 아무것도 안 듦 · 고르지 않음. - 새로 놓기에서 시작 — 옛 집계 항목 · 그룹 잇기 · 메모가 새 항목에 안 실림. - 세 길(B05 종단 · B06 종단 · 3D)이 모두 패널 `addAt` 한 곳 · 패널을 열고 「구조물 배치」를 폄. @@ -47,10 +47,10 @@ def test_필수_빈칸_없는_종류만_바로_추가(out): assert once["requiredEmpty"] is False -def test_노면공_세월교_물넘이포장은_폼만(out): +def test_노면공만_폼만_세월교_물넘이포장은_바로(out): once = out["once"] assert once["pavement"] is False - assert once["fordBridge"] is False and once["fordPavement"] is False + assert once["fordBridge"] is True and once["fordPavement"] is True # 126-1 def test_구조물은_목록에_들고_새_임시_id로_고른_채(out): diff --git a/resources/tester/test_126_ford_right_click.py b/resources/tester/test_126_ford_right_click.py new file mode 100644 index 000000000..6a5fb03a7 --- /dev/null +++ b/resources/tester/test_126_ford_right_click.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +"""126-1 우클릭 세월교 · 물넘이포장도 바로 추가 · 월류 높이 실시간. + +못박는 것 + - 세월교 · 물넘이포장 = 바로 추가 + 그 측점 관을 고른 채 · 알림 한 줄(높이는 유량 뒤). + - 배수유역 재계산 응답에 유량이 오면 기다리던 항목의 빈 월류 높이 = 필요 수심(서버 `ford_height_m` 거울). + - 유량이 아직 없으면 빈칸으로 다음 응답까지 · 손 값은 안 덮음 · 기다리지 않는 옛 항목은 그대로. + - 세부유역 요청은 최신 응답만 채택 · 채운 뒤 세션 초안에 다시 담음. + +Node 헬퍼 `helper_126_ford_fill.cjs` 가 판정 · 끝맺음 · 채움을 돌림. 화면은 사용자 확인. +""" + +import json +import os +import subprocess + +import pytest + +from B04_PreProcess import B04_PreProcess_Router_Basins as basins + +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 f: + return f.read() + + +@pytest.fixture(scope="module") +def out(): + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_126_ford_fill.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["once"] == {"fordBridge": True, "fordPavement": True, "pavement": False} + + +def test_추가_뒤_목록에_들고_그_관을_고른_채_알림_한_줄(out): + for key in ("bridge", "paveRc"): + r = out[key] + assert r["done"] is True and r["commits"] == 1 + assert r["picked"] == [["pipe", 120]] + assert r["toasts"] == [out["notice"]] + assert out["pipe"]["toasts"] == [] # 배관은 알림 없음 + + +def test_빈_높이만_기다림(out): + assert out["waits"] == { + "bridgeEmpty": True, + "paveEmpty": True, + "bridgeHand": False, + "pipe": False, + } + + +def test_유량_오기_전엔_빈칸_오면_그_항목에_채움(out): + assert out["noFlow"] == {"filled": False, "heights": [None], "pending": [120]} + assert out["withFlow"] == {"filled": True, "heights": [0.16], "pending": []} + + +def test_채운_높이는_서버_풀이와_같음(out): + assert out["need"]["w10"] == basins.ford_height_m(8.0, 10) == 0.16 + assert out["need"]["w5"] == basins.ford_height_m(8.0, 5) + + +def test_손_값은_안_덮음(out): + assert out["hand"] == {"filled": False, "heights": [0.5], "pending": []} + + +def test_기다리지_않는_옛_항목은_그대로_폭_없으면_기본_폭(out): + assert out["other"]["heights"] == [0.16, None] # 물넘이 기본 폭 10 · 옛 세월교 빈칸 그대로 + assert out["other"]["pending"] == [] + + +def test_관이_사라지거나_종류가_바뀌면_그만_기다림(out): + assert out["gone"] == {"filled": False, "heights": [None], "pending": []} + + +def test_배수유역_패널_배선(): + src = _read("B05_Profile", "B05_Profile_UI_Drainage_Panel.ts") + # 최신 응답만 채택 + run = src[src.index("async function run(") : src.index("/** 지금 관 목록")] + assert "const seq = ++requestSeq;" in run + assert run.index("await request()") < run.index("if (seq !== requestSeq) return;") + assert run.index("if (seq !== requestSeq) return;") < run.index("apply(response)") + # 채움은 보관소가 응답을 받기 전에 · 채웠으면 세션 초안에 다시 담음 + apply = src[src.index("function apply(") : src.index("async function run(")] + order = [ + apply.index("fillFordHeights(response.pipe_points, response.basins, fordWaits)"), + apply.index("facilityStore.replaceFromResponse(response.pipe_points)"), + apply.index("if (fordFilled) rememberPipes()"), + apply.index("callbacks.onPipesChanged?.("), + ] + assert order == sorted(order) + add = src[src.index(" addPipe(chainageM, attributes) {") : src.index(" removePipe(")] + assert "waitsFordHeight(attributes?.facility, attributes?.options)" in add + assert add.index("fordWaits.add(chainageM)") < add.index("pipeEditor.addAtChainage(chainageM)") + assert "if (fordWaits.delete(fromChainageM)) fordWaits.add(toChainageM);" in add