feat(spreadsheet): C 격자 — 시트 기본값 반영 · data-addr · 판별 editorSlot · 메모 세모 · 시험 틀
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VVM3xZ1aUPcUTuW2WZSnwg
This commit is contained in:
@@ -44,7 +44,7 @@ export interface ResolvedBorders {
|
||||
오른?: BorderSide;
|
||||
}
|
||||
|
||||
/** `서식` 표 번호 — 칸 → 행 → 열 → 0(기본) 차례(칸 안의 계약 주석과 같음). */
|
||||
/** `서식` 표 번호 — 칸 → 행 → 열 → 시트 `기본.서식` → 0(기본) 차례(칸 안의 계약 주석과 같음). */
|
||||
export function styleIndexAt(sheet: Sheet, r: number, c: number, key: string): number {
|
||||
const cell = sheet.칸[key];
|
||||
if (cell?.서식 !== undefined) return cell.서식;
|
||||
@@ -52,6 +52,7 @@ export function styleIndexAt(sheet: Sheet, r: number, c: number, key: string): n
|
||||
if (rowFmt !== undefined) return rowFmt;
|
||||
const colFmt = sheet.열?.[colName(c)]?.서식;
|
||||
if (colFmt !== undefined) return colFmt;
|
||||
if (sheet.기본?.서식 !== undefined) return sheet.기본.서식;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -172,6 +172,19 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 메모 칸 — 엑셀처럼 오른 위 모서리 빨간 세모. */
|
||||
.aislo-cell.has-comment::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 0 6px 6px 0;
|
||||
border-color: transparent #c00 transparent transparent;
|
||||
}
|
||||
|
||||
.aislo-grid__ref-highlight {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -130,6 +130,7 @@ interface CellDisplay {
|
||||
hAlign: HAlign;
|
||||
title: string;
|
||||
isError: boolean;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
function cellDisplay(
|
||||
@@ -151,7 +152,7 @@ function cellDisplay(
|
||||
const isError = typeof scalar === "object" && scalar !== null && "error" in scalar;
|
||||
const hAlign: HAlign = hAlign0 !== "general" ? hAlign0 : formatted.정렬;
|
||||
const title = isError ? ((scalar as ErrorValue).why ?? formatted.글) : "";
|
||||
return { text: formatted.글, hAlign, title, isError };
|
||||
return { text: formatted.글, hAlign, title, isError, color: formatted.색 };
|
||||
}
|
||||
|
||||
export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
||||
@@ -214,6 +215,7 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
||||
let frozenCols = 0;
|
||||
let frozenW = 0;
|
||||
let frozenH = 0;
|
||||
let lastSheetId = "";
|
||||
|
||||
const resize = new ResizeObserver(() => layoutPanes());
|
||||
resize.observe(root);
|
||||
@@ -313,6 +315,8 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
||||
cellEl.style.height = `${h}px`;
|
||||
cellEl.dataset.r = String(r0);
|
||||
cellEl.dataset.c = String(c0);
|
||||
const addr = toA1(r0, c0);
|
||||
cellEl.dataset.addr = addr;
|
||||
const style = cellStyleAt(book, sheet, r0, c0);
|
||||
const borders: ResolvedBorders = resolveBoxBorders(
|
||||
book,
|
||||
@@ -326,8 +330,11 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
||||
);
|
||||
const display = cellDisplay(ctx, sheet, r0, c0, style.가로 ?? "general", style.형식);
|
||||
applyCellStyle(cellEl, style, borders, display.hAlign);
|
||||
setCellText(cellEl, display.text, display.title || undefined);
|
||||
if (display.color) cellEl.style.color = display.color;
|
||||
const comment = sheet.comments?.[addr];
|
||||
setCellText(cellEl, display.text, display.title || comment || undefined);
|
||||
cellEl.classList.toggle("is-error", display.isError);
|
||||
cellEl.classList.toggle("has-comment", !!comment);
|
||||
cellEl.classList.toggle(
|
||||
"is-selected",
|
||||
ctx.selection.활성.r === r0 && ctx.selection.활성.c === c0,
|
||||
@@ -478,6 +485,8 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
||||
|
||||
function render(): void {
|
||||
const sheet = ctx.sheet();
|
||||
root.style.fontFamily = ctx.book.기본?.글꼴 ?? "";
|
||||
root.style.fontSize = ctx.book.기본?.크기 ? `${ctx.book.기본.크기}pt` : "";
|
||||
colGeo = buildColGeometry(ctx.book, sheet);
|
||||
rowGeo = buildRowGeometry(ctx.book, sheet);
|
||||
extent = computeExtent(sheet);
|
||||
@@ -486,8 +495,11 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
||||
frozenCols = Math.min(sheet.틀고정?.열 ?? 0, extent.cols);
|
||||
spacer.style.width = "1px";
|
||||
spacer.style.height = "1px";
|
||||
paneBR.scrollLeft = 0;
|
||||
paneBR.scrollTop = 0;
|
||||
if (sheet.id !== lastSheetId) {
|
||||
lastSheetId = sheet.id;
|
||||
paneBR.scrollLeft = 0;
|
||||
paneBR.scrollTop = 0;
|
||||
}
|
||||
layoutPanes();
|
||||
}
|
||||
|
||||
@@ -667,8 +679,17 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
||||
};
|
||||
}
|
||||
|
||||
/** 칸이 든 판(틀고정 4 판 중 하나)의 안쪽 층 — 어느 판인지는 이 반환 요소로 앎. */
|
||||
function paneLayerOf(r: number, c: number): HTMLElement {
|
||||
const inFrozenRow = r < frozenRows;
|
||||
const inFrozenCol = c < frozenCols;
|
||||
if (inFrozenRow) return inFrozenCol ? paneTLInner : paneTRInner;
|
||||
return inFrozenCol ? paneBLInner : paneBRInner;
|
||||
}
|
||||
|
||||
/** 편집기 자리 — `layer` = 칸이 든 판의 안쪽 층 · `box` = 격자 뿌리 기준 화면 좌표(D 가 층 차이를 빼서 맞춤). */
|
||||
function editorSlot(r: number, c: number): { layer: HTMLElement; box: CellBox } {
|
||||
return { layer: overlay, box: screenBoxOf(r, c) };
|
||||
return { layer: paneLayerOf(r, c), box: screenBoxOf(r, c) };
|
||||
}
|
||||
|
||||
function destroy(): void {
|
||||
|
||||
@@ -100,7 +100,7 @@ class SparseGeometry implements LineGeometry {
|
||||
}
|
||||
|
||||
export function buildColGeometry(book: Workbook, sheet: Sheet): LineGeometry {
|
||||
const base = colCharsToPx(book.기본?.열폭 ?? DEFAULT_COL_CHARS);
|
||||
const base = colCharsToPx(sheet.기본?.열폭 ?? book.기본?.열폭 ?? DEFAULT_COL_CHARS);
|
||||
const overrides = new Map<number, number>();
|
||||
for (const [key, info] of Object.entries(sheet.열 ?? {})) {
|
||||
const c = colIndex(key);
|
||||
@@ -112,7 +112,7 @@ export function buildColGeometry(book: Workbook, sheet: Sheet): LineGeometry {
|
||||
}
|
||||
|
||||
export function buildRowGeometry(book: Workbook, sheet: Sheet): LineGeometry {
|
||||
const base = ptToPx(book.기본?.행높이 ?? DEFAULT_ROW_PT);
|
||||
const base = ptToPx(sheet.기본?.행높이 ?? book.기본?.행높이 ?? DEFAULT_ROW_PT);
|
||||
const overrides = new Map<number, number>();
|
||||
for (const [key, info] of Object.entries(sheet.행 ?? {}) as [string, RowInfo][]) {
|
||||
const r = Number(key) - 1;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>C 격자 시험 틀</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
#app {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./spreadsheet_grid_test.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,161 @@
|
||||
/* =============================================================================
|
||||
* spreadsheet_grid_test.ts
|
||||
* C(격자) 화면 시험 틀 — 병합 · 틀고정 · 숨김 · 폭 · hair 선 · 균등 분할 · 선택 영역 가운데 ·
|
||||
* [Red] 음수 서식 · 1만 행 스크롤 때 DOM 칸 수를 ORCA 로 확인. D · 엔진이 없으니 이 파일이
|
||||
* `SpreadsheetContext` 를 최소로 흉내 낸다(엔진 값은 안 씀 — 식 칸이 없음).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createGrid } from "../../../A00_Common/spreadsheet/spreadsheet_grid";
|
||||
import type {
|
||||
CalcEngine,
|
||||
Command,
|
||||
History,
|
||||
Sheet,
|
||||
Workbook,
|
||||
} from "../../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
import { colIndex } from "../../../A00_Common/spreadsheet/spreadsheet_address";
|
||||
import type {
|
||||
GridHandle,
|
||||
SpreadsheetContext,
|
||||
} from "../../../A00_Common/spreadsheet/spreadsheet_view_types";
|
||||
|
||||
const sheet: Sheet = {
|
||||
id: "s1",
|
||||
이름: "시험",
|
||||
칸: {
|
||||
A1: { 값: "□격자 시험 — 병합 · 틀고정 · 숨김 · hair 선", 서식: 1 },
|
||||
A3: { 값: "공종", 서식: 2 },
|
||||
B3: { 값: "규격", 서식: 2 },
|
||||
C3: { 값: "산출 내역", 서식: 2 },
|
||||
G3: { 값: "단위", 서식: 2 },
|
||||
H3: { 값: "수량", 서식: 2 },
|
||||
A4: { 값: "터파기", 서식: 3 },
|
||||
C4: { 값: "균등 분할 정렬 보기 글", 서식: 4 },
|
||||
H4: { 값: -12.5, 서식: 6 },
|
||||
A6: { 값: "선택 영역 가운데(centerContinuous) 보기", 서식: 5 },
|
||||
A8: { 값: "hair 테두리 칸", 서식: 3 },
|
||||
B8: { 값: 3.14159, 서식: 7 },
|
||||
J10000: { 값: "맨 끝 칸(1만 행 스크롤 확인용)" },
|
||||
},
|
||||
comments: { H4: "메모 — ROUNDDOWN 끝수 확인" },
|
||||
병합: ["A1:H1", "B8:C9"],
|
||||
열: { A: { 폭: 14 }, D: { 숨김: true } },
|
||||
행: { "1": { 높이: 22 }, "5": { 숨김: true } },
|
||||
틀고정: { 행: 3, 열: 1 },
|
||||
보기: { 눈금선: true },
|
||||
};
|
||||
|
||||
const book: Workbook = {
|
||||
종류: "통합문서",
|
||||
판: 1,
|
||||
열: "test",
|
||||
기본: { 글꼴: "굴림", 크기: 10, 열폭: 8.43, 행높이: 15 },
|
||||
서식: [
|
||||
{},
|
||||
{ 크기: 14, 굵게: true },
|
||||
{
|
||||
가로: "center",
|
||||
세로: "center",
|
||||
채움: "#F2F2F2",
|
||||
테두리: {
|
||||
위: { 선: "thin" },
|
||||
아래: { 선: "thin" },
|
||||
왼: { 선: "thin" },
|
||||
오른: { 선: "thin" },
|
||||
},
|
||||
},
|
||||
{ 세로: "center", 테두리: { 위: { 선: "hair" }, 아래: { 선: "hair" } } },
|
||||
{ 가로: "distributed", 세로: "center" },
|
||||
{ 가로: "centerContinuous", 세로: "center" },
|
||||
{ 형식: "0.00;[Red]-0.00" },
|
||||
{
|
||||
형식: "0.0000",
|
||||
테두리: {
|
||||
위: { 선: "hair" },
|
||||
아래: { 선: "hair" },
|
||||
왼: { 선: "hair" },
|
||||
오른: { 선: "hair" },
|
||||
},
|
||||
},
|
||||
],
|
||||
시트: [sheet],
|
||||
활성: "s1",
|
||||
};
|
||||
|
||||
function noopEngine(): CalcEngine {
|
||||
return {
|
||||
update: () => [],
|
||||
rebuild: () => {},
|
||||
value: () => null,
|
||||
precedents: () => [],
|
||||
snapshot: () => ({}),
|
||||
};
|
||||
}
|
||||
|
||||
function noopHistory(): History {
|
||||
return {
|
||||
push: () => {},
|
||||
undo: () => null,
|
||||
redo: () => null,
|
||||
canUndo: () => false,
|
||||
canRedo: () => false,
|
||||
clear: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
let grid!: GridHandle;
|
||||
|
||||
const ctx: SpreadsheetContext = {
|
||||
root: document.getElementById("app")!,
|
||||
book,
|
||||
readOnly: false,
|
||||
engine: noopEngine(),
|
||||
history: noopHistory(),
|
||||
selection: {
|
||||
시트: "s1",
|
||||
범위: [{ r0: 2, c0: 2, r1: 2, c1: 2 }],
|
||||
활성: { r: 2, c: 2 },
|
||||
기준: { r: 2, c: 2 },
|
||||
},
|
||||
sheet: () => sheet,
|
||||
dispatch: (command: Command) => {
|
||||
if (command.종류 === "열폭") {
|
||||
const c0 = command.열[0];
|
||||
const key = c0 >= 0 ? String.fromCharCode(65 + c0) : "A"; // 시험 데이터가 A~J 안쪽이라 단순 변환으로 충분
|
||||
sheet.열 = { ...sheet.열, [key]: { ...sheet.열?.[key], 폭: command.폭 ?? undefined } };
|
||||
} else if (command.종류 === "행높이") {
|
||||
const r0 = command.행[0];
|
||||
sheet.행 = {
|
||||
...sheet.행,
|
||||
[String(r0 + 1)]: { ...sheet.행?.[String(r0 + 1)], 높이: command.높이 ?? undefined },
|
||||
};
|
||||
}
|
||||
grid.render();
|
||||
},
|
||||
undo: () => {},
|
||||
redo: () => {},
|
||||
select: () => grid.renderSelection(),
|
||||
showSheet: () => {},
|
||||
get grid() {
|
||||
return grid;
|
||||
},
|
||||
editor: {
|
||||
editing: () => false,
|
||||
begin: () => {},
|
||||
commit: () => {},
|
||||
cancel: () => {},
|
||||
sync: () => {},
|
||||
},
|
||||
} as SpreadsheetContext;
|
||||
|
||||
grid = createGrid(ctx);
|
||||
ctx.root.append(grid.root);
|
||||
|
||||
// ORCA 수치 확인용 — 눈에 안 보이는 전역에 걸어 둠(콘솔 · eval 로 셈).
|
||||
(window as unknown as { __gridTest: unknown }).__gridTest = {
|
||||
grid,
|
||||
sheet,
|
||||
book,
|
||||
colIndex,
|
||||
countCells: () => grid.root.querySelectorAll(".aislo-cell").length,
|
||||
};
|
||||
Reference in New Issue
Block a user