- 구조물마다 조합 줄 뒤에 좌측 패널 옵션 줄 — 폼과 같은 컨테이너 · 이름 · 보이는 칸(`structureOptionLines`) · 구조 갈래에 안 맞는 옛 값 뺌 · 필수 상자 빈 칸 = 폼 기본값 - B06 [상세] 모달 = 화면 TS 직접 · B07 = 같은 TS 를 Node 번들(B07_DesignDetail_Note_Options_Node · build:b07-note)로 받아 파이썬 짝이 붙임 · 실패는 조합 줄만 - 번들 낡음 판정에 A00_Common/design 더함 - 시험 test_166_1(진짜 명세 · B07 = 화면 같은 줄) · test_260 · test_265 기대값 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
213 lines
9.4 KiB
TypeScript
213 lines
9.4 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_Structures_Balloon.ts
|
|
* 종단 구조물 알약 정보 라벨 줄(54-14) — 화면 요소 없는 순수 규칙. B05 · B06 알약 레인이 같이 씀.
|
|
*
|
|
* · 2행 — 1행 = 종류 이름(기타 = 손 이름) + 기준 측점(구간 = 시작 ~ 끝) · 2행 = 컨테이너마다 첫 옵션
|
|
* 값만 「 · 」 로 이음(제목 · 옵션 이름 없음 · 빈 값은 건너뜀) · 값이 하나도 없으면 1행만 · 메모 · 그 밖 옵션은 안 씀
|
|
* · 컨테이너 = 좌측 패널 폼과 같은 상자 — 명세 종류(`STRUCTURE_SPECS`)는 명세 컨테이너(선택 컨테이너는
|
|
* 인스턴스마다) · 그 밖 종류는 정의 옵션 그룹(`StructureType.groups`)
|
|
* · 첫 옵션 = 지금 보이는 첫 입력 칸(자동 칸 · 갈래가 없어 가린 칸 · 끈 칸은 건너뜀)
|
|
* · 값 `auto` = 「(자동)」 · 빈 값은 건너뜀(기본값으로 안 바꿈 · 기본값은 새 구조물 칸에 실제 값)
|
|
* · `structureOptionLines`(166-1) = 같은 컨테이너의 **보이는 칸 전부** 「제목: 이름 값 · …」 — B06 [상세] CAD 칸
|
|
* 미리 넣음 · B07 짝(서버 Node `B07_DesignDetail_Note_Options_Node`)이 같이 씀
|
|
* ========================================================================== */
|
|
|
|
import type { StructureInstance, StructureType } from "./B05_Profile_Api_Structures";
|
|
import { shapeFields, shapeKey } from "./B05_Profile_UI_Structures_Define";
|
|
import { STRUCTURE_SPECS } from "../A00_Common/design/design_structure_pipe";
|
|
import { bodyOf, boxOptionsIn } from "../A00_Common/design/design_structure_box";
|
|
import { withLegacyGuards } from "../A00_Common/design/design_structure_ford";
|
|
import {
|
|
branchOf,
|
|
choiceLabel,
|
|
choicesOf,
|
|
containerTitle,
|
|
itemLabel,
|
|
readState,
|
|
type Instance,
|
|
type SpecContainer,
|
|
type SpecItem,
|
|
type StructureSpec,
|
|
} from "../A00_Common/design/design_structure_spec";
|
|
|
|
const AUTO_MARK = "(자동)";
|
|
const SEP = " · ";
|
|
|
|
/** 선택 컨테이너 개수를 밖에서 정함(배관 흙막이 = 유출 단 수 · 폼 `hooks.count` 와 같은 값). */
|
|
export type ContainerCount = (containerId: string) => number | null | undefined;
|
|
|
|
/** 라벨 줄 — [이름 기준 측점, 컨테이너 값 이음?]. */
|
|
export function balloonLines(
|
|
structure: StructureInstance,
|
|
type: StructureType | undefined,
|
|
head: { name: string; position: string },
|
|
count?: ContainerCount,
|
|
): string[] {
|
|
const spec = STRUCTURE_SPECS[structure.type_id];
|
|
const values = (
|
|
spec ? specLines(spec, structure, type, count) : groupLines(structure.options ?? {}, type)
|
|
).filter(Boolean);
|
|
const first = `${head.name} ${head.position}`;
|
|
return values.length ? [first, values.join(SEP)] : [first];
|
|
}
|
|
|
|
/** 명세 이름 바꿈 표 — 배관은 정의 파일의 옛 이름 「흙막이 추가」보다 명세 이름 「흙막이」가 앞섬(54-1). */
|
|
function specNames(structure: StructureInstance, type: StructureType | undefined) {
|
|
const names = type?.spec_names ?? {};
|
|
if (structure.type_id !== "pipe") return names;
|
|
const { soil_add: _old, ...pipeNames } = names;
|
|
return pipeNames;
|
|
}
|
|
|
|
/** 명세 컨테이너 값 — 폼(`Drainage_Facility.setFacility` · `paintStructureSpec`)과 같은 값 · 이름(빈 값 = ""). */
|
|
function specLines(
|
|
spec: StructureSpec,
|
|
structure: StructureInstance,
|
|
type: StructureType | undefined,
|
|
count?: ContainerCount,
|
|
): string[] {
|
|
const names = specNames(structure, type);
|
|
const state = formState(spec, structure.type_id, structure.options ?? {});
|
|
return spec.containers.flatMap((c) =>
|
|
instancesOf(c, state[c.id] ?? [], count).map((values) => {
|
|
const item = c.items.find(
|
|
(entry) => entry.kind !== "auto" && (!entry.by || branchOf(entry, values)),
|
|
);
|
|
return item ? specValue(c, item, values, names) : "";
|
|
}),
|
|
);
|
|
}
|
|
|
|
/** 저장 값 → 폼 칸 값 — 폼(`Drainage_Facility.setFacility`)과 같은 손질(BOX 옛 설치 · 본체 규격 ·
|
|
* 세월교 옛 날개벽). */
|
|
function formState(
|
|
spec: StructureSpec,
|
|
typeId: string,
|
|
options: Record<string, string | number>,
|
|
): Record<string, Instance[]> {
|
|
const input =
|
|
typeId === "box_culvert"
|
|
? boxOptionsIn(options)
|
|
: typeId === "ford_bridge"
|
|
? withLegacyGuards(options)
|
|
: options;
|
|
const state = readState(spec, input);
|
|
// 본체 규격은 저장 안 함 — 폭 · 높이로 되찾음
|
|
if (typeId === "box_culvert") state.body = [{ ...state.body?.[0], ...bodyOf(options) }];
|
|
return state;
|
|
}
|
|
|
|
/** 보이는 인스턴스 — 필수 = 한 벌 · 선택 = 개수가 정해졌으면 그 수(모자라면 빈 인스턴스) · 아니면 저장된 것. */
|
|
function instancesOf(c: SpecContainer, list: Instance[], count?: ContainerCount): Instance[] {
|
|
if (c.required) return [list[0] ?? {}];
|
|
const n = count?.(c.id);
|
|
return n === undefined || n === null
|
|
? list
|
|
: Array.from({ length: n }, (_, index) => list[index] ?? {});
|
|
}
|
|
|
|
function specValue(
|
|
c: SpecContainer,
|
|
item: SpecItem,
|
|
values: Instance,
|
|
names: Record<string, string>,
|
|
): string {
|
|
const shown = (raw: string): string => {
|
|
const choice = choicesOf(item, values).find((ch) => ch.value === raw);
|
|
return choice ? choiceLabel(c, item, choice, names) : raw;
|
|
};
|
|
const raw = (values[item.id] ?? "").trim();
|
|
if (raw === "auto") return AUTO_MARK;
|
|
return raw ? shown(raw) : "";
|
|
}
|
|
|
|
/** 정의 옵션 그룹 값 — 그룹마다 첫 옵션(정의에 있고 켜진 칸 · 빈 값 = ""). */
|
|
function groupLines(
|
|
options: Record<string, string | number>,
|
|
type: StructureType | undefined,
|
|
): string[] {
|
|
const byKey = new Map((type?.options ?? []).map((option) => [option.key, option]));
|
|
return (type?.groups ?? []).map((group) => {
|
|
const option = group.options
|
|
.map((key) => byKey.get(key))
|
|
.find((entry) => entry && entry.visible !== false && entry.enabled !== false);
|
|
if (!option) return "";
|
|
const raw = options[option.key];
|
|
if (raw === "auto") return AUTO_MARK;
|
|
return raw !== undefined && raw !== "" ? `${raw}${option.unit ?? ""}` : "";
|
|
});
|
|
}
|
|
|
|
/** 「이름 (단위)」 칸 이름 + 값 → 「이름 값단위」(예 높이 (m) + 1.3 = 높이 1.3m) · 단위 없는 이름은 그대로 띄움. */
|
|
function nameValue(label: string, shown: string): string {
|
|
const hit = /^(.*?)\s*\(([^()]+)\)$/.exec(label);
|
|
return hit && shown !== AUTO_MARK ? `${hit[1]} ${shown}${hit[2]}` : `${label} ${shown}`;
|
|
}
|
|
|
|
/**
|
|
* CAD 칸 미리 넣음 옵션 줄(166-1) — 좌측 패널 폼과 같은 컨테이너 · 이름 · 보이는 칸 전부 · 빈 칸 뺌.
|
|
* 명세 종류 = 컨테이너(선택 컨테이너는 저장된 인스턴스마다 · 뒤에 번호)마다 「제목: 이름 값 · 이름 값」 ·
|
|
* 그 밖 종류 = 정의 그룹마다(그룹 형상 칸 포함) + 그룹 밖 칸은 종류 이름 줄 · 값 하나 없는 상자는 줄 없음.
|
|
*/
|
|
export function structureOptionLines(
|
|
structure: Pick<StructureInstance, "type_id" | "options">,
|
|
type: StructureType | undefined,
|
|
): string[] {
|
|
const options = structure.options ?? {};
|
|
const line = (title: string, parts: string[]): string[] => {
|
|
const kept = parts.filter(Boolean);
|
|
return kept.length ? [`${title}: ${kept.join(SEP)}`] : [];
|
|
};
|
|
const spec = STRUCTURE_SPECS[structure.type_id];
|
|
if (spec) {
|
|
const names = specNames(structure as StructureInstance, type);
|
|
const state = formState(spec, structure.type_id, options);
|
|
return spec.containers.flatMap((c) =>
|
|
(state[c.id] ?? []).flatMap((values, index) => {
|
|
const group = type?.groups?.find((g) => g.id === c.link)?.name;
|
|
const title = containerTitle(c, names, group) + (c.required ? "" : ` ${index + 1}`);
|
|
const parts = c.items
|
|
.filter((item) => item.kind !== "auto" && (!item.by || branchOf(item, values)))
|
|
.map((item) => {
|
|
// 필수 상자 빈 칸 = 폼이 채워 보이는 기본값(177-2 · 예 동물이동형 유) · 선택 상자는 안내뿐이라 뺌
|
|
const empty = !values[item.id]?.trim() && c.required && item.default;
|
|
const filled = empty ? { ...values, [item.id]: item.default ?? "" } : values;
|
|
const shown = specValue(c, item, filled, names);
|
|
return shown ? nameValue(itemLabel(c, item, names), shown) : "";
|
|
});
|
|
return line(title, parts);
|
|
}),
|
|
);
|
|
}
|
|
const byKey = new Map((type?.options ?? []).map((option) => [option.key, option]));
|
|
const valueOf = (key: string, unit?: string | null): string => {
|
|
const raw = options[key];
|
|
if (raw === undefined || raw === null || raw === "") return "";
|
|
return raw === "auto" ? AUTO_MARK : `${raw}${unit ?? ""}`;
|
|
};
|
|
const optionParts = (keys: string[]): string[] =>
|
|
keys.map((key) => {
|
|
const option = byKey.get(key);
|
|
if (!option || option.visible === false || option.enabled === false) return "";
|
|
const shown = valueOf(key, option.unit);
|
|
return shown ? `${option.label} ${shown}` : "";
|
|
});
|
|
const groups = type?.groups ?? [];
|
|
const grouped = new Set(groups.flatMap((group) => group.options));
|
|
return [
|
|
...groups.flatMap((group) =>
|
|
line(group.name, [
|
|
...optionParts(group.options),
|
|
...shapeFields(group.shape).map((field) => {
|
|
const shown = valueOf(shapeKey(group.id, field.suffix));
|
|
return shown ? nameValue(field.label, shown) : "";
|
|
}),
|
|
]),
|
|
),
|
|
...line(
|
|
type?.name ?? structure.type_id,
|
|
optionParts((type?.options ?? []).map((o) => o.key).filter((key) => !grouped.has(key))),
|
|
),
|
|
];
|
|
}
|