Merge remote-tracking branch 'origin/sub_laptop_5' into main_laptop_1

This commit is contained in:
2026-10-06 11:11:54 +09:00
11 changed files with 580 additions and 18 deletions
+21
View File
@@ -145,6 +145,27 @@ function relocateSideRows(panelRoot: HTMLElement): void {
const group = target.closest<HTMLElement>("fieldset");
if (!group) return;
const ownerSlot = slotOf(panelRoot.dataset.extraSide === "inlet" ? "inlet" : "outlet");
// 배관 유출 다단 = 그 단의 「흙막이」 컨테이너(54-10) — 옛 추가 기슭막이 칸 대신 그 상자를 켬 ·
// 단 수 · 옵션은 부모(유출) 칸에 그대로
const soil =
ownerSlot === sideSlots.outlet && slotUsable(ownerSlot)
? ownerSlot
.closest(".b05-structure__spec")
?.querySelectorAll<HTMLElement>('fieldset[data-spec-container="soil_add"]')[
Number(panelRoot.dataset.extraIndex)
]
: undefined;
if (soil) {
moveRows(
panelRoot,
SIDE_ROWS.replace(".b06-structure-panel__struct--readout, ", ""),
ownerSlot,
);
setGroupLabel(soil, "");
soil.classList.add("is-active");
soil.scrollIntoView({ block: "nearest" });
return;
}
const owner = ownerSlot.closest("fieldset");
if (owner && owner !== group) owner.after(group);
group.hidden = false;
@@ -588,6 +588,7 @@ export function buildStructurePanel(
// 이름 자체로(2026-08-30 사용자 지시 1). 어느 구조물을 만지는지 폼에서 읽힌다.
// 다단은 칸 이름이 이미 "추가 기슭막이"라 단 번호만 붙인다 — "추가 기슭막이 (2단)".
root.dataset.panelTitle = isExtra ? `${extraIndex + 1}단` : (title.textContent ?? "");
root.dataset.extraIndex = isExtra ? String(extraIndex) : ""; // 흙막이 컨테이너 차례(54-10)
const tiers =
tierChannel === "outlet"
? deps.extraState()
@@ -12,7 +12,15 @@ 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 { revetWallSpec } from "./B06_Section_UI_Cross_Culvert_Const";
import {
applySpanPatch,
EXTRA_WALL_MIN_HEIGHT_M,
REVET_EMBED_DEPTH_M,
revetFormOf,
revetHeightLimit,
revetWallSpec,
} 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>;
@@ -23,6 +31,10 @@ export interface PipeOptionsContext {
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;
@@ -129,6 +141,7 @@ export function applyPipeOptionsToCache(
);
}
}
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"));
@@ -149,6 +162,89 @@ export function applyPipeOptionsToCache(
}
}
/** 흙막이 컨테이너가 마지막으로 보낸 높이 · 형태(`측점: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 · 빈 칸 = 자동) · 길이 · 전 · 후 = 단별 구간값(빈 칸 = 그대로). 지형이 세운 단까지만 —
* 넘친 컨테이너 값은 파일에만 남음. 높이 = 요청 · 서는 키는 기하 규칙 그대로(지형 맞춤 · 형태 한계) ·
* 형태 한계 · 하한을 넘은 요청만 폼에 되돌림.
*/
function applySoilTiers(
ctx: PipeOptionsContext,
owner: CrossSection,
patch: Record<string, number | string>,
whole: boolean,
): void {
const culvert = owner.culvert!;
const ownerForm = revetWallSpec(
culvert.outlet,
ctx.revetOffset.adjustFor(owner, "outlet"),
false,
culvert.diameter_m,
).form;
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 limit = revetHeightLimit(
revetFormOf(want.m ?? ctx.revetOffset.adjustFor(owner, role).m, ownerForm),
);
const h = Math.min(
Math.max(want.h, EXTRA_WALL_MIN_HEIGHT_M, REVET_EMBED_DEPTH_M + 0.1),
limit,
);
if (Math.abs(h - want.h) > 0.05) {
clipped[`soil_add_${n}_height`] = Number(h.toFixed(1));
showToast(
`흙막이 ${n} 높이는 ${h < want.h ? "형태 한계로" : "최소"} ${h.toFixed(1)}m` +
`${h < want.h ? "까지만" : "로"} 적용됩니다.`,
"error",
);
}
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)$/;
@@ -78,9 +78,9 @@ export interface B06StructuresPanelDeps {
detail?: () => SectionDetailResponse | null;
/** 지금 그려진 벽 높이(m) — 배관 벽은 스펙이 아니라 기하가 정하므로 폼 기본
* 높이도 이 값을 쓴다(조정창 표시값과 같다). */
wallHeight?: (chainageM: number, role: "inlet" | "outlet") => number | null;
wallHeight?: (chainageM: number, role: "inlet" | "outlet" | `extra${number}`) => number | null;
/** 지금 그려진 벽 형태 — 조정값(m)이 있으면 그쪽이 정본이라 폼도 이 값을 보인다. */
wallForm?: (chainageM: number, role: "inlet" | "outlet") => string | null;
wallForm?: (chainageM: number, role: "inlet" | "outlet" | `extra${number}`) => string | null;
/** 그 측점에서 유입·유출 벽이 각각 좌·우 어느 쪽인가(독립 기슭막이 칸 이름표). */
wallSideLabels?: (chainageM: number) => { inlet: "좌" | "우"; outlet: "좌" | "우" } | null;
/** 측점 간격(m) — 측점번호+잔여거리 환산용. */
@@ -140,8 +140,8 @@ export function withSpecDefaults(
detail: SectionDetailResponse | null,
chainageM: number,
types: StructureType[],
wallHeight?: (chainageM: number, role: "inlet" | "outlet") => number | null,
wallForm?: (chainageM: number, role: "inlet" | "outlet") => string | null,
wallHeight?: (chainageM: number, role: "inlet" | "outlet" | `extra${number}`) => number | null,
wallForm?: (chainageM: number, role: "inlet" | "outlet" | `extra${number}`) => string | null,
): Merged | undefined {
const owner = detail?.cross_sections.find(
(section) => Math.abs(section.chainage_m - chainageM) < PIPE_MATCH_M,
@@ -160,5 +160,16 @@ export function withSpecDefaults(
const drawnHeight = wallHeight?.(chainageM, prefix);
if (drawnHeight != null) merged[`${prefix}_revet_height_m`] = Number(drawnHeight.toFixed(1));
}
// 다단 n 단 조작값 → 흙막이 n 컨테이너(54-10) — 앞 컨테이너가 비었으면 자리만 둠(차례 지킴)
for (let i = 0; i < 9 && !culvert.hidden_pipe; i++) {
const form = wallForm?.(chainageM, `extra${i}`);
const height = wallHeight?.(chainageM, `extra${i}`);
if (!form && height == null) continue;
for (let n = 1; n <= i; n++)
if (!Object.keys(merged).some((key) => key.startsWith(`soil_add_${n}_`)))
merged[`soil_add_${n}_structure`] = "";
if (form) merged[`soil_add_${i + 1}_form`] = form;
if (height != null) merged[`soil_add_${i + 1}_height`] = Number(height.toFixed(1));
}
return merged;
}