PLAN 11-8 스프레드시트 2단계. 0 계약(spreadsheet_types.ts · spreadsheet_view_types.ts) 타입만 보고 새 파일 다섯 짬 — 잇기는 1단계 끝에 D(sub7)가 파일 머리 주석대로: - spreadsheet_find.ts 찾기 · 바꾸기(대소문자 · 전체칸 · 수식 안) - spreadsheet_paste_special.ts 붙여넣기 골라서(값만 · 서식만 · 수식만) - spreadsheet_format_painter.ts 서식 붓(칸→행→열→기본 차례로 서식을 찾음) - spreadsheet_autofit.ts 열 머리 두 번 눌러 폭 자동 맞춤(어림 재기 · 실제 재기 자리 열어 둠) - spreadsheet_func_help.ts 함수 자동 완성 · 인자 도움말(1단계 함수 48 개) 여러 범위 · 확대 · 눈금선 끄기는 이미 계약에 있어 새 파일 없이 붙일 자리만 확인(autofit.ts 머리). 시험 = resources/tester/spreadsheet/test_stage2_*.ts(공용 견본 · 가짜 컨텍스트는 test_stage2_helpers.ts). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K7JS47SacjPxZ718QsZKyr
141 lines
5.3 KiB
TypeScript
141 lines
5.3 KiB
TypeScript
/* =============================================================================
|
|
* 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<string, Cell | null> = {};
|
|
|
|
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;
|
|
}
|