/* ============================================================================= * M02_MasterTemplete_AreaFromDrawing.ts * 도면 → 면적산출 한 번 옮기기(PLAN 83-3 · 연동 아님) — 오른쪽 도면 패널(`AreaDrawing`)이 받은 표 자료를 * 면적 통합문서로 — 받은 뒤 작은 창 [새 시트](「도면에서 n」) / [고른 칸부터](지금 시트 · 덮으면 물음 · 105-1). * 병합만 옮김 — 칸 크기 · 글 크기는 기본(도면 크기 안 옮김). * 표 자료 꼴 = CAD `helpers/table-from-drawing.ts` 의 `DrawingTable`(rows · merges · colWidths · rowHeights · 도면 mm). * 싣기는 공개 손잡이 — 새 시트 `addSheet` · 고른 칸 `pasteTable`(붙여넣기 길) · 둘 다 되돌리기 한 번 · onChange. * ========================================================================== */ import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import type { PasteCheck } from "../A00_Common/spreadsheet/spreadsheet_clipboard"; import type { Cell, Sheet, Workbook } from "../A00_Common/spreadsheet/spreadsheet_types"; const TEXT = { done: (name: string) => `도면 표를 「${name}」 시트로 옮김 — [저장]을 눌러야 저장 · 되돌리기 Ctrl+Z`, pasted: (range: string) => `도면 표를 ${range} 에 넣음 — [저장]을 눌러야 저장 · 되돌리기 Ctrl+Z`, loose: "표 선을 못 찾아 글 자리로 줄 · 칸을 나눔 — 칸이 맞는지 확인", where: (rows: number, cols: number) => `도면 표(${rows}행 × ${cols}열)를 어디에 넣을지`, cancel: "취소", atCell: "고른 칸부터", newSheet: "새 시트", overwrite: (range: string, cells: number, links: number) => `${range} 에 값 있는 칸 ${cells}개${links ? ` · 알약 이은 칸 ${links}개` : ""} — 덮어씀?`, overwriteOk: "덮어쓰기", noHandle: "스프레드시트가 시트 추가를 아직 못 함 — 옮기지 않음", noSheet: "면적산출 시트가 열려 있지 않음", }; /** CAD 가 보낸 표 자료(`DrawingTable`) */ export interface DrawingTable { kind: "table" | "grid" | "text"; rows: (string | number)[][]; merges: { row: number; col: number; rowSpan: number; colSpan: number }[]; colWidths: number[]; rowHeights: number[]; } export const SHEET_PREFIX = "도면에서"; /** 0 → `A` · 27 → `AB`(`spreadsheet_address.colName` 과 같은 규칙) */ function colName(c: number): string { let s = ""; for (let n = c + 1; n > 0; n = Math.floor((n - 1) / 26)) s = String.fromCharCode(65 + ((n - 1) % 26)) + s; return s; } const a1 = (r: number, c: number): string => `${colName(c)}${r + 1}`; /** 「도면에서 n」 — 안 쓴 가장 작은 n */ export function nextSheetName(book: Pick): string { const used = new Set(book.시트.map((s) => s.이름)); let n = 1; while (used.has(`${SHEET_PREFIX} ${n}`)) n += 1; return `${SHEET_PREFIX} ${n}`; } /** 안 쓴 시트 id(`s1` …) */ function nextSheetId(book: Pick): string { const used = new Set(book.시트.map((s) => s.id)); let n = book.시트.length + 1; while (used.has(`s${n}`)) n += 1; return `s${n}`; } /** 표 자료 → 새 시트(이름 · id 는 문서에서 안 쓴 것 · 칸 크기 · 글 크기는 기본) */ export function tableToSheet(table: DrawingTable, book: Pick): Sheet { const 칸: Record = {}; table.rows.forEach((line, r) => line.forEach((value, c) => { if (value === "" || value === null || value === undefined) return; 칸[a1(r, c)] = { 값: value }; }), ); const 병합 = table.merges .filter((m) => m.rowSpan > 1 || m.colSpan > 1) .map((m) => `${a1(m.row, m.col)}:${a1(m.row + m.rowSpan - 1, m.col + m.colSpan - 1)}`); const sheet: Sheet = { id: nextSheetId(book), 이름: nextSheetName(book), 칸 }; if (병합.length) sheet.병합 = 병합; return sheet; } const escape = (v: string | number): string => String(v).replace(/&/g, "&").replace(//g, ">"); /** 표 자료 → `` 글(병합 = rowspan · colspan) — 손잡이 `pasteTable` 이 붙여넣기 길로 읽음 */ export function tableToHtml(table: DrawingTable): string { const cols = Math.max(0, ...table.rows.map((line) => line.length)); const span = new Map(); const covered = new Set(); for (const m of table.merges) { span.set(`${m.row},${m.col}`, m); for (let r = m.row; r < m.row + m.rowSpan; r += 1) for (let c = m.col; c < m.col + m.colSpan; c += 1) if (r !== m.row || c !== m.col) covered.add(`${r},${c}`); } const rows = table.rows.map((line, r) => { let tds = ""; for (let c = 0; c < cols; c += 1) { if (covered.has(`${r},${c}`)) continue; const m = span.get(`${r},${c}`); const attrs = (m && m.rowSpan > 1 ? ` rowspan="${m.rowSpan}"` : "") + (m && m.colSpan > 1 ? ` colspan="${m.colSpan}"` : ""); tds += `${escape(line[c] ?? "")}`; } return `${tds}`; }); return `
${rows.join("")}
`; } export type Where = "sheet" | "cell" | null; /** 넣을 자리 작은 창 — [취소] [고른 칸부터] [새 시트](주 단추 · 기본) · Esc = 취소 */ export function askWhere(rows: number, cols: number): Promise { return new Promise((resolve) => { const overlay = el("div", { className: "ui-confirm" }); const done = (got: Where): void => { overlay.remove(); resolve(got); }; const main = createButton({ label: TEXT.newSheet, variant: "filled", onClick: () => done("sheet"), }); const actions = el("div", { className: "ui-confirm__actions", children: [ createButton({ label: TEXT.cancel, variant: "ghost", onClick: () => done(null) }), createButton({ label: TEXT.atCell, onClick: () => done("cell") }), main, ], }); overlay.dataset.areaWhere = ""; overlay.addEventListener("keydown", (ev) => { if (ev.key !== "Escape") return; ev.stopPropagation(); done(null); }); overlay.append( el("div", { className: "ui-confirm__panel", children: [ el("p", { className: "ui-confirm__message", text: TEXT.where(rows, cols) }), actions, ], }), ); document.body.append(overlay); main.focus(); }); } /** 덮을 칸(값 · 알약)이 있으면 물음 */ const askOverwrite: PasteCheck = (hit) => !hit.칸 && !hit.연결 ? true : showConfirmDialog(TEXT.overwrite(hit.범위, hit.칸, hit.연결), TEXT.overwriteOk); /** 스프레드시트 공개 손잡이 중 쓰는 것 — 새 시트(탭 줄은 부품 몫) · 고른 칸부터 붙이기 */ export interface AddSheetHandle { getDoc(): Workbook; addSheet?: (sheet: Sheet, opts?: { activate?: boolean }) => unknown; pasteTable?: (html: string, check?: PasteCheck) => Promise; } /** 받은 표 → 넣을 자리 물음 → 새 시트(활성) 또는 지금 시트 고른 칸부터 · 알림 — * 옮긴 시트 이름 · 붙인 범위 · 못 하거나 그만두면 null */ export async function importDrawingTable( handle: AddSheetHandle | null, table: DrawingTable, choose: (rows: number, cols: number) => Promise = askWhere, ): Promise { if (!handle?.addSheet) { showToast(handle ? TEXT.noHandle : TEXT.noSheet, "error"); return null; } const cols = Math.max(0, ...table.rows.map((line) => line.length)); const where = await choose(table.rows.length, cols); if (!where) return null; let done: string | null; if (where === "cell") { if (!handle.pasteTable) { showToast(TEXT.noHandle, "error"); return null; } done = await handle.pasteTable(tableToHtml(table), askOverwrite); if (!done) return null; showToast(TEXT.pasted(done), "success"); } else { const sheet = tableToSheet(table, handle.getDoc()); handle.addSheet(sheet, { activate: true }); done = sheet.이름; showToast(TEXT.done(done), "success"); } if (table.kind === "text") showToast(TEXT.loose, "info"); return done; }