diff --git a/A00_Common/spreadsheet/spreadsheet_cellstyle.ts b/A00_Common/spreadsheet/spreadsheet_cellstyle.ts index 28b2db92..375998a7 100644 --- a/A00_Common/spreadsheet/spreadsheet_cellstyle.ts +++ b/A00_Common/spreadsheet/spreadsheet_cellstyle.ts @@ -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; } diff --git a/A00_Common/spreadsheet/spreadsheet_grid.css b/A00_Common/spreadsheet/spreadsheet_grid.css index 6ec0e9dd..024233ba 100644 --- a/A00_Common/spreadsheet/spreadsheet_grid.css +++ b/A00_Common/spreadsheet/spreadsheet_grid.css @@ -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; diff --git a/A00_Common/spreadsheet/spreadsheet_grid.ts b/A00_Common/spreadsheet/spreadsheet_grid.ts index de32e299..2a7d5fbd 100644 --- a/A00_Common/spreadsheet/spreadsheet_grid.ts +++ b/A00_Common/spreadsheet/spreadsheet_grid.ts @@ -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 { diff --git a/A00_Common/spreadsheet/spreadsheet_headers.ts b/A00_Common/spreadsheet/spreadsheet_headers.ts index ac634b4f..3d8be157 100644 --- a/A00_Common/spreadsheet/spreadsheet_headers.ts +++ b/A00_Common/spreadsheet/spreadsheet_headers.ts @@ -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(); 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(); for (const [key, info] of Object.entries(sheet.행 ?? {}) as [string, RowInfo][]) { const r = Number(key) - 1; diff --git a/resources/tester/spreadsheet/spreadsheet_grid_test.html b/resources/tester/spreadsheet/spreadsheet_grid_test.html new file mode 100644 index 00000000..f393d1d4 --- /dev/null +++ b/resources/tester/spreadsheet/spreadsheet_grid_test.html @@ -0,0 +1,22 @@ + + + + + C 격자 시험 틀 + + + +
+ + + diff --git a/resources/tester/spreadsheet/spreadsheet_grid_test.ts b/resources/tester/spreadsheet/spreadsheet_grid_test.ts new file mode 100644 index 00000000..ed13e30c --- /dev/null +++ b/resources/tester/spreadsheet/spreadsheet_grid_test.ts @@ -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, +};