fix(b05·b06): 저장 전 구조물 자리 값 다시 셈 — 물넘이포장 · 맹암거 측구 없음 · 종류 바꿈 · 기준측점 옮김 (314 · 요구 210 · 211)
- 낡음 판정(B05 · B06 공용 isStaleSection)에 서버 _enforce_stored_designs 짝 셋: 포장 · 기울기(물넘이 · 맹암거) · 세월교 노면 하강 - 구조물 바뀐 뒤 횡단 다시 받으면 낡은 측점 다시 셈(refreshDetailForStructures) - 다시 셈의 세월교 노면 하강 = 그 측점에 얹힌 세월교 제원(옛 저장값 안 이음) - 물넘이포장: 옮기거나 지운 옛 자리 걷고 새 자리 · 걸친 기존 측점에 얹음(빈 관 목록도) - 월류 높이 기다림: 빈 글자도 빈칸(채우는 쪽과 같은 규칙) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C6BNBFXFndbqYxu6sZuknK
This commit is contained in:
@@ -235,6 +235,7 @@ export async function mountCrossView(
|
||||
);
|
||||
}
|
||||
renderSectionDetail();
|
||||
void reconcileStaleDesigns(); // 구조물 제원 ↔ 저장분 값 다른 측점 다시 셈(측구 · 포장 · 노면 하강 · 314)
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||
showToast(`구조물을 횡단에 반영하지 못했습니다.${detail}`, "error");
|
||||
@@ -562,7 +563,7 @@ export async function mountCrossView(
|
||||
// 종단 보기에서 고름을 풀었으면 횡단 강조도 풂(54-38)
|
||||
if (!readStructurePick(projectId)) releaseCross();
|
||||
void structuresPanel.load().then(restoreStructurePick);
|
||||
void refreshDetailForStructures().then(() => reconcileStaleDesigns());
|
||||
void refreshDetailForStructures(); // 안에서 낡은 설계도 맞춤(314)
|
||||
});
|
||||
host.onDispose(() => {
|
||||
window.clearTimeout(pipeReloadTimer);
|
||||
|
||||
@@ -32,7 +32,9 @@ export function waitsFordHeight(
|
||||
facility: string | undefined,
|
||||
options?: Record<string, string | number>,
|
||||
): boolean {
|
||||
return !!facility && facility in FORD_KINDS && options?.ford_height_m === undefined;
|
||||
// 빈 글자도 빈칸 — 채우는 쪽(`fillFordHeights`)과 같은 규칙(314)
|
||||
const height = options?.ford_height_m;
|
||||
return !!facility && facility in FORD_KINDS && (height === undefined || height === "");
|
||||
}
|
||||
|
||||
/** 기다리는 측점의 빈 높이를 채움 — 채운(또는 사용자 값이라 그만 기다릴) 측점은 `pending` 에서 뺌.
|
||||
|
||||
@@ -377,8 +377,9 @@ function refreshLocally(input: CrossRefreshInput): number[] | null {
|
||||
? design.ditch_choice
|
||||
: null,
|
||||
ditchEnabled: typeof design.ditch_enabled === "boolean" ? design.ditch_enabled : null,
|
||||
// 세월교 월류 하강은 계획선 편집으로 바뀌지 않는다 — 저장분 값을 그대로 잇는다.
|
||||
surfaceDropM: typeof design.surface_drop_m === "number" ? design.surface_drop_m : 0,
|
||||
// 세월교 월류 하강 = 그 측점에 얹힌 세월교 제원(초안 · 저장분 같은 자리 · 서버 `ford_drop_at` 짝 · 314)
|
||||
surfaceDropM:
|
||||
Number(section.ford?.overflow_depth_m) > 0 ? Number(section.ford?.overflow_depth_m) : 0,
|
||||
// 곡선부 확폭 입력 — 측점 기록에 실려 온다(서버 엔진과 같은 값, 2026-09-06).
|
||||
planRadiusM: section.plan_radius_m ?? null,
|
||||
curveOuterSide:
|
||||
|
||||
@@ -132,10 +132,17 @@ const facilityKey = (facility: string): (typeof FACILITY_KEYS)[number] =>
|
||||
*/
|
||||
async function withDraftPipeKinds(detail: SectionDetailResponse, projectId: string): Promise<void> {
|
||||
const pipes = readPendingPipes(projectId);
|
||||
if (!pipes?.length) return;
|
||||
if (!pipes) return;
|
||||
const sections = detail.cross_sections as unknown as Array<Record<string, unknown>>;
|
||||
const near = (spec: Record<string, unknown>, section: Record<string, unknown>, at: number) =>
|
||||
Math.abs(Number(spec.chainage_m ?? section.chainage_m) - at) < PIPE_MATCH_M;
|
||||
// 물넘이포장(구간형 · 걸친 측점마다 얹힘) — 옮기거나 지운 옛 자리는 걷음 · 새 자리는 아래에서 얹음(314)
|
||||
for (const section of sections) {
|
||||
const spec = section.ford_pavement as Record<string, unknown> | undefined;
|
||||
const kept = (pipe: (typeof pipes)[number]) =>
|
||||
pipe.facility === "ford_pavement" && !!spec && near(spec, section, pipe.chainage_m);
|
||||
if (spec && !pipes.some(kept)) delete section.ford_pavement;
|
||||
}
|
||||
/** 그 관 자리에 얹힌 시설 키가 이 종류와 맞는가 — 얹힌 것이 없으면(새 관 · 옮긴 관) 대상 밖. */
|
||||
const fits = (pipe: (typeof pipes)[number], key: string, spec: Record<string, unknown>) =>
|
||||
key === facilityKey(pipe.facility ?? "pipe") &&
|
||||
@@ -147,7 +154,9 @@ async function withDraftPipeKinds(detail: SectionDetailResponse, projectId: stri
|
||||
return spec && near(spec, section, pipe.chainage_m) ? [[key, spec] as const] : [];
|
||||
}),
|
||||
);
|
||||
return here.length > 0 && !here.some(([key, spec]) => fits(pipe, key, spec));
|
||||
// 얹힌 것이 없는 관(새 관 · 옮긴 관)은 미리 뜬 횡단이 맡음 — 물넘이포장만 걸친 기존 측점까지 여기서(314)
|
||||
if (!here.length) return pipe.facility === "ford_pavement";
|
||||
return !here.some(([key, spec]) => fits(pipe, key, spec));
|
||||
});
|
||||
if (!stale.length) return;
|
||||
const types = await fetchStructureTypes().catch(() => []);
|
||||
|
||||
@@ -101,6 +101,11 @@ export function hasStaleDesigns(
|
||||
|
||||
interface StaleCandidate {
|
||||
chainage_m: number;
|
||||
// 측점에 얹힌 구조물 제원 — 저장분 설계값(포장 · 기울기 · 노면 하강)과 맞대 본다(314)
|
||||
pavement?: unknown[] | null;
|
||||
ford_pavement?: unknown;
|
||||
underdrain?: unknown;
|
||||
ford?: { overflow_depth_m?: number } | null;
|
||||
design?: {
|
||||
design_elevation_m: number;
|
||||
geometry_preset?: string;
|
||||
@@ -108,6 +113,8 @@ interface StaleCandidate {
|
||||
surface_drop_m?: number;
|
||||
section_mode?: string;
|
||||
ditch_enabled?: boolean;
|
||||
paved?: boolean;
|
||||
tilted?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -124,6 +131,15 @@ function isStaleSection(
|
||||
if (design.geometry_preset === "rock" && design.two_stage_slope === undefined) return true;
|
||||
// 옛 엔진이 양측 성토에 측구를 세워 둔 저장분(206-1) — 지금 엔진으로 다시 셈.
|
||||
if (design.section_mode === "both_fill" && design.ditch_enabled === true) return true;
|
||||
// 얹힌 구조물 제원과 저장분 설계값이 다르면 낡음 — 서버 `_enforce_stored_designs` 짝(314).
|
||||
// 저장 전에 놓거나 · 옮기거나 · 종류를 바꾼 물넘이포장 · 맹암거 · 세월교 · 노면공이 [저장] 전에도
|
||||
// 측구 없음 · 포장 · 노면 하강으로 다시 셈(포장 = `pavedBySpecs` · 기울기 = `Cross_Refresh` 와 같은 규칙).
|
||||
const paved = Boolean(section.pavement?.length) || Boolean(section.ford_pavement);
|
||||
if (Boolean(design.paved) !== paved) return true;
|
||||
if (Boolean(design.tilted) !== Boolean(section.ford_pavement || section.underdrain)) return true;
|
||||
const drop =
|
||||
Number(section.ford?.overflow_depth_m) > 0 ? Number(section.ford?.overflow_depth_m) : 0;
|
||||
if (Math.abs((design.surface_drop_m ?? 0) - drop) > 1e-6) return true;
|
||||
if (!profiles?.length) return false;
|
||||
const planned = designElevationAt(profiles, section.chainage_m);
|
||||
if (planned === undefined) return false;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/* PLAN 314 단위검증 헬퍼 — 저장분 횡단 값(포장 · 기울기 · 노면 하강) 낡음 판정 · 월류 높이 기다림.
|
||||
* 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 결과 = 표준출력 JSON 한 벌.
|
||||
* 짝 = `test_314_stale_structure.py`.
|
||||
*/
|
||||
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 load = (file) => {
|
||||
if (cache.has(file)) return cache.get(file);
|
||||
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = { exports: {} };
|
||||
cache.set(file, box.exports);
|
||||
const near = (m) => {
|
||||
if (m.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${m.slice(6)}.ts`));
|
||||
if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`));
|
||||
return new Proxy({}, { get: () => () => {} });
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, near);
|
||||
return box.exports;
|
||||
};
|
||||
|
||||
const { hasStaleDesigns } = load(
|
||||
path.join(ROOT, "B06_Section", "B06_Section_UI_Section_Common.ts"),
|
||||
);
|
||||
const { waitsFordHeight } = load(
|
||||
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Drainage_Ford_Fill.ts"),
|
||||
);
|
||||
|
||||
const stale = (section) =>
|
||||
hasStaleDesigns({ longitudinal: { design_profiles: [] }, cross_sections: [section] });
|
||||
const design = (extra) => ({ design_elevation_m: 100, ...extra });
|
||||
|
||||
const out = {
|
||||
ford_untilted: stale({ chainage_m: 10, ford_pavement: {}, design: design({ paved: true }) }),
|
||||
ford_tilted: stale({
|
||||
chainage_m: 10,
|
||||
ford_pavement: {},
|
||||
design: design({ paved: true, tilted: true }),
|
||||
}),
|
||||
left_tilted: stale({ chainage_m: 10, design: design({ tilted: true }) }),
|
||||
underdrain_tilted: stale({ chainage_m: 10, underdrain: {}, design: design({ tilted: true }) }),
|
||||
pavement_unpaved: stale({ chainage_m: 10, pavement: [{}], design: design({ paved: false }) }),
|
||||
ford_drop_missing: stale({ chainage_m: 10, ford: { overflow_depth_m: 0.3 }, design: design({}) }),
|
||||
ford_drop_kept: stale({
|
||||
chainage_m: 10,
|
||||
ford: { overflow_depth_m: 0.3 },
|
||||
design: design({ surface_drop_m: 0.3 }),
|
||||
}),
|
||||
plain: stale({ chainage_m: 10, design: design({}) }),
|
||||
waits: {
|
||||
pave_empty: waitsFordHeight("ford_pavement", {}),
|
||||
pave_blank: waitsFordHeight("ford_pavement", { ford_height_m: "" }),
|
||||
pave_set: waitsFordHeight("ford_pavement", { ford_height_m: 0.5 }),
|
||||
bridge_none: waitsFordHeight("ford_bridge", undefined),
|
||||
pipe: waitsFordHeight("pipe", {}),
|
||||
},
|
||||
};
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,81 @@
|
||||
"""PLAN 314 — 물넘이포장 · 맹암거 후속(요구 210 · 211).
|
||||
|
||||
- 314-1 · 314-2 · 314-4 저장 전에 놓거나 · 옮기거나 · 종류를 바꾼 구조물 자리 = 저장분 횡단 값(포장 · 기울기 ·
|
||||
세월교 노면 하강)과 얹힌 제원이 다르면 낡음 → 다시 셈(서버 `_enforce_stored_designs` 짝) ·
|
||||
물넘이포장 옛 자리 걷고 새 자리(걸친 기존 측점까지) 얹음
|
||||
- 314-3 월류 높이 기다림 = 빈 글자도 빈칸(채우는 쪽과 같은 규칙)
|
||||
|
||||
실제 TS 를 `helper_314_stale_structure.cjs` 가 Node 로 돌린다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(__file__)
|
||||
ROOT = os.path.join(HERE, "..", "..")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.path.isdir(os.path.join(ROOT, "config", "node_modules")),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def _read(*parts: str) -> str:
|
||||
with open(os.path.join(ROOT, *parts), encoding="utf-8") as handle:
|
||||
return handle.read()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def result() -> dict:
|
||||
out = subprocess.run(
|
||||
["node", os.path.join(HERE, "helper_314_stale_structure.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=120,
|
||||
check=True,
|
||||
)
|
||||
return json.loads(out.stdout)
|
||||
|
||||
|
||||
def test_tilt_mismatch_is_stale(result: dict) -> None:
|
||||
# 물넘이포장 · 맹암거 자리인데 저장분이 안 기울었음(측구 있음) = 낡음 · 맞으면 그대로
|
||||
assert result["ford_untilted"] is True
|
||||
assert result["ford_tilted"] is False
|
||||
assert result["underdrain_tilted"] is False
|
||||
# 구조물이 떠났는데 저장분이 아직 기울었음(옮김 · 종류 바꿈) = 낡음
|
||||
assert result["left_tilted"] is True
|
||||
|
||||
|
||||
def test_paved_and_surface_drop_mismatch_is_stale(result: dict) -> None:
|
||||
assert result["pavement_unpaved"] is True
|
||||
assert result["ford_drop_missing"] is True
|
||||
assert result["ford_drop_kept"] is False
|
||||
assert result["plain"] is False
|
||||
|
||||
|
||||
def test_ford_height_wait_treats_blank_as_empty(result: dict) -> None:
|
||||
waits = result["waits"]
|
||||
assert waits["pave_empty"] is True
|
||||
assert waits["pave_blank"] is True
|
||||
assert waits["pave_set"] is False
|
||||
assert waits["bridge_none"] is True
|
||||
assert waits["pipe"] is False
|
||||
|
||||
|
||||
def test_structure_change_reconciles_and_ford_reattach() -> None:
|
||||
view = _read("A00_Common", "design", "design_view_cross.ts")
|
||||
body = view.split("async function refreshDetailForStructures", 1)[1].split("\n }\n", 1)[0]
|
||||
assert "void reconcileStaleDesigns();" in body
|
||||
refresh = _read("B06_Section", "B06_Section_Cross_Refresh.ts")
|
||||
assert "Number(section.ford?.overflow_depth_m) > 0" in refresh
|
||||
store = _read("B06_Section", "B06_Section_Section_Store.ts")
|
||||
kinds = store.split("async function withDraftPipeKinds", 1)[1].split("\n}\n", 1)[0]
|
||||
assert "if (!pipes) return;" in kinds
|
||||
assert "delete section.ford_pavement" in kinds
|
||||
assert 'if (!here.length) return pipe.facility === "ford_pavement";' in kinds
|
||||
Reference in New Issue
Block a user