fix(B06): 폼 높이·형태가 실제로 벽을 제어하게 한다

원인 추적 결과 두 겹이었다.
① 배관 벽 높이는 spec.revet_height_m을 쓰지 않는다 — revetWallSpec이 관경 기준
   하한과 조정값(adjust.h)만 본다. 폼 값을 스펙에만 넣어 그림이 안 바뀌었다.
   이제 폼의 높이·형태를 조정창과 같은 채널(revetOffset.update)로 보낸다.
② 형태마다 높이 한계가 있다(돌쌓기(메) 2.0m). 한계를 넘겨 입력하면 그림은 그대로인데
   폼 숫자만 커져 '제어가 안 된다'로 보였다. 기하가 적용한 값을 폼에 되돌리고
   한계를 토스트로 알린다.

폼 기본 높이도 스펙이 아니라 지금 그려진 순수 높이(조정창 표시값)를 쓴다.
This commit is contained in:
2026-08-29 17:09:20 +09:00
parent 510731dd17
commit b3e6f01eb4
2 changed files with 66 additions and 10 deletions
+40 -7
View File
@@ -43,6 +43,7 @@ import { computeMassHaul, massHaulPayload } from "@util/common_util_mass_haul";
import { computeHaulPlan } from "@util/common_util_mass_haul_balance";
import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view";
import { staleDesignChainages } from "./B06_Section_UI_Section_Common";
import { revetWallSpec } from "./B06_Section_UI_Cross_Culvert_Const";
import { createStandardPanel, type StandardPanelController } from "./B06_Section_UI_Standard_Panel";
import {
createB06StructuresPanel,
@@ -138,6 +139,18 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
const structuresPanel = createB06StructuresPanel({
projectId,
detail: () => sectionDetail,
// 폼 기본 높이 = 지금 도면에 그려진 순수 높이(조정창이 보여주던 값과 같은 계산).
wallHeight: (chainageM, role) => {
const owner = sectionDetail?.cross_sections.find(
(section) => Math.abs(section.chainage_m - chainageM) < 0.51,
);
const culvert = owner?.culvert;
if (!owner || !culvert) return null;
const spec = role === "outlet" ? culvert.outlet : culvert.inlet;
const adjust = stationControls.revetOffset.adjustFor(owner, role);
return revetWallSpec(spec, adjust, culvert.hidden_pipe === true, culvert.diameter_m)
.pureHeight;
},
stationInterval: () => stationInterval ?? 20,
focusChainage: focusStationAt,
queuePipeOptions: (chainageM, patch) => stationControls.queueCulvertOptions(chainageM, patch),
@@ -183,19 +196,39 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
put(side, "revet_after_m", num(`${prefix}_revet_after_m`));
put(side, "structure", patch[`${prefix}_type`]);
}
// 폼이 높이·형태를 정하면 그 벽의 **조정값**(revet_adjust.h·m)은 비운다 —
// 조정값이 남아 있으면 그림이 폼 값을 따라오지 않는다(2026-08-29 사용자 보고).
// 배관 벽의 **높이·형태는 스펙이 아니라 조정값**(revet_adjust.h·m)이 정한다
// — `revetWallSpec`이 배관에서는 spec.revet_height_m을 쓰지 않기 때문이다.
// 폼에서 고친 값을 조정창과 같은 채널로 보내야 도면이 따라온다(2026-08-29 사용자).
for (const [role, prefix] of [
["inlet", "inlet"],
["outlet", "outlet"],
] as const) {
const patchHeight = patch[`${prefix}_revet_height_m`] !== undefined;
const patchForm = patch[`${prefix}_revet_form`] !== undefined;
if (!patchHeight && !patchForm) continue;
const height = num(`${prefix}_revet_height_m`);
const form = patch[`${prefix}_revet_form`];
if (height === undefined && typeof form !== "string") continue;
stationControls.revetOffset.update(owner.chainage_m, role, {
...(patchHeight ? { h: null } : {}),
...(patchForm ? { m: null } : {}),
...(height !== undefined ? { h: height } : {}),
...(typeof form === "string" ? { m: form } : {}),
});
// 형태마다 높이 한계가 있다(돌쌓기(메) 2.0m 등). 기하가 잘라 낸 실제 높이를
// 폼에 되돌린다 — 숫자만 커지고 그림은 그대로인 상태를 남기지 않는다.
if (height === undefined) continue;
const applied = revetWallSpec(
role === "outlet" ? culvert.outlet : culvert.inlet,
stationControls.revetOffset.adjustFor(owner, role),
culvert.hidden_pipe === true,
culvert.diameter_m,
).pureHeight;
if (Math.abs(applied - height) > 0.05) {
structuresPanel.overrideOptions(owner.chainage_m, {
[`${prefix}_revet_height_m`]: Number(applied.toFixed(1)),
});
showToast(
`${prefix === "outlet" ? "유출" : "유입"} 기슭막이 높이는 형태 한계로 ` +
`${applied.toFixed(1)}m까지만 적용됩니다.`,
"error",
);
}
}
put(culvert.inlet, "basin_length_m", num("inlet_basin_length_m"));
put(culvert.inlet, "basin_before_m", num("inlet_basin_before_m"));
@@ -38,6 +38,9 @@ export interface B06StructuresPanelDeps {
/** 횡단 캐시 — 폼 기본값을 조정창과 **같은 원천**(culvert 스펙)에서 읽는다
* (2026-08-29 사용자: 예전 조정창이 보여주던 값이 기본값이다). */
detail?: () => SectionDetailResponse | null;
/** 지금 그려진 벽 높이(m) — 배관 벽은 스펙이 아니라 기하가 정하므로 폼 기본
* 높이도 이 값을 쓴다(조정창 표시값과 같다). */
wallHeight?: (chainageM: number, role: "inlet" | "outlet") => number | null;
/** 측점 간격(m) — 측점번호+잔여거리 환산용. */
stationInterval: () => number;
/** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다. */
@@ -62,6 +65,9 @@ export interface B06StructuresPanel {
load: () => Promise<void>;
/** 횡단도에서 고른 벽·구체의 시설을 폼에 올린다(null = 해제). */
showPipeAt: (chainageM: number | null) => void;
/** 기하가 한계로 자른 실제 값을 폼에 되돌린다 — 입력 숫자만 커지고 그림은 그대로인
* 상태를 막는다(2026-08-29 사용자: 재질 높이 한계에 걸린 값). */
overrideOptions: (chainageM: number, patch: Record<string, string | number>) => void;
/** 고른 횡단도(측점)에 있는 구조물을 폼에 올린다 — 계곡 통과 시설이 먼저고,
* 없으면 그 측점을 덮는 구조물 정본 항목. 둘 다 없으면 해제(2026-08-29). */
showAtChainage: (chainageM: number | null) => void;
@@ -94,6 +100,7 @@ function withSpecDefaults(
options: Record<string, string | number> | undefined,
detail: SectionDetailResponse | null,
chainageM: number,
wallHeight?: (chainageM: number, role: "inlet" | "outlet") => number | null,
): Record<string, string | number> | undefined {
const owner = detail?.cross_sections.find(
(section) => Math.abs(section.chainage_m - chainageM) < PIPE_MATCH_M,
@@ -112,7 +119,8 @@ function withSpecDefaults(
] as const) {
put(`${prefix}_type`, side.structure);
put(`${prefix}_revet_form`, side.revet_form);
put(`${prefix}_revet_height_m`, side.revet_height_m);
// 높이는 기하가 정한 실제 값이 기준이다 — 못 구하면 스펙 값으로 물러난다.
put(`${prefix}_revet_height_m`, wallHeight?.(chainageM, prefix) ?? side.revet_height_m);
put(`${prefix}_revet_length_m`, side.revet_length_m);
put(`${prefix}_revet_before_m`, side.revet_before_m);
put(`${prefix}_revet_after_m`, side.revet_after_m);
@@ -218,7 +226,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
end_m: pipe.end_m,
source: pipe.source,
// 폼 기본값 = 조정창이 쓰던 값(횡단 스펙). 저장된 옵션이 있으면 그쪽이 이긴다.
options: withSpecDefaults(pipe.options, detail, pipe.chainage_m),
options: withSpecDefaults(pipe.options, detail, pipe.chainage_m, deps.wallHeight),
design_flow_m3s: null,
}));
section.setPipeFacilities(pipeFacilities);
@@ -236,7 +244,12 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
(entry) => Math.abs(entry.chainage_m - chainageM) < PIPE_MATCH_M,
);
if (!hit) return;
hit.options = withSpecDefaults(hit.options, deps.detail?.() ?? null, hit.chainage_m);
hit.options = withSpecDefaults(
hit.options,
deps.detail?.() ?? null,
hit.chainage_m,
deps.wallHeight,
);
section.setPipeFacilities(pipeFacilities);
}
@@ -249,6 +262,16 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
setInletStructure: (value) => section.facility.setInletStructure(value),
onInletStructureChange: (handler) => section.facility.onInletStructureChange(handler),
},
overrideOptions: (chainageM, patch) => {
const hit = pipeFacilities.find(
(entry) => Math.abs(entry.chainage_m - chainageM) < PIPE_MATCH_M,
);
if (!hit) return;
hit.options = { ...(hit.options ?? {}), ...patch };
section.setPipeFacilities(pipeFacilities);
// 폼을 그 값으로 다시 세운다 — 조정창이 한계를 알려 주던 것과 같은 자리다.
section.selectPipeByChainage(hit.chainage_m);
},
showPipeAt: (chainageM) => {
currentChainageM = chainageM;
if (chainageM !== null) refreshSpecDefaults(chainageM);