Files
Aislo/A00_Common/spreadsheet/spreadsheet_cellstyle.ts
T
eomsangdonandClaude Sonnet 5 4f7470bbbd feat(spreadsheet): C 격자 — 앱 밝은 · 어두운 테마 따라가게
.aislo-grid 안 색 변수(--aislo-grid-bg · -fg · -line · -head-bg · -head-fg ·
-head-border · -selected) 를 전부 앱 전역 토큰(ui_template_theme.css 의
--color-surface-raised · -text-body · -border · -surface · -text-secondary ·
-accent)에 연결 — 새 값 안 적음, 앱이 data-theme 를 바꾸면 그대로 따라감(다른
스프레드시트 파일(spreadsheet_toolbar.css)과 같은 방식). 칸 · 머리 배경 · 눈금선 ·
선택 테두리 · 채우기 손잡이 테두리의 하드코딩 흰색 · 회색 · 파랑을 이 변수로 바꿈.
테두리 기본 색(사용자가 색 안 지정)은 #000000 대신 currentColor 로 — 칸 글자색과
같이 테마를 따라감. 사용자가 지정한 채움색 · 글자색(조건부 서식 포함)은 그대로 둠
(엑셀과 같음 — 이미 빈 문자열 인라인 스타일로 CSS 기본값에 떨어지는 구조라 따로 안 고침).
시험 틀(html)에 ui_template_theme.css 를 넣어 data-theme 전환을 실제로 시험.

ORCA 수치(시험 틀 · data-theme 전환) — 밝은: 격자 배경 rgb(255,255,255) · 글자
rgb(51,51,51) · 선택 #3e0079 · 눈금선 #e6e2e3. 어두운: 격자 배경 rgb(37,31,56) ·
글자 rgb(228,224,240) · 선택 #b99aff · 눈금선 #322a45 — 모두 전환됨. 사용자 지정
채움(A3 #F2F2F2)·조건부 서식 글자색(H4 #c00000)은 두 테마에서 값 그대로(안 바뀜)
확인. A3 무색 테두리는 currentColor 로 밝은 rgb(51,51,51)/어두운 rgb(228,224,240)
로 글자색과 같이 바뀜 확인.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVM3xZ1aUPcUTuW2WZSnwg
2026-09-28 09:48:20 +09:00

180 lines
7.1 KiB
TypeScript

/* =============================================================================
* spreadsheet_cellstyle.ts (주인 C)
* 서식 → CSS — 글꼴 · 정렬(균등 분할 · 선택 영역 가운데 · 세로 · 줄 바꿈) · 채움색 ·
* 테두리(hair · thin · medium · thick · double · dotted · dashed · 색) · 겹친 테두리 고르기.
* 칸 하나가 위 · 왼 이웃과 테두리를 나눠 가짐(표 `border-collapse` 방식) — 겹쳐 그리지 않게
* 위 · 왼만 두 이웃 중 굵은 쪽으로 그리고 오른 · 아래는 시트 끝 칸만 그림(`resolveEdgeBorders`).
* 0 계약 머리 — 몸은 C 가 채움. 계약은 `spreadsheet_types.ts`.
* ========================================================================== */
import { colName, toA1 } from "./spreadsheet_address";
import type {
BorderLine,
BorderSide,
CellStyle,
HAlign,
Sheet,
VAlign,
Workbook,
} from "./spreadsheet_types";
/** 선 굵기 차례 — 겹치면 굵은 쪽이 이김(엑셀과 완전히 같지 않음 · 근사). */
const BORDER_WEIGHT: Record<BorderLine, number> = {
hair: 0,
dotted: 1,
dashed: 2,
thin: 3,
double: 4,
medium: 5,
thick: 6,
};
/** 두 이웃 칸이 한 자리에 그리려는 테두리 중 굵은 쪽. 없으면 있는 쪽 · 둘 다 없으면 undefined. */
export function strongerBorder(a?: BorderSide, b?: BorderSide): BorderSide | undefined {
if (!a) return b;
if (!b) return a;
return BORDER_WEIGHT[b.선] > BORDER_WEIGHT[a.선] ? b : a;
}
/** 칸 하나가 실제로 그릴 네 변 — 위 · 왼은 이웃과 겹쳐 고름 · 오른 · 아래는 시트 끝(그 다음 칸이 없음)일 때만. */
export interface ResolvedBorders {
위?: BorderSide;
아래?: BorderSide;
왼?: BorderSide;
오른?: BorderSide;
}
/** `서식` 표 번호 — 칸 → 행 → 열 → 시트 `기본.서식` → 0(기본) 차례(칸 안의 계약 주석과 같음). */
export function styleIndexAt(sheet: Sheet, r: number, c: number, key: string): number {
const cell = sheet.칸[key];
if (cell?.서식 !== undefined) return cell.서식;
const rowFmt = sheet.행?.[String(r + 1)]?.서식;
if (rowFmt !== undefined) return rowFmt;
const colFmt = sheet.열?.[colName(c)]?.서식;
if (colFmt !== undefined) return colFmt;
if (sheet.기본?.서식 !== undefined) return sheet.기본.서식;
return 0;
}
/** (r, c) 칸이 쓸 서식 — 서식 표 밖 번호는 기본(빈 `{}`)으로. */
export function cellStyleAt(book: Workbook, sheet: Sheet, r: number, c: number): CellStyle {
const key = toA1(r, c);
const idx = styleIndexAt(sheet, r, c, key);
return book.서식[idx] ?? {};
}
/** 칸 하나 · 병합 칸 한 덩이(box)의 테두리 — 네 변마다 안쪽 이웃과 겹쳐 굵은 쪽으로 고름.
* 병합이면 왼위(r0,c0) · 오른아래(r1,c1) 이 다른 칸이라 네 변을 따로 봄(테두리는 칸마다 있음). */
export function resolveBoxBorders(
book: Workbook,
sheet: Sheet,
r0: number,
c0: number,
r1: number,
c1: number,
lastRow: number,
lastCol: number,
): ResolvedBorders {
const top = cellStyleAt(book, sheet, r0, c0).테두리?.위;
const aboveOf = r0 > 0 ? cellStyleAt(book, sheet, r0 - 1, c0).테두리?.아래 : undefined;
const left = cellStyleAt(book, sheet, r0, c0).테두리?.왼;
const leftOf = c0 > 0 ? cellStyleAt(book, sheet, r0, c0 - 1).테두리?.오른 : undefined;
const bottomOwn = cellStyleAt(book, sheet, r1, c0).테두리?.아래;
const belowOf = r1 < lastRow ? cellStyleAt(book, sheet, r1 + 1, c0).테두리?.위 : undefined;
const rightOwn = cellStyleAt(book, sheet, r0, c1).테두리?.오른;
const rightOf = c1 < lastCol ? cellStyleAt(book, sheet, r0, c1 + 1).테두리?.왼 : undefined;
return {
위: strongerBorder(top, aboveOf),
왼: strongerBorder(left, leftOf),
아래: r1 >= lastRow ? bottomOwn : strongerBorder(bottomOwn, belowOf),
오른: c1 >= lastCol ? rightOwn : strongerBorder(rightOwn, rightOf),
};
}
/** 빈 글 = 인라인 테두리를 안 그림(css `aislo-grid--gridlines` 눈금선이 비쳐 보임). `scale` = 확대 · 축소 배율(선 굵기도 같이). */
function borderCss(side: BorderSide | undefined, scale: number): string {
if (!side) return "";
const color = side.색 ?? "currentColor";
const w = (px: number): number => Math.max(1, Math.round(px * scale));
switch (side.선) {
case "hair":
return `${w(1)}px dotted ${color}`;
case "thin":
return `${w(1)}px solid ${color}`;
case "medium":
return `${w(2)}px solid ${color}`;
case "thick":
return `${w(3)}px solid ${color}`;
case "double":
return `${w(3)}px double ${color}`;
case "dotted":
return `${w(1)}px dotted ${color}`;
case "dashed":
return `${w(1)}px dashed ${color}`;
}
}
/** 가로 정렬 → `text-align`(균등 분할 · 선택 영역 가운데는 근사: `justify` · `center`). */
const HALIGN_CSS: Record<HAlign, string> = {
general: "left",
left: "left",
right: "right",
center: "center",
distributed: "justify",
centerContinuous: "center",
};
const VALIGN_CSS: Record<VAlign, string> = {
top: "flex-start",
center: "center",
bottom: "flex-end",
};
/** 칸 하나에 서식 · 겹쳐 고른 테두리를 입힘. `hAlign` 은 부른 쪽이 정함(일반 정렬은 값 종류로 갈림 — B `formatValue` 의 `정렬`).
* `scale` = 확대 · 축소 배율(칸 글자 크기 · 테두리 굵기가 같이 커짐 · 기본 1). */
export function applyCellStyle(
cell: HTMLElement,
style: CellStyle,
borders: ResolvedBorders,
hAlign: HAlign,
scale = 1,
): void {
const s = cell.style;
s.fontFamily = style.글꼴 ?? "";
s.fontSize = style.크기 ? `${style.크기 * scale}pt` : "";
s.fontWeight = style.굵게 ? "700" : "400";
s.fontStyle = style.기울임 ? "italic" : "normal";
s.textDecorationLine = [style.밑줄 && "underline", style.취소선 && "line-through"]
.filter(Boolean)
.join(" ");
s.color = style.글자색 ?? "";
s.backgroundColor = style.채움 ?? "";
s.borderTop = borderCss(borders.위, scale);
s.borderLeft = borderCss(borders.왼, scale);
s.borderBottom = borderCss(borders.아래, scale);
s.borderRight = borderCss(borders.오른, scale);
s.justifyContent = VALIGN_CSS[style.세로 ?? "bottom"];
s.whiteSpace = style.줄바꿈 ? "normal" : "nowrap";
let text = cell.firstElementChild as HTMLElement | null;
if (!text || !text.classList.contains("aislo-cell__text")) {
text = document.createElement("div");
text.className = "aislo-cell__text";
cell.replaceChildren(text);
}
text.style.textAlign = HALIGN_CSS[hAlign];
text.style.textAlignLast = hAlign === "distributed" ? "justify" : "";
}
/** 칸 글 — 서식 CSS 와 따로 두어 `invalidate()` 가 값만 바꿀 때 서식을 다시 잴 필요가 없게. */
export function setCellText(cell: HTMLElement, text: string, title?: string): void {
const node = cell.firstElementChild as HTMLElement | null;
if (node && node.classList.contains("aislo-cell__text")) {
node.textContent = text;
} else {
cell.textContent = text;
}
if (title) cell.title = title;
else cell.removeAttribute("title");
}