- 관 흙막이 관경 기준 최소(관경 + 1.0) · 벽 최저(근입 + 0.1) 하한 걷음 — 관경 기준값은 빈 칸 기본값만 - 하한은 그림이 서는 0.1 하나(WALL_MIN_PURE_HEIGHT_M) · 0 이하는 토스트 없이 0.1 에서 멈춤 - 좌측 칸 최소 높이 토스트 · 조정창 「최소 높이」 토스트 뺌 - 높이 · 지형이 벽 자리를 옮긴 것은 자리 한계 토스트 안 띄움 — 자리 버튼이 막혔을 때만 - 집수정 내공 폭 · 높이 1~2m 한도 걷음 · 옹벽 0 입력도 0.1 에서 멈춤 - 근입보다 낮은 관 벽도 그림(노출 높이 음수 허용) · 서버 번들 다시 빌드 - 시험 test_b06_height_unlock Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
236 lines
11 KiB
TypeScript
236 lines
11 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Page_Pipe_Options.ts
|
|
* 「구조물 배치」 폼에서 바꾼 **관 옵션을 횡단 캐시에 얹는** 경로.
|
|
*
|
|
* `B06_Section_UI_Page` 에서 떼어낸 몫이다(700줄 제한, 2026-09-02). 여기서는 화면
|
|
* 캐시(`section.culvert`·조정창 제어기)만 고친다 — 영구저장은 [저장]·[확정] 몫이다.
|
|
* ========================================================================== */
|
|
|
|
import { pipeDiameterM } from "@util/common_util_culvert_sets";
|
|
import type { DetailPipeInput } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
|
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
|
import { applyBoxFormOptions, applyFordFormOptions } from "./B06_Section_UI_Page_Ford_Controls";
|
|
import {
|
|
applySpanPatch,
|
|
revetWallSpec,
|
|
WALL_MIN_PURE_HEIGHT_M,
|
|
} from "./B06_Section_UI_Cross_Culvert_Const";
|
|
import type { CrossSection } from "./B06_Section_Api_Fetch";
|
|
import type { createStationControls } from "./B06_Section_UI_Page_Station_Controls";
|
|
|
|
type StationControls = ReturnType<typeof createStationControls>;
|
|
|
|
/** 폼 → 캐시 반영에 필요한 페이지 창구만 모은 것. */
|
|
export interface PipeOptionsContext {
|
|
detail: () => SectionDetailResponse | null;
|
|
ford: StationControls["ford"];
|
|
box: StationControls["box"];
|
|
revetOffset: StationControls["revetOffset"];
|
|
/** 다단 단별 구간값 — 흙막이 컨테이너 길이 · 전 · 후(54-10). */
|
|
structureSpan?: StationControls["structureSpan"];
|
|
/** 흙막이 컨테이너 수 = 지형이 세운 유출 다단 수(`pipeExtraCount` · 54-10) — 없으면 다단 안 만짐. */
|
|
soilTiers?: (chainageM: number) => number | null;
|
|
/** 기하가 잘라 낸 실제 값을 폼에 되돌린다. */
|
|
overrideOptions: (chainageM: number, values: Record<string, number | string>) => void;
|
|
refreshCard: (chainageM: number) => void;
|
|
}
|
|
|
|
/**
|
|
* 폼에서 바꾼 관 옵션을 **횡단 캐시**(`section.culvert` 스펙)에 얹고, 그 구조물이
|
|
* 덮는 측점 카드를 다시 그린다 — 조정창 조작과 같은 흐름이라 도면이 바로 따라온다
|
|
* (2026-08-29 사용자 보고: 값만 바뀌고 횡단도가 그대로였다).
|
|
* 여기서는 화면 캐시만 고친다 — 영구저장은 [저장]·[확정] 몫이다.
|
|
* `whole` — 폼이 보낸 **칸 전부**(비운 칸 = 지움 · 기슭막이 높이 · 형태 없음 = 자동 · 24-4a).
|
|
* 초안 되얹기처럼 일부만 오면 false — 온 칸만 얹는다.
|
|
*/
|
|
export function applyPipeOptionsToCache(
|
|
ctx: PipeOptionsContext,
|
|
chainageM: number,
|
|
patch: Record<string, number | string>,
|
|
whole = true,
|
|
): void {
|
|
const owner = ctx
|
|
.detail()
|
|
?.cross_sections.find((section) => Math.abs(section.chainage_m - chainageM) < PIPE_MATCH_M);
|
|
if (!owner) return;
|
|
// 세월교는 스펙 자리(`section.ford`)가 배수관과 달라 조정창 제어기로 보낸다 —
|
|
// 로직은 그쪽 것을 쓰고 프론트만 폼 UI다(2026-08-30 사용자 확정).
|
|
if (owner.ford) {
|
|
applyFordFormOptions(ctx.ford, owner.chainage_m, patch);
|
|
return;
|
|
}
|
|
// BOX암거도 스펙 자리가 따로다(`section.box`) — 같은 규칙으로 제어기에 보낸다
|
|
// (2026-08-30 사용자: 세월교와 같은 문제).
|
|
if (owner.box) {
|
|
applyBoxFormOptions(ctx.box, owner.chainage_m, patch);
|
|
return;
|
|
}
|
|
const culvert = owner.culvert;
|
|
if (!culvert) return;
|
|
const num = (key: string): number | undefined => {
|
|
const value = Number(patch[key]);
|
|
return Number.isFinite(value) ? value : undefined;
|
|
};
|
|
const put = <T extends object>(target: T, field: keyof T, value: unknown): void => {
|
|
if (value !== undefined) (target as Record<string, unknown>)[field as string] = value;
|
|
};
|
|
/** 폼 칸 값 — 폼이 보낸 것이 칸 전부라 비운 칸은 지운다(계획서 24-4a 빈 값). */
|
|
const own = <T extends object>(target: T, field: keyof T, value: unknown): void => {
|
|
if (value === undefined && !whole) return;
|
|
if (value === undefined) delete (target as Record<string, unknown>)[field as string];
|
|
else (target as Record<string, unknown>)[field as string] = value;
|
|
};
|
|
put(culvert, "pipe_kind", patch.pipe_kind);
|
|
// 기슭막이 기초(기초유/기초버림) — **터파기 파선을 그리는 값**이다(`foundationChoice`).
|
|
// 옮겨 적지 않아 폼에서 고쳐도 도면이 다음 조회까지 안 따라왔다(2026-09-09 실측).
|
|
// ⚠ 폼 키는 `revet_foundation`, 스펙 자리는 `foundation` 이라 이름이 다르다.
|
|
put(culvert, "foundation", patch.revet_foundation);
|
|
const diameterMm = num("pipe_diameter_mm");
|
|
if (diameterMm !== undefined) culvert.diameter_m = pipeDiameterM(diameterMm);
|
|
// 유입·유출 기슭막이 제원과 집수정 구간값 — 폼 옵션 키를 스펙 자리로 옮긴다.
|
|
for (const [side, prefix] of [
|
|
[culvert.inlet, "inlet"],
|
|
[culvert.outlet, "outlet"],
|
|
] as const) {
|
|
own(side, "revet_form", patch[`${prefix}_revet_form`]);
|
|
own(side, "revet_height_m", num(`${prefix}_revet_height_m`));
|
|
own(side, "revet_length_m", num(`${prefix}_revet_length_m`));
|
|
own(side, "revet_before_m", num(`${prefix}_revet_before_m`));
|
|
own(side, "revet_after_m", num(`${prefix}_revet_after_m`));
|
|
put(side, "structure", patch[`${prefix}_type`]);
|
|
}
|
|
// 배관 벽의 **높이·형태는 스펙이 아니라 조정값**(revet_adjust.h·m)이 정한다
|
|
// — `revetWallSpec`이 배관에서는 spec.revet_height_m을 쓰지 않기 때문이다.
|
|
// 폼에서 고친 값을 조정창과 같은 채널로 보내야 도면이 따라온다(2026-08-29 사용자).
|
|
// 높이 · 형태를 고치는 칸은 폼 하나라(24-4a) 비운 칸 = 자동(h · m 없음).
|
|
for (const [role, prefix] of [
|
|
["inlet", "inlet"],
|
|
["outlet", "outlet"],
|
|
] as const) {
|
|
const height = num(`${prefix}_revet_height_m`);
|
|
const form = patch[`${prefix}_revet_form`];
|
|
if (!whole && height === undefined && typeof form !== "string") continue;
|
|
// 빈 칸 = 자동(null) · 일부만 온 되얹기는 안 온 칸을 그대로(undefined).
|
|
const blank = whole ? null : undefined;
|
|
ctx.revetOffset.update(owner.chainage_m, role, {
|
|
h: height ?? blank,
|
|
m: typeof form === "string" ? form : blank,
|
|
});
|
|
// 그림이 안 서는 값(0 이하)만 하한에서 멈춰 폼에 되돌린다 — 토스트 없음(54-51).
|
|
if (height === undefined) continue;
|
|
const applied = revetWallSpec(
|
|
role === "outlet" ? culvert.outlet : culvert.inlet,
|
|
ctx.revetOffset.adjustFor(owner, role),
|
|
culvert.hidden_pipe === true,
|
|
culvert.diameter_m,
|
|
).pureHeight;
|
|
if (Math.abs(applied - height) > 0.05) {
|
|
ctx.overrideOptions(owner.chainage_m, {
|
|
[`${prefix}_revet_height_m`]: Number(applied.toFixed(1)),
|
|
});
|
|
}
|
|
}
|
|
if (!culvert.hidden_pipe) applySoilTiers(ctx, owner, patch, whole);
|
|
own(culvert.inlet, "basin_length_m", num("inlet_basin_length_m"));
|
|
own(culvert.inlet, "basin_before_m", num("inlet_basin_before_m"));
|
|
own(culvert.inlet, "basin_after_m", num("inlet_basin_after_m"));
|
|
own(culvert.inlet, "basin_height_m", num("inlet_basin_height_m"));
|
|
// 연장이 바뀌면 옆 측점 링크도 달라진다 — 그 구조물이 덮는 범위만 다시 그린다.
|
|
const reach = Math.max(
|
|
culvert.inlet.revet_before_m ?? 0,
|
|
culvert.inlet.revet_after_m ?? 0,
|
|
culvert.outlet.revet_before_m ?? 0,
|
|
culvert.outlet.revet_after_m ?? 0,
|
|
num("inlet_revet_length_m") ?? 0,
|
|
num("outlet_revet_length_m") ?? 0,
|
|
);
|
|
for (const other of ctx.detail()?.cross_sections ?? []) {
|
|
if (Math.abs(other.chainage_m - owner.chainage_m) <= reach + 1e-9) {
|
|
ctx.refreshCard(other.chainage_m);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 흙막이 컨테이너가 마지막으로 보낸 높이 · 형태(`측점:extra{n}`) — 기하가 다시 그릴 때마다 키를 지형에
|
|
* 맞춰 고쳐 되받으므로(syncApplied) 되받은 값과 견주면 안 바꾼 단도 다시 보냄(54-10). */
|
|
const soilSent = new Map<string, { h: number | null; m: string | null }>();
|
|
|
|
/**
|
|
* 흙막이 n 컨테이너(`soil_add_{n}_*`) → 유출 다단 n 단(`extra{n-1}`) 그림(54-10). 형태 · 높이 = 조정값
|
|
* (m · h · 빈 칸 = 자동) · 길이 · 전 · 후 = 단별 구간값(빈 칸 = 그대로). 지형이 세운 단까지만 —
|
|
* 넘친 컨테이너 값은 파일에만 남음. 높이 = 요청 · 서는 키는 기하 규칙 그대로(지형 맞춤 · 형태 한계) ·
|
|
* 그림이 안 서는 값(0 이하)만 폼에 되돌림(형태별 상한 없음 · 토스트 없음).
|
|
*/
|
|
function applySoilTiers(
|
|
ctx: PipeOptionsContext,
|
|
owner: CrossSection,
|
|
patch: Record<string, number | string>,
|
|
whole: boolean,
|
|
): void {
|
|
const value = (n: number, item: string): number | undefined => {
|
|
const raw = patch[`soil_add_${n}_${item}`];
|
|
return raw === undefined || raw === "" || !Number.isFinite(Number(raw))
|
|
? undefined
|
|
: Number(raw);
|
|
};
|
|
const blank = whole ? null : undefined;
|
|
const clipped: Record<string, number> = {};
|
|
for (let n = 1; n <= (ctx.soilTiers?.(owner.chainage_m) ?? 0); n++) {
|
|
const role = `extra${n - 1}` as const;
|
|
const key = `${owner.chainage_m.toFixed(2)}:${role}`;
|
|
const form = patch[`soil_add_${n}_form`];
|
|
const want = {
|
|
h: value(n, "height") ?? blank,
|
|
m: typeof form === "string" && form ? form : blank,
|
|
};
|
|
if (want.h != null) {
|
|
const h = Math.max(want.h, WALL_MIN_PURE_HEIGHT_M);
|
|
if (Math.abs(h - want.h) > 0.05) clipped[`soil_add_${n}_height`] = Number(h.toFixed(1));
|
|
want.h = Number(h.toFixed(1));
|
|
}
|
|
const sent = soilSent.get(key) ?? ctx.revetOffset.adjustFor(owner, role);
|
|
if (
|
|
(want.h !== undefined && want.h !== sent.h) ||
|
|
(want.m !== undefined && want.m !== sent.m)
|
|
) {
|
|
ctx.revetOffset.update(owner.chainage_m, role, want);
|
|
soilSent.set(key, { h: want.h ?? sent.h, m: want.m ?? sent.m });
|
|
}
|
|
const span = {
|
|
lengthM: value(n, "length"),
|
|
beforeM: value(n, "before"),
|
|
afterM: value(n, "after"),
|
|
};
|
|
if (!ctx.structureSpan || Object.values(span).every((v) => v === undefined)) continue;
|
|
const current = ctx.structureSpan.tierValuesFor(owner, role);
|
|
const next = applySpanPatch(current, span);
|
|
if (
|
|
(["lengthM", "beforeM", "afterM"] as const).some((k) => Math.abs(next[k] - current[k]) > 1e-6)
|
|
)
|
|
ctx.structureSpan.updateTier(owner, role, span);
|
|
}
|
|
// 폼에 되돌림은 이 변경을 패널이 옵션에 담은 뒤 — 지금 되돌리면 옛 옵션으로 폼을 다시 세움
|
|
if (Object.keys(clipped).length)
|
|
queueMicrotask(() => ctx.overrideOptions(owner.chainage_m, clipped));
|
|
}
|
|
|
|
/** 조정창(기슭막이 조정값)이 정본인 칸 — 초안에서 되살리지 않는다. */
|
|
const ADJUST_OWNED = /_revet_(height_m|form)$/;
|
|
|
|
/**
|
|
* 관 초안 옵션을 **횡단 캐시에 다시 얹는다**(24-7a). 새로고침 뒤 캐시 상세는 저장분이라
|
|
* 저장 전에 고친 월류 폭·관경 등이 폼·카드에서 옛 값으로 돌아간다 — 초안이 관 옵션의 정본이다.
|
|
*/
|
|
export function replayDraftPipeOptions(
|
|
ctx: PipeOptionsContext,
|
|
pipes: ReadonlyArray<DetailPipeInput> | null,
|
|
): void {
|
|
for (const pipe of pipes ?? []) {
|
|
const patch = Object.fromEntries(
|
|
Object.entries(pipe.options ?? {}).filter(([key]) => !ADJUST_OWNED.test(key)),
|
|
);
|
|
if (Object.keys(patch).length) applyPipeOptionsToCache(ctx, pipe.chainage_m, patch, false);
|
|
}
|
|
}
|