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:
2026-09-27 20:54:17 +09:00
parent 8ad456d1e1
commit fddaa4edfb
12 changed files with 122 additions and 326 deletions
@@ -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";
+44 -162
View File
@@ -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="{&quot;1&quot;:3,&quot;3&quot;:12.5}">12.5</td>
<td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;"
data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;: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"),