From fddaa4edfb8d5a70f9190d1528f6096c149bad13 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 27 Sep 2026 20:54:17 +0900 Subject: [PATCH] =?UTF-8?q?feat(spreadsheet):=20E=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=8B=9C=ED=97=98=20=EC=A7=84=EC=A7=9C=20A=20=C2=B7=20B=20?= =?UTF-8?q?=EC=97=94=EC=A7=84=EC=9C=BC=EB=A1=9C=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=C2=B7=20=ED=81=B4=EB=A6=BD=EB=B3=B4=EB=93=9C=20HTML=20?= =?UTF-8?q?=EA=B2=AC=EB=B3=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 화면 시험 틀 가짜(fakes/) 걷고 진짜 `applyCommand` · `createCalcEngine` · `createHistory` · 주소 · 숫자 형식으로 재시험 - 서식 · 병합 · 탭 이름 바꾸면 다른 시트 참조 식도 옮김 · 안↔안 클립보드 수식 상대 이동을 실제 엔진 계산값으로 확인 - 엑셀( + 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`) 잇는 자리 설명 추가 --- .../spreadsheet/spreadsheet_clipboard.ts | 13 +- A00_Common/spreadsheet/spreadsheet_menu.ts | 3 + A00_Common/spreadsheet/spreadsheet_tabs.ts | 3 + A00_Common/spreadsheet/spreadsheet_toolbar.ts | 4 + resources/tester/spreadsheet/e_harness.ts | 206 ++++-------------- .../tester/spreadsheet/fakes/fake_address.ts | 78 ------- .../tester/spreadsheet/fakes/fake_commands.ts | 10 - .../tester/spreadsheet/fakes/fake_numfmt.ts | 17 -- .../tester/spreadsheet/fakes/fake_refshift.ts | 41 ---- .../spreadsheet/fixture_excel_paste.html | 39 ++++ .../spreadsheet/fixture_google_paste.html | 17 ++ .../tester/spreadsheet/vite.harness.config.ts | 17 +- 12 files changed, 122 insertions(+), 326 deletions(-) delete mode 100644 resources/tester/spreadsheet/fakes/fake_address.ts delete mode 100644 resources/tester/spreadsheet/fakes/fake_commands.ts delete mode 100644 resources/tester/spreadsheet/fakes/fake_numfmt.ts delete mode 100644 resources/tester/spreadsheet/fakes/fake_refshift.ts create mode 100644 resources/tester/spreadsheet/fixture_excel_paste.html create mode 100644 resources/tester/spreadsheet/fixture_google_paste.html diff --git a/A00_Common/spreadsheet/spreadsheet_clipboard.ts b/A00_Common/spreadsheet/spreadsheet_clipboard.ts index 19f3cd96..4cad6c73 100644 --- a/A00_Common/spreadsheet/spreadsheet_clipboard.ts +++ b/A00_Common/spreadsheet/spreadsheet_clipboard.ts @@ -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"; diff --git a/A00_Common/spreadsheet/spreadsheet_menu.ts b/A00_Common/spreadsheet/spreadsheet_menu.ts index e9ae8e67..f2b7f2ee 100644 --- a/A00_Common/spreadsheet/spreadsheet_menu.ts +++ b/A00_Common/spreadsheet/spreadsheet_menu.ts @@ -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"; diff --git a/A00_Common/spreadsheet/spreadsheet_tabs.ts b/A00_Common/spreadsheet/spreadsheet_tabs.ts index c82fda65..ab731679 100644 --- a/A00_Common/spreadsheet/spreadsheet_tabs.ts +++ b/A00_Common/spreadsheet/spreadsheet_tabs.ts @@ -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"; diff --git a/A00_Common/spreadsheet/spreadsheet_toolbar.ts b/A00_Common/spreadsheet/spreadsheet_toolbar.ts index d388fcd3..30846b46 100644 --- a/A00_Common/spreadsheet/spreadsheet_toolbar.ts +++ b/A00_Common/spreadsheet/spreadsheet_toolbar.ts @@ -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"; diff --git a/resources/tester/spreadsheet/e_harness.ts b/resources/tester/spreadsheet/e_harness.ts index e6cee49e..eae07abd 100644 --- a/resources/tester/spreadsheet/e_harness.ts +++ b/resources/tester/spreadsheet/e_harness.ts @@ -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("#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): 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)[k]; - else (cur as Record)[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; diff --git a/resources/tester/spreadsheet/fakes/fake_address.ts b/resources/tester/spreadsheet/fakes/fake_address.ts deleted file mode 100644 index b46dc19c..00000000 --- a/resources/tester/spreadsheet/fakes/fake_address.ts +++ /dev/null @@ -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 }; -} diff --git a/resources/tester/spreadsheet/fakes/fake_commands.ts b/resources/tester/spreadsheet/fakes/fake_commands.ts deleted file mode 100644 index 822b604c..00000000 --- a/resources/tester/spreadsheet/fakes/fake_commands.ts +++ /dev/null @@ -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, 칸: {} }; -} diff --git a/resources/tester/spreadsheet/fakes/fake_numfmt.ts b/resources/tester/spreadsheet/fakes/fake_numfmt.ts deleted file mode 100644 index 72bcb3f1..00000000 --- a/resources/tester/spreadsheet/fakes/fake_numfmt.ts +++ /dev/null @@ -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" }; -} diff --git a/resources/tester/spreadsheet/fakes/fake_refshift.ts b/resources/tester/spreadsheet/fakes/fake_refshift.ts deleted file mode 100644 index e6240b6f..00000000 --- a/resources/tester/spreadsheet/fakes/fake_refshift.ts +++ /dev/null @@ -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}`; - }, - ); -} diff --git a/resources/tester/spreadsheet/fixture_excel_paste.html b/resources/tester/spreadsheet/fixture_excel_paste.html new file mode 100644 index 00000000..6eb2b486 --- /dev/null +++ b/resources/tester/spreadsheet/fixture_excel_paste.html @@ -0,0 +1,39 @@ + + + + + + + + +
++ + + + + + + + + +
측구수로40012.50
0.27=ROUNDDOWN((G31+J31/2)*P31,2)=
+ + + diff --git a/resources/tester/spreadsheet/fixture_google_paste.html b/resources/tester/spreadsheet/fixture_google_paste.html new file mode 100644 index 00000000..b7345821 --- /dev/null +++ b/resources/tester/spreadsheet/fixture_google_paste.html @@ -0,0 +1,17 @@ + + + + + + + + + +
12.525
+
diff --git a/resources/tester/spreadsheet/vite.harness.config.ts b/resources/tester/spreadsheet/vite.harness.config.ts index 8b47d9a0..d27581e5 100644 --- a/resources/tester/spreadsheet/vite.harness.config.ts +++ b/resources/tester/spreadsheet/vite.harness.config.ts @@ -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"),