Files
Aislo/A00_Common/spreadsheet/spreadsheet_area_table.ts
T
eomsangdonandClaude Sonnet 5.5 5638bd5101 feat(M02): 면적 값 고침이 저장 전 도면 패널 표에 바로 비침 · 이름 · 보내기 취소는 [저장] 때 서버가 기록(112-1·112-2)
- 112-1 면적 표 변환을 공용 파일(spreadsheet_area_table.ts)로 빼 서버 Node 와 브라우저가 한 벌로 씀 · 도면 패널이 칸 고침마다(값 바뀐 표만 · 펼친 동안만) CAD 연결 표 갈기를 보기 전용 갈음(preview)으로 보냄
- 112-2 보낸 범위 이름은 도면 [저장] 요청 안에서 서버가 면적 문서에 둠(놓기만 하고 저장 안 하면 안 생김) · [보내기 취소]는 화면에서만 빼고 면적 [저장] 때 서버가 연결표 줄 · 이름을 뺌 · /name · /unlink 길 삭제

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
2026-10-08 17:36:44 +09:00

148 lines
6.6 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.
/* =============================================================================
* spreadsheet_area_table.ts
* 면적산출 시트 · 범위 → CAD 표 객체(TableEntity 꼴) — 한 벌의 코드를 두 곳이 씀(CLAUDE.md 5장 ②):
* 서버 Node(`common_util/common_util_spreadsheet_node.ts` · [저장] 때 정본) ·
* 브라우저(M02 면적 상세 오른쪽 도면 패널 · 저장 전 실시간 표 값 · 112-1).
* 표시 글(숫자 형식) · 병합 · 가로 정렬 · 굵게 · 열 너비 · 행 높이 비율 · 숨긴 행열 뺌 · 칸 key 「시트id!A1」 ·
* style.연결 빈 글(화면이 붙임) · 저장 안 함.
* ========================================================================== */
import { colName, parseRange, rangeToA1, rangesOverlap, toA1 } from "./spreadsheet_address";
import { cellStyleAt } from "./spreadsheet_cellstyle";
import { createCalcEngine } from "./spreadsheet_graph";
import { DEFAULT_COL_CHARS, DEFAULT_ROW_PT } from "./spreadsheet_headers";
import { resolveName } from "./spreadsheet_names";
import { formatValue } from "./spreadsheet_numfmt";
import type { CellRange, HAlign, Workbook } from "./spreadsheet_types";
/** 범위 대신 이름을 줬는데 그 이름이 지금 책에 없음(104-1 · 범위가 통째로 지워짐) — python 쪽이 문자열로 가름 */
export const NAME_GONE = "이름없음";
/** 표 하나 물음 — `시트` = 시트 id 또는 이름 · `범위` = `B3:F12`(이름 없을 때만 씀) · `이름` 이 있으면
* 그 이름이 지금 가리키는 범위로 품(104-1 · 행열 넣기 · 지우기를 따라간 뒤 범위) · `글자크기` = 도면 단위(기본 2.5) */
export interface TableAsk {
book: Workbook;
시트: string;
범위: string;
이름?: string;
글자크기?: number;
한도?: number;
}
/** CAD `TableCell`(openwebcad `helpers/table-geometry.ts`) 꼴 */
export interface TableCell {
text: string;
colSpan?: number;
rowSpan?: number;
align?: "left" | "center" | "right";
bold?: boolean;
key: string;
}
const FONT = 2.5;
const LIMIT = 2000;
/** 엑셀 글자 폭 1 = 글자 크기 × 0.7 · 기본 행 높이(15pt) = 글자 크기 × 1.8 */
const COL_K = 0.7;
const ROW_K = 1.8;
const ALIGN: Partial<Record<HAlign, TableCell["align"]>> = {
left: "left",
center: "center",
right: "right",
distributed: "center",
centerContinuous: "center",
};
export function areaTable(ask: TableAsk) {
const book = ask.book;
let sheetId = ask.시트;
let range: CellRange;
if (ask.이름) {
const found = resolveName(book, ask.시트, ask.이름);
if (!found) throw new Error(NAME_GONE); // 이름을 지웠거나 범위가 통째로 지워짐
sheetId = found.시트;
range = found.범위;
} else {
const parsed = parseRange(ask.범위);
if (!parsed) throw new Error(`범위 꼴 아님 「${ask.범위}」`);
range = parsed;
}
const sheet =
book.시트.find((s) => s.id === sheetId) ?? book.시트.find((s) => s.이름 === sheetId);
if (!sheet) throw new Error(`없는 시트 「${sheetId}」`);
const font = ask.글자크기 && ask.글자크기 > 0 ? ask.글자크기 : FONT;
const limit = ask.한도 ?? LIMIT;
const span = (a: number, b: number): number[] =>
Array.from({ length: b - a + 1 }, (_, i) => a + i);
const rowShown = (r: number): boolean => {
const info = sheet.행?.[String(r + 1)];
return !info?.숨김 && !info?.걸러짐;
};
const colShown = (c: number): boolean => !sheet.열?.[colName(c)]?.숨김;
if ((range.r1 - range.r0 + 1) * (range.c1 - range.c0 + 1) > limit * 4)
throw new Error(`칸이 너무 많음 — 한도 ${limit}`); // 숨김을 세기 전 큰 범위 막음
const rows = span(range.r0, range.r1).filter(rowShown);
const cols = span(range.c0, range.c1).filter(colShown);
if (!rows.length || !cols.length) throw new Error("보이는 칸이 없음");
if (rows.length * cols.length > limit)
throw new Error(`칸이 너무 많음 ${rows.length * cols.length} — 한도 ${limit}`);
const engine = createCalcEngine(book);
const merges: { whole: CellRange; part: CellRange }[] = [];
for (const text of sheet.병합 ?? []) {
const whole = parseRange(text);
if (!whole || !rangesOverlap(whole, range)) continue;
const part = {
r0: Math.max(whole.r0, range.r0),
c0: Math.max(whole.c0, range.c0),
r1: Math.min(whole.r1, range.r1),
c1: Math.min(whole.c1, range.c1),
}; // 범위 밖 병합은 잘라 냄
merges.push({ whole, part });
}
const mergeAt = (r: number, c: number) =>
merges.find(({ part }) => r >= part.r0 && r <= part.r1 && c >= part.c0 && c <= part.c1);
const 값: Record<string, string> = {};
const cellOf = (r: number, c: number, at: { r: number; c: number }): TableCell => {
const style = cellStyleAt(book, sheet, at.r, at.c);
const has = sheet.칸[toA1(at.r, at.c)];
const shown =
has && (has.값 !== undefined || has.식 !== undefined)
? formatValue(engine.value(sheet.id, at.r, at.c), style.형식)
: { 글: "", 정렬: "left" as const };
const key = `${sheet.id}!${toA1(r, c)}`;
값[key] = shown.글;
const align = ALIGN[style.가로 ?? "general"] ?? (shown.정렬 as TableCell["align"]);
return { text: shown.글, align, ...(style.굵게 ? { bold: true } : {}), key };
};
const cells = rows.map((r) =>
cols.map((c): TableCell | null => {
const merge = mergeAt(r, c);
if (!merge) return cellOf(r, c, { r, c });
const inRows = rows.filter((x) => x >= merge.part.r0 && x <= merge.part.r1);
const inCols = cols.filter((x) => x >= merge.part.c0 && x <= merge.part.c1);
if (r !== inRows[0] || c !== inCols[0]) return null; // 앞 칸 병합에 먹힌 자리
const cell = cellOf(r, c, { r: merge.whole.r0, c: merge.whole.c0 }); // 글 = 병합 왼위 칸
return {
...cell,
...(inCols.length > 1 ? { colSpan: inCols.length } : {}),
...(inRows.length > 1 ? { rowSpan: inRows.length } : {}),
};
}),
);
const colChars = sheet.기본?.열폭 ?? book.기본?.열폭 ?? DEFAULT_COL_CHARS;
const rowPt = sheet.기본?.행높이 ?? book.기본?.행높이 ?? DEFAULT_ROW_PT;
const 표 = {
type: "Table",
lineWidth: 1,
shapeData: {
origin: { x: 0, y: 0 },
columnWidths: cols.map((c) => (sheet.열?.[colName(c)]?.폭 ?? colChars) * font * COL_K),
rowHeights: rows.map(
(r) => ((sheet.행?.[String(r + 1)]?.높이 ?? rowPt) / DEFAULT_ROW_PT) * font * ROW_K,
),
cells,
style: { fontSize: font, padding: 1, 연결: "" },
},
};
return { 표, 시트: sheet.id, 범위: rangeToA1(range), 값 };
}