Files
Aislo/ui_template/sheet/ui_template_sheet_labels.ts
T
eomsangdonandClaude Opus 5.5 6cc98d68f1 fix(sheet): 마스터 딱지는 식 먼저 · 식 줄은 열 이름 · 머리 글 안 잘리는 최소 폭 · 쪽 나눔 시험 (M02 브레인 확인)
- 식 있는 열 = 계산 · 바인딩만 = 설계값(펼침이면 값마다 열) · 손 = 손 입력
- 식 줄은 [열id] 대신 머리 글 이름 · 같은 종류는 짧게 · 풍선은 긴 이름 + id 식 · 변수도 이름
- 열 폭 최소 = 머리 글 · 단위 폭(캔버스로 잼) · 들어갈 것 · 식 줄은 세 줄로 자르고 풍선에 전문 · 머리 칸도 풍선
- 쪽 나눔을 pagePlan 으로 떼어 시험 test_sheet_pages.py(줄 120 → 쪽 3 · 합계 마지막 쪽만)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tjoit7rxvpLMM7cafeVTo1
2026-09-25 12:02:13 +09:00

104 lines
4.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.
/* =============================================================================
* ui_template_sheet_labels.ts
* 사람이 읽는 글 — 식의 열 id 를 열 이름(머리 글)으로 · 머리 글이 안 잘리는 최소 열 폭.
*
* 열 이름 = 맨 위 글(종류) + 맨 아래 글 · 다른 열과 겹치면 가운데 층을 더 붙임(자리 글 「(…마다)」 는 뺌).
* 보이기만 바꿈 — 문서의 식은 그대로 id 식(고칠 때도 id 식).
* ========================================================================== */
import { layoutHead } from "./ui_template_sheet_header";
import type { SheetColumn } from "./ui_template_sheet_types";
const PLACEHOLDER = /[((][^))]*마다[))]/;
const NAME_PAD = 16;
const MAX_MIN_WIDTH = 220;
export interface ColumnName {
/** 맨 위 글(종류) + 아래 글 — 표 전체에서 안 겹침. */
full: string;
/** 같은 종류 안에서 부를 이름 — 겹치면 `full`. */
short: string;
top: string;
}
/** 열 id → 열 이름. */
export function columnNames(cols: SheetColumn[]): Map<string, ColumnName> {
const paths = cols.map((c) => {
const path = c.머리.filter((label): label is string => !!label && !PLACEHOLDER.test(label));
return path.length ? path : [c.id];
});
/** 맨 위 + 아래서 `take` 층 — 모자라면 통째. */
const nameOf = (path: string[], take: number): string =>
(take + 1 >= path.length ? path : [path[0], ...path.slice(-take)]).join(" ");
const names = new Map<string, ColumnName>();
cols.forEach((col, j) => {
const path = paths[j];
let full = path.join(" ");
for (let take = 1; take + 1 < path.length; take += 1) {
const name = nameOf(path, take);
if (!paths.some((other, k) => k !== j && nameOf(other, take) === name)) {
full = name;
break;
}
}
const leaf = path.length > 1 ? path.slice(1).join(" ") : path[0];
const clash = paths.some(
(other, k) =>
k !== j &&
other[0] === path[0] &&
(other.length > 1 ? other.slice(1).join(" ") : other[0]) === leaf,
);
names.set(col.id, { full, short: clash ? full : leaf, top: path[0] });
});
return names;
}
/** 식 → 읽는 글 — `[열id]` → 열 이름 · `[열id@줄]` → 이름@줄 · `[$변수]` → 변수 이름 · `*` `/` → × ÷.
* `host` 를 주면 같은 종류 열은 짧은 이름. */
export function formulaLabel(
formula: string,
names: Map<string, ColumnName>,
host?: string,
): string {
const top = host ? names.get(host)?.top : undefined;
return formula
.replace(/\[([^\]]+)\]/g, (_, ref: string) => {
const text = ref.trim();
if (text.startsWith("$")) return text.slice(1).replace(/_/g, " ");
const [col, row] = text.split("@");
const hit = names.get(col.trim());
const name = !hit ? col.trim() : top !== undefined && hit.top === top ? hit.short : hit.full;
return row ? `${name}@${row.trim()}` : name;
})
.replace(/\s*\*\s*/g, " × ")
.replace(/\s*\/\s*/g, " ÷ ");
}
let ruler: CanvasRenderingContext2D | null = null;
function textWidth(text: string, font: string): number {
ruler ??= document.createElement("canvas").getContext("2d");
if (!ruler) return text.length * 13;
ruler.font = font;
return ruler.measureText(text).width;
}
/** 열마다 머리 글 · 단위가 안 잘리는 최소 폭(px) — 여러 열을 덮는 칸은 모자란 만큼 나눠 얹음. */
export function headMinWidths(
cols: SheetColumn[],
depth: number,
font: string,
): Map<string, number> {
const mins = cols.map((c) => textWidth(c.단위 ?? "", font) + NAME_PAD);
for (const level of layoutHead(cols, depth).reverse()) {
for (const cell of level) {
const need = textWidth(cell.label, font) + NAME_PAD;
const have = mins.slice(cell.col, cell.col + cell.colspan).reduce((a, b) => a + b, 0);
if (need <= have) continue;
const extra = (need - have) / cell.colspan;
for (let k = cell.col; k < cell.col + cell.colspan; k += 1) mins[k] += extra;
}
}
return new Map(cols.map((c, j) => [c.id, Math.min(MAX_MIN_WIDTH, Math.ceil(mins[j]))]));
}