Files
Aislo/A00_Common/spreadsheet/spreadsheet_autofit.ts
T
eomsangdonandClaude Sonnet 5.5 afb9710423 feat(741-1): 불러오기 엑셀 보기 열 폭 — xlsx 파일 열 폭 · 폭 없는 열은 글 길이로 · 머리 경계 끌기 · 두 번 눌러 맞춤
- 서버: xlsx 시트 XML 열 폭 · 숨김을 보냄(엑셀 보기 격자 `열`)
- 화면: 폭 없는 열은 글 길이로 처음 폭(최소 6 · 최대 60 · 여러 열 병합 제목 칸은 뺌)
- 읽기 전용 격자도 열 폭 끌기 · 경계 두 번 누름 맞춤(보기만 · 저장 · 되돌리기 · onChange 없음)
- 열 경계 오른쪽 반에서도 경계로 읽음 · 단가 카드 미리보기는 사본으로 보임
- 시험: test_741_sheet_widths · test_stage2_autofit(esbuild 묶어 node)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 18:56:36 +09:00

83 lines
4.1 KiB
TypeScript

/* =============================================================================
* spreadsheet_autofit.ts (2단계 · 주인 sub_laptop_1)
* 열 머리 경계를 두 번 눌러 폭 자동 맞춤 — 그 열 안 글자 수로 어림(엑셀 「글자 단위」).
* 진짜 픽셀 값은 C(격자)가 쓰는 글꼴로 재야 더 정확 — `measure` 자리에 C 의 실제 재기 함수를
* 나중에 꽂을 수 있게 열어 둠(안 주면 아래 어림 규칙을 씀).
*
* 잇는 법(D · sub7 · C 격자 머리) — 열 머리 경계에서 두 번 누르면(dblclick)
* `autofitColumn(ctx, col)` 을 부름. C 가 완성되면 `measure` 자리에 캔버스 `measureText` 를 넘겨 더 정밀하게.
*
* ── 이미 계약에 있어 새 파일이 필요 없는 것(붙일 자리만 확인) ──────────────────────
* · 여러 범위(Ctrl+누르기) — `spreadsheet_view_types.ts` `Selection.범위` 가 이미 배열(D `spreadsheet_selection.ts` 가 씀).
* · 눈금선 끄기 — `Command`(`종류:"눈금선"`) · `Sheet.보기?.눈금선` 이미 있음(A 가 처리).
* · 확대 · 축소 — 값 자체를 저장하지 않는 화면 전용 CSS 배율(C 격자 뿌리에 transform) · 계약 밖.
* ========================================================================== */
import { colIndex, colName, parseRange } from "./spreadsheet_address";
import type { Sheet, Workbook } from "./spreadsheet_types";
import type { SpreadsheetContext } from "./spreadsheet_view_types";
export type TextMeasurer = (text: string) => number;
const PADDING = 2;
const MIN_WIDTH = 4;
/** 글꼴을 모를 때 어림 — 한글 · 한자 · 전각은 1.9 배 폭 · 나머지는 1 배. */
const defaultMeasure: TextMeasurer = (text) => {
let width = 0;
for (const ch of text) width += ch.codePointAt(0)! >= 0x1100 ? 1.9 : 1;
return width;
};
const colFromKey = (key: string): number => {
const m = /^([A-Z]+)\d+$/.exec(key);
return m ? colIndex(m[1]) : -1;
};
/** 열마다 가장 넓은 칸 글자 폭(식 칸은 식 글로 어림 — 계산값은 여기서 모름) · 여러 열 병합 칸은 뺌(제목 줄). */
function widestByColumn(sheet: Sheet, measure: TextMeasurer): Map<number, number> {
const wide = new Set<string>();
for (const text of sheet.병합 ?? []) {
const r = parseRange(text);
if (r && r.c1 > r.c0) wide.add(`${colName(r.c0)}${r.r0 + 1}`);
}
const out = new Map<number, number>();
for (const [key, cell] of Object.entries(sheet.칸)) {
const text = cell.식 !== undefined ? cell.식 : cell.값 !== undefined ? String(cell.값) : "";
const col = colFromKey(key);
if (!text || col < 0 || wide.has(key)) continue;
out.set(col, Math.max(out.get(col) ?? 0, measure(text)));
}
return out;
}
/** 그 열에 든 칸 글자 중 가장 넓은 것 + 여백. */
export function estimateColumnWidth(
book: Workbook,
sheetId: string,
col: number,
measure: TextMeasurer = defaultMeasure,
): number {
const sheet = book.시트.find((s) => s.id === sheetId);
if (!sheet) return MIN_WIDTH;
return Math.max(MIN_WIDTH, Math.ceil((widestByColumn(sheet, measure).get(col) ?? 0) + PADDING));
}
/** 폭 정보 없는 열(엑셀 폭을 못 읽은 파일)에 글 길이로 처음 폭을 채움 — 최소 · 최대 안(읽기 전용 보기). */
export function fillMissingWidths(book: Workbook, min = 6, max = 60): void {
for (const sheet of book.시트) {
for (const [col, widest] of widestByColumn(sheet, defaultMeasure)) {
const letter = colName(col);
if (sheet.열?.[letter]?.폭 !== undefined) continue;
const 폭 = Math.min(max, Math.max(min, Math.ceil(widest + PADDING)));
sheet.열 = { ...sheet.열, [letter]: { ...sheet.열?.[letter], 폭 } };
}
}
}
/** 폭을 자동 맞춤 값으로 바꿈(열넓이 명령 하나). */
export function autofitColumn(ctx: SpreadsheetContext, col: number, measure?: TextMeasurer): void {
const width = estimateColumnWidth(ctx.book, ctx.selection.시트, col, measure);
ctx.dispatch({ 종류: "열폭", 시트: ctx.selection.시트, 열: [col], 폭: width });
}