From 8ce01dfa35b9ad0ce23a3e203b837ef4c7573dd3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 12 Oct 2026 22:09:00 +0900 Subject: [PATCH] =?UTF-8?q?fix(b06):=20=EC=8B=A0=EA=B7=9C=20=EA=B4=80=20?= =?UTF-8?q?=EA=B3=84=EC=97=B4=20[=EC=B6=94=EA=B0=80]=20=E2=80=94=20B06=20?= =?UTF-8?q?=EA=B8=B8=EB=8F=84=20=EB=B9=88=20=EC=9B=94=EB=A5=98=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=20=EC=B1=84=EC=9B=80=20=C2=B7=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B8=80=20=E3=80=8C=EB=92=A4=EC=97=90=EC=84=9C=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EB=82=98=EB=88=94=E3=80=8D=20=C2=B7=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EB=82=98=EB=88=94=20=EC=8B=A4=ED=8C=A8=20=ED=86=A0?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20(298-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - B05 · B06 [추가] = 그 측점 횡단(카드 · 측점선) 바로 + 세부유역 뒤에서 다시 나눔(세션 초안 · 저장 안 함 · 최신 응답만 · 지도 칸 표시만) — 있던 길 그대로 - 까닭 1: B06 [추가]는 높이 기다림을 판에 안 알려 새 세월교 · 물넘이 월류 높이(126-1)가 빈칸 — 기다림 표를 프로젝트마다 한 벌(fordWaitsOf)로 판 · B06 이 같이 씀 · 채운 높이를 B06 목록 · 폼이 받음(adoptFordHeights) - 까닭 2: B06 알림 글이 「세부 배수유역은 [저장]·[확정] 뒤 계산」 — 실제는 뒤에서 바로 다시 나눔 · 넣기 · 빼기 · 옮기기 글 고침 - 다시 나눔 실패 = 지도 칸 문구 + 토스트 한 줄(analyzeFailed) - 시험: test_298_add_reanalyze 새로 · test_126 · test_291_1 · helper_165 · helper_231_1 맞춤 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn --- A00_Common/design/design_view_cross.ts | 2 +- .../B05_Profile_UI_Drainage_Ford_Fill.ts | 8 + B05_Profile/B05_Profile_UI_Drainage_Panel.ts | 16 +- B05_Profile/B05_Profile_UI_Drainage_Parts.ts | 8 + .../B06_Section_UI_Page_Structures_Panel.ts | 34 +++- .../B06_Section_UI_Section_View_Profile.ts | 9 +- resources/tester/helper_165_pavement_flow.cjs | 4 + resources/tester/helper_231_1_pave_reload.cjs | 4 + resources/tester/helper_298_add_reanalyze.cjs | 88 +++++++++ resources/tester/test_126_ford_right_click.py | 8 +- .../tester/test_291_1_add_only_on_add.py | 2 +- resources/tester/test_298_add_reanalyze.py | 167 ++++++++++++++++++ 12 files changed, 329 insertions(+), 21 deletions(-) create mode 100644 resources/tester/helper_298_add_reanalyze.cjs create mode 100644 resources/tester/test_298_add_reanalyze.py diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index f0c61f60d..392fe54b4 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -351,7 +351,7 @@ export async function mountCrossView( }, selectStructure: (structureId) => structuresPanel.selectStructure(structureId), selectPipe: (chainageM) => structuresPanel.selectPipeAt(chainageM), // 측점 없는 관(요구 178) - pipesChanged: () => void refreshDetailForStructures(), + pipesChanged: () => (structuresPanel.adoptFordHeights(), void refreshDetailForStructures()), }); // 폼 → 횡단 캐시 반영은 따로 뗀 모듈이 맡는다(2026-09-02 분리). const pipeOptionsContext: PipeOptionsContext = { diff --git a/B05_Profile/B05_Profile_UI_Drainage_Ford_Fill.ts b/B05_Profile/B05_Profile_UI_Drainage_Ford_Fill.ts index f412bcb3a..5a0b67e48 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Ford_Fill.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Ford_Fill.ts @@ -19,6 +19,14 @@ const FORD_KINDS: Record = { ford_pavement: FORD_PAVEMENT_DEFAULT_WIDTH_M, }; +/** 프로젝트마다 높이를 기다리는 측점 — 배수유역도 판 · B06 [추가] 가 같은 표를 씀(298-1). */ +const waits = new Map>(); +export function fordWaitsOf(projectId: string | null): Set { + const key = projectId ?? ""; + if (!waits.has(key)) waits.set(key, new Set()); + return waits.get(key) as Set; +} + /** 새로 놓으면 높이를 기다리는 종류 — 높이 칸이 빈 세월교 · 물넘이포장. */ export function waitsFordHeight( facility: string | undefined, diff --git a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts index 856a97175..532bbf907 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts @@ -36,7 +36,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 { fillFordHeights, fordWaitsOf, 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"; @@ -47,6 +47,7 @@ import { DRAINAGE_LAYERS, fetchDrainageLayers, createProgressReporter, + analyzeFailed, fitCanvasToViewport, fitViewToRoute, observeViewportSize, @@ -180,8 +181,8 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra let frameView: Window = window; let loadSequence = 0; let requestSeq = 0; // 세부유역 요청 — 최신 응답만 채택 - /** 우클릭으로 바로 놓은 세월교 · 물넘이포장 — 재계산 유량이 오면 빈 월류 높이를 채울 측점(126-1). */ - const fordWaits = new Set(); + /** 바로 놓은 세월교 · 물넘이포장 — 재계산 유량이 오면 빈 월류 높이를 채울 측점(126-1 · B06 [추가]도 298-1). */ + const fordWaits = () => fordWaitsOf(projectId); let canvasWidth = 0; let canvasHeight = 0; let canvasDpr = 0; @@ -358,7 +359,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra })), ); // 시설 확장 정보(종류·구간·옵션)는 응답이 정본 — 통째로 다시 채운다(기다리던 월류 높이를 먼저 얹음). - const fordFilled = fillFordHeights(response.pipe_points, response.basins, fordWaits); + const fordFilled = fillFordHeights(response.pipe_points, response.basins, fordWaits()); facilityStore.replaceFromResponse(response.pipe_points); if (fordFilled) rememberPipes(); // 재계산 뒤에는 선택을 다시 알려야 한다 — 관을 새로 넣은 직후에는 목록이 아직 @@ -391,8 +392,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra } catch (error) { if (seq !== requestSeq) return; status.hidden = false; - status.textContent = - error instanceof Error ? error.message : L("B05_Drainage_Status_AnalyzeFailed"); + status.textContent = analyzeFailed(error, L("B05_Drainage_Status_AnalyzeFailed")); } finally { if (seq === requestSeq) { analyzeButton.disabled = false; @@ -619,7 +619,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra chainageM = snapStructureStation(chainageM); // 관 기준 측점 = 정수 m(234-1) // 시설 정보를 먼저 보관해야 addAtChainage가 촉발하는 재계산 요청에 실려 간다. facilityStore.set(chainageM, attributes ?? null); - if (waitsFordHeight(attributes?.facility, attributes?.options)) fordWaits.add(chainageM); + if (waitsFordHeight(attributes?.facility, attributes?.options)) fordWaits().add(chainageM); pipeEditor.addAtChainage(chainageM); }, updatePipeFacility(fromChainageM, toChainageM, attributes) { @@ -630,7 +630,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 (fordWaits().delete(fromChainageM)) fordWaits().add(toChainageM); if (Math.abs(fromChainageM - toChainageM) > 0.005) { // 기준점이 옮겨졌다 — 관을 이동시키면 onCommit이 재계산을 돌리고, // attach가 새 위치로 시설 정보를 승계한다. diff --git a/B05_Profile/B05_Profile_UI_Drainage_Parts.ts b/B05_Profile/B05_Profile_UI_Drainage_Parts.ts index 4875593c6..868e450c5 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Parts.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Parts.ts @@ -9,6 +9,7 @@ import { stateKey } from "../A00_Common/b_page_state"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; import { themeColor } from "@ui/ui_template_palette"; +import { showToast } from "@ui/ui_template_elements"; import { DRAINAGE_SHEET_LAYERS, fetchCachedSheetLayer } from "../A00_Common/b_asset_cache"; import { fetchVWorldMeta, type VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { @@ -515,6 +516,13 @@ export function createProgressReporter(progress: { }; } +/** 세부유역 요청 실패 — 지도 칸 문구를 돌려주고 토스트 한 줄(뒤에서 다시 나눔이 조용히 안 끝나게 · 298-1). */ +export function analyzeFailed(error: unknown, fallback: string): string { + const text = error instanceof Error ? error.message : fallback; + showToast(text === fallback ? text : `${fallback} ${text}`, "error"); + return text; +} + /** 배관 마커 색 — 같은 누가거리 유역의 파스텔색(불투명). 유역이 없으면 회색. */ export function pipeMarkerColor(basins: ReadonlyArray, chainageM: number): string { const basin = basins.find((item) => Math.abs(item.chainage_m - chainageM) < PIPE_MATCH_M); diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 622aac49f..2b18e04d5 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -30,6 +30,7 @@ import { } from "../B05_Profile/B05_Profile_Api_Structures"; import { readPendingPipes, writePendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft"; import { mergeFacilityOptions } from "../B05_Profile/B05_Profile_UI_Drainage_Facility_Merge"; +import { fordWaitsOf, waitsFordHeight } from "../B05_Profile/B05_Profile_UI_Drainage_Ford_Fill"; import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { linkFields, @@ -61,15 +62,14 @@ import { withSpecDefaults } from "./B06_Section_UI_Page_Structures_Spec"; const PIPE_ADD_GUIDE = "계곡 통과 시설을 넣고 빼려면 프로젝트를 먼저 여세요."; const PIPE_MOVE_GUIDE = "기준 측점 이동은 배수유역을 다시 나눠야 해 B05(종단) 화면에서 합니다."; -/** B05·B06 은 한 페이지라 넣고 빼는 자리도 같아야 한다(2026-09-12 사용자). 재분할은 저장 뒤. */ +/** B05·B06 은 한 페이지라 넣고 빼는 자리도 같아야 한다(2026-09-12 사용자). 재분할은 뒤에서 바로(298-1). */ const PIPE_ADD_NOTICE = - "계곡 통과 시설을 넣었습니다 — 횡단도는 바로 보이고, 세부 배수유역은 [저장]·[확정] 뒤에 다시 계산됩니다."; -const PIPE_REMOVE_NOTICE = - "계곡 통과 시설을 뺐습니다 — 세부 배수유역은 [저장]·[확정] 뒤에 다시 계산됩니다."; + "계곡 통과 시설을 넣었습니다 — 횡단도는 바로 보이고, 세부 배수유역은 뒤에서 다시 나눕니다."; +const PIPE_REMOVE_NOTICE = "계곡 통과 시설을 뺐습니다 — 세부 배수유역은 뒤에서 다시 나눕니다."; /** 고른 것 없이 폼만 만졌을 때 — 값이 어디로도 가지 않으므로 이유를 알린다. */ const PIPE_PICK_GUIDE = "먼저 횡단도나 목록에서 구조물을 고르세요 — 고른 것에만 값이 반영됩니다."; const PIPE_MOVE_NOTICE = - "기준 측점을 옮겼습니다 — 횡단도는 바로 보이고, 세부 배수유역은 [저장]·[확정] 뒤에 다시 계산됩니다."; + "기준 측점을 옮겼습니다 — 횡단도는 바로 보이고, 세부 배수유역은 뒤에서 다시 나눕니다."; export interface B06StructuresPanelDeps { projectId: string | null; @@ -132,6 +132,8 @@ export interface B06StructuresPanel { selectStructure: (structureId: string) => void; /** 측점 없는 관(물넘이포장) 하나를 폼에 올림 — 카드 안 고름 · 스크롤만 · 알약 · 고름 값에 알약 id(요구 178). */ selectPipeAt: (chainageM: number) => void; + /** 배수유역도가 관 초안에 채운 빈 월류 높이(126-1)를 목록 · 폼에 받는다(298-1). */ + adoptFordHeights: () => void; /** 지금 목록의 구조물 정본(미저장분 포함) — 카드 찾기에 쓴다. */ getStructures: () => StructureInstance[]; /** 지금 폼에 올라온 계곡 통과 시설의 누가거리(없으면 null). */ @@ -259,6 +261,9 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc }); pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m); currentChainageM = at; + // 빈 월류 높이 = 다시 나눈 유량으로 채움(126-1) — 배수유역도 판이 이 표를 보고 채움(298-1) + if (waitsFordHeight(attributes.facility, attributes.options)) + fordWaitsOf(deps.projectId).add(at); persistPipes(); section.setPipeFacilities(pipeFacilities); pushMarks(); @@ -266,7 +271,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc showToast(PIPE_ADD_NOTICE, "success"); }, // 값 수정·기준점 이동 모두 B06 에서 받는다 — 이동은 종단 끌기와 **같은 함수**를 탄다 - // (2026-09-12 일원화). 재분할은 [저장]·[확정] 뒤 서버 몫이다. + // (2026-09-12 일원화). 세부유역은 관 초안을 쓰면 배수유역도가 뒤에서 다시 나눈다(298-1). onPipeUpdate: (fromChainageM, toChainageM, attributes) => { // 값만 고치면 폼이 옛 측점을 그대로 돌려줌(안 고친 칸) — 0.005m 넘으면 옮긴 것(80.17 → 80 도 · 234-1) if (Math.abs(fromChainageM - toChainageM) > 0.005) movePipeTo(fromChainageM, toChainageM); @@ -548,6 +553,23 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc section.selectPipeByChainage(hit.chainage_m); }, getStructures: () => structures, + adoptFordHeights: () => { + // 배수유역도가 초안에 채운 월류 높이(126-1)를 목록 · 폼에 받음 — 빈 칸만(298-1) + const draft = deps.projectId ? readPendingPipes(deps.projectId) : null; + const filled = pipeFacilities.filter((entry) => { + if (entry.options?.ford_height_m !== undefined) return false; + const height = draft?.find( + (pipe) => Math.abs(pipe.chainage_m - entry.chainage_m) < PIPE_MATCH_M, + )?.options?.ford_height_m; + if (height === undefined) return false; + entry.options = { ...entry.options, ford_height_m: height }; + return true; + }); + if (!filled.length) return; + section.setPipeFacilities(pipeFacilities); + const open = filled.find((entry) => entry.chainage_m === currentChainageM); + if (open) section.selectPipeByChainage(open.chainage_m); + }, releasePipe: () => { currentChainageM = null; section.selectPipeByChainage(null); diff --git a/B06_Section/B06_Section_UI_Section_View_Profile.ts b/B06_Section/B06_Section_UI_Section_View_Profile.ts index 817c0fed9..ccf3c0b14 100644 --- a/B06_Section/B06_Section_UI_Section_View_Profile.ts +++ b/B06_Section/B06_Section_UI_Section_View_Profile.ts @@ -220,9 +220,14 @@ export function createSectionProfile( onPipesChanged: (pipes) => { if (shared.view() === "cross" && shared.profile?.side) paintMapPick(shared.profile.side, readSelection(shared.projectId)); - // 관 종류 · 관경이 바뀜(새 관 관경은 배수 재계산이 정함) — 카드 관 그림도 다시(273-2) + // 관 종류 · 관경 · 채운 월류 높이가 바뀜(새 관 관경 · 빈 높이는 배수 재계산이 정함) — 카드 관 그림도 다시(273-2 · 298-1) const signature = JSON.stringify( - pipes.map((p) => [p.chainage_m, p.facility, p.options?.pipe_diameter_mm ?? null]), + pipes.map((p) => [ + p.chainage_m, + p.facility, + p.options?.pipe_diameter_mm ?? null, + p.options?.ford_height_m ?? null, + ]), ); const changed = pipeSignature !== null && signature !== pipeSignature; pipeSignature = signature; diff --git a/resources/tester/helper_165_pavement_flow.cjs b/resources/tester/helper_165_pavement_flow.cjs index c914d345f..9da73a054 100644 --- a/resources/tester/helper_165_pavement_flow.cjs +++ b/resources/tester/helper_165_pavement_flow.cjs @@ -137,6 +137,10 @@ const panelModule = loadTs( writePendingPipes() {}, }, "../B05_Profile/B05_Profile_UI_Drainage_Facility_Merge": { mergeFacilityOptions: (x) => x }, + "../B05_Profile/B05_Profile_UI_Drainage_Ford_Fill": { + fordWaitsOf: () => new Set(), + waitsFordHeight: () => false, + }, "../B04_PreProcess/B04_PreProcess_Api_Fetch": { fetchDetailPipePoints: async () => ({ pipe_points: PIPES, basins: [] }), }, diff --git a/resources/tester/helper_231_1_pave_reload.cjs b/resources/tester/helper_231_1_pave_reload.cjs index 96625385b..749a362d8 100644 --- a/resources/tester/helper_231_1_pave_reload.cjs +++ b/resources/tester/helper_231_1_pave_reload.cjs @@ -156,6 +156,10 @@ const panelModule = loadTs( writePendingPipes() {}, }, "../B05_Profile/B05_Profile_UI_Drainage_Facility_Merge": { mergeFacilityOptions: (x) => x }, + "../B05_Profile/B05_Profile_UI_Drainage_Ford_Fill": { + fordWaitsOf: () => new Set(), + waitsFordHeight: () => false, + }, "../B04_PreProcess/B04_PreProcess_Api_Fetch": { fetchDetailPipePoints: async () => ({ pipe_points: PIPES, basins: [] }), }, diff --git a/resources/tester/helper_298_add_reanalyze.cjs b/resources/tester/helper_298_add_reanalyze.cjs new file mode 100644 index 000000000..8edfbfa25 --- /dev/null +++ b/resources/tester/helper_298_add_reanalyze.cjs @@ -0,0 +1,88 @@ +/* PLAN 298-1 헬퍼 — B06 [추가]가 적은 「높이 기다림」을 배수유역도 판이 같은 표(`fordWaitsOf`)로 보고 + * 다시 나눈 유량으로 빈 월류 높이를 채움(126-1) · 프로젝트마다 따로 · 실패 = 토스트 한 줄(`analyzeFailed`). + * TS 로더는 `helper_126_ford_fill.cjs` 와 같다 · 결과 = 표준출력 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 cache = new Map(); +const toasts = []; +const STUBS = { + "@ui/ui_template_elements": { showToast: (message, kind) => toasts.push([message, kind]) }, +}; + +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 fill = 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 flow = 8.0; +const points = () => [ + { chainage_m: 185, facility: "ford_bridge", options: { ford_width_m: 10 } }, + { chainage_m: 300, facility: "ford_pavement", options: {} }, +]; +const basins = [ + { chainage_m: 185, design_flow_m3s: flow }, + { chainage_m: 300, design_flow_m3s: flow }, +]; + +// ① B06 [추가] — 그 프로젝트 표에 측점을 적음(판과 같은 표) +const b06Wants = fill.waitsFordHeight("ford_bridge", { ford_width_m: 10 }); +if (b06Wants) fill.fordWaitsOf("p1").add(185); +const same = fill.fordWaitsOf("p1") === fill.fordWaitsOf("p1"); +// ② 판의 다시 나눔 응답 — 같은 표로 채움 · 기다리지 않던 300 은 빈칸 그대로 +const p1 = points(); +const filled = fill.fillFordHeights(p1, basins, fill.fordWaitsOf("p1")); +// ③ 다른 프로젝트 같은 측점은 안 채움 +const p2 = points(); +const otherFilled = fill.fillFordHeights(p2, basins, fill.fordWaitsOf("p2")); + +let failed = null; +try { + const parts = loadTs(path.join(B05, "B05_Profile_UI_Drainage_Parts.ts")); + failed = { + status: parts.analyzeFailed(new Error("502 Bad Gateway"), "세부유역 산정에 실패했습니다."), + plain: parts.analyzeFailed("x", "세부유역 산정에 실패했습니다."), + toasts: [...toasts], + }; +} catch (error) { + failed = { error: String(error) }; +} + +process.stdout.write( + JSON.stringify({ + b06Wants, + same, + filled, + heights: p1.map((p) => p.options.ford_height_m ?? null), + left: [...fill.fordWaitsOf("p1")], + otherFilled, + otherHeights: p2.map((p) => p.options.ford_height_m ?? null), + need: fordHeightM(flow, 10), + failed, + }), +); diff --git a/resources/tester/test_126_ford_right_click.py b/resources/tester/test_126_ford_right_click.py index 6a5fb03a7..cdfff91e9 100644 --- a/resources/tester/test_126_ford_right_click.py +++ b/resources/tester/test_126_ford_right_click.py @@ -95,7 +95,7 @@ def test_배수유역_패널_배선(): # 채움은 보관소가 응답을 받기 전에 · 채웠으면 세션 초안에 다시 담음 apply = src[src.index("function apply(") : src.index("async function run(")] order = [ - apply.index("fillFordHeights(response.pipe_points, response.basins, fordWaits)"), + 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?.("), @@ -103,5 +103,7 @@ def test_배수유역_패널_배선(): 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 + assert add.index("fordWaits().add(chainageM)") < add.index( + "pipeEditor.addAtChainage(chainageM)" + ) + assert "if (fordWaits().delete(fromChainageM)) fordWaits().add(toChainageM);" in add diff --git a/resources/tester/test_291_1_add_only_on_add.py b/resources/tester/test_291_1_add_only_on_add.py index b9b8dd4b4..7510271b5 100644 --- a/resources/tester/test_291_1_add_only_on_add.py +++ b/resources/tester/test_291_1_add_only_on_add.py @@ -61,7 +61,7 @@ def test_고르던_항목만_실시간_반영(): def test_새_세월교_물넘이_월류_높이는_추가_뒤_채움(): drainage = _read("B05_Profile_UI_Drainage_Panel.ts") body = _body(drainage, " addPipe(chainageM, attributes) {", " updatePipeFacility(") - assert "fordWaits.add(chainageM)" in body + assert "fordWaits().add(chainageM)" in body def test_패널_700줄_안(): diff --git a/resources/tester/test_298_add_reanalyze.py b/resources/tester/test_298_add_reanalyze.py new file mode 100644 index 000000000..0429a2dd3 --- /dev/null +++ b/resources/tester/test_298_add_reanalyze.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +"""PLAN 298-1 — 신규 관 계열(배관 · 박스암거 · 세월교 · 물넘이포장) [추가] = 횡단도 생김 + 세부유역 뒤에서 다시 나눔. + +B05 [추가] = 배수유역도 판 `addPipe` → 다시 나눔(이미 있던 길) · B06 [추가] = 관 초안 → 판 `reload`(같은 길). +고친 것 — B06 길도 빈 월류 높이를 채움(126-1 · 판과 같은 기다림 표 `fordWaitsOf`) · 채운 높이를 B06 목록 · 폼이 +받음 · B06 알림 글이 「저장 뒤 계산」이라 틀리던 것 · 다시 나눔 실패 = 토스트 한 줄. +Node(`helper_298_add_reanalyze.cjs`) + 배선 대조. +""" + +import json +import subprocess +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] + + +def _read(*parts: str) -> str: + return ROOT.joinpath(*parts).read_text(encoding="utf-8") + + +def _body(src: str, start: str, end: str) -> str: + at = src.index(start) + return src[at : src.index(end, at)] + + +@pytest.fixture(scope="module") +def out(): + proc = subprocess.run( + ["node", str(HERE / "helper_298_add_reanalyze.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +PANEL = _read("B05_Profile", "B05_Profile_UI_Drainage_Panel.ts") +B06 = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts") +CROSS = _read("A00_Common", "design", "design_view_cross.ts") + + +# ── 126-1 채움 — B06 [추가]도 판과 같은 표 ────────────────────────────────── + + +def test_B06_추가가_적은_기다림을_판이_채움(out): + assert out["b06Wants"] is True and out["same"] is True + assert out["filled"] is True + assert out["heights"] == [out["need"], None] # 기다리던 측점만 · 옛 빈칸은 그대로 + assert out["left"] == [] + + +def test_다른_프로젝트_같은_측점은_안_채움(out): + assert out["otherFilled"] is False + assert out["otherHeights"] == [None, None] + + +def test_B06_추가_순간_기다림_적고_초안_씀(): + body = _body(B06, " onPipeAdd: (chainageM, attributes) => {", " onPipeUpdate:") + order = [ + body.index("pipeFacilities.push({"), + body.index("if (waitsFordHeight(attributes.facility, attributes.options))"), + body.index("fordWaitsOf(deps.projectId).add(at);"), + body.index("persistPipes();"), + body.index("deps.onStructuresChanged?.();"), + ] + assert order == sorted(order) + + +def test_판은_같은_표로_채움(): + assert "const fordWaits = () => fordWaitsOf(projectId);" in PANEL + assert "new Set()" not in _body(PANEL, "let requestSeq = 0;", "let canvasWidth") + + +def test_채운_높이를_B06_목록_폼이_받음(): + adopt = _body(B06, " adoptFordHeights: () => {", " releasePipe:") + assert "readPendingPipes(deps.projectId)" in adopt + assert "if (entry.options?.ford_height_m !== undefined) return false;" in adopt # 빈 칸만 + assert "section.setPipeFacilities(pipeFacilities);" in adopt + assert "persistPipes" not in adopt # 받기만 — 초안을 다시 안 씀(되돌이 없음) + assert ( + "pipesChanged: () => (structuresPanel.adoptFordHeights(), void refreshDetailForStructures())," + in CROSS + ) + view = _read("B06_Section", "B06_Section_UI_Section_View_Profile.ts") + sig = _body(view, "const signature = JSON.stringify(", "const changed") + assert "p.options?.ford_height_m ?? null" in sig # 늦게 온 유량으로 채운 높이도 알림 + + +# ── 다시 나눔 = 요청 한 번 · 저장 안 함 · 최신 응답만 · 잠금 없음 ──────────── + + +def test_B06_초안_쓰면_판이_한_번_다시_나눔(): + written = _body(CROSS, " onPipesWritten: () => {", " detail: () => sectionDetail,") + assert "window.clearTimeout(pipeReloadTimer);" in written # 잇단 조작 = 한 번 + assert "shared.profile?.side?.reload()" in written + persist = _body(B06, " function persistPipes(): void {", " /** 관 옮기기") + assert persist.index("writePendingPipes(") < persist.index("deps.onPipesWritten?.();") + + +def test_다시_나눔은_세션_초안으로_계산만_저장_안_함(): + load = _body(PANEL, "const loadSaved = ", "const resetPipes") + assert "readPendingPipes(projectId as string)" in load + assert "computeDetailBasins(projectId as string, pending)" in load + analyze = _body(PANEL, "const analyze = ", "analyzeButton.addEventListener") + assert "computeDetailBasins(" in analyze + run = _body(PANEL, "async function run(", "/** 지금 관 목록") + assert "saveDetailPipePoints" not in run + load + analyze + # B05 [추가] = 판 addPipe → 배치 바뀜 → 초안 + 다시 나눔 + commit = _body(PANEL, "const pipeEditor = createPipeEditor(", "(fromM, toM) =>") + assert commit.index("rememberPipes();") < commit.index("void analyze();") + + +def test_최신_응답만_잠금_없이_지도_칸_표시(): + run = _body(PANEL, "async function run(", "/** 지금 관 목록") + assert "const seq = ++requestSeq;" in run + assert run.index("if (seq !== requestSeq) return;") < run.index("apply(response)") + # 지도 칸 안 작은 표시(문구 · 지도 가운데 서클)만 — 화면 전체 잠금 없음 + assert 'status.textContent = L("B05_Drainage_Status_Analyzing");' in run + assert 'showProgress(null, L("B05_Drainage_Status_Analyzing"));' in run + assert "Overlay" not in run and "lock" not in run.lower() + chrome = _read("B05_Profile", "B05_Profile_UI_Drainage_Chrome.ts") + assert "viewport.append(" in chrome and "progress.root," in chrome + + +def test_실패하면_토스트_한_줄(out): + run = _body(PANEL, "async function run(", "/** 지금 관 목록") + assert ( + 'status.textContent = analyzeFailed(error, L("B05_Drainage_Status_AnalyzeFailed"));' in run + ) + failed = out["failed"] + assert failed["status"] == "502 Bad Gateway" + assert failed["toasts"] == [ + ["세부유역 산정에 실패했습니다. 502 Bad Gateway", "error"], + ["세부유역 산정에 실패했습니다.", "error"], + ] + + +# ── 횡단도 생김 · 알림 글 ──────────────────────────────────────────────── + + +def test_추가_뒤_새_측점_횡단_미리_뜸(): + body = _body(B06, " onPipeAdd: (chainageM, attributes) => {", " onPipeUpdate:") + assert "deps.onStructuresChanged?.();" in body + assert "onStructuresChanged: () => void refreshDetailForStructures()," in CROSS + drafts = _read("B06_Section", "B06_Section_Section_Drafts.ts") + stations = _body( + drafts, "export async function withDraftStations(", "parked.set(detail, stale);" + ) + assert "readPendingPipes(projectId)" in stations + assert "previewOf(projectId, routeId, missing)" in stations + + +def test_B06_알림_글_뒤에서_다시_나눔(): + for name in ("PIPE_ADD_NOTICE", "PIPE_REMOVE_NOTICE", "PIPE_MOVE_NOTICE"): + line = _body(B06, f"const {name} =", ";\n") + assert "세부 배수유역은 뒤에서 다시 나눕니다." in line, name + assert "[저장]·[확정] 뒤에" not in line, name + + +def test_줄_수(): + assert len(PANEL.splitlines()) <= 700 + assert len(CROSS.splitlines()) <= 685