Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_AreaFromDrawing.ts
T
eomsangdonandClaude Opus 5.5 01d819bfb6 feat(m02): 도면 → 면적 칸 크기 기본 · 넣을 자리 고르기(105-1)
- 도면 표 받은 뒤 작은 창 [새 시트] / [고른 칸부터] · 칸 크기 · 글 크기는 기본(도면 mm 안 옮김)
- 스프레드시트 공개 손잡이 pasteTable — 붙여넣기 길(parseClipboard + pasteBlock) · 덮을 값 · 알약 칸 셈(pasteHits)
- 값 · 알약 이은 칸을 덮으면 물음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
2026-10-08 16:15:54 +09:00

200 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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<Workbook, "시트">): 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<Workbook, "시트">): 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<Workbook, "시트">): Sheet {
const 칸: Record<string, Cell> = {};
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
/** 표 자료 → `<table>` 글(병합 = rowspan · colspan) — 손잡이 `pasteTable` 이 붙여넣기 길로 읽음 */
export function tableToHtml(table: DrawingTable): string {
const cols = Math.max(0, ...table.rows.map((line) => line.length));
const span = new Map<string, DrawingTable["merges"][number]>();
const covered = new Set<string>();
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 += `<td${attrs}>${escape(line[c] ?? "")}</td>`;
}
return `<tr>${tds}</tr>`;
});
return `<table>${rows.join("")}</table>`;
}
export type Where = "sheet" | "cell" | null;
/** 넣을 자리 작은 창 — [취소] [고른 칸부터] [새 시트](주 단추 · 기본) · Esc = 취소 */
export function askWhere(rows: number, cols: number): Promise<Where> {
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<string | null>;
}
/** 받은 표 → 넣을 자리 물음 → 새 시트(활성) 또는 지금 시트 고른 칸부터 · 알림 —
* 옮긴 시트 이름 · 붙인 범위 · 못 하거나 그만두면 null */
export async function importDrawingTable(
handle: AddSheetHandle | null,
table: DrawingTable,
choose: (rows: number, cols: number) => Promise<Where> = askWhere,
): Promise<string | null> {
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;
}