feat(spreadsheet): E 화면 시험 진짜 A · B 엔진으로 다시 · 클립보드 HTML 견본 추가
- 화면 시험 틀 가짜(fakes/) 걷고 진짜 `applyCommand` · `createCalcEngine` · `createHistory` · 주소 · 숫자 형식으로 재시험 - 서식 · 병합 · 탭 이름 바꾸면 다른 시트 참조 식도 옮김 · 안↔안 클립보드 수식 상대 이동을 실제 엔진 계산값으로 확인 - 엑셀(<table> + mso 스타일 · colspan · x:str/x:num · 「수식 표시」) · 구글(google-sheets-html-origin · data-sheets-formula) 클립보드 HTML 견본 추가 · `parseClipboard` 로 붙여넣기 시험 - 이 PC 는 엑셀 앱 없음 · 구글 시트는 로그인 상태로 새 문서까지 열었으나 자동화 창이 OS 포커스를 못 받아(Document is not focused) 실제 클립보드 왕복은 못 함 — 견본 파일 시험으로 갈음 - toolbar · tabs · menu · clipboard 머리 주석에 D(`spreadsheet.ts`) 잇는 자리 설명 추가
This commit is contained in:
@@ -1,9 +1,16 @@
|
||||
/* =============================================================================
|
||||
* spreadsheet_clipboard.ts (주인 E)
|
||||
* 복사 · 잘라내기 · 붙여넣기 — `copy` · `cut` · `paste` 사건만(허락 창 없음 · navigator.clipboard 안 씀).
|
||||
* 형 · 수식 살리기 규칙은 `spreadsheet_types.ts` `ClipBlock` 머리. 잘라 붙이기(안쪽) = `옮기기` 명령 ·
|
||||
* 나머지 = `칸` 명령(식은 A `moveFormula` · 구글 R1C1 은 `r1c1ToA1`) + 서식 · 병합 명령 묶음.
|
||||
* 편집 중(`ctx.editor.editing()`)이면 사건을 편집기에 맡김.
|
||||
* 형 · 수식 살리기 규칙은 `spreadsheet_types.ts` `ClipBlock` 머리. 잘라내기 = 복사 + `내용지움`(누른 순간
|
||||
* 바로 지움) · 붙여넣기 = `칸` 명령(식은 A `moveFormula` · 구글 R1C1 은 `r1c1ToA1`) + 서식 · 병합 명령
|
||||
* 묶음(칸 하나하나). ⚠ 잘라내기는 OS 클립보드를 거치는 비동기 사건(다른 창 · 다른 앱에 붙일 수 있음)이라
|
||||
* `옮기기` 명령(같은 문서 안에서 그 칸을 가리키던 다른 식도 따라감) 은 못 씀 — 잘라낸 칸을 가리키던
|
||||
* 다른 칸의 식은 안 따라옴(엑셀의 「점선 테두리 유지 · 붙일 때만 지움」 도 아님). 편집 중
|
||||
* (`ctx.editor.editing()`)이면 사건을 편집기에 맡김.
|
||||
*
|
||||
* 이음(D `spreadsheet.ts` 잇는 자리) — `attachClipboard(ctx)` 한 번 부름(반환 `root` 없음 · 안 붙여도 됨).
|
||||
* `ctx.root` 의 copy/cut/paste 사건을 직접 받으므로 그 요소가 포커스를 받을 수 있어야 함
|
||||
* (D 의 숨은 textarea 가 늘 그 안에 있어야 함).
|
||||
* ========================================================================== */
|
||||
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
* 우클릭 메뉴 — 칸 · 행 머리 · 열 머리마다: 잘라내기 · 복사 · (붙여넣기 = 「Ctrl+V 를 쓸 것」 안내) ·
|
||||
* 행열 넣기 · 지우기 · 숨기기 · 보이기 · 내용 지우기 · 서식 지우기 · 병합 · 풀기.
|
||||
* `ui_template_context_menu.ts` 재사용 — 우클릭 밖 자리는 그냥 눌러 선택부터 옮김.
|
||||
*
|
||||
* 이음(D `spreadsheet.ts` 잇는 자리) — `attachMenu(ctx)` 한 번 부름(반환 `root` = 메뉴 껍데기 ·
|
||||
* 안 붙여도 됨 · `ctx.root` 안에 스스로 넣음). `ctx.grid.hitTest` 가 준비된 뒤(C 격자 붙은 뒤) 불러야 함.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu";
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
* 시트 탭 — 고르기(`ctx.showSheet`) · 더하기 · 이름 바꾸기(두 번 누르기) · 지우기 ·
|
||||
* 끌어 옮기기 · 복사. 우클릭 메뉴는 `ui_template_context_menu.ts` 재사용.
|
||||
* 이름 바꾸기 · 지우기 · 옮기기는 명령(`시트이름` · `시트지우기` · `시트옮기기` · `시트더하기`).
|
||||
*
|
||||
* 이음(D `spreadsheet.ts` 잇는 자리) — `mountTabs(ctx)` 한 번 부르고 `root` 를 탭 자리(격자 아래)에
|
||||
* 붙임. `ctx.showSheet` 로 활성 시트가 바뀌면(다른 길로도) `refresh()` 를 불러 다시 그림.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu";
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
* 글자색 · 채움색 · 가로/세로 정렬 · 줄 바꿈 · 병합 · 테두리 · 숫자 형식.
|
||||
* 모두 `ctx.dispatch` 명령으로 — 이 파일은 문서를 직접 안 건드림.
|
||||
* 활성 칸 서식을 `refresh()` 때 단추 상태로. readOnly 면 안 붙임(root null).
|
||||
*
|
||||
* 이음(D `spreadsheet.ts` 잇는 자리) — `mountToolbar(ctx)` 한 번 부르고 `root`(null 이면 안 붙임)를
|
||||
* 도구 모음 자리에 붙임. 고름 · 문서가 바뀔 때마다(자기 dispatch 뒤 포함) `refresh()` 를 부름 —
|
||||
* 스스로 구독하지 않음. `destroy()` 는 부품을 뗄 때.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* =============================================================================
|
||||
* e_harness.ts — E(도구 모음 · 탭 · 메뉴 · 클립보드) 화면 시험 틀.
|
||||
* A(주소 · 명령 · 참조 옮김) · B(숫자 형식) 는 아직 0 계약 몸만 있어 `fakes/`(진짜 대신 최소
|
||||
* 흉내)를 `vite.harness.config.ts` 별칭으로 끼움. dispatch 는 이 파일이 직접 문서에 적용해
|
||||
* 화면에 곧바로 비침 — 진짜 A `applyCommand` 가 아님(시험 전용).
|
||||
* A(주소 · 명령 · 엔진 · 참조 옮김) · B(숫자 형식) 가 이제 실제로 있어 진짜 모듈을 그대로 씀
|
||||
* (`vite.harness.config.ts` 는 `@ui` 별칭만 · 가짜 없음). dispatch 는 진짜 `applyCommand` →
|
||||
* 진짜 `createCalcEngine` → 화면 갱신 순서로 실제 앱과 같은 길을 탐.
|
||||
* ========================================================================== */
|
||||
|
||||
import { mountToolbar } from "../../../A00_Common/spreadsheet/spreadsheet_toolbar";
|
||||
@@ -12,19 +12,15 @@ import {
|
||||
attachClipboard,
|
||||
parseClipboard,
|
||||
} from "../../../A00_Common/spreadsheet/spreadsheet_clipboard";
|
||||
import type {
|
||||
Cell,
|
||||
CellRange,
|
||||
Command,
|
||||
Sheet,
|
||||
Workbook,
|
||||
} from "../../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
import { applyCommand, emptyWorkbook } from "../../../A00_Common/spreadsheet/spreadsheet_commands";
|
||||
import { createCalcEngine } from "../../../A00_Common/spreadsheet/spreadsheet_graph";
|
||||
import { createHistory } from "../../../A00_Common/spreadsheet/spreadsheet_history";
|
||||
import type { Workbook } from "../../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
import type {
|
||||
GridHandle,
|
||||
Selection,
|
||||
SpreadsheetContext,
|
||||
} from "../../../A00_Common/spreadsheet/spreadsheet_view_types";
|
||||
import { colName, rangeToA1, toA1 } from "./fakes/fake_address";
|
||||
|
||||
const log: string[] = [];
|
||||
const logEl = document.querySelector<HTMLPreElement>("#log")!;
|
||||
@@ -33,133 +29,22 @@ function say(msg: string): void {
|
||||
logEl.textContent = log.join("\n");
|
||||
}
|
||||
|
||||
const book: Workbook = {
|
||||
종류: "통합문서",
|
||||
판: 1,
|
||||
열: "test",
|
||||
서식: [{}, { 굵게: true }],
|
||||
시트: [
|
||||
{
|
||||
id: "s1",
|
||||
이름: "측구수로400",
|
||||
칸: {
|
||||
A1: { 값: "터파기 계산서", 서식: 1 },
|
||||
B1: { 값: 12.5 },
|
||||
C1: { 식: "B1*2" },
|
||||
},
|
||||
병합: [],
|
||||
},
|
||||
],
|
||||
활성: "s1",
|
||||
const book: Workbook = emptyWorkbook("test", "측구수로400");
|
||||
book.서식 = [{}, { 굵게: true }];
|
||||
book.시트[0].칸 = {
|
||||
A1: { 값: "터파기 계산서", 서식: 1 },
|
||||
B1: { 값: 12.5 },
|
||||
C1: { 식: "B1*2" },
|
||||
};
|
||||
// 탭 이름 바꾸기가 다른 시트 참조 글도 고치는지 보는 둘째 시트.
|
||||
book.시트.push({
|
||||
id: "s2",
|
||||
이름: "둘째",
|
||||
칸: { A1: { 식: "측구수로400!B1+1" } },
|
||||
});
|
||||
|
||||
function forEachCell(range: CellRange, fn: (r: number, c: number) => void): void {
|
||||
for (let r = range.r0; r <= range.r1; r++) for (let c = range.c0; c <= range.c1; c++) fn(r, c);
|
||||
}
|
||||
|
||||
function styleIndexAt(sheet: Sheet, r: number, c: number): number {
|
||||
return (
|
||||
sheet.칸[toA1(r, c)]?.서식 ??
|
||||
sheet.열?.[colName(c)]?.서식 ??
|
||||
sheet.행?.[String(r + 1)]?.서식 ??
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
function internStyle(book: Workbook, style: Record<string, unknown>): number {
|
||||
const json = JSON.stringify(style);
|
||||
let idx = book.서식.findIndex((s) => JSON.stringify(s) === json);
|
||||
if (idx === -1) {
|
||||
book.서식.push(style as never);
|
||||
idx = book.서식.length - 1;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
/** dispatch 가 흉내로 적용 — 진짜 A `applyCommand` 아님(시험 전용, 모양만 맞춤). */
|
||||
function applyMock(cmd: Command): void {
|
||||
switch (cmd.종류) {
|
||||
case "묶음":
|
||||
cmd.명령.forEach(applyMock);
|
||||
return;
|
||||
case "서식": {
|
||||
const sheet = book.시트.find((s) => s.id === cmd.시트)!;
|
||||
cmd.범위.forEach((range) =>
|
||||
forEachCell(range, (r, c) => {
|
||||
const cur = { ...(book.서식[styleIndexAt(sheet, r, c)] ?? {}) };
|
||||
for (const [k, v] of Object.entries(cmd.바꿀)) {
|
||||
if (v === null) delete (cur as Record<string, unknown>)[k];
|
||||
else (cur as Record<string, unknown>)[k] = v;
|
||||
}
|
||||
const key = toA1(r, c);
|
||||
sheet.칸[key] = { ...sheet.칸[key], 서식: internStyle(book, cur) };
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
case "서식지움": {
|
||||
const sheet = book.시트.find((s) => s.id === cmd.시트)!;
|
||||
cmd.범위.forEach((range) =>
|
||||
forEachCell(range, (r, c) => {
|
||||
const key = toA1(r, c);
|
||||
if (sheet.칸[key]) delete sheet.칸[key].서식;
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
case "내용지움": {
|
||||
const sheet = book.시트.find((s) => s.id === cmd.시트)!;
|
||||
cmd.범위.forEach((range) =>
|
||||
forEachCell(range, (r, c) => {
|
||||
const key = toA1(r, c);
|
||||
const cell = sheet.칸[key];
|
||||
if (!cell) return;
|
||||
delete cell.값;
|
||||
delete cell.식;
|
||||
if (!Object.keys(cell).length) delete sheet.칸[key];
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
case "칸": {
|
||||
const sheet = book.시트.find((s) => s.id === cmd.시트)!;
|
||||
for (const [key, val] of Object.entries(cmd.칸)) {
|
||||
if (val === null) delete sheet.칸[key];
|
||||
else sheet.칸[key] = val as Cell;
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "병합": {
|
||||
const sheet = book.시트.find((s) => s.id === cmd.시트)!;
|
||||
sheet.병합 = [...(sheet.병합 ?? []), rangeToA1(cmd.범위)];
|
||||
return;
|
||||
}
|
||||
case "병합풀기": {
|
||||
const sheet = book.시트.find((s) => s.id === cmd.시트)!;
|
||||
const a1 = rangeToA1(cmd.범위);
|
||||
sheet.병합 = (sheet.병합 ?? []).filter((m) => m !== a1);
|
||||
return;
|
||||
}
|
||||
case "시트더하기":
|
||||
book.시트.splice(cmd.자리, 0, cmd.시트);
|
||||
return;
|
||||
case "시트지우기":
|
||||
book.시트 = book.시트.filter((s) => s.id !== cmd.시트);
|
||||
if (book.활성 === cmd.시트) book.활성 = book.시트[0]?.id;
|
||||
return;
|
||||
case "시트이름":
|
||||
book.시트.find((s) => s.id === cmd.시트)!.이름 = cmd.이름;
|
||||
return;
|
||||
case "시트옮기기": {
|
||||
const idx = book.시트.findIndex((s) => s.id === cmd.시트);
|
||||
const [sheet] = book.시트.splice(idx, 1);
|
||||
book.시트.splice(cmd.자리, 0, sheet);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
const engine = createCalcEngine(book);
|
||||
const history = createHistory();
|
||||
|
||||
const selection: Selection = {
|
||||
시트: "s1",
|
||||
@@ -190,45 +75,40 @@ const grid: GridHandle = {
|
||||
destroy() {},
|
||||
};
|
||||
|
||||
function applyAndRefresh(command: import("../../../A00_Common/spreadsheet/spreadsheet_types").Command): void {
|
||||
const effect = applyCommand(book, command);
|
||||
if (effect.rebuild) engine.rebuild(book);
|
||||
else engine.update(effect.cells);
|
||||
toolbar.refresh();
|
||||
tabs.refresh();
|
||||
}
|
||||
|
||||
const ctx: SpreadsheetContext = {
|
||||
root,
|
||||
book,
|
||||
readOnly: false,
|
||||
engine: {
|
||||
update: () => [],
|
||||
rebuild() {},
|
||||
value: (sheetId, r, c) => {
|
||||
const sheet = book.시트.find((s) => s.id === sheetId);
|
||||
const cell = sheet?.칸[toA1(r, c)];
|
||||
if (!cell) return null;
|
||||
if (cell.식 !== undefined) return { n: 999n, d: 1n };
|
||||
if (typeof cell.값 === "number") return { n: BigInt(Math.round(cell.값 * 100)), d: 100n };
|
||||
if (cell.값 === undefined) return null;
|
||||
return cell.값;
|
||||
},
|
||||
precedents: () => [],
|
||||
snapshot: () => ({}),
|
||||
},
|
||||
history: {
|
||||
push() {},
|
||||
undo: () => null,
|
||||
redo: () => null,
|
||||
canUndo: () => false,
|
||||
canRedo: () => false,
|
||||
},
|
||||
engine,
|
||||
history,
|
||||
selection,
|
||||
sheet: () => book.시트.find((s) => s.id === selection.시트)!,
|
||||
dispatch(command) {
|
||||
say(`dispatch: ${JSON.stringify(command)}`);
|
||||
applyMock(command);
|
||||
const effect = applyCommand(book, command);
|
||||
history.push(command, effect.undo);
|
||||
if (effect.rebuild) engine.rebuild(book);
|
||||
else engine.update(effect.cells);
|
||||
toolbar.refresh();
|
||||
tabs.refresh();
|
||||
},
|
||||
undo() {
|
||||
say("undo");
|
||||
const cmd = history.undo();
|
||||
say(`undo: ${cmd ? JSON.stringify(cmd) : "(없음)"}`);
|
||||
if (cmd) applyAndRefresh(cmd);
|
||||
},
|
||||
redo() {
|
||||
say("redo");
|
||||
const cmd = history.redo();
|
||||
say(`redo: ${cmd ? JSON.stringify(cmd) : "(없음)"}`);
|
||||
if (cmd) applyAndRefresh(cmd);
|
||||
},
|
||||
select(sel) {
|
||||
Object.assign(selection, sel);
|
||||
@@ -238,6 +118,8 @@ const ctx: SpreadsheetContext = {
|
||||
showSheet(sheetId) {
|
||||
book.활성 = sheetId;
|
||||
selection.시트 = sheetId;
|
||||
selection.범위 = [{ r0: 0, c0: 0, r1: 0, c1: 0 }];
|
||||
selection.활성 = { r: 0, c: 0 };
|
||||
say(`showSheet: ${sheetId}`);
|
||||
tabs.refresh();
|
||||
toolbar.refresh();
|
||||
@@ -260,7 +142,7 @@ attachClipboard(ctx);
|
||||
document.querySelector("#toolbar-slot")!.append(toolbar.root!);
|
||||
document.querySelector("#tabs-slot")!.append(tabs.root!);
|
||||
|
||||
say("mounted — 도구 모음 · 탭 · 메뉴 · 클립보드 붙음");
|
||||
say("mounted — 도구 모음 · 탭 · 메뉴 · 클립보드 붙음(진짜 A · B 엔진)");
|
||||
|
||||
(window as unknown as { __ctx: unknown; __book: unknown }).__ctx = ctx;
|
||||
(window as unknown as { __ctx: unknown; __book: unknown }).__book = book;
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
/* =============================================================================
|
||||
* fake_address.ts — E 시험 전용 가짜(A 몫 `spreadsheet_address.ts` 대역).
|
||||
* A 가 아직 없어(0 계약 직후) 도구 모음 · 탭 · 메뉴가 `toA1` 등을 부르면 던지므로
|
||||
* 화면 시험에서만 `vite.harness.config.ts` 별칭으로 이것을 끼움. 진짜 코드는 안 건드림.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CellAddress, CellRange } from "../../../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
|
||||
export function colName(c: number): string {
|
||||
let n = c + 1;
|
||||
let s = "";
|
||||
while (n > 0) {
|
||||
const rem = (n - 1) % 26;
|
||||
s = String.fromCharCode(65 + rem) + s;
|
||||
n = Math.floor((n - 1) / 26);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export function colIndex(letters: string): number {
|
||||
let n = 0;
|
||||
for (const ch of letters.toUpperCase()) {
|
||||
const d = ch.charCodeAt(0) - 64;
|
||||
if (d < 1 || d > 26) return -1;
|
||||
n = n * 26 + d;
|
||||
}
|
||||
return n - 1;
|
||||
}
|
||||
|
||||
export function toA1(r: number, c: number): string {
|
||||
return `${colName(c)}${r + 1}`;
|
||||
}
|
||||
|
||||
export function parseA1(text: string): CellAddress | null {
|
||||
const m = /^\$?([A-Za-z]+)\$?(\d+)$/.exec(text.trim());
|
||||
if (!m) return null;
|
||||
const c = colIndex(m[1]);
|
||||
const r = Number(m[2]) - 1;
|
||||
if (c < 0 || r < 0) return null;
|
||||
return { r, c };
|
||||
}
|
||||
|
||||
export function rangeToA1(range: CellRange): string {
|
||||
const a = toA1(range.r0, range.c0);
|
||||
const b = toA1(range.r1, range.c1);
|
||||
return a === b ? a : `${a}:${b}`;
|
||||
}
|
||||
|
||||
export function parseRange(text: string): CellRange | null {
|
||||
const t = text.trim();
|
||||
if (t.includes(":")) {
|
||||
const [a, b] = t.split(":");
|
||||
const pa = parseA1(a);
|
||||
const pb = parseA1(b);
|
||||
if (!pa || !pb) return null;
|
||||
return {
|
||||
r0: Math.min(pa.r, pb.r),
|
||||
c0: Math.min(pa.c, pb.c),
|
||||
r1: Math.max(pa.r, pb.r),
|
||||
c1: Math.max(pa.c, pb.c),
|
||||
};
|
||||
}
|
||||
const p = parseA1(t);
|
||||
return p ? { r0: p.r, c0: p.c, r1: p.r, c1: p.c } : null;
|
||||
}
|
||||
|
||||
export function quoteSheet(name: string): string {
|
||||
return /^[A-Za-z0-9_]+$/.test(name) ? name : `'${name.replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
const MAX_COLS = 16_384;
|
||||
export function cellId(r: number, c: number): number {
|
||||
return r * MAX_COLS + c;
|
||||
}
|
||||
|
||||
export function fromCellId(id: number): CellAddress {
|
||||
return { r: Math.floor(id / MAX_COLS), c: id % MAX_COLS };
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
/* =============================================================================
|
||||
* fake_commands.ts — E 시험 전용 가짜(A 몫 `spreadsheet_commands.ts` 대역).
|
||||
* 탭의 [+] 가 부르는 `emptySheet` 만 흉내(진짜 `applyCommand` 는 안 씀 · dispatch 는 harness 가 spy).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { Sheet } from "../../../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
|
||||
export function emptySheet(id: string, name: string): Sheet {
|
||||
return { id, 이름: name, 칸: {} };
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/* =============================================================================
|
||||
* fake_numfmt.ts — E 시험 전용 가짜(B 몫 `spreadsheet_numfmt.ts` 대역).
|
||||
* 클립보드 「안 → 밖」 시험이 보이는 글을 만들 수 있도록 아주 단순한 형식만 흉내.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { FormattedText, Scalar } from "../../../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
|
||||
export function formatValue(value: Scalar, code: string | undefined): FormattedText {
|
||||
if (value === null) return { 글: "", 정렬: "left" };
|
||||
if (typeof value === "boolean") return { 글: value ? "TRUE" : "FALSE", 정렬: "center" };
|
||||
if (typeof value === "string") return { 글: value, 정렬: "left" };
|
||||
if (typeof value === "object" && "error" in value) return { 글: value.error, 정렬: "center" };
|
||||
const num = Number(value.n) / Number(value.d);
|
||||
const text =
|
||||
code === "0.00" ? num.toFixed(2) : code === "0" ? String(Math.round(num)) : String(num);
|
||||
return { 글: text, 정렬: "right" };
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
/* =============================================================================
|
||||
* fake_refshift.ts — E 시험 전용 가짜(A 몫 `spreadsheet_refshift.ts` 대역).
|
||||
* 클립보드 붙여넣기 시험이 상대 참조 옮김 · 구글 R1C1 을 확인할 수 있도록 아주 단순히 흉내.
|
||||
* ========================================================================== */
|
||||
|
||||
import { colIndex, colName } from "./fake_address";
|
||||
import type { CellAddress } from "../../../../A00_Common/spreadsheet/spreadsheet_types";
|
||||
|
||||
/** 복사 · 붙여넣기 — `$` 없는 A1 참조만 (dr, dc) 옮김. */
|
||||
export function moveFormula(formula: string, dr: number, dc: number): string {
|
||||
return formula.replace(/(\$?)([A-Za-z]{1,3})(\$?)(\d+)/g, (_m, absCol, col, absRow, row) => {
|
||||
let c = colIndex(col);
|
||||
let r = Number(row) - 1;
|
||||
if (!absCol) c += dc;
|
||||
if (!absRow) r += dr;
|
||||
if (c < 0 || r < 0) return "#REF!";
|
||||
return `${absCol}${colName(c)}${absRow}${r + 1}`;
|
||||
});
|
||||
}
|
||||
|
||||
/** 구글 `R[0]C[-1]` 꼴 → 붙일 자리(`at`) 기준 A1 식. */
|
||||
export function r1c1ToA1(formula: string, at: CellAddress): string {
|
||||
return formula.replace(
|
||||
/R(\[-?\d+\]|\d+)?C(\[-?\d+\]|\d+)?/g,
|
||||
(_m, rp: string | undefined, cp: string | undefined) => {
|
||||
const row =
|
||||
rp === undefined
|
||||
? at.r + 1
|
||||
: rp.startsWith("[")
|
||||
? at.r + 1 + Number(rp.slice(1, -1))
|
||||
: Number(rp);
|
||||
const col =
|
||||
cp === undefined
|
||||
? at.c + 1
|
||||
: cp.startsWith("[")
|
||||
? at.c + 1 + Number(cp.slice(1, -1))
|
||||
: Number(cp);
|
||||
return `${colName(col - 1)}${row}`;
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<!-- fixture_excel_paste.html — 엑셀이 클립보드에 싣는 실제 HTML 모양 견본(복사 → 붙여넣기 시험용).
|
||||
StartFragment/EndFragment 표지 · mso-number-format 스타일 클래스 · x:str/x:num 데이터 형 ·
|
||||
colspan(병합) 을 실제 엑셀 내보내기와 같은 짜임으로 재현. E `parseClipboard` 가 값(텍스트) ·
|
||||
colspan → 병합만 읽고 mso 서식은 1단계 밖(설계 `9_엑셀검토.md` 3-2 표 참고). -->
|
||||
<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
<!--table
|
||||
{mso-displayed-decimal-separator:"\.";
|
||||
mso-displayed-thousand-separator:"\,";}
|
||||
.xl65
|
||||
{mso-number-format:"0.00";
|
||||
text-align:right;}
|
||||
.xl66
|
||||
{mso-number-format:"General";
|
||||
font-weight:700;
|
||||
text-align:center;}
|
||||
-->
|
||||
</style>
|
||||
</head>
|
||||
<body link="#0563C1" vlink="#954F72">
|
||||
<!--StartFragment-->
|
||||
<table border=0 cellpadding=0 cellspacing=0 width=192 style='border-collapse:
|
||||
collapse;width:144pt'>
|
||||
<col width=64 span=3 style='width:48pt'>
|
||||
<tr height=20 style='height:15.0pt'>
|
||||
<td class=xl66 colspan=2 width=128 style='width:96pt' x:str>측구수로400</td>
|
||||
<td class=xl65 width=64 style='width:48pt' x:num>12.50</td>
|
||||
</tr>
|
||||
<tr height=20 style='height:15.0pt'>
|
||||
<td class=xl65 x:num>0.27</td>
|
||||
<td x:str>=ROUNDDOWN((G31+J31/2)*P31,2)</td>
|
||||
<td x:str>=</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!--EndFragment-->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
<!-- fixture_google_paste.html — 구글 시트가 클립보드에 싣는 실제 HTML 모양 견본.
|
||||
google-sheets-html-origin 표지 · data-sheets-value(형 붙은 값 JSON, E 는 안 읽음 —
|
||||
보이는 글로 대신) · data-sheets-formula(R1C1 상대 수식, E 가 `r1c1ToA1` 로 절대 A1 로 풂). -->
|
||||
<google-sheets-html-origin>
|
||||
<table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr"
|
||||
border="1" style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none">
|
||||
<colgroup><col width="100"/><col width="100"/></colgroup>
|
||||
<tbody>
|
||||
<tr style="height:21px;">
|
||||
<td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;"
|
||||
data-sheets-value="{"1":3,"3":12.5}">12.5</td>
|
||||
<td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;"
|
||||
data-sheets-value="{"1":3,"3":25}"
|
||||
data-sheets-formula="=R[0]C[-1]*2">25</td>
|
||||
</tr>
|
||||
</tbody></table>
|
||||
</google-sheets-html-origin>
|
||||
@@ -7,26 +7,13 @@ const repoRoot = resolve(__dirname, "../../..");
|
||||
|
||||
/* =============================================================================
|
||||
* vite.harness.config.ts — E 화면 시험 전용 빌드(정본 `vite.config.ts` 는 안 건드림).
|
||||
* A(`spreadsheet_address`) · A(`spreadsheet_commands`) 가 아직 0 계약 몸만 있어(던짐) 이
|
||||
* 폴더의 가짜로 바꿔 끼움 — E 가 만든 화면 부품만 시험. 진짜 앱 빌드에는 안 쓰임.
|
||||
* A · B 가 이제 실제로 있어 가짜 없이 진짜 모듈을 그대로 번들 — `@ui` 별칭만 다시 잡음.
|
||||
* ========================================================================== */
|
||||
export default {
|
||||
root: __dirname,
|
||||
publicDir: false,
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: "./spreadsheet_address", replacement: resolve(__dirname, "./fakes/fake_address.ts") },
|
||||
{
|
||||
find: "./spreadsheet_commands",
|
||||
replacement: resolve(__dirname, "./fakes/fake_commands.ts"),
|
||||
},
|
||||
{ find: "./spreadsheet_numfmt", replacement: resolve(__dirname, "./fakes/fake_numfmt.ts") },
|
||||
{
|
||||
find: "./spreadsheet_refshift",
|
||||
replacement: resolve(__dirname, "./fakes/fake_refshift.ts"),
|
||||
},
|
||||
{ find: "@ui", replacement: resolve(repoRoot, "./ui_template") },
|
||||
],
|
||||
alias: [{ find: "@ui", replacement: resolve(repoRoot, "./ui_template") }],
|
||||
},
|
||||
build: {
|
||||
outDir: resolve(tmpdir(), "aislo_e_harness_dist"),
|
||||
|
||||
Reference in New Issue
Block a user