/* ============================================================================= * B08_Quantity_Summary_Data.ts * 구조물 집계표의 줄 · 열 고르기(화면 없음 · PLAN 61-1). * * 줄 = 구조물 · 배관이 놓인 측점(같은 측점은 한 줄 · 오름차순) * 열 = 저장된 구조물 조합(본체 `summary_item` + 그룹 `group_items`) — 같은 층 · 주인 · 키는 한 열 * * 읽기만 — B05/B06 저장 자료(structures.json · pipe_points.json)를 고치지 않는다. * 런타임 import 는 `common_util_summary_item` 하나 — 시험이 두 파일을 이어 붙여 돌림. * ========================================================================== */ import { readGroupItems, readSummaryItem, type SummaryItem, } from "../common_util/common_util_summary_item"; /** 구조물 · 관 지점 한 건 — 필요한 칸만. 구간형은 기준점(`chainage_m`) · 없으면 시점. */ export interface PlacedItem { chainage_m?: number | null; start_m?: number | null; summary_item?: unknown; group_items?: unknown; } /** M02 조합 정보에서 읽은 구분 · 이름 — 못 찾으면 null(저장된 이름으로 보임). */ export interface ComboLabel { group: string; name: string; } export interface ComboColumn { /** `{층}|{주인}|{키}` — 항목 고르기 `rowId` 와 같은 꼴 */ id: string; item: SummaryItem; group: string; name: string; /** M02 에 그 조합이 없음 */ missing: boolean; } export const comboId = (item: SummaryItem): string => `${item.layer}|${item.owner ?? ""}|${item.key}`; function stationOf(thing: PlacedItem): number | null { const value = thing.chainage_m ?? thing.start_m; return typeof value === "number" && Number.isFinite(value) ? value : null; } /** 놓인 측점 — 1cm 안이면 같은 측점 · 오름차순. */ export function summaryStations(things: readonly PlacedItem[]): number[] { const seen = new Map(); for (const thing of things) { const station = stationOf(thing); if (station !== null) seen.set(Math.round(station * 100), station); } return [...seen.values()].sort((a, b) => a - b); } /** 저장된 조합 열 — 구분 · 이름 차례(같은 구분은 붙어 머리 칸을 합침). */ export function summaryColumns( things: readonly PlacedItem[], describe: (item: SummaryItem) => ComboLabel | null, ): ComboColumn[] { const found = new Map(); for (const thing of things) { const own = readSummaryItem(thing.summary_item); for (const item of [...(own ? [own] : []), ...Object.values(readGroupItems(thing.group_items))]) if (!found.has(comboId(item))) found.set(comboId(item), item); } const columns = [...found].map(([id, item]): ComboColumn => { const label = describe(item); return label ? { id, item, ...label, missing: false } : { id, item, group: "", name: item.name || item.key, missing: true }; }); return columns.sort( (a, b) => a.group.localeCompare(b.group, "ko") || a.name.localeCompare(b.name, "ko") || a.id.localeCompare(b.id), ); } /** 머리 첫 줄 — 이어진 같은 구분을 한 칸으로 `[구분, 열 수]`. */ export function groupSpans(columns: readonly ComboColumn[]): Array<[string, number]> { const spans: Array<[string, number]> = []; for (const column of columns) { const last = spans[spans.length - 1]; if (last && last[0] === column.group) last[1] += 1; else spans.push([column.group, 1]); } return spans; }