Merge remote-tracking branches 'origin/main_laptop_1' and 'origin/sub_laptop_5' into sub_laptop_6
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -19,6 +19,14 @@ const FORD_KINDS: Record<string, number> = {
|
||||
ford_pavement: FORD_PAVEMENT_DEFAULT_WIDTH_M,
|
||||
};
|
||||
|
||||
/** 프로젝트마다 높이를 기다리는 측점 — 배수유역도 판 · B06 [추가] 가 같은 표를 씀(298-1). */
|
||||
const waits = new Map<string, Set<number>>();
|
||||
export function fordWaitsOf(projectId: string | null): Set<number> {
|
||||
const key = projectId ?? "";
|
||||
if (!waits.has(key)) waits.set(key, new Set());
|
||||
return waits.get(key) as Set<number>;
|
||||
}
|
||||
|
||||
/** 새로 놓으면 높이를 기다리는 종류 — 높이 칸이 빈 세월교 · 물넘이포장. */
|
||||
export function waitsFordHeight(
|
||||
facility: string | undefined,
|
||||
|
||||
@@ -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<number>();
|
||||
/** 바로 놓은 세월교 · 물넘이포장 — 재계산 유량이 오면 빈 월류 높이를 채울 측점(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가 새 위치로 시설 정보를 승계한다.
|
||||
|
||||
@@ -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<DetailBasin>, chainageM: number): string {
|
||||
const basin = basins.find((item) => Math.abs(item.chainage_m - chainageM) < PIPE_MATCH_M);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: [] }),
|
||||
},
|
||||
|
||||
@@ -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: [] }),
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -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줄_안():
|
||||
|
||||
@@ -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<number>()" 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
|
||||
Reference in New Issue
Block a user