- 폼 옵션 아래 「집계 항목」 줄(이름 · 미지정 · [고르기] · [해제]) — 구조물 · 관 계열 하위 폼 · B05 · B06 한 코드 - 옵션 `visible:false` 는 폼에서 숨김(정의 · 옛 값 그대로) · [수정]은 폼이 그린 칸만 갈아 끼우고 안 그린 칸(상세 · 숨긴 칸) 보존 — 옵션 덮어쓰기 흠 고침 - 목록 줄 `.is-assigned` 색 · 툴팁 항목 이름 · 캐시는 지금 초안 기록 칸(structures · pipes) 안 summary_item - 미지정이면 칸을 안 실음 · 관은 서버 응답에 칸이 없으면 같은 측점에서 화면이 이어받음 - 항목 고르기 모달은 21-5 가 오기 전까지 같은 이름의 임시 함수 · Structures_Panel 700줄 넘어 옵션 그리기를 Panel_Options 로 나눔 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TULoa94ZFL26KU6ZqVpjkF
120 lines
6.2 KiB
TypeScript
120 lines
6.2 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_Structures_Panel_Options.ts
|
|
* 구조물 배치 패널의 **옵션 칸 그리기** — 타입의 옵션 스키마대로 입력 칸을 다시 그린다.
|
|
*
|
|
* `B05_Profile_UI_Structures_Panel` 이 700줄을 넘겨 떼어낸 조각이다(2026-09-29 · PLAN 21-4).
|
|
* 그리는 규칙은 옮기기 전 그대로이고, 패널 클로저가 쥐던 값만 `ctx` 로 받는다.
|
|
* 새로 얹은 것 하나 — 서버가 `visible: false` 로 채운 옵션은 그리지 않는다(정의 · 옛 값은 그대로,
|
|
* 저장 때 안 그린 칸은 보존한다 → `B05_Profile_UI_Structures_Summary.mergeStructureOptions`).
|
|
* ========================================================================== */
|
|
|
|
import { isB05Option, type StructureType } from "./B05_Profile_Api_Structures";
|
|
import type { PipeFacility } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
import { field, optionControl, suggestButton } from "./B05_Profile_UI_Structures_Fields";
|
|
import { isOptionVisible } from "./B05_Profile_UI_Structures_Summary";
|
|
|
|
/** 패널이 쥔 값 중 옵션 그리기에 필요한 것들. 이름은 분리 전 지역변수와 같다. */
|
|
export interface OptionRenderContext {
|
|
optionInputs: Array<{
|
|
key: string;
|
|
required: boolean;
|
|
input: HTMLInputElement | HTMLSelectElement;
|
|
read: () => string | number;
|
|
isEmpty: () => boolean;
|
|
}>;
|
|
optionRow: HTMLElement;
|
|
/** B06/B07 은 상세(detail) 옵션까지 그린다. */
|
|
includeDetail: boolean;
|
|
currentType: () => StructureType | null;
|
|
facilityFormKind: (type: StructureType | null) => PipeFacility | null;
|
|
liveCommit: () => void;
|
|
syncOptionLock: () => void;
|
|
syncRangeDisplay: () => void;
|
|
syncSplitInputs: (source: "length" | "before_m" | "after_m", normalizeSource: boolean) => void;
|
|
}
|
|
|
|
/** 타입의 옵션 스키마대로 입력 칸을 다시 그린다. 상세(detail)는 `includeDetail` 인 화면
|
|
* (B06/B07)에서만 — 안 받으면 뒷길이·돌규격·형식이 비어 **수량이 갈래를 못 고른다**. */
|
|
export function renderOptionFields(
|
|
ctx: OptionRenderContext,
|
|
values: Record<string, string | number> = {},
|
|
): void {
|
|
const { optionInputs, optionRow } = ctx;
|
|
const type = ctx.currentType();
|
|
// 여기도 배열을 갈아끼우지 않는다 — 조각들이 참조로 받아 두므로 새 배열로
|
|
// 바꾸면 옛 칸 목록을 읽어 저장값이 어긋난다(2026-09-04).
|
|
optionInputs.length = 0;
|
|
optionRow.replaceChildren();
|
|
// 서브폼이 담당하는 타입(계곡 통과 시설·독립 기슭막이)은 여기서 그리지 않는다.
|
|
const shown =
|
|
type && !ctx.facilityFormKind(type)
|
|
? type.options.filter((o) => (ctx.includeDetail || isB05Option(o)) && isOptionVisible(o))
|
|
: [];
|
|
if (!shown.length) {
|
|
optionRow.hidden = true;
|
|
ctx.syncOptionLock();
|
|
return;
|
|
}
|
|
optionRow.hidden = false;
|
|
const suggestions: Array<{ input: HTMLInputElement | HTMLSelectElement; suggested: string }> = [];
|
|
shown.forEach((option) => {
|
|
// ⭐ 저장된 값만 칸에 · 등록부 기본값은 제안(회색 글씨·빈 보기 이름)으로만 — 고르기 칸은
|
|
// 첫 보기를 미리 안 고름(2026-09-14 브레인 판정 ①②, `optionControl` 주석).
|
|
const input = optionControl(option, values[option.key]);
|
|
suggestions.push({ input, suggested: String(option.default ?? "") });
|
|
const label = `${option.unit ? `${option.label} (${option.unit})` : option.label}${option.not_in_table ? " · 표에 안 쓰임" : ""}`;
|
|
// `enabled:false` 는 칸을 남기고 잠그기만 한다 — 값은 기본값이 그대로 저장된다
|
|
// (고를 수 없는 칸이라 제안이 아니라 프로그램 값).
|
|
const locked = option.enabled === false;
|
|
if (locked && !input.value) input.value = String(option.default ?? "");
|
|
if (locked) {
|
|
input.disabled = true;
|
|
input.classList.add("is-locked");
|
|
input.title = "지금은 고를 수 없는 항목입니다.";
|
|
}
|
|
optionRow.append(field(label, input, option.default_basis));
|
|
input.addEventListener("change", () => ctx.liveCommit());
|
|
optionInputs.push({
|
|
key: option.key,
|
|
required: !locked && !!option.required,
|
|
input,
|
|
read: () => (option.input === "number" ? Number(input.value) || 0 : input.value),
|
|
isEmpty: () => input.value.trim() === "",
|
|
});
|
|
// 전면 기울기는 품셈 표준경사 표(0.20~0.50) 밖 값도 받는다 — 실무 도면이 S0.7·S0.8 을
|
|
// 쓴다(2026-09-09 실무 DWG 확인). **막지 않고 알리기만** 한다: 막으면 실물이 안 들어간다.
|
|
if (option.key === "face_slope_ratio") {
|
|
const warn = (): void => {
|
|
const value = Number(input.value);
|
|
const outside =
|
|
input.value.trim() !== "" && Number.isFinite(value) && (value < 0.2 || value > 0.5);
|
|
input.title = outside
|
|
? "품셈 표준경사 표 범위(0.20~0.50) 밖 값입니다 — 근거를 적어 두시기 바랍니다."
|
|
: "비워 두면 품셈 표대로 자동 판정합니다.";
|
|
input.classList.toggle("is-outside-standard", outside);
|
|
};
|
|
warn();
|
|
input.addEventListener("input", warn);
|
|
input.addEventListener("change", warn);
|
|
}
|
|
// 길이·기준측점 전/후가 바뀌면 서로 연동하고 측점 범위 표시가 즉시 따라온다
|
|
// (범위 계산 타입, 2026-08-19). 전·후는 **나중에 고친 쪽**이 살아남고 반대쪽이
|
|
// 길이 − 그 값으로 맞춰진다. 길이를 고치면 나중에 고쳤던 쪽을 지키며 재배분.
|
|
if (option.key === "length_m" || option.key === "before_m" || option.key === "after_m") {
|
|
const source = (option.key === "length_m" ? "length" : option.key) as
|
|
"length" | "before_m" | "after_m";
|
|
const refresh = (normalize: boolean): void => {
|
|
input.classList.remove("is-invalid");
|
|
ctx.syncSplitInputs(source, normalize);
|
|
};
|
|
input.addEventListener("input", () => refresh(false));
|
|
input.addEventListener("change", () => refresh(true));
|
|
}
|
|
});
|
|
if (suggestions.some((entry) => entry.suggested)) {
|
|
optionRow.append(suggestButton(suggestions));
|
|
}
|
|
ctx.syncOptionLock();
|
|
ctx.syncRangeDisplay();
|
|
}
|