Files
Aislo/A00_Common/spreadsheet/spreadsheet_paste_special.ts
T
eomsangdonandClaude Sonnet 5 d38ef573e0 feat(spreadsheet): 2단계 다섯 — 찾기 · 붙여넣기 골라서 · 서식 붓 · 폭 자동 맞춤 · 함수 도움말
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
2026-09-27 20:20:16 +09:00

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;
}