diff --git a/A00_Common/spreadsheet/spreadsheet.ts b/A00_Common/spreadsheet/spreadsheet.ts new file mode 100644 index 00000000..8374051b --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet.ts @@ -0,0 +1,45 @@ +/* ============================================================================= + * spreadsheet.ts (주인 D) + * 산출근거 스프레드시트 진입 — `createSpreadsheet(칸, 통합문서, {readOnly, onChange, onSelect})` → + * `{getDoc, setDoc, recalc, destroy}`(표 부품 `createSheet` 와 같은 모양 · M02 는 부르는 줄만 바꿈). + * 문맥(`SpreadsheetContext`)을 만들어 C 격자 · E 도구 · 탭 · 메뉴 · 클립보드를 붙이고 D 입력(고름 · 편집기 · + * 키 · 마우스)을 잇음. 한글 조합 = 숨은 textarea 에 늘 초점 · compositionstart 로 편집 시작(keydown 글자 방식 금지). + * ⚠ M02 만 `import()` 로 불러옴 — 정적 import 금지(다른 페이지 번들에 안 섞이게). + * 저장은 부른 쪽 몫(`onChange` 로 문서 · 자동저장 없음 · [저장] 때 서버가 Node 로 다시 풂). + * 0 계약 머리 — 몸은 D 가 채움. + * ========================================================================== */ + +import type { CalcValues, Workbook } from "./spreadsheet_types"; + +export type { Workbook, CalcValues } from "./spreadsheet_types"; + +export interface SpreadsheetOptions { + /** 읽기만 — 도구 모음 · 메뉴 없음 · 편집 · 붙여넣기 · 끌기 막음(고르기 · 복사는 됨) */ + readOnly?: boolean; + /** 명령마다 문서 사본 — 페이지가 캐시(sessionStorage)에 쌓고 [저장] 단추를 켬 */ + onChange?: (book: Workbook) => void; + /** 고름이 바뀜 — 시트 id · 활성 칸 A1 · 범위 A1 */ + onSelect?: (selection: { 시트: string; 칸: string; 범위: string }) => void; +} + +export interface SpreadsheetHandle { + /** 문서 사본 */ + getDoc(): Workbook; + /** 문서 바꿔 끼움 — 되돌리기 이력 비움 · 고름 A1 */ + setDoc(book: Workbook): void; + /** 전부 다시 풀고 다시 그림 · 결과(저장 모양) */ + recalc(): CalcValues; + destroy(): void; +} + +const todo = (): never => { + throw new Error("spreadsheet: 아직 없음(D)"); +}; + +export function createSpreadsheet( + _host: HTMLElement, + _book: Workbook, + _opts: SpreadsheetOptions, +): SpreadsheetHandle { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_address.ts b/A00_Common/spreadsheet/spreadsheet_address.ts new file mode 100644 index 00000000..fbe436f5 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_address.ts @@ -0,0 +1,55 @@ +/* ============================================================================= + * spreadsheet_address.ts (주인 A) + * 주소 — A1 ↔ 행열(0 부터) · 범위 글 · 열 글자 · 시트 이름 따옴표 · Map 열쇠. + * 0 계약 머리 — 몸은 A 가 채움. 계약은 `spreadsheet_types.ts`. + * ========================================================================== */ + +import type { CellAddress, CellRange } from "./spreadsheet_types"; + +const todo = (): never => { + throw new Error("spreadsheet_address: 아직 없음(A)"); +}; + +/** 0 → `A` · 27 → `AB` */ +export function colName(_c: number): string { + return todo(); +} + +/** `AB` → 27 · 못 읽으면 -1 (대소문자 가리지 않음) */ +export function colIndex(_letters: string): number { + return todo(); +} + +/** (30, 4) → `E31` */ +export function toA1(_r: number, _c: number): string { + return todo(); +} + +/** `E31` · `$E$31` → {r:30, c:4} · 못 읽으면 null */ +export function parseA1(_text: string): CellAddress | null { + return todo(); +} + +/** 칸 하나면 `E31` · 아니면 `C3:AD3` · 행 전체 `3:3` · 열 전체 `A:A` */ +export function rangeToA1(_range: CellRange): string { + return todo(); +} + +/** `C3:AD3` · `E31` · `A:A` · `3:5` → 범위(뒤집힌 글은 바로 세움) · 못 읽으면 null */ +export function parseRange(_text: string): CellRange | null { + return todo(); +} + +/** 식에 넣을 시트 이름 — 글자 · 숫자 · _ 밖이 있으면 `'…'`(안의 `'` 는 `''`) */ +export function quoteSheet(_name: string): string { + return todo(); +} + +/** Map 열쇠 — r × MAX_COLS + c */ +export function cellId(_r: number, _c: number): number { + return todo(); +} + +export function fromCellId(_id: number): CellAddress { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_clipboard.ts b/A00_Common/spreadsheet/spreadsheet_clipboard.ts new file mode 100644 index 00000000..add6efd6 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_clipboard.ts @@ -0,0 +1,33 @@ +/* ============================================================================= + * spreadsheet_clipboard.ts (주인 E) + * 복사 · 잘라내기 · 붙여넣기 — `copy` · `cut` · `paste` 사건만(허락 창 없음 · navigator.clipboard 안 씀). + * 형 · 수식 살리기 규칙은 `spreadsheet_types.ts` `ClipBlock` 머리. 잘라 붙이기(안쪽) = `옮기기` 명령 · + * 나머지 = `칸` 명령(식은 A `moveFormula` · 구글 R1C1 은 `r1c1ToA1`) + 병합 명령 묶음. + * 편집 중(`ctx.editor.editing()`)이면 사건을 편집기에 맡김. 0 계약 머리 — 몸은 E 가 채움. + * ========================================================================== */ + +import type { CellAddress, CellRange, ClipBlock, Workbook } from "./spreadsheet_types"; +import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types"; + +const todo = (): never => { + throw new Error("spreadsheet_clipboard: 아직 없음(E)"); +}; + +/** `ctx.root` 의 copy · cut · paste 를 받음(root null) */ +export function attachClipboard(_ctx: SpreadsheetContext): PartHandle { + return todo(); +} + +/** 클립보드 글 → 뭉치(HTML 먼저 · 없으면 TSV) · 읽을 것 없으면 null · `at` = 붙일 왼위(구글 R1C1 풀이) */ +export function parseClipboard(_html: string, _text: string, _at: CellAddress): ClipBlock | null { + return todo(); +} + +/** 범위 → 클립보드 글 둘(TSV = 보이는 값 · HTML = 인라인 서식 표 + 안쪽 뭉치 JSON) */ +export function blockToClipboard( + _book: Workbook, + _sheet: string, + _range: CellRange, +): { html: string; text: string } { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_commands.ts b/A00_Common/spreadsheet/spreadsheet_commands.ts new file mode 100644 index 00000000..a646b15d --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_commands.ts @@ -0,0 +1,35 @@ +/* ============================================================================= + * spreadsheet_commands.ts (주인 A) + * 명령 적용 — 문서를 그 자리에서 바꾸고 되돌림 짝 · 바뀐 칸 · 통째 다시 여부를 돌려줌. + * 병합 규칙(엑셀): 병합 일부를 가르는 칸 쓰기 · 옮기기는 막음(오류 던짐 → 화면이 알림) · + * 병합 안에 행열을 넣으면 병합이 늘고 지우면 줆 · 병합하면 왼위 칸 값만 남김. + * 서식 표는 같은 서식을 한 번만 둠(칸은 번호) · 안 쓰는 서식 정리는 저장 전에(`compactStyles`). + * 0 계약 머리 — 몸은 A 가 채움. + * ========================================================================== */ + +import type { Command, CommandEffect, Sheet, Workbook } from "./spreadsheet_types"; + +const todo = (): never => { + throw new Error("spreadsheet_commands: 아직 없음(A)"); +}; + +/** 막힌 명령(병합 가름 · 마지막 시트 지우기 · 같은 시트 이름 …) — 화면이 `message` 를 알림 */ +export class CommandError extends Error {} + +export function applyCommand(_book: Workbook, _command: Command): CommandEffect { + return todo(); +} + +/** 빈 통합문서 — 시트 하나(`s1` · 이름은 부른 쪽) · 서식 [{}] */ +export function emptyWorkbook(_column: string, _sheetName: string): Workbook { + return todo(); +} + +export function emptySheet(_id: string, _name: string): Sheet { + return todo(); +} + +/** 안 쓰는 서식을 빼고 번호를 다시 매김 — 저장 직전(되돌리기 짝은 안 만듦) */ +export function compactStyles(_book: Workbook): void { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_eval.ts b/A00_Common/spreadsheet/spreadsheet_eval.ts new file mode 100644 index 00000000..c1b62a31 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_eval.ts @@ -0,0 +1,58 @@ +/* ============================================================================= + * spreadsheet_eval.ts (주인 A) + * 나무 풀기 · 오류 값 전파 · 형 바꾸기(엑셀 규칙). B 의 함수들이 형 바꾸기 도우미를 씀. + * 빈 칸 = 수에서 0 · 글에서 "" · `"12"+1 = 13` · `TRUE+1 = 2` · 글이 수로 안 읽히면 `#VALUE!`. + * 무리수(SQRT · 삼각 · PI · 소수 거듭제곱)는 double → 유효 15 자리 십진 → 분수(B · A 같이 `fromDouble`). + * 0 계약 머리 — 몸은 A 가 채움. + * ========================================================================== */ + +import type { + CalcValue, + ErrorCode, + ErrorValue, + EvalContext, + EvalResult, + FormulaNode, + Frac, + Scalar, +} from "./spreadsheet_types"; + +const todo = (): never => { + throw new Error("spreadsheet_eval: 아직 없음(A)"); +}; + +export function evaluate(_node: FormulaNode, _ctx: EvalContext): EvalResult { + return todo(); +} + +export function err(_code: ErrorCode, _why?: string): ErrorValue { + return todo(); +} + +export function isError(_value: unknown): _value is ErrorValue { + return todo(); +} + +/** 수로 — 빈 칸 0 · 참거짓 1/0 · 수 글 · 아니면 `#VALUE!` · 범위는 칸 하나일 때만 */ +export function asNumber(_value: EvalResult): Frac | ErrorValue { + return todo(); +} + +/** 글로 — 수는 일반 형식 글(엑셀 `&` 와 같게) · 참거짓 `TRUE`/`FALSE` */ +export function asText(_value: EvalResult): string | ErrorValue { + return todo(); +} + +export function asBool(_value: EvalResult): boolean | ErrorValue { + return todo(); +} + +/** double → 유효 15 자리 십진 → 분수 · NaN · 무한은 `#NUM!` */ +export function fromDouble(_x: number): Frac | ErrorValue { + return todo(); +} + +/** 저장 모양 — 수는 십진 글(30 자리까지) */ +export function toCalcValue(_value: Scalar): CalcValue { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_fill.ts b/A00_Common/spreadsheet/spreadsheet_fill.ts new file mode 100644 index 00000000..9619ea06 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_fill.ts @@ -0,0 +1,22 @@ +/* ============================================================================= + * spreadsheet_fill.ts (주인 B) + * 채우기 핸들 — 원본 범위를 대상 범위로 늘림: 수 하나 = 복사 · 수 둘 이상 = 등차 연속 · + * 글 끝 번호(`구-01` → `구-02`) · 식 = 상대 참조 옮김(A `moveFormula`) · 서식 같이. + * 결과는 `{ 종류: "칸" }` 명령에 넣을 칸 뭉치. 0 계약 머리 — 몸은 B 가 채움. + * ========================================================================== */ + +import type { Cell, CellRange, Workbook } from "./spreadsheet_types"; + +const todo = (): never => { + throw new Error("spreadsheet_fill: 아직 없음(B)"); +}; + +/** `target` 은 `source` 를 한 방향으로 늘린 범위(방향은 둘을 견줘 앎) · A1 → 칸(null = 비움) */ +export function fillCells( + _book: Workbook, + _sheet: string, + _source: CellRange, + _target: CellRange, +): Record { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_functions.ts b/A00_Common/spreadsheet/spreadsheet_functions.ts new file mode 100644 index 00000000..5727a888 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_functions.ts @@ -0,0 +1,25 @@ +/* ============================================================================= + * spreadsheet_functions.ts (주인 B) + * 함수 표 — 이름(대문자) → 뜻. 몸은 `spreadsheet_functions_math.ts` · `_misc.ts` 에 두고 여기서 모음. + * 1단계 약 45 — 수학 SUM PRODUCT SUMPRODUCT ROUND ROUNDUP ROUNDDOWN INT TRUNC CEILING FLOOR ABS MOD + * POWER SQRT PI SIN COS TAN ASIN ACOS ATAN RADIANS DEGREES MIN MAX AVERAGE · 논리 IF AND OR NOT IFERROR · + * 글 CONCATENATE FIXED TEXT LEN LEFT RIGHT MID VALUE · 찾기 VLOOKUP HLOOKUP INDEX MATCH CHOOSE · + * 세기 COUNT COUNTA COUNTIF SUMIF. 뜻 · 경계값은 엑셀과 같게(Formula.js 는 시험 대조 답으로만). + * 없는 이름 = `#NAME?` · 인자 수가 min · max 밖이면 A 의 eval 이 `#VALUE!` + 까닭(엑셀은 입력을 막음). + * 0 계약 머리 — 몸은 B 가 채움. + * ========================================================================== */ + +import type { SheetFunction } from "./spreadsheet_types"; + +const todo = (): never => { + throw new Error("spreadsheet_functions: 아직 없음(B)"); +}; + +export function getFunction(_name: string): SheetFunction | undefined { + return todo(); +} + +/** 아는 함수 이름 전부(가나다 · 자동 완성은 2단계) */ +export function functionNames(): string[] { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_graph.ts b/A00_Common/spreadsheet/spreadsheet_graph.ts new file mode 100644 index 00000000..76882474 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_graph.ts @@ -0,0 +1,17 @@ +/* ============================================================================= + * spreadsheet_graph.ts (주인 A) + * 계산 엔진 — 식마다 가리키는 칸 · 범위로 의존 그래프 · 바뀐 칸에 딸린 칸만 차례로 다시 풂. + * 순환은 고리의 칸마다 `#CYCLE!`(까닭 = 고리 칸 목록) · 반복 계산 안 함 · 나머지 칸은 계속 풂. + * 식 나무는 식 글마다 캐시. 0 계약 머리 — 몸은 A 가 채움. + * ========================================================================== */ + +import type { CalcEngine, Workbook } from "./spreadsheet_types"; + +const todo = (): never => { + throw new Error("spreadsheet_graph: 아직 없음(A)"); +}; + +/** 문서를 참조로 쥠 — 만들 때 전부 풂. */ +export function createCalcEngine(_book: Workbook): CalcEngine { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_grid.ts b/A00_Common/spreadsheet/spreadsheet_grid.ts new file mode 100644 index 00000000..30f251ac --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_grid.ts @@ -0,0 +1,18 @@ +/* ============================================================================= + * spreadsheet_grid.ts (주인 C) + * 격자 — DOM(칸 = 절대 위치 div) · 틀고정 4 판(왼위 · 위 · 왼 · 본문) · 판마다 보이는 행열만 그림 · + * 병합 칸 = div 하나 · 숨긴 행열 건너뜀 · 눈금선 · 고름 · 참조 색 테두리 · 채우기 손잡이. + * 칸 글은 엔진 값 → B `formatValue` → 서식 CSS(`spreadsheet_cellstyle.ts`). 머리 · 폭 끌기는 `spreadsheet_headers.ts`. + * 폭 · 높이 끌기는 `ctx.dispatch({종류:"열폭"|"행높이"})`. 0 계약 머리 — 몸은 C 가 채움. + * ========================================================================== */ + +import type { GridHandle, SpreadsheetContext } from "./spreadsheet_view_types"; + +const todo = (): never => { + throw new Error("spreadsheet_grid: 아직 없음(C)"); +}; + +/** `ctx.grid` 는 D 가 이 결과로 채움 — 만들 때 `ctx.grid` · `ctx.editor` 를 부르지 말 것 */ +export function createGrid(_ctx: SpreadsheetContext): GridHandle { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_history.ts b/A00_Common/spreadsheet/spreadsheet_history.ts new file mode 100644 index 00000000..ebf7bce5 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_history.ts @@ -0,0 +1,16 @@ +/* ============================================================================= + * spreadsheet_history.ts (주인 A) + * 되돌리기 · 다시 — 명령과 되돌림 짝을 쌓음(메모리만 · 캐시에 안 넣음). + * 0 계약 머리 — 몸은 A 가 채움. + * ========================================================================== */ + +import type { History } from "./spreadsheet_types"; + +const todo = (): never => { + throw new Error("spreadsheet_history: 아직 없음(A)"); +}; + +/** `limit` = 쌓을 수(넘으면 오래된 것부터 버림) */ +export function createHistory(_limit?: number): History { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_menu.ts b/A00_Common/spreadsheet/spreadsheet_menu.ts new file mode 100644 index 00000000..6b99c139 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_menu.ts @@ -0,0 +1,17 @@ +/* ============================================================================= + * spreadsheet_menu.ts (주인 E) + * 우클릭 메뉴 — 칸 · 행 머리 · 열 머리마다: 잘라내기 · 복사 · (붙여넣기 = 「Ctrl+V 를 쓸 것」 안내) · + * 행열 넣기 · 지우기 · 숨기기 · 보이기 · 내용 지우기 · 서식 지우기 · 병합 · 풀기. + * 0 계약 머리 — 몸은 E 가 채움. + * ========================================================================== */ + +import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types"; + +const todo = (): never => { + throw new Error("spreadsheet_menu: 아직 없음(E)"); +}; + +/** `ctx.root` 의 contextmenu 를 받음(root null) */ +export function attachMenu(_ctx: SpreadsheetContext): PartHandle { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_numfmt.ts b/A00_Common/spreadsheet/spreadsheet_numfmt.ts new file mode 100644 index 00000000..5142e4d4 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_numfmt.ts @@ -0,0 +1,19 @@ +/* ============================================================================= + * spreadsheet_numfmt.ts (주인 B) + * 숫자 형식 코드 → 보이는 글 — 엑셀 코드 부분 집합: 일반 · `0` `#` `?` `.` `,`(천 단위 · 끝 쉼표 = 천으로 나눔) · + * 구역 `;`(양 · 음 · 0 · 글) · `"글"` · `\x` · `_x`(폭만큼 띄움) · `*x`(채움) · `[Red]` 등 색 · `%` · `@` · `E+00`. + * 실무 형식(`0.00` · `_-* #,##0.00_-;\-* #,##0.00_-;_-* "-"_-;_-@_-` · `0.00_);[Red]\(0.00\)` …)은 모두 맞아야 함. + * 끝수는 분수로 사사오입(엑셀 표시와 같음). 날짜 · 시간 형식은 1단계 밖(글 그대로). + * 0 계약 머리 — 몸은 B 가 채움. + * ========================================================================== */ + +import type { FormattedText, Scalar } from "./spreadsheet_types"; + +const todo = (): never => { + throw new Error("spreadsheet_numfmt: 아직 없음(B)"); +}; + +/** `code` 없음 = 일반(수는 유효 11 자리 안쪽 · 칸 폭은 격자 몫) · 오류 값은 코드 글 그대로 */ +export function formatValue(_value: Scalar, _code: string | undefined): FormattedText { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_parser.ts b/A00_Common/spreadsheet/spreadsheet_parser.ts new file mode 100644 index 00000000..ab069c7d --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_parser.ts @@ -0,0 +1,29 @@ +/* ============================================================================= + * spreadsheet_parser.ts (주인 A) + * 수식 글 → 낱말 · 나무 · 나무 → 글. `eval` 금지 — 직접 짠 파서만. + * 우선순위는 엑셀과 같음: 범위 `:` > 부호 `-` `+` > `%` > `^` > `* /` > `+ -` > `&` > 비교 + * (부호가 `^` 보다 먼저 — `-2^2 = 4`). 같은 단은 왼쪽부터(`^` 도 왼쪽부터 · 엑셀과 같음). + * 글 `"…"` 안 `""` = 따옴표 하나 · 시트 `'이름'!A1` · `이름!A1` · 오류 값 글자 · TRUE/FALSE. + * 0 계약 머리 — 몸은 A 가 채움. + * ========================================================================== */ + +import type { FormulaNode, ParseResult, Token } from "./spreadsheet_types"; + +const todo = (): never => { + throw new Error("spreadsheet_parser: 아직 없음(A)"); +}; + +/** 앞 `=` 뺀 식 글 → 낱말(빈칸 낱말 포함 · 끝까지 · 못 읽는 글은 `unknown`) — 편집기 색칠 · 가리키기용 */ +export function tokenize(_formula: string): Token[] { + return todo(); +} + +/** 앞 `=` 뺀 식 글 → 나무. 앞에 `=` 가 붙어 와도 됨. 함수 이름은 대문자로. */ +export function parseFormula(_formula: string): ParseResult { + return todo(); +} + +/** 나무 → 앞 `=` 뺀 식 글(참조 옮김 뒤 다시 적기) — 수는 원래 글(`text`) 그대로 */ +export function formulaToText(_node: FormulaNode): string { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_recalc.ts b/A00_Common/spreadsheet/spreadsheet_recalc.ts new file mode 100644 index 00000000..7fa012da --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_recalc.ts @@ -0,0 +1,16 @@ +/* ============================================================================= + * spreadsheet_recalc.ts (주인 A) + * 통합문서 한 벌 풀이 — 서버 Node 진입(`common_util_spreadsheet_node.ts`) · 시험이 부름. + * 화면과 같은 엔진(`createCalcEngine`)을 한 번 돌려 `snapshot()` — 계산이 두 벌이 되지 않게. + * 0 계약 머리 — 몸은 A 가 채움. + * ========================================================================== */ + +import type { CalcValues, Workbook } from "./spreadsheet_types"; + +const todo = (): never => { + throw new Error("spreadsheet_recalc: 아직 없음(A)"); +}; + +export function recalcWorkbook(_book: Workbook): CalcValues { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_refshift.ts b/A00_Common/spreadsheet/spreadsheet_refshift.ts new file mode 100644 index 00000000..75d87001 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_refshift.ts @@ -0,0 +1,33 @@ +/* ============================================================================= + * spreadsheet_refshift.ts (주인 A) + * 참조 옮김 — 행열 넣기 · 지우기 · 잘라 붙이기 · 시트 이름 바꾸기 때 식 글을 고쳐 적음(엑셀 규칙) · + * 복사 · 채우기 때 상대 참조만 옮김 · 구글 R1C1 식 → A1. + * 지운 칸 · 격자 밖으로 나간 참조는 `#REF!` · 범위는 남은 만큼 줄어듦(끝까지 지우면 `#REF!`). + * 0 계약 머리 — 몸은 A 가 채움. + * ========================================================================== */ + +import type { CellAddress, Command, Workbook } from "./spreadsheet_types"; + +const todo = (): never => { + throw new Error("spreadsheet_refshift: 아직 없음(A)"); +}; + +/** 행열 · 옮기기 · 시트이름 명령에 맞춰 식 글 고침(식이 든 시트 id = `host`) — 안 바뀌면 같은 글 */ +export function shiftForCommand( + _formula: string, + _host: string, + _command: Command, + _book: Workbook, +): string { + return todo(); +} + +/** 복사 · 채우기 — 상대 참조만 (dr, dc) 옮김 · 격자 밖은 `#REF!` */ +export function moveFormula(_formula: string, _dr: number, _dc: number): string { + return todo(); +} + +/** 구글 `data-sheets-formula`(`=2*R[0]C[-1]`) → 붙일 자리 기준 A1 식(앞 `=` 뺌) */ +export function r1c1ToA1(_formula: string, _at: CellAddress): string { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_sample.json b/A00_Common/spreadsheet/spreadsheet_sample.json new file mode 100644 index 00000000..9d078d35 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_sample.json @@ -0,0 +1,93 @@ +{ + "종류": "통합문서", + "판": 1, + "열": "h_soil_ditch", + "서식": [ + {}, + { "크기": 12, "굵게": true }, + { + "가로": "center", + "세로": "center", + "채움": "#F2F2F2", + "테두리": { + "위": { "선": "thin" }, + "아래": { "선": "thin" }, + "왼": { "선": "thin" }, + "오른": { "선": "thin" } + } + }, + { + "가로": "distributed", + "세로": "center", + "테두리": { "위": { "선": "hair" }, "아래": { "선": "hair" } } + }, + { "세로": "center", "테두리": { "위": { "선": "hair" }, "아래": { "선": "hair" } } }, + { + "세로": "center", + "형식": "0.00", + "테두리": { "위": { "선": "hair" }, "아래": { "선": "hair" } } + }, + { "형식": "0.00" } + ], + "시트": [ + { + "id": "s1", + "이름": "토사개거", + "칸": { + "A1": { "값": "□수로형 토사개거 산출근거", "서식": 1 }, + "A3": { "값": "공종", "서식": 2 }, + "B3": { "값": "규격", "서식": 2 }, + "C3": { "값": "산출 내역", "서식": 2 }, + "D3": { "서식": 2 }, + "E3": { "서식": 2 }, + "F3": { "서식": 2 }, + "G3": { "값": "단위", "서식": 2 }, + "H3": { "값": "수량", "서식": 2 }, + "A4": { "값": "터파기", "서식": 3 }, + "B4": { "값": "토사", "서식": 4 }, + "C4": { "값": "(윗폭+밑폭)÷2×깊이", "서식": 4 }, + "G4": { "값": "㎥", "서식": 4 }, + "H4": { "식": "ROUNDDOWN((J4+J5)/2*J6,2)", "서식": 5 }, + "A5": { "값": "되메우기", "서식": 3 }, + "B5": { "값": "토사", "서식": 4 }, + "C5": { "값": "터파기×0.3", "서식": 4 }, + "G5": { "값": "㎥", "서식": 4 }, + "H5": { "식": "ROUNDDOWN(H4*0.3,2)", "서식": 5 }, + "A6": { "값": "계", "서식": 3 }, + "H6": { "식": "SUM(H4:H5)", "서식": 5 }, + "J4": { "식": "치수!B1", "서식": 6 }, + "J5": { "값": 0.3, "서식": 6 }, + "J6": { "값": 0.3, "서식": 6 } + }, + "병합": ["A1:H1", "C3:F3", "C4:F4", "C5:F5", "A6:G6"], + "열": { + "A": { "폭": 10 }, + "C": { "폭": 6 }, + "D": { "폭": 6 }, + "E": { "폭": 6 }, + "F": { "폭": 6 }, + "J": { "숨김": true } + }, + "행": { "1": { "높이": 20 } }, + "틀고정": { "행": 3 }, + "보기": { "눈금선": false } + }, + { + "id": "s2", + "이름": "치수", + "칸": { + "A1": { "값": "윗폭(m)" }, + "B1": { "값": 0.9, "서식": 6 } + } + } + ], + "활성": "s1", + "계산값": { + "s1": { + "H4": { "수": "0.18" }, + "H5": { "수": "0.05" }, + "H6": { "수": "0.23" }, + "J4": { "수": "0.9" } + } + } +} diff --git a/A00_Common/spreadsheet/spreadsheet_tabs.ts b/A00_Common/spreadsheet/spreadsheet_tabs.ts new file mode 100644 index 00000000..a52e143d --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_tabs.ts @@ -0,0 +1,16 @@ +/* ============================================================================= + * spreadsheet_tabs.ts (주인 E) + * 시트 탭 — 고르기(`ctx.showSheet`) · 더하기 · 이름 바꾸기(두 번 누르기) · 지우기 · 끌어 옮기기 · 복사. + * 이름 바꾸기 · 지우기 · 옮기기는 명령(`시트이름` · `시트지우기` · `시트옮기기` · `시트더하기`). + * 0 계약 머리 — 몸은 E 가 채움. + * ========================================================================== */ + +import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types"; + +const todo = (): never => { + throw new Error("spreadsheet_tabs: 아직 없음(E)"); +}; + +export function mountTabs(_ctx: SpreadsheetContext): PartHandle { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_toolbar.ts b/A00_Common/spreadsheet/spreadsheet_toolbar.ts new file mode 100644 index 00000000..8bd4cef7 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_toolbar.ts @@ -0,0 +1,17 @@ +/* ============================================================================= + * spreadsheet_toolbar.ts (주인 E) + * 서식 도구 모음 — 되돌리기 · 다시 · 글꼴 · 크기 · 굵게 · 기울임 · 밑줄 · 글자색 · 채움색 · 가로 · 세로 정렬 · + * 줄 바꿈 · 테두리(위치 × 선) · 숫자 형식 · 병합 · 틀고정 · 눈금선. 모두 `ctx.dispatch` 명령으로. + * 활성 칸 서식을 `refresh()` 때 단추 상태로. readOnly 면 안 붙임(root null). + * 0 계약 머리 — 몸은 E 가 채움. + * ========================================================================== */ + +import type { PartHandle, SpreadsheetContext } from "./spreadsheet_view_types"; + +const todo = (): never => { + throw new Error("spreadsheet_toolbar: 아직 없음(E)"); +}; + +export function mountToolbar(_ctx: SpreadsheetContext): PartHandle { + return todo(); +} diff --git a/A00_Common/spreadsheet/spreadsheet_types.ts b/A00_Common/spreadsheet/spreadsheet_types.ts new file mode 100644 index 00000000..c4e84397 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_types.ts @@ -0,0 +1,429 @@ +/* ============================================================================= + * spreadsheet_types.ts + * 산출근거 스프레드시트 계약 — 저장 문서 · 칸 · 서식 · 수식 나무 · 값 · 명령 · 계산 엔진 모양. + * 근거 `tmp/M02_분석/9_엑셀검토.md` · PLAN 11-8. 이 파일 · `spreadsheet_view_types.ts` · 견본 JSON 은 + * 0 계약 주인만 고침 — 다른 창은 브레인에게 고침을 청함. 타입만 둠(서버 Node · 시험이 그대로 씀). + * + * ── 규칙 ──────────────────────────────────────────────────────────────────── + * · 외부 라이브러리 0 · 오픈소스 코드 안 베낌(Formula.js · ssf 는 시험 대조 답으로만). + * · 가져다 쓰는 것 = `@ui/ui_template_elements`(el · createButton · 이미 앱 번들) · + * `@ui/sheet/ui_template_sheet_frac`(BigInt 분수 · 집계표와 끝수 한 벌) 둘뿐. + * · M02 만 `import()` 로 늦게 불러옴 — 다른 페이지 · `A00_Common/main.ts` 가 정적 import 금지. + * · 엔진 파일(A · B)은 DOM 을 모름 — 화면 즉시 풀이와 [저장] 때 서버 Node 풀이가 같은 TS(CLAUDE.md 5장 ②). + * · 보이는 칸만 그림 · 파일마다 700 줄 안쪽(예산 넘기 전에 쪼갬) · 화면 글자는 그 파일 안에 둠. + * · xlsx 가져오기 · 내보내기 · 인쇄 · PDF 는 만들지 않음(단추도 없음). xlsx 는 시험(F)만 읽음. + * + * ── 크기 예산 (vite build · 압축(gzip) 뒤 · M02 산출근거를 열 때만 받는 조각) ────── + * 엔진 A+B 22 KB · 화면 C+D+E 28 KB · css 4 KB · 합 54 KB 안쪽. 재는 곳 = F(빌드 조각 크기 시험). + * + * ── 파일 · 주인 · 예산 줄 수 ───────────────────────────────────────────────── + * 0 spreadsheet_types.ts 이 계약(타입) ~450 + * 0 spreadsheet_view_types.ts 화면 부품 사이 계약(DOM 타입) ~150 + * 0 spreadsheet_sample.json 저장 견본 = `basis/<열 id>.json` 한 벌 + * A spreadsheet_address.ts A1 ↔ 행열 · 범위 · 열 글자 · 시트 이름 따옴표 ~150 + * A spreadsheet_parser.ts 낱말 나누기 · 수식 나무 · 나무 → 글 ~500 + * A spreadsheet_eval.ts 나무 풀기 · 오류 전파 · 형 바꾸기 ~350 + * A spreadsheet_graph.ts 계산 엔진 — 의존 그래프 · 딸린 칸만 · 순환 ~350 + * A spreadsheet_refshift.ts 넣기 · 지우기 · 옮기기 · 복사 때 참조 옮김 ~300 + * A spreadsheet_commands.ts 명령 적용 → 되돌림 짝 · 빈 통합문서 ~550 + * A spreadsheet_history.ts 되돌리기 · 다시 ~100 + * A spreadsheet_recalc.ts 통합문서 한 벌 풀이(서버 Node · 시험 진입) ~80 + * B spreadsheet_functions.ts 함수 표(이름 → 뜻) · 인자 수 검사 ~120 + * B spreadsheet_functions_math.ts 수학 · 삼각 · 끝수 ~400 + * B spreadsheet_functions_misc.ts 논리 · 글 · 찾기 · 세기 ~500 + * B spreadsheet_numfmt.ts 숫자 형식 코드 → 보이는 글 ~400 + * B spreadsheet_fill.ts 채우기 핸들 연속값 ~200 + * C spreadsheet_grid.ts 격자 — 보이는 칸만 · 틀고정 4 판 · 병합 · 고름 그림 ~550 + * C spreadsheet_headers.ts 열 글자 · 행 번호 · 폭 · 높이 끌기 · 숨김 표시 ~350 + * C spreadsheet_cellstyle.ts 서식 → CSS(hair · 균등 분할 · 겹친 테두리) ~250 + * C spreadsheet_grid.css ~200 + * D spreadsheet.ts 진입 createSpreadsheet · 부품 잇기 · 문맥 ~350 + * D spreadsheet_selection.ts 고름 모델 · Shift/Ctrl 방향 · 병합으로 늘림 ~300 + * D spreadsheet_editor.ts 칸 편집기 · 수식 입력줄 · 주소 상자 · 한글 조합 · 참조 가리키기 ~600 + * D spreadsheet_keys.ts 엑셀 단축키 지도 ~300 + * D spreadsheet_mouse.ts 끌기 고르기 · 채우기 끌기 · 두 번 누르기 ~300 + * D spreadsheet.css ~150 + * E spreadsheet_toolbar.ts 서식 도구 모음 ~550 + * E spreadsheet_tabs.ts 시트 탭 ~300 + * E spreadsheet_menu.ts 우클릭 메뉴 ~300 + * E spreadsheet_clipboard.ts 복사 · 잘라내기 · 붙여넣기(TSV · HTML) ~550 + * E spreadsheet_toolbar.css ~150 + * F common_util/common_util_spreadsheet_node.ts Node 진입(재계산 · 폴더 밖) ~40 + * F common_util/common_util_spreadsheet.py 파이썬 껍데기(common_util_node_bundle) ~60 + * F package.json `build:spreadsheet` · M02 `basis` 종류(Store · Layers) · 옛 산출근거 옮김 · + * M02_MasterTemplete_Structure.ts 끼우기 · resources/tester/test_spreadsheet_*.py(실무 2,699 칸 대조 · 크기) + * + * ── 좌표 ──────────────────────────────────────────────────────────────────── + * 안쪽은 0 부터(r = 행 · c = 열) · 저장 · 식 글은 엑셀 A1(1 부터). 행 최대 1,048,576 · 열 최대 16,384(XFD). + * ========================================================================== */ + +import type { Frac } from "@ui/sheet/ui_template_sheet_frac"; + +export type { Frac }; + +// ═════════════════════════════════════════════════════════════════════════════ +// 1. 저장 문서 — 구조물집계표 열마다 한 파일 `resources/master_template/basis/<열 id>.json` +// (층 규칙은 M02 와 같음 · 프로젝트 `templates/basis/` · `_initial/` 사본). 견본 = spreadsheet_sample.json. +// 키는 한글 · 열거 값(정렬 · 선 · 오류)은 엑셀 이름 그대로. +// ═════════════════════════════════════════════════════════════════════════════ + +export const MAX_ROWS = 1_048_576; +export const MAX_COLS = 16_384; + +/** 칸에 넣은 값 — 수는 JSON 수(엑셀처럼 유효 15 자리) · 글 · 참거짓. 식 칸에는 없음. */ +export type CellInput = number | string | boolean; + +export interface Cell { + /** 입력 값 — `식` 이 있으면 없음. `'` 로 시작해 친 글은 `'` 를 떼고 글로 둠. */ + 값?: CellInput; + /** A1 식 — 앞 `=` 뺌(`ROUNDDOWN((G31+J31/2)*P31,2)`). 행열을 넣고 지우면 글을 고쳐 적음. */ + 식?: string; + /** `서식` 표 번호 — 없으면 행 · 열 서식 · 그다음 0(기본). */ + 서식?: number; +} + +export type HAlign = "general" | "left" | "center" | "right" | "distributed" | "centerContinuous"; +export type VAlign = "top" | "center" | "bottom"; +export type BorderLine = "hair" | "thin" | "medium" | "thick" | "double" | "dotted" | "dashed"; + +export interface BorderSide { + 선: BorderLine; + /** `#RRGGBB` · 없으면 검정. */ + 색?: string; +} + +/** 칸 서식 한 벌 — 없는 칸은 기본값. 같은 서식은 표에 한 번만(칸은 번호). */ +export interface CellStyle { + 글꼴?: string; + /** pt */ + 크기?: number; + 굵게?: boolean; + 기울임?: boolean; + 밑줄?: boolean; + 취소선?: boolean; + /** `#RRGGBB` */ + 글자색?: string; + /** `#RRGGBB` — 없으면 채움 없음. */ + 채움?: string; + 가로?: HAlign; + 세로?: VAlign; + 줄바꿈?: boolean; + 테두리?: { 위?: BorderSide; 아래?: BorderSide; 왼?: BorderSide; 오른?: BorderSide }; + /** 엑셀 숫자 형식 코드 그대로(`0.00` · `#,##0` · `_-* #,##0.00_-;…` · `@`) · 없으면 일반. */ + 형식?: string; +} + +export interface ColumnInfo { + /** 엑셀 글자 폭 단위(기본 글꼴 숫자 폭 기준) — 화면이 px 로 바꿈. */ + 폭?: number; + 숨김?: boolean; + 서식?: number; +} + +export interface RowInfo { + /** pt */ + 높이?: number; + 숨김?: boolean; + 서식?: number; +} + +export interface Sheet { + /** 안 바뀌는 id(`s1` …) — 이름을 바꿔도 그대로. 식 글은 이름으로 가리킴. */ + id: string; + 이름: string; + /** A1 열쇠 · 빈 칸은 안 적음. */ + 칸: Record; + /** `C3:AD3` — 겹치지 않음 · 왼위 칸만 값 · 서식은 칸마다(테두리 때문). */ + 병합?: string[]; + /** 열 글자 열쇠(`A` · `AB`). */ + 열?: Record; + /** 행 번호 열쇠(1 부터 · `"31"`). */ + 행?: Record; + /** 위에서 고정할 행 수 · 왼쪽에서 고정할 열 수. */ + 틀고정?: { 행?: number; 열?: number }; + 보기?: { 눈금선?: boolean }; +} + +/** 새 시트 기본값 — 값은 사용자 협의로 확정(9_엑셀검토 8장 · 임의 확정 금지). 없으면 A 의 임시값. */ +export interface WorkbookDefaults { + 글꼴?: string; + 크기?: number; + 열폭?: number; + 행높이?: number; +} + +export interface Workbook { + 종류: "통합문서"; + 판: 1; + /** 구조물집계표 열 id(파일 이름과 같음). */ + 열: string; + 기본?: WorkbookDefaults; + /** 서식 표 — [0] = 기본(빈 `{}`). */ + 서식: CellStyle[]; + 시트: Sheet[]; + /** 열 때 보일 시트 id. */ + 활성?: string; + /** 서버만 씀 — [저장] 때 Node 풀이 결과(시트 id → A1 → 값). 브라우저 값은 안 적음 · 화면은 안 읽음. */ + 계산값?: CalcValues; +} + +// ═════════════════════════════════════════════════════════════════════════════ +// 2. 주소 +// ═════════════════════════════════════════════════════════════════════════════ + +export interface CellAddress { + r: number; + c: number; +} + +/** 닫힌 범위 — r0 ≤ r1 · c0 ≤ c1. 행 전체 = c0 0 · c1 MAX_COLS-1(열 전체도 같은 꼴). */ +export interface CellRange { + r0: number; + c0: number; + r1: number; + c1: number; +} + +/** 시트까지 붙은 칸 — 엔진 · 명령이 씀. */ +export interface SheetCellAddress extends CellAddress { + 시트: string; +} + +// ═════════════════════════════════════════════════════════════════════════════ +// 3. 수식 — 낱말 · 나무 (A 가 만들고 B · D · E 가 읽음) +// ═════════════════════════════════════════════════════════════════════════════ + +export type ErrorCode = + "#NULL!" | "#DIV/0!" | "#VALUE!" | "#REF!" | "#NAME?" | "#NUM!" | "#N/A" | "#CYCLE!"; // 순환 참조 — 엑셀엔 없음 · 칸에 보임(9_엑셀검토 3-3) + +export type TokenKind = + | "number" + | "string" + | "bool" + | "error" + | "ref" // 칸 · 범위 · 행 · 열 참조(시트 붙은 것 포함) — 편집기가 색 테두리로 씀 + | "func" // 함수 이름(뒤 `(` 앞까지) + | "op" + | "paren" + | "comma" + | "space" + | "unknown"; + +/** 낱말 — `start` · `end` 는 식 글(앞 `=` 뺀) 안 글자 자리. 편집기 참조 가리키기 · 색칠용. */ +export interface Token { + kind: TokenKind; + text: string; + start: number; + end: number; +} + +/** 나무 안 칸 참조 — 시트 없으면 식이 든 시트. `$` 는 abs. */ +export interface RefCell { + r: number; + c: number; + absR: boolean; + absC: boolean; +} + +export type FormulaNode = + | { type: "number"; value: Frac; text: string } + | { type: "string"; value: string } + | { type: "bool"; value: boolean } + | { type: "error"; code: ErrorCode } + /** 칸 하나 */ + | { type: "cell"; sheet: string | null; ref: RefCell } + /** 범위(`A1:B5` · `A:A` · `3:3`) — 행 · 열 전체는 끝을 MAX 로. 한쪽이라도 `#REF!` 면 error 나무 */ + | { type: "range"; sheet: string | null; from: RefCell; to: RefCell } + /** 글자 그대로 남은 이름(정의 없음) → 풀면 `#NAME?` */ + | { type: "name"; name: string } + | { type: "unary"; op: "-" | "+" | "%"; arg: FormulaNode } + | { + type: "binary"; + op: "+" | "-" | "*" | "/" | "^" | "&" | "=" | "<>" | "<" | "<=" | ">" | ">="; + left: FormulaNode; + right: FormulaNode; + } + | { type: "call"; name: string; args: FormulaNode[] }; + +/** 파싱 실패 — 칸에는 식 글을 그대로 두고 값은 `#NAME?`(엑셀은 입력을 막지만 우리는 까닭과 함께 받음). */ +export interface ParseFailure { + ok: false; + message: string; + /** 식 글 안 자리 */ + at: number; +} + +export type ParseResult = { ok: true; node: FormulaNode } | ParseFailure; + +// ═════════════════════════════════════════════════════════════════════════════ +// 4. 값 — 풀이 중(분수) · 저장(십진 글) +// ═════════════════════════════════════════════════════════════════════════════ + +export interface ErrorValue { + error: ErrorCode; + /** 풍선 까닭(한글) */ + why?: string; +} + +/** 칸 하나 값 — 수는 BigInt 분수(무리수 함수만 double → 유효 15 자리 십진 → 분수). + * null = 빈 칸 — 수에서 0 · 글에서 "" · 범위 함수는 건너뜀(엑셀 규칙). */ +export type Scalar = Frac | string | boolean | ErrorValue | null; + +/** 범위 값 — 함수가 칸마다 읽음(빈 칸 null). */ +export interface RangeValue { + kind: "range"; + rows: number; + cols: number; + at(r: number, c: number): Scalar; +} + +export type EvalResult = Scalar | RangeValue; + +/** 저장 · 서버 결과 한 칸 — 수는 십진 글(끝수 뒤 · 30 자리까지). */ +export type CalcValue = + { 수: string } | { 글: string } | { 참: boolean } | { 오류: ErrorCode; 까닭?: string }; + +/** 시트 id → A1 → 값 (식 칸만). */ +export type CalcValues = Record>; + +// ═════════════════════════════════════════════════════════════════════════════ +// 5. 풀이 문맥 · 함수 (A 의 eval ↔ B 의 함수 표) +// ═════════════════════════════════════════════════════════════════════════════ + +export interface EvalContext { + /** 식이 든 칸 */ + sheet: string; + r: number; + c: number; + /** 칸 값 — 식 칸이면 엔진이 먼저 풀어 줌 · 순환이면 `#CYCLE!` */ + cell(sheet: string, r: number, c: number): Scalar; + range(sheet: string, range: CellRange): RangeValue; + /** 시트 이름 → id (없으면 null → `#REF!`) */ + sheetId(name: string): string | null; +} + +/** 인자는 늦게 풂(IF · IFERROR · AND 가 고른 것만 풀게). 범위 인자는 RangeValue 로 옴. */ +export type LazyArg = () => EvalResult; + +export interface SheetFunction { + /** 인자 수 — 최대 없으면 끝없음 */ + min: number; + max?: number; + call(args: LazyArg[], ctx: EvalContext): EvalResult; +} + +// ═════════════════════════════════════════════════════════════════════════════ +// 6. 명령 — 문서를 바꾸는 길은 이것뿐(되돌리기 · 서버 재계산 · 캐시가 한 길로 감) +// ═════════════════════════════════════════════════════════════════════════════ + +export type Command = + /** 칸 여럿 한 번에 — null = 칸 지움(값 · 식 · 서식 모두). 붙여넣기 · 채우기 · 지우기도 이것. */ + | { 종류: "칸"; 시트: string; 칸: Record } + /** 내용만 지움(Delete) — 서식은 남김 */ + | { 종류: "내용지움"; 시트: string; 범위: CellRange[] } + /** 서식 바꿈 — 칸마다 지금 서식에 `바꿀` 을 덮어 새 서식 번호(표에 없으면 더함). null 항목 = 그 속성 지움 */ + | { + 종류: "서식"; + 시트: string; + 범위: CellRange[]; + 바꿀: { [K in keyof CellStyle]?: CellStyle[K] | null }; + } + | { 종류: "서식지움"; 시트: string; 범위: CellRange[] } + /** 행 넣기 · 지우기 — 참조 옮김(지운 칸 참조는 #REF!) · 병합 늘고 줆 */ + | { 종류: "행넣기" | "행지우기" | "열넣기" | "열지우기"; 시트: string; at: number; 수: number } + /** 잘라 붙이기 — 옮긴 칸을 가리키던 식도 따라감 */ + | { 종류: "옮기기"; 시트: string; 범위: CellRange; 대상시트: string; r: number; c: number } + | { 종류: "병합"; 시트: string; 범위: CellRange } + | { 종류: "병합풀기"; 시트: string; 범위: CellRange } + /** 폭(글자 단위) · 높이(pt) — null = 기본으로 */ + | { 종류: "열폭"; 시트: string; 열: number[]; 폭: number | null } + | { 종류: "행높이"; 시트: string; 행: number[]; 높이: number | null } + | { 종류: "숨김"; 시트: string; 축: "행" | "열"; 번호: number[]; 숨김: boolean } + | { 종류: "틀고정"; 시트: string; 행: number; 열: number } + | { 종류: "눈금선"; 시트: string; 보임: boolean } + | { 종류: "시트더하기"; 시트: Sheet; 자리: number } + | { 종류: "시트지우기"; 시트: string } + /** 이름 바꾸면 다른 시트 식 글의 이름도 바꿈 */ + | { 종류: "시트이름"; 시트: string; 이름: string } + | { 종류: "시트옮기기"; 시트: string; 자리: number } + /** 되돌림 짝 전용 — 시트 통째 바꿔 넣기(행열 지우기 되돌림 등) */ + | { 종류: "시트통째"; 시트: Sheet; 서식?: CellStyle[] } + /** 여러 명령 = 되돌리기 한 번 */ + | { 종류: "묶음"; 명령: Command[] }; + +/** 명령을 적용한 뒤 계산 엔진에 알릴 것 — 칸만 바뀌었으면 칸 목록 · 모양이 바뀌었으면 rebuild. */ +export interface CommandEffect { + /** 되돌림 짝 */ + undo: Command; + /** 값 · 식이 바뀐 칸 — 딸린 칸만 다시 풂 */ + cells: SheetCellAddress[]; + /** 행열 · 옮기기 · 시트 명령 — 엔진을 통째로 다시 */ + rebuild: boolean; +} + +// ═════════════════════════════════════════════════════════════════════════════ +// 7. 계산 엔진 — 통합문서를 참조로 쥠(명령이 문서를 바꾸면 알려 줌) +// ═════════════════════════════════════════════════════════════════════════════ + +export interface CalcEngine { + /** 바뀐 칸 → 딸린 칸만 다시 풂 · 다시 푼 칸(바뀐 칸 포함) 목록 */ + update(changed: SheetCellAddress[]): SheetCellAddress[]; + /** 문서를 통째로 다시 읽고 전부 풂(행열 · 시트 명령 · setDoc 뒤) */ + rebuild(book: Workbook): void; + /** 칸 값 — 식 칸이면 푼 값 · 아니면 입력 값(수는 분수) */ + value(sheet: string, r: number, c: number): Scalar; + /** 식 칸이 가리키는 칸 · 범위(편집기 색 테두리 · 오류 추적) */ + precedents(sheet: string, r: number, c: number): { 시트: string; 범위: CellRange }[]; + /** 식 칸 전부 → 저장 모양(서버 `계산값` · 시험 대조) */ + snapshot(): CalcValues; +} + +// ═════════════════════════════════════════════════════════════════════════════ +// 8. 되돌리기 · 채우기 · 숫자 형식 · 클립보드 (A · B · E 공개 모양) +// ═════════════════════════════════════════════════════════════════════════════ + +export interface History { + /** 적용이 끝난 명령과 그 되돌림 짝을 쌓음 · 다시 스택은 비움 */ + push(done: Command, undo: Command): void; + undo(): Command | null; + redo(): Command | null; + canUndo(): boolean; + canRedo(): boolean; + clear(): void; +} + +export type FillDirection = "down" | "up" | "right" | "left"; + +/** 숫자 형식 결과 — `색` 은 `[Red]` 같은 형식 색 · `채움글` 은 `*x` 반복 글(격자가 폭만큼 늘림). */ +export interface FormattedText { + 글: string; + 색?: string; + 채움글?: string; + /** 형식이 정한 정렬(일반 정렬일 때 수 = 오른쪽 · 글 = 왼쪽) */ + 정렬: "left" | "right" | "center"; +} + +/** 붙여넣기로 읽은 칸 뭉치 — 왼위 기준 상대 자리. */ +export interface ClipCell { + 값?: CellInput; + /** 앞 `=` 뺀 A1 식 — `원점` 기준으로 적힌 글 */ + 식?: string; + 서식?: CellStyle; +} + +/** 클립보드 형 — 브라우저가 읽는 것은 `text/plain`(TSV) · `text/html` 둘. + * 우리 → 밖: 둘 다 씀(보이는 값 + 인라인 서식 표) · 우리 → 우리: HTML 속 data 속성에 뭉치 JSON. + * 밖 → 우리 수식: ① 구글 `data-sheets-formula`(R1C1 상대) ② 엑셀 「수식 표시」(Ctrl+`) 로 복사한 글 칸이 + * `=` 뒤에 글이 있고 식으로 읽히면 식(「=」 한 글자 칸은 글) — 실제 되는지는 E 가 이 PC 엑셀로 시험. */ +export interface ClipBlock { + rows: number; + cols: number; + /** `r,c` 열쇠(0 부터 · 뭉치 왼위 기준) */ + 칸: Record; + /** 뭉치 안 병합(0 부터) */ + 병합: CellRange[]; + /** 식 글이 적힌 기준 왼위 — 붙일 때 (붙일 자리 − 원점) 만큼 상대 참조를 옮김. + * null = 옮기지 않음(엑셀 수식 표시 글 — 적힌 주소 그대로 · 구글 R1C1 은 붙일 자리로 풀어 null). */ + 원점: CellAddress | null; + 출처: "aislo" | "google" | "excel" | "text"; +} diff --git a/A00_Common/spreadsheet/spreadsheet_view_types.ts b/A00_Common/spreadsheet/spreadsheet_view_types.ts new file mode 100644 index 00000000..4c9a97e6 --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_view_types.ts @@ -0,0 +1,117 @@ +/* ============================================================================= + * spreadsheet_view_types.ts + * 화면 부품(C 격자 · D 입력 · E 도구) 사이 계약 — 문맥 하나를 D 가 만들어 C · E 에 넘김. + * 0 계약 주인만 고침. 파일 목록 · 주인 · 규칙은 `spreadsheet_types.ts` 머리. + * + * 흐름: 사용자 조작 → (D · E) `ctx.dispatch(명령)` → A 적용 · 되돌림 쌓기 · 엔진 update/rebuild → + * C `grid.invalidate(칸)`(보이는 칸만 다시) → `onChange(문서)`. 서버 왕복 없음 · 자동저장 없음. + * ========================================================================== */ + +import type { + CalcEngine, + CellAddress, + CellRange, + Command, + History, + Sheet, + SheetCellAddress, + Workbook, +} from "./spreadsheet_types"; + +/** 고름 — 범위 여럿(Ctrl+누르기) · 활성 칸은 편집 · 수식 입력줄이 보이는 칸 · 기준은 Shift 늘림의 닻. */ +export interface Selection { + 시트: string; + 범위: CellRange[]; + 활성: CellAddress; + 기준: CellAddress; +} + +/** 격자 안 한 점이 가리키는 것 — 머리 끝(`colEdge` · `rowEdge`)은 폭 · 높이 끌기 자리. */ +export interface HitResult { + kind: "cell" | "colHead" | "rowHead" | "corner" | "colEdge" | "rowEdge" | "fillHandle"; + r: number; + c: number; +} + +/** 칸 자리 — 격자 내용 좌표(px · 스크롤 전) · 병합이면 병합 전체. */ +export interface CellBox { + x: number; + y: number; + w: number; + h: number; +} + +/** 편집 중 식 참조 색 테두리(D 가 채우고 C 가 그림). */ +export interface RefHighlight { + 시트: string; + 범위: CellRange; + /** 0 부터 차례 — 색은 C 가 차례로 고름 */ + 색번호: number; +} + +/** C 격자 — `createGrid(ctx)` 가 돌려줌. */ +export interface GridHandle { + /** 격자 뿌리(스크롤 판 · 틀고정 판 포함) — D 가 창 안에 붙임 */ + root: HTMLElement; + /** 보이는 칸 통째로 다시(시트 바꿈 · 행열 · 폭 · 틀고정 뒤) */ + render(): void; + /** 값 · 서식이 바뀐 칸만 다시(보이는 것만) */ + invalidate(cells: SheetCellAddress[]): void; + /** 고름 · 참조 색 테두리 · 채우기 손잡이 다시 그림 */ + renderSelection(): void; + setRefHighlights(list: RefHighlight[]): void; + cellBox(r: number, c: number): CellBox; + /** 화면 좌표 → 가리킨 것(격자 밖 null) */ + hitTest(clientX: number, clientY: number): HitResult | null; + /** 칸이 보이게 스크롤(틀고정 판이면 그대로) */ + reveal(r: number, c: number): void; + /** 지금 보이는 행열(틀고정 판 뺀 본문 판) */ + visibleRange(): CellRange; + /** 편집기를 얹을 층과 칸 자리(화면 좌표 · 병합이면 전체) — D 편집기가 씀 */ + editorSlot(r: number, c: number): { layer: HTMLElement; box: CellBox }; + destroy(): void; +} + +/** D 편집기 — E 도구 모음 · C 가 부름. */ +export interface EditorHandle { + /** 편집 중인가 — 방향키 · 붙여넣기 뜻이 달라짐 */ + editing(): boolean; + /** 활성 칸 편집 시작(`initial` = 친 글자로 시작) */ + begin(initial?: string): void; + /** 확정 · 취소 */ + commit(): void; + cancel(): void; + /** 수식 입력줄 · 주소 상자를 활성 칸으로 다시 */ + sync(): void; +} + +/** D 가 만들어 C · E 에 넘기는 문맥 — 문서는 이것 하나를 여럿이 봄. */ +export interface SpreadsheetContext { + /** 부품 뿌리 */ + root: HTMLElement; + /** 지금 문서 — 명령만이 바꿈(직접 고치지 말 것) */ + book: Workbook; + readOnly: boolean; + engine: CalcEngine; + history: History; + selection: Selection; + /** 활성 시트 */ + sheet(): Sheet; + /** 명령 하나 적용 → 되돌림 쌓기 → 엔진 → 다시 그림 → onChange. readOnly 면 안 함 */ + dispatch(command: Command): void; + undo(): void; + redo(): void; + /** 고름 바꿈 → 다시 그림 · 수식 입력줄 · onSelect */ + select(selection: Selection, reveal?: boolean): void; + /** 활성 시트 바꿈(탭) */ + showSheet(sheetId: string): void; + grid: GridHandle; + editor: EditorHandle; +} + +/** E 부품 — D 가 `mountToolbar(ctx)` 등으로 붙이고 고름 · 문서가 바뀌면 `refresh()`. */ +export interface PartHandle { + root: HTMLElement | null; + refresh(): void; + destroy(): void; +}