feat(spreadsheet): C 격자 — 행·열 묶어 접기(윤곽 +/- 단추, 계약 더함)
RowInfo·ColumnInfo.묶음{층,접힘} · Command「묶음설정」더함(계약 더함) — 접힘 상태 저장 ·
접으면 숨김처럼 그리되 머리 이중선 표시는 숨김·걸러짐과 갈라 안 보임(따로 표시).
commands.ts 는 기존 setAxis 틀에 「묶음」prop·case 만 최소로 얹음(exhaustive switch 깨짐 막음).
ORCA 시험 틀에서 접기·펴기 dispatch·저장·머리 이동·표시 구분 수치로 확인.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVM3xZ1aUPcUTuW2WZSnwg
This commit is contained in:
@@ -450,7 +450,7 @@ function setAxis<V>(
|
|||||||
s: Sheet,
|
s: Sheet,
|
||||||
axis: "행" | "열",
|
axis: "행" | "열",
|
||||||
idx: number[],
|
idx: number[],
|
||||||
prop: "폭" | "높이" | "숨김",
|
prop: "폭" | "높이" | "숨김" | "묶음",
|
||||||
value: V | null,
|
value: V | null,
|
||||||
undoOf: (idx: number[], prev: V | null) => Command,
|
undoOf: (idx: number[], prev: V | null) => Command,
|
||||||
): Command {
|
): Command {
|
||||||
@@ -561,6 +561,19 @@ export function applyCommand(book: Workbook, command: Command): CommandEffect {
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case "묶음설정": {
|
||||||
|
const s = sheetOf(book, command.시트);
|
||||||
|
const { 축 } = command;
|
||||||
|
return plain(
|
||||||
|
setAxis(s, 축, command.번호, "묶음", command.묶음, (번호, prev) => ({
|
||||||
|
종류: "묶음설정",
|
||||||
|
시트: s.id,
|
||||||
|
축,
|
||||||
|
번호,
|
||||||
|
묶음: prev,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
case "틀고정": {
|
case "틀고정": {
|
||||||
const s = sheetOf(book, command.시트);
|
const s = sheetOf(book, command.시트);
|
||||||
const prev = { 행: s.틀고정?.행 ?? 0, 열: s.틀고정?.열 ?? 0 };
|
const prev = { 행: s.틀고정?.행 ?? 0, 열: s.틀고정?.열 ?? 0 };
|
||||||
|
|||||||
@@ -116,6 +116,19 @@
|
|||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.aislo-grid__outline-btn {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--aislo-grid-head-border);
|
||||||
|
background: #fff;
|
||||||
|
color: var(--aislo-grid-head-fg);
|
||||||
|
font-size: 0.7em;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.aislo-grid__resize--row {
|
.aislo-grid__resize--row {
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: -3px;
|
bottom: -3px;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
import "./spreadsheet_grid.css";
|
import "./spreadsheet_grid.css";
|
||||||
import { toFrac } from "@ui/sheet/ui_template_sheet_frac";
|
import { toFrac } from "@ui/sheet/ui_template_sheet_frac";
|
||||||
import { colIndex, parseA1, parseRange, toA1 } from "./spreadsheet_address";
|
import { colIndex, colName, parseA1, parseRange, toA1 } from "./spreadsheet_address";
|
||||||
import {
|
import {
|
||||||
applyCellStyle,
|
applyCellStyle,
|
||||||
cellStyleAt,
|
cellStyleAt,
|
||||||
@@ -22,14 +22,17 @@ import {
|
|||||||
} from "./spreadsheet_cellstyle";
|
} from "./spreadsheet_cellstyle";
|
||||||
import {
|
import {
|
||||||
buildColGeometry,
|
buildColGeometry,
|
||||||
|
buildOutlineRuns,
|
||||||
buildRowGeometry,
|
buildRowGeometry,
|
||||||
commitColWidth,
|
commitColWidth,
|
||||||
commitRowHeight,
|
commitRowHeight,
|
||||||
|
isExplicitHidden,
|
||||||
makeColHeaderCell,
|
makeColHeaderCell,
|
||||||
makeRowHeaderCell,
|
makeRowHeaderCell,
|
||||||
startDragResize,
|
startDragResize,
|
||||||
visibleIndices,
|
visibleIndices,
|
||||||
type LineGeometry,
|
type LineGeometry,
|
||||||
|
type OutlineRun,
|
||||||
} from "./spreadsheet_headers";
|
} from "./spreadsheet_headers";
|
||||||
import { formatValue } from "./spreadsheet_numfmt";
|
import { formatValue } from "./spreadsheet_numfmt";
|
||||||
import { MAX_COLS, MAX_ROWS } from "./spreadsheet_types";
|
import { MAX_COLS, MAX_ROWS } from "./spreadsheet_types";
|
||||||
@@ -51,6 +54,7 @@ import type {
|
|||||||
|
|
||||||
const HEAD_ROW_H_BASE = 24;
|
const HEAD_ROW_H_BASE = 24;
|
||||||
const HEAD_COL_W_BASE = 46;
|
const HEAD_COL_W_BASE = 46;
|
||||||
|
const OUTLINE_GUTTER_BASE = 14;
|
||||||
const RESIZE_HOTZONE = 4;
|
const RESIZE_HOTZONE = 4;
|
||||||
const REF_COLORS = ["#1a73e8", "#e8710a", "#0f9d58", "#a142f4", "#d93025"];
|
const REF_COLORS = ["#1a73e8", "#e8710a", "#0f9d58", "#a142f4", "#d93025"];
|
||||||
const ZOOM_MIN = 0.1;
|
const ZOOM_MIN = 0.1;
|
||||||
@@ -232,8 +236,15 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
|||||||
let frozenH = 0;
|
let frozenH = 0;
|
||||||
let lastSheetId = "";
|
let lastSheetId = "";
|
||||||
let zoom = 1;
|
let zoom = 1;
|
||||||
const headRowH = (): number => Math.round(HEAD_ROW_H_BASE * zoom);
|
let rowOutlineRuns: OutlineRun[] = [];
|
||||||
const headColW = (): number => Math.round(HEAD_COL_W_BASE * zoom);
|
let colOutlineRuns: OutlineRun[] = [];
|
||||||
|
/** 행 묶음 윤곽 띠 — 행 머리 왼쪽(있을 때만) · 열 묶음 띠는 열 머리 위. */
|
||||||
|
const rowGutterW = (): number =>
|
||||||
|
rowOutlineRuns.length ? Math.round(OUTLINE_GUTTER_BASE * zoom) : 0;
|
||||||
|
const colGutterH = (): number =>
|
||||||
|
colOutlineRuns.length ? Math.round(OUTLINE_GUTTER_BASE * zoom) : 0;
|
||||||
|
const headRowH = (): number => Math.round(HEAD_ROW_H_BASE * zoom) + colGutterH();
|
||||||
|
const headColW = (): number => Math.round(HEAD_COL_W_BASE * zoom) + rowGutterW();
|
||||||
|
|
||||||
const resize = new ResizeObserver(() => layoutPanes());
|
const resize = new ResizeObserver(() => layoutPanes());
|
||||||
resize.observe(root);
|
resize.observe(root);
|
||||||
@@ -381,18 +392,33 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
|||||||
poolSweep(pool, keep);
|
poolSweep(pool, keep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 건너뛴 자리(prev+1..cur-1) 안에 사용자 숨김 · 필터 걸러짐이 있나(묶음 접힘은 빼고 — 화면 표시가 다름). */
|
||||||
|
function colHiddenGap(sheet: Sheet, prev: number, cur: number): boolean {
|
||||||
|
for (let i = prev + 1; i < cur; i += 1)
|
||||||
|
if (isExplicitHidden(sheet.열?.[colName(i)])) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function rowHiddenGap(sheet: Sheet, prev: number, cur: number): boolean {
|
||||||
|
for (let i = prev + 1; i < cur; i += 1)
|
||||||
|
if (isExplicitHidden(sheet.행?.[String(i + 1)])) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function layoutColHeaders(
|
function layoutColHeaders(
|
||||||
cols: number[],
|
cols: number[],
|
||||||
pool: CellPool,
|
pool: CellPool,
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
base: number,
|
base: number,
|
||||||
): void {
|
): void {
|
||||||
|
const sheet = ctx.sheet();
|
||||||
const keep = new Set<string>();
|
const keep = new Set<string>();
|
||||||
|
const top = colGutterH();
|
||||||
|
const height = Math.round(HEAD_ROW_H_BASE * zoom);
|
||||||
cols.forEach((c, i) => {
|
cols.forEach((c, i) => {
|
||||||
const key = String(c);
|
const key = String(c);
|
||||||
keep.add(key);
|
keep.add(key);
|
||||||
const prev = i > 0 ? cols[i - 1] : c - 1;
|
const prev = i > 0 ? cols[i - 1] : c - 1;
|
||||||
const hiddenBefore = c - prev > 1;
|
const hiddenBefore = c - prev > 1 && colHiddenGap(sheet, prev, c);
|
||||||
let cellEl = pool.used.get(key);
|
let cellEl = pool.used.get(key);
|
||||||
if (!cellEl) {
|
if (!cellEl) {
|
||||||
const built = makeColHeaderCell(c, 0, 0, hiddenBefore);
|
const built = makeColHeaderCell(c, 0, 0, hiddenBefore);
|
||||||
@@ -411,7 +437,9 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
cellEl.style.left = `${colGeo.offset(c) - base}px`;
|
cellEl.style.left = `${colGeo.offset(c) - base}px`;
|
||||||
|
cellEl.style.top = `${top}px`;
|
||||||
cellEl.style.width = `${colGeo.size(c)}px`;
|
cellEl.style.width = `${colGeo.size(c)}px`;
|
||||||
|
cellEl.style.height = `${height}px`;
|
||||||
cellEl.classList.toggle("aislo-grid__colhead--hidden-before", hiddenBefore);
|
cellEl.classList.toggle("aislo-grid__colhead--hidden-before", hiddenBefore);
|
||||||
});
|
});
|
||||||
poolSweep(pool, keep);
|
poolSweep(pool, keep);
|
||||||
@@ -423,12 +451,15 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
|||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
base: number,
|
base: number,
|
||||||
): void {
|
): void {
|
||||||
|
const sheet = ctx.sheet();
|
||||||
const keep = new Set<string>();
|
const keep = new Set<string>();
|
||||||
|
const left = rowGutterW();
|
||||||
|
const width = Math.round(HEAD_COL_W_BASE * zoom);
|
||||||
rows.forEach((r, i) => {
|
rows.forEach((r, i) => {
|
||||||
const key = String(r);
|
const key = String(r);
|
||||||
keep.add(key);
|
keep.add(key);
|
||||||
const prev = i > 0 ? rows[i - 1] : r - 1;
|
const prev = i > 0 ? rows[i - 1] : r - 1;
|
||||||
const hiddenBefore = r - prev > 1;
|
const hiddenBefore = r - prev > 1 && rowHiddenGap(sheet, prev, r);
|
||||||
let cellEl = pool.used.get(key);
|
let cellEl = pool.used.get(key);
|
||||||
if (!cellEl) {
|
if (!cellEl) {
|
||||||
const built = makeRowHeaderCell(r, 0, 0, hiddenBefore);
|
const built = makeRowHeaderCell(r, 0, 0, hiddenBefore);
|
||||||
@@ -447,12 +478,79 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
cellEl.style.top = `${rowGeo.offset(r) - base}px`;
|
cellEl.style.top = `${rowGeo.offset(r) - base}px`;
|
||||||
|
cellEl.style.left = `${left}px`;
|
||||||
cellEl.style.height = `${rowGeo.size(r)}px`;
|
cellEl.style.height = `${rowGeo.size(r)}px`;
|
||||||
|
cellEl.style.width = `${width}px`;
|
||||||
cellEl.classList.toggle("aislo-grid__rowhead--hidden-before", hiddenBefore);
|
cellEl.classList.toggle("aislo-grid__rowhead--hidden-before", hiddenBefore);
|
||||||
});
|
});
|
||||||
poolSweep(pool, keep);
|
poolSweep(pool, keep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 묶음 접힘 뒤집기(같은 묶음 자리 모두) → `묶음설정` dispatch. */
|
||||||
|
function outlineToggle(axis: "행" | "열", run: OutlineRun): void {
|
||||||
|
const idx: number[] = [];
|
||||||
|
for (let i = run.from; i <= run.to; i += 1) idx.push(i);
|
||||||
|
ctx.dispatch({
|
||||||
|
종류: "묶음설정",
|
||||||
|
시트: ctx.sheet().id,
|
||||||
|
축: axis,
|
||||||
|
번호: idx,
|
||||||
|
묶음: { 층: 1, 접힘: !run.collapsed },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 윤곽 +/- 단추 — 행 묶음 마지막 다음 자리(머리 왼쪽 띠) · 열 묶음도 같은 이치(머리 위 띠).
|
||||||
|
* 여러 층(중첩)은 1단계 다음(지금은 늘 층 1). */
|
||||||
|
function renderRowOutline(): void {
|
||||||
|
rowHeadInnerFrozen.querySelectorAll(".aislo-grid__outline-btn").forEach((n) => n.remove());
|
||||||
|
rowHeadInnerScroll.querySelectorAll(".aislo-grid__outline-btn").forEach((n) => n.remove());
|
||||||
|
if (!rowOutlineRuns.length) return;
|
||||||
|
const gw = rowGutterW();
|
||||||
|
for (const run of rowOutlineRuns) {
|
||||||
|
const btnRow = Math.min(run.to + 1, extent.rows - 1);
|
||||||
|
const container = btnRow < frozenRows ? rowHeadInnerFrozen : rowHeadInnerScroll;
|
||||||
|
const base = btnRow < frozenRows ? 0 : rowGeo.offset(frozenRows);
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "aislo-grid__outline-btn";
|
||||||
|
btn.textContent = run.collapsed ? "+" : "−";
|
||||||
|
btn.style.top = `${rowGeo.offset(btnRow) - base}px`;
|
||||||
|
btn.style.left = "0";
|
||||||
|
btn.style.width = `${gw}px`;
|
||||||
|
btn.style.height = `${rowGeo.size(btnRow) || Math.round(HEAD_ROW_H_BASE * zoom)}px`;
|
||||||
|
btn.addEventListener("click", (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
outlineToggle("행", run);
|
||||||
|
});
|
||||||
|
container.append(btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderColOutline(): void {
|
||||||
|
colHeadInnerFrozen.querySelectorAll(".aislo-grid__outline-btn").forEach((n) => n.remove());
|
||||||
|
colHeadInnerScroll.querySelectorAll(".aislo-grid__outline-btn").forEach((n) => n.remove());
|
||||||
|
if (!colOutlineRuns.length) return;
|
||||||
|
const gh = colGutterH();
|
||||||
|
for (const run of colOutlineRuns) {
|
||||||
|
const btnCol = Math.min(run.to + 1, extent.cols - 1);
|
||||||
|
const container = btnCol < frozenCols ? colHeadInnerFrozen : colHeadInnerScroll;
|
||||||
|
const base = btnCol < frozenCols ? 0 : colGeo.offset(frozenCols);
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "aislo-grid__outline-btn";
|
||||||
|
btn.textContent = run.collapsed ? "+" : "−";
|
||||||
|
btn.style.left = `${colGeo.offset(btnCol) - base}px`;
|
||||||
|
btn.style.top = "0";
|
||||||
|
btn.style.width = `${colGeo.size(btnCol) || Math.round(HEAD_COL_W_BASE * zoom)}px`;
|
||||||
|
btn.style.height = `${gh}px`;
|
||||||
|
btn.addEventListener("click", (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
outlineToggle("열", run);
|
||||||
|
});
|
||||||
|
container.append(btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderVisible(): void {
|
function renderVisible(): void {
|
||||||
root.classList.toggle("aislo-grid--gridlines", gridlines());
|
root.classList.toggle("aislo-grid--gridlines", gridlines());
|
||||||
const w = root.clientWidth;
|
const w = root.clientWidth;
|
||||||
@@ -490,6 +588,8 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
|||||||
rowHeadInnerScroll,
|
rowHeadInnerScroll,
|
||||||
rowGeo.offset(frozenRows),
|
rowGeo.offset(frozenRows),
|
||||||
);
|
);
|
||||||
|
renderRowOutline();
|
||||||
|
renderColOutline();
|
||||||
|
|
||||||
renderRowOfCells(frozenRowsVisible, frozenColsVisible, pools.tl, paneTLInner, 0, 0);
|
renderRowOfCells(frozenRowsVisible, frozenColsVisible, pools.tl, paneTLInner, 0, 0);
|
||||||
renderRowOfCells(
|
renderRowOfCells(
|
||||||
@@ -528,6 +628,8 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
|
|||||||
rowGeo = scaledGeometry(baseRowGeo, zoom);
|
rowGeo = scaledGeometry(baseRowGeo, zoom);
|
||||||
extent = computeExtent(sheet);
|
extent = computeExtent(sheet);
|
||||||
merges = buildMergeMap(sheet);
|
merges = buildMergeMap(sheet);
|
||||||
|
rowOutlineRuns = buildOutlineRuns(sheet.행, (k) => Number(k) - 1);
|
||||||
|
colOutlineRuns = buildOutlineRuns(sheet.열, colIndex);
|
||||||
frozenRows = Math.min(sheet.틀고정?.행 ?? 0, extent.rows);
|
frozenRows = Math.min(sheet.틀고정?.행 ?? 0, extent.rows);
|
||||||
frozenCols = Math.min(sheet.틀고정?.열 ?? 0, extent.cols);
|
frozenCols = Math.min(sheet.틀고정?.열 ?? 0, extent.cols);
|
||||||
spacer.style.width = "1px";
|
spacer.style.width = "1px";
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export function buildColGeometry(book: Workbook, sheet: Sheet): LineGeometry {
|
|||||||
for (const [key, info] of Object.entries(sheet.열 ?? {})) {
|
for (const [key, info] of Object.entries(sheet.열 ?? {})) {
|
||||||
const c = colIndex(key);
|
const c = colIndex(key);
|
||||||
if (c < 0) continue;
|
if (c < 0) continue;
|
||||||
if (info.숨김) overrides.set(c, 0);
|
if (info.숨김 || info.묶음?.접힘) overrides.set(c, 0);
|
||||||
else if (info.폭 !== undefined) overrides.set(c, colCharsToPx(info.폭));
|
else if (info.폭 !== undefined) overrides.set(c, colCharsToPx(info.폭));
|
||||||
}
|
}
|
||||||
return new SparseGeometry(base, overrides);
|
return new SparseGeometry(base, overrides);
|
||||||
@@ -117,12 +117,41 @@ export function buildRowGeometry(book: Workbook, sheet: Sheet): LineGeometry {
|
|||||||
for (const [key, info] of Object.entries(sheet.행 ?? {}) as [string, RowInfo][]) {
|
for (const [key, info] of Object.entries(sheet.행 ?? {}) as [string, RowInfo][]) {
|
||||||
const r = Number(key) - 1;
|
const r = Number(key) - 1;
|
||||||
if (!Number.isInteger(r) || r < 0) continue;
|
if (!Number.isInteger(r) || r < 0) continue;
|
||||||
if (info.숨김) overrides.set(r, 0);
|
if (info.숨김 || info.걸러짐 || info.묶음?.접힘) overrides.set(r, 0);
|
||||||
else if (info.높이 !== undefined) overrides.set(r, ptToPx(info.높이));
|
else if (info.높이 !== undefined) overrides.set(r, ptToPx(info.높이));
|
||||||
}
|
}
|
||||||
return new SparseGeometry(base, overrides);
|
return new SparseGeometry(base, overrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 접힌 윤곽 묶음(연속 자리) — 묶음 있는 자리를 모아 붙은 것끼리 한 덩이로(같은 접힘 값끼리만). */
|
||||||
|
export interface OutlineRun {
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
collapsed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildOutlineRuns<T extends { 묶음?: { 층: number; 접힘?: boolean } }>(
|
||||||
|
records: Record<string, T> | undefined,
|
||||||
|
indexOf: (key: string) => number,
|
||||||
|
): OutlineRun[] {
|
||||||
|
const items = Object.entries(records ?? {})
|
||||||
|
.map(([key, info]) => ({ i: indexOf(key), collapsed: !!info.묶음?.접힘, has: !!info.묶음 }))
|
||||||
|
.filter((x) => x.has && x.i >= 0)
|
||||||
|
.sort((a, b) => a.i - b.i);
|
||||||
|
const runs: OutlineRun[] = [];
|
||||||
|
for (const it of items) {
|
||||||
|
const last = runs[runs.length - 1];
|
||||||
|
if (last && last.to === it.i - 1 && last.collapsed === it.collapsed) last.to = it.i;
|
||||||
|
else runs.push({ from: it.i, to: it.i, collapsed: it.collapsed });
|
||||||
|
}
|
||||||
|
return runs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 그 자리가 숨김 · 걸러짐(사용자 · 필터) 인가 — 묶음 접힘은 빼고(화면 표시가 다름). */
|
||||||
|
export function isExplicitHidden(info: { 숨김?: boolean; 걸러짐?: boolean } | undefined): boolean {
|
||||||
|
return !!info?.숨김 || !!info?.걸러짐;
|
||||||
|
}
|
||||||
|
|
||||||
/** `from..to` 안에서 숨기지 않은 자리만. */
|
/** `from..to` 안에서 숨기지 않은 자리만. */
|
||||||
export function visibleIndices(geo: LineGeometry, from: number, to: number): number[] {
|
export function visibleIndices(geo: LineGeometry, from: number, to: number): number[] {
|
||||||
const list: number[] = [];
|
const list: number[] = [];
|
||||||
|
|||||||
@@ -128,11 +128,18 @@ export interface CellStyle {
|
|||||||
형식?: string;
|
형식?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 묶음(윤곽) — 층 1 부터(중첩 단계) · 접힘 이면 격자가 숨김처럼 그림(숨김 · 걸러짐과는 화면 표시가 다름 — 윤곽 +/- 단추). */
|
||||||
|
export interface Outline {
|
||||||
|
층: number;
|
||||||
|
접힘?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ColumnInfo {
|
export interface ColumnInfo {
|
||||||
/** 엑셀 글자 폭 단위(기본 글꼴 숫자 폭 기준) — 화면이 px 로 바꿈. */
|
/** 엑셀 글자 폭 단위(기본 글꼴 숫자 폭 기준) — 화면이 px 로 바꿈. */
|
||||||
폭?: number;
|
폭?: number;
|
||||||
숨김?: boolean;
|
숨김?: boolean;
|
||||||
서식?: number;
|
서식?: number;
|
||||||
|
묶음?: Outline;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RowInfo {
|
export interface RowInfo {
|
||||||
@@ -142,6 +149,7 @@ export interface RowInfo {
|
|||||||
서식?: number;
|
서식?: number;
|
||||||
/** 자동 필터가 숨긴 행 — 사용자 `숨김` 과 따로(필터 풀면 보임) · 격자는 숨김처럼 그림. */
|
/** 자동 필터가 숨긴 행 — 사용자 `숨김` 과 따로(필터 풀면 보임) · 격자는 숨김처럼 그림. */
|
||||||
걸러짐?: boolean;
|
걸러짐?: boolean;
|
||||||
|
묶음?: Outline;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 자동 필터 — `범위` 첫 행 = 머리(단추) · `조건` = 열 글자 → 보일 값(보이는 글 · 빈 칸은 ""). 조건 없는 열은 다 보임. */
|
/** 자동 필터 — `범위` 첫 행 = 머리(단추) · `조건` = 열 글자 → 보일 값(보이는 글 · 빈 칸은 ""). 조건 없는 열은 다 보임. */
|
||||||
@@ -394,6 +402,8 @@ export type Command =
|
|||||||
| { 종류: "정렬"; 시트: string; 범위: CellRange; 차례: number[] }
|
| { 종류: "정렬"; 시트: string; 범위: CellRange; 차례: number[] }
|
||||||
/** 자동 필터 켜기 · 조건 바꾸기 · 끄기(null) — `걸러짐` = 숨길 행(`filterCommand` 가 값으로 셈) */
|
/** 자동 필터 켜기 · 조건 바꾸기 · 끄기(null) — `걸러짐` = 숨길 행(`filterCommand` 가 값으로 셈) */
|
||||||
| { 종류: "필터"; 시트: string; 필터: AutoFilter | null; 걸러짐: number[] }
|
| { 종류: "필터"; 시트: string; 필터: AutoFilter | null; 걸러짐: number[] }
|
||||||
|
/** 행 · 열 묶기(윤곽) · 접기 · 펴기 · 풀기(`묶음` null) — 격자 윤곽 +/- 단추가 씀 */
|
||||||
|
| { 종류: "묶음설정"; 시트: string; 축: "행" | "열"; 번호: number[]; 묶음: Outline | null }
|
||||||
/** 여러 명령 = 되돌리기 한 번 */
|
/** 여러 명령 = 되돌리기 한 번 */
|
||||||
| { 종류: "묶음"; 명령: Command[] };
|
| { 종류: "묶음"; 명령: Command[] };
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,18 @@ const sheet: Sheet = {
|
|||||||
},
|
},
|
||||||
comments: { H4: "메모 — ROUNDDOWN 끝수 확인" },
|
comments: { H4: "메모 — ROUNDDOWN 끝수 확인" },
|
||||||
병합: ["A1:H1", "B8:C9"],
|
병합: ["A1:H1", "B8:C9"],
|
||||||
열: { A: { 폭: 14 }, D: { 숨김: true } },
|
열: {
|
||||||
행: { "1": { 높이: 22 }, "5": { 숨김: true } },
|
A: { 폭: 14 },
|
||||||
|
D: { 숨김: true },
|
||||||
|
E: { 묶음: { 층: 1 } },
|
||||||
|
F: { 묶음: { 층: 1 } },
|
||||||
|
},
|
||||||
|
행: {
|
||||||
|
"1": { 높이: 22 },
|
||||||
|
"5": { 숨김: true },
|
||||||
|
"11": { 묶음: { 층: 1 } },
|
||||||
|
"12": { 묶음: { 층: 1 } },
|
||||||
|
},
|
||||||
틀고정: { 행: 3, 열: 1 },
|
틀고정: { 행: 3, 열: 1 },
|
||||||
보기: { 눈금선: true },
|
보기: { 눈금선: true },
|
||||||
};
|
};
|
||||||
@@ -129,6 +139,14 @@ const ctx: SpreadsheetContext = {
|
|||||||
...sheet.행,
|
...sheet.행,
|
||||||
[String(r0 + 1)]: { ...sheet.행?.[String(r0 + 1)], 높이: command.높이 ?? undefined },
|
[String(r0 + 1)]: { ...sheet.행?.[String(r0 + 1)], 높이: command.높이 ?? undefined },
|
||||||
};
|
};
|
||||||
|
} else if (command.종류 === "묶음설정") {
|
||||||
|
const table = command.축 === "열" ? "열" : "행";
|
||||||
|
const next = { ...sheet[table] } as Record<string, { 묶음?: unknown }>;
|
||||||
|
for (const i of command.번호) {
|
||||||
|
const key = command.축 === "열" ? String.fromCharCode(65 + i) : String(i + 1);
|
||||||
|
next[key] = { ...next[key], 묶음: command.묶음 ?? undefined };
|
||||||
|
}
|
||||||
|
(sheet as unknown as Record<string, unknown>)[table] = next;
|
||||||
}
|
}
|
||||||
grid.render();
|
grid.render();
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user