From fbd97219316b89e8aa4c110a012e54318553e823 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 25 Oct 2026 19:40:31 +0900 Subject: [PATCH] =?UTF-8?q?feat(534-1):=20=EB=82=B4=EC=9E=A5=20=EC=97=91?= =?UTF-8?q?=EC=85=80=20=EA=B7=B8=EB=A6=BC=20=EA=B2=B9=ED=8C=90=20=C2=B7=20?= =?UTF-8?q?=EC=AA=BD=20=EB=82=98=EB=88=94=20=EC=A0=90=EC=84=A0=20=E2=80=94?= =?UTF-8?q?=20=EB=8A=A6=EA=B2=8C=20=EB=B6=88=EB=9F=AC=EC=98=B4=20=C2=B7=20?= =?UTF-8?q?=ED=96=89=EC=97=B4=20=EB=84=A3=EA=B8=B0=20=C2=B7=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A1=A4=20=EB=94=B0=EB=9D=BC=EA=B0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - spreadsheet_figures: 시트 그림을 닻 범위에 얹음(figureSrc 없으면 이름 상자) · 쪽 나눔 행 아래 점선 · overlay 맨 아래 층 - spreadsheet: figureSrc 선택 · 그림 · 쪽 나눔 있는 문서일 때만 import() Co-Authored-By: Claude Opus 5.5 --- A00_Common/spreadsheet/spreadsheet.ts | 16 +++ A00_Common/spreadsheet/spreadsheet_figures.ts | 105 ++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 A00_Common/spreadsheet/spreadsheet_figures.ts diff --git a/A00_Common/spreadsheet/spreadsheet.ts b/A00_Common/spreadsheet/spreadsheet.ts index 6d7fefd78..ef629b80f 100644 --- a/A00_Common/spreadsheet/spreadsheet.ts +++ b/A00_Common/spreadsheet/spreadsheet.ts @@ -24,6 +24,7 @@ import { createGrid } from "./spreadsheet_grid"; import { createHistory } from "./spreadsheet_history"; import { attachKeys } from "./spreadsheet_keys"; import type { ConditionOptions } from "./spreadsheet_condition"; +import type { FigureSrc } from "./spreadsheet_figures"; import type { LinkOptions, Links } from "./spreadsheet_links"; import { linkCommand, type LinkInfo } from "./spreadsheet_link_set"; import { attachMenu } from "./spreadsheet_menu"; @@ -69,6 +70,8 @@ export interface SpreadsheetOptions { preview?: boolean; /** 칸 우클릭 「조건 만들기」 — 이것을 줄 때만 붙음(늦게 불러옴) · 안 주면 화면 그대로 */ conditions?: ConditionOptions; + /** 그림 열쇠 → 주소(요구 347-5 · 시트 `그림`) — 없으면 이름 적힌 빈 상자 */ + figureSrc?: FigureSrc; } export interface SpreadsheetHandle { @@ -185,9 +188,21 @@ export function createSpreadsheet( }); } + /** 그림 겹판 · 쪽 나눔 점선 — 문서에 그림 · 쪽 나눔이 있을 때만 불러옴(크기 예산) */ + let figuring = false; + function loadFigures(): void { + const any = ctx.book.시트.some((s) => s.그림?.length || s.인쇄?.쪽나눔?.length); + if (figuring || !any) return; + figuring = true; + void import("./spreadsheet_figures").then((m) => { + if (root.isConnected) others.push(m.attachFigures(ctx, opts.figureSrc)); + }); + } + grid.render(); select(ctx.selection, false, true); loadLinks(); + loadFigures(); if (opts.conditions && !readOnly) void import("./spreadsheet_condition").then((m) => { if (root.isConnected) others.push(m.attachCondition(ctx, opts.conditions!)); @@ -292,6 +307,7 @@ export function createSpreadsheet( grid.render(); select(ctx.selection, false, true); loadLinks(); + loadFigures(); }, recalc() { ctx.engine.rebuild(ctx.book); diff --git a/A00_Common/spreadsheet/spreadsheet_figures.ts b/A00_Common/spreadsheet/spreadsheet_figures.ts new file mode 100644 index 000000000..fedbc7043 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_figures.ts @@ -0,0 +1,105 @@ +/* ============================================================================= + * spreadsheet_figures.ts (주인 P · 534 sub6) + * 그림 겹판 · 쪽 나눔 점선(요구 347-5) — 시트 `그림`(범위 닻)을 칸 위에 얹고 `인쇄.쪽나눔` 행 아래에 점선. + * 격자 overlay 맨 아래 층(고름 테두리가 위) · 누름은 칸으로 통과 · 스크롤 · 확대 · 다시 그림마다 자리를 다시 셈. + * 그림 주소 = 부른 쪽 `figureSrc(열쇠)` · 없으면 이름 적힌 빈 상자. 그림 옮기기 · 나눔 고치기 화면은 없음(1차). + * `createSpreadsheet` 가 그림 · 쪽 나눔이 있는 문서일 때만 불러옴(크기 예산). + * ========================================================================== */ + +import { parseRange } from "./spreadsheet_address"; +import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types"; + +export type FigureSrc = (열쇠: string) => string | undefined; + +const css = (node: HTMLElement, style: Partial) => + Object.assign(node.style, style); + +export function attachFigures(ctx: SpreadsheetContext, figureSrc?: FigureSrc): PartHandle { + const layer = document.createElement("div"); + layer.className = "ss-figures"; + css(layer, { position: "absolute", inset: "0", overflow: "hidden", pointerEvents: "none" }); + const imgs = new Map(); // 열쇠 → 그림(다시 그려도 다시 안 받음) + + function draw(): void { + const overlay = ctx.grid.root.querySelector(".aislo-grid__overlay"); + if (!overlay) return; + if (overlay.firstChild !== layer) overlay.prepend(layer); + // editorSlot 상자 = 격자 뿌리 기준 → overlay 기준(머리 칸만큼 밀림) + const box = (r: number, c: number) => { + const b = ctx.grid.editorSlot(r, c).box; + return { ...b, x: b.x - overlay.offsetLeft, y: b.y - overlay.offsetTop }; + }; + const sheet = ctx.sheet(); + const parts: HTMLElement[] = []; + for (const f of sheet.그림 ?? []) { + const rg = parseRange(f.닻); + if (!rg) continue; + const a = box(rg.r0, rg.c0); + const b = box(rg.r1, rg.c1); + const frame = document.createElement("div"); + frame.className = "ss-figure"; + frame.dataset.key = f.열쇠; + css(frame, { + position: "absolute", + left: `${a.x}px`, + top: `${a.y}px`, + width: `${Math.max(0, b.x + b.w - a.x)}px`, + height: `${Math.max(0, b.y + b.h - a.y)}px`, + boxSizing: "border-box", + background: "#fff", + }); + const src = figureSrc?.(f.열쇠); + if (src) { + let img = imgs.get(f.열쇠); + if (!img || img.getAttribute("src") !== src) { + img = document.createElement("img"); + img.src = src; + img.alt = f.이름 ?? f.열쇠; + css(img, { width: "100%", height: "100%", objectFit: "contain", display: "block" }); + imgs.set(f.열쇠, img); + } + frame.append(img); + } else { + css(frame, { + border: "1px dashed #888", + color: "#666", + font: "12px sans-serif", + padding: "4px", + }); + frame.textContent = f.이름 ?? f.열쇠; + } + parts.push(frame); + } + const view = ctx.grid.visibleRange(); + for (const k of sheet.인쇄?.쪽나눔 ?? []) { + const at = box(k - 1, view.c0); + const line = document.createElement("div"); + line.className = "ss-pagebreak"; + line.dataset.row = String(k); + css(line, { + position: "absolute", + left: "0", + right: "0", + top: `${at.y + at.h - 1}px`, + borderTop: "2px dashed #3b6fd8", + }); + parts.push(line); + } + layer.replaceChildren(...parts); + } + + const onScroll = () => draw(); + ctx.grid.root.addEventListener("scroll", onScroll, true); + ctx.grid.root.addEventListener("aislo-grid-zoom", onScroll); + draw(); + + return { + root: null, + refresh: draw, + destroy() { + ctx.grid.root.removeEventListener("scroll", onScroll, true); + ctx.grid.root.removeEventListener("aislo-grid-zoom", onScroll); + layer.remove(); + }, + }; +}