- 횡단도 다단 벽 누르기 = 옛 「추가 기슭막이 (n단)」 칸 대신 그 단의 흙막이 컨테이너를 켜고 보임 - 흙막이 n 컨테이너 형태 · 높이 → 다단 n 단 조정값 · 길이 · 전 · 후 → 단별 구간값(높이 한도 · 지형 맞춤 규칙 그대로 · 한도 넘은 요청은 폼에 되돌림) - 그린 다단 형태 · 높이 → 컨테이너 빈 칸 채움(유입 · 유출 벽과 같은 규칙) - 컨테이너 수 = 지형이 세운 단 수 · 넘친 컨테이너 · 단 수 0 의 흙막이 값은 가리기만 하고 파일에 남김 - 시험 test_54_10_soil_tiers · 화면 점검 ui_checks/54-10_다단_흙막이 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
176 lines
7.7 KiB
TypeScript
176 lines
7.7 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_UI_Page_Structures_Spec.ts
|
|
* 횡단 보기 「구조물 배치」 폼의 **기본값 채움** — 조정창 · 도면이 쓰는 스펙 값 중 조작 · 저장분만
|
|
* 폼 옵션에 싣는다(2026-09-14 브레인 판정 ④ 「기본값을 몰래 확정으로 안 바꿈」).
|
|
* `_Page_Structures_Panel` 이 700줄을 넘어 여기로 뗐다(계획서 24-3b).
|
|
* ========================================================================== */
|
|
|
|
import { FORD_BRIDGE_DEFAULT_WIDTH_M } from "@config/config_frontend";
|
|
import type { StructureType } from "../B05_Profile/B05_Profile_Api_Structures";
|
|
import { PIPE_MATCH_M } from "../A00_Common/design/design_selection";
|
|
import { wingInstalled } from "@util/common_util_culvert_sets";
|
|
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
|
|
|
type Merged = Record<string, string | number>;
|
|
|
|
/** 그 종류의 등록부 기본값 `{키: 기본값}` — 스펙 값이 기본값에서 왔는지 가르는 데 씀. */
|
|
function registryDefaults(types: StructureType[], typeId: string): Record<string, unknown> {
|
|
const options = types.find((type) => type.type_id === typeId)?.options ?? [];
|
|
return Object.fromEntries(options.map((option) => [option.key, option.default]));
|
|
}
|
|
|
|
/**
|
|
* 스펙 값 한 칸을 옵션에 싣는 규칙 — **저장돼 있던 칸이거나, 조작해 기본값과 달라진 칸만.**
|
|
* 기본값에서 온 값을 실으면 페이지를 열기만 해도 임시 저장·[저장]에 기본값이 확정으로 굳음
|
|
* (2026-09-14 브레인 판정 ④ 「기본값을 몰래 확정으로 바꾸지 않는다」). 폼은 빈 칸에 제안을 보임.
|
|
*/
|
|
function keepSpec(
|
|
merged: Merged,
|
|
stored: Merged | undefined,
|
|
key: string,
|
|
spec: string | number | null | undefined,
|
|
fromDefault: unknown,
|
|
): void {
|
|
if (spec === null || spec === undefined) return;
|
|
if (stored?.[key] !== undefined || String(spec) !== String(fromDefault ?? "")) merged[key] = spec;
|
|
}
|
|
|
|
type WingSpec = {
|
|
installed: boolean;
|
|
height_m: number | null;
|
|
length_m: number | null;
|
|
angle_deg: number | null;
|
|
};
|
|
|
|
function keepWings(
|
|
merged: Merged,
|
|
stored: Merged | undefined,
|
|
defaults: Record<string, unknown>,
|
|
wings: ReadonlyArray<readonly [WingSpec, "wing_in" | "wing_out"]>,
|
|
): void {
|
|
for (const [wing, prefix] of wings) {
|
|
// 백엔드 `_wing_spec` 과 같은 채움 — 설치는 「없음」이 아니면 있음 · 치수는 기본 0/0/45.
|
|
const install = String(defaults[prefix] ?? "") === "없음" ? "없음" : "있음";
|
|
// 저장 글이 그린 설치와 같은 뜻이면 그대로 둠(BOX 유입·유출 구조 날개벽 · 흙막이 글을 안 덮음)
|
|
if (stored?.[prefix] === undefined || wingInstalled(stored[prefix]) !== wing.installed)
|
|
keepSpec(merged, stored, prefix, wing.installed ? "있음" : "없음", install);
|
|
keepSpec(
|
|
merged,
|
|
stored,
|
|
`${prefix}_height_m`,
|
|
wing.height_m,
|
|
defaults[`${prefix}_height_m`] ?? 0,
|
|
);
|
|
keepSpec(
|
|
merged,
|
|
stored,
|
|
`${prefix}_length_m`,
|
|
wing.length_m,
|
|
defaults[`${prefix}_length_m`] ?? 0,
|
|
);
|
|
keepSpec(
|
|
merged,
|
|
stored,
|
|
`${prefix}_angle_deg`,
|
|
wing.angle_deg,
|
|
defaults[`${prefix}_angle_deg`] ?? 45,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 세월교 폼 옵션 — **지금 그려진 스펙**(`section.ford`) 중 조작·저장분만 싣는다. 조정창 조작이
|
|
* 스펙을 제자리에서 고치므로 그 값은 폼에 보여야 함(2026-08-30 사용자: 수량(련) 칸이 비고 조정창은
|
|
* 1련) — 다만 1련이 **기본값에서 온 값이면** 제안으로만(판정 ④).
|
|
*/
|
|
function withFordSpec(
|
|
options: Merged | undefined,
|
|
ford: NonNullable<SectionDetailResponse["cross_sections"][number]["ford"]>,
|
|
defaults: Record<string, unknown>,
|
|
): Merged {
|
|
const merged: Merged = { ...(options ?? {}) };
|
|
keepSpec(merged, options, "pipe_kind", ford.pipe_kind, defaults.pipe_kind);
|
|
const diameter = Math.round(ford.diameter_m * 1000);
|
|
keepSpec(merged, options, "pipe_diameter_mm", diameter, defaults.pipe_diameter_mm ?? 1000);
|
|
const count = Number(defaults.pipe_count ?? 0);
|
|
keepSpec(
|
|
merged,
|
|
options,
|
|
"pipe_count",
|
|
ford.pipe_count,
|
|
count > 0 ? Math.max(Math.trunc(count), 1) : 1,
|
|
);
|
|
keepSpec(
|
|
merged,
|
|
options,
|
|
"ford_width_m",
|
|
ford.span_m,
|
|
defaults.ford_width_m ?? FORD_BRIDGE_DEFAULT_WIDTH_M,
|
|
);
|
|
keepWings(merged, options, defaults, [
|
|
[ford.wing_in, "wing_in"],
|
|
[ford.wing_out, "wing_out"],
|
|
]);
|
|
return merged;
|
|
}
|
|
|
|
/** BOX암거 폼 옵션 — 세월교와 같은 규칙(조작·저장분만). 구체 길이·표고는 조정 채널이라 여기 안 옴. */
|
|
function withBoxSpec(
|
|
options: Merged | undefined,
|
|
box: NonNullable<SectionDetailResponse["cross_sections"][number]["box"]>,
|
|
defaults: Record<string, unknown>,
|
|
): Merged {
|
|
const merged: Merged = { ...(options ?? {}) };
|
|
keepSpec(merged, options, "body_width_m", box.inner_width_m, defaults.body_width_m ?? 2);
|
|
keepSpec(merged, options, "body_height_m", box.inner_height_m, defaults.body_height_m ?? 2);
|
|
keepWings(merged, options, defaults, [
|
|
[box.wing_in, "wing_in"],
|
|
[box.wing_out, "wing_out"],
|
|
]);
|
|
return merged;
|
|
}
|
|
|
|
/**
|
|
* 관 지점 옵션에 **조정창에서 조작한 값**만 싣는다 — 조정창과 폼이 어긋나지 않게(2026-08-29 사용자).
|
|
* ⚠ 스펙의 기본값(관종·관경·구조·벽 길이·전/후·집수정 길이)은 **안 실음** — 페이지를 열기만 해도
|
|
* 임시 저장에 기본값이 확정으로 굳던 자리(2026-09-14 브레인 판정 ④). 폼 빈 칸은 제안을 보임.
|
|
*/
|
|
export function withSpecDefaults(
|
|
options: Merged | undefined,
|
|
detail: SectionDetailResponse | null,
|
|
chainageM: number,
|
|
types: StructureType[],
|
|
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,
|
|
);
|
|
// 세월교·BOX는 폼과 조정창이 **같은 캐시**를 봄 — 조작한 값은 저장된 옵션보다 앞섬(2026-08-30).
|
|
if (owner?.ford) return withFordSpec(options, owner.ford, registryDefaults(types, "ford_bridge"));
|
|
if (owner?.box) return withBoxSpec(options, owner.box, registryDefaults(types, "box_culvert"));
|
|
const culvert = owner?.culvert;
|
|
if (!culvert) return options;
|
|
const merged: Merged = { ...(options ?? {}) };
|
|
for (const prefix of ["inlet", "outlet"] as const) {
|
|
// 형태·높이는 **조작한 값**이 이긴다 — 조정값이 저장된 옵션보다 앞서기 때문(2026-08-30 사용자:
|
|
// 배관 유출구 기슭막이 형태가 폼과 다르게 그려졌다). 조작이 없으면 부르는 쪽이 null 을 줌.
|
|
const drawnForm = wallForm?.(chainageM, prefix);
|
|
if (drawnForm) merged[`${prefix}_revet_form`] = drawnForm;
|
|
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;
|
|
}
|