/* ============================================================================= * ui_template_sheet_header.ts * 여러 층 머리 짜기 — 열마다 `머리`(층 수만큼 글) → 층마다 칸(colspan · rowspan). * * `null` = 위 칸과 합침(세로) · 모자란 층도 `null` 로 채움(위 칸이 늘어남). * 옆 열과 그 층까지 앞머리가 모두 같고 아래 합침 모양도 같으면 가로로 합침. * ========================================================================== */ import type { SheetColumn } from "./ui_template_sheet_types"; export interface HeadCell { level: number; col: number; colspan: number; rowspan: number; label: string; } export function headDepth(cols: SheetColumn[], layers?: string[]): number { return Math.max(1, layers?.length ?? 0, ...cols.map((c) => c.머리.length)); } /** 층마다 칸 목록 — 앞 층부터. */ export function layoutHead(cols: SheetColumn[], depth: number): HeadCell[][] { const paths = cols.map((c) => Array.from({ length: depth }, (_, i) => (i === 0 ? (c.머리[0] ?? "") : (c.머리[i] ?? null))), ); const span = (j: number, i: number): number => { let k = i + 1; while (k < depth && paths[j][k] === null) k += 1; return k - i; }; const samePrefix = (a: number, b: number, i: number): boolean => paths[a].slice(0, i + 1).every((label, lv) => label === paths[b][lv]); const levels: HeadCell[][] = []; for (let i = 0; i < depth; i += 1) { const cells: HeadCell[] = []; let j = 0; while (j < cols.length) { if (paths[j][i] === null) { j += 1; continue; } const rowspan = span(j, i); let colspan = 1; while ( j + colspan < cols.length && paths[j + colspan][i] !== null && samePrefix(j, j + colspan, i) && span(j + colspan, i) === rowspan ) { colspan += 1; } cells.push({ level: i, col: j, colspan, rowspan, label: paths[j][i] as string }); j += colspan; } levels.push(cells); } return levels; }