/* ============================================================================= * spreadsheet_paste_special.ts (2단계 · 주인 sub_laptop_1) * 붙여넣기 골라서 — 값만 · 서식만 · 수식만(Ctrl+Shift+V). `ClipBlock`(0 계약) 을 받아 * 대상 왼위(`at`)에 맞게 명령을 만듦 — 서식은 `서식` 명령(칸마다 하나 · A 가 표 번호를 매김) · * 값 · 식은 `칸` 명령(대상 칸의 지금 서식 번호는 그대로 둠). 참조 옮김은 「원점」 이 있을 때만 * `$` 없는 A1 참조를 얕게 옮김(진짜 옮김은 A `spreadsheet_refshift` 몫 — 여긴 메뉴용 편의). * * 잇는 법(D · sub7) — 클립보드(E)가 만든 `ClipBlock` 을 들고: * 1) 우클릭 메뉴 · Ctrl+Shift+V 로 모드 고르는 작은 팝업을 띄움(값만 · 서식만 · 수식만). * 2) 고른 모드로 `pasteSpecial(ctx, block, ctx.selection.활성, mode, sourceOrigin?)` 호출. * 3) 「값만」 은 원본이 아직 화면에 있을 때만 계산값을 살릴 수 있음 — `sourceOrigin` 을 줌(없으면 식 칸은 건너뜀). * ========================================================================== */ import type { Cell, CellAddress, CellRange, CellStyle, ClipBlock, ClipCell, Command, Frac, Scalar, } from "./spreadsheet_types"; import type { SpreadsheetContext } from "./spreadsheet_view_types"; import { fracToString } from "@ui/sheet/ui_template_sheet_frac"; export type PasteSpecialMode = "값" | "서식" | "수식"; const a1 = (r: number, c: number): string => { let col = c + 1; let letters = ""; while (col > 0) { const rem = (col - 1) % 26; letters = String.fromCharCode(65 + rem) + letters; col = Math.floor((col - 1) / 26); } return `${letters}${r + 1}`; }; /** `$` 없는 A1 참조만 (dr, dc) 만큼 옮김 — 절대참조 · 다른 시트 이름은 손대지 않음(안전한 최소). */ export function shiftRelativeRefs(formula: string, dr: number, dc: number): string { return formula.replace( /(\$?)([A-Z]{1,3})(\$?)(\d+)/g, (whole, absC, colLetters, absR, rowDigits) => { if (absC === "$" && absR === "$") return whole; let col = 0; for (const ch of colLetters) col = col * 26 + (ch.charCodeAt(0) - 64); const nextCol = absC === "$" ? col : col + dc; const nextRow = absR === "$" ? Number(rowDigits) : Number(rowDigits) + dr; if (nextCol < 1 || nextRow < 1) return "#REF!"; let letters = ""; let n = nextCol; while (n > 0) { const rem = (n - 1) % 26; letters = String.fromCharCode(65 + rem) + letters; n = Math.floor((n - 1) / 26); } return `${absC}${letters}${absR}${nextRow}`; }, ); } export interface PasteSpecialSource { 시트: string; r: number; c: number; } /** `block` 을 `at` 에 골라 붙임. `sourceOrigin` 은 「값만」 때 식 칸을 계산값으로 굳히는 데 씀(없으면 식 칸은 건너뜀). */ export function pasteSpecial( ctx: SpreadsheetContext, block: ClipBlock, at: CellAddress, mode: PasteSpecialMode, sourceOrigin?: PasteSpecialSource, ): void { const sheetId = ctx.selection.시트; const sheet = ctx.sheet(); const dr = at.r - (block.원점?.r ?? at.r); const dc = at.c - (block.원점?.c ?? at.c); const commands: Command[] = []; const 칸: Record = {}; for (const [key, clip] of Object.entries(block.칸)) { const [rowStr, colStr] = key.split(","); const rr = at.r + Number(rowStr); const cc = at.c + Number(colStr); if (rr < 0 || cc < 0) continue; const target = a1(rr, cc); const existing = sheet.칸[target]; if (mode === "서식") { if (clip.서식) commands.push(styleCommand(sheetId, rr, cc, clip.서식)); continue; } if (mode === "수식") { if (clip.식 === undefined) continue; const moved = block.원점 ? shiftRelativeRefs(clip.식, dr, dc) : clip.식; 칸[target] = { 식: moved, 서식: existing?.서식 }; continue; } // mode === "값" const value = resolveClipValue(clip, sourceOrigin, Number(rowStr), Number(colStr), ctx); if (value === undefined) continue; 칸[target] = { 값: value, 서식: existing?.서식 }; } if (Object.keys(칸).length > 0) commands.push({ 종류: "칸", 시트: sheetId, 칸 }); if (commands.length === 0) return; ctx.dispatch(commands.length === 1 ? commands[0] : { 종류: "묶음", 명령: commands }); } function styleCommand(sheetId: string, r: number, c: number, style: CellStyle): Command { const 범위: CellRange = { r0: r, c0: c, r1: r, c1: c }; return { 종류: "서식", 시트: sheetId, 범위: [범위], 바꿀: { ...style } }; } function resolveClipValue( clip: ClipCell, origin: PasteSpecialSource | undefined, dr: number, dc: number, ctx: SpreadsheetContext, ): Cell["값"] | undefined { if (clip.식 !== undefined) { if (!origin) return undefined; const scalar = ctx.engine.value(origin.시트, origin.r + dr, origin.c + dc); return scalarFrom(scalar); } return clip.값; } function scalarFrom(value: Scalar): Cell["값"] | undefined { if (value === null) return undefined; if (typeof value === "string" || typeof value === "boolean") return value; if (typeof value === "object" && "n" in value && "d" in value) return Number(fracToString(value as Frac)); return undefined; }