/** * 표 명령 — 표 객체 하나를 만들고 칸을 고친다 (조사표 5절 표 · 12절 테이블 셀). * 셀을 고르는 방법은 이 CAD의 다른 명령과 같다: 점을 찍으면 그 자리의 칸을 집는다. */ import type { Point } from '@flatten-js/core'; import { toast } from 'react-toastify'; import { getTableColumnWidth, getTableRowHeight, setTableStyle } from '../../commands/dim-settings'; import type { Entity } from '../../entities/Entity'; import { TableEntity } from '../../entities/TableEntity'; import { cellAt } from '../../helpers/table-geometry'; import { addEntities, getActiveLayerId, getActiveTextStyle, getEntities, setEntities, } from '../../state'; import { Tool } from '../../tools'; import { createSequenceTool } from '../factories/sequence-tool'; /** 점이 놓인 표와 그 칸. 표를 못 찾으면 null */ function findTableCell(point: Point): { table: TableEntity; row: number; column: number } | null { for (const entity of getEntities()) { if (!(entity instanceof TableEntity)) continue; const hit = cellAt( entity.getOrigin(), entity.getColumnWidths(), entity.getRowHeights(), entity.getCells(), point ); if (hit) return { table: entity, row: hit.row, column: hit.column }; } return null; } /** 표를 고친 뒤 화면·실행취소에 반영한다 */ function commitTableChange(table: TableEntity): void { setEntities( getEntities().map((entity: Entity) => (entity.id === table.id ? table : entity)), true ); } function withTableAt( point: Point, apply: (hit: NonNullable>) => string | null ): void { const hit = findTableCell(point); if (!hit) { toast.info('표 안의 칸을 지정하십시오.'); return; } const message = apply(hit); commitTableChange(hit.table); if (message) toast.success(message); } export const tableToolStateMachine = createSequenceTool({ tool: Tool.TABLE, steps: [ { kind: 'number', instructions: '열 수를 입력하십시오 <3>.', defaultValue: 3 }, { kind: 'number', instructions: '행 수를 입력하십시오 <3>.', defaultValue: 3 }, { kind: 'point', instructions: '표의 좌측 상단 삽입점을 지정하십시오.' }, ], commit: (input) => { const columns = Math.max(1, Math.round(input.number(0))); const rows = Math.max(1, Math.round(input.number(1))); const origin = input.point(2); const table = new TableEntity( getActiveLayerId(), origin, Array.from({ length: columns }, () => getTableColumnWidth()), Array.from({ length: rows }, () => getTableRowHeight()), undefined, { fontSize: getActiveTextStyle().fontSize, textColor: getActiveTextStyle().textColor } ); addEntities([table], true); toast.info('표를 만들었습니다. 칸 내용은 TABLEEDIT 명령으로 채우십시오.'); }, }); export const tableStyleToolStateMachine = createSequenceTool({ tool: Tool.TABLESTYLE, helpers: false, steps: [ { kind: 'number', instructions: '열 너비를 입력하십시오 <40>.', defaultValue: 40 }, { kind: 'number', instructions: '행 높이를 입력하십시오 <10>.', defaultValue: 10 }, ], commit: (input) => { setTableStyle(input.number(0), input.number(1)); toast.success(`표 스타일: 열 ${input.number(0)} · 행 ${input.number(1)}`); }, }); export const tableEditToolStateMachine = createSequenceTool({ tool: Tool.TABLEEDIT, steps: [ { kind: 'point', instructions: '내용을 채울 칸을 지정하십시오.' }, { kind: 'text', instructions: '칸에 넣을 문자를 입력하십시오.' }, ], commit: (input) => { const text = input.text(1); withTableAt(input.point(0), (hit) => { hit.table.setCell(hit.row, hit.column, { text }); return `칸에 '${text}'를 넣었습니다.`; }); }, }); export const tableRowToolStateMachine = createSequenceTool({ tool: Tool.TABLEROW, steps: [ { kind: 'point', instructions: '기준이 될 칸을 지정하십시오.' }, { kind: 'number', instructions: '위에 넣으려면 1, 아래에 넣으려면 2, 지우려면 0 <1>.', defaultValue: 1, }, ], commit: (input) => { const mode = Math.round(input.number(1)); withTableAt(input.point(0), (hit) => { if (mode === 0) { hit.table.deleteRow(hit.row); return '행을 지웠습니다.'; } hit.table.insertRow(mode === 2 ? hit.row + 1 : hit.row); return '행을 넣었습니다.'; }); }, }); export const tableColumnToolStateMachine = createSequenceTool({ tool: Tool.TABLECOL, steps: [ { kind: 'point', instructions: '기준이 될 칸을 지정하십시오.' }, { kind: 'number', instructions: '왼쪽에 넣으려면 1, 오른쪽에 넣으려면 2, 지우려면 0 <1>.', defaultValue: 1, }, ], commit: (input) => { const mode = Math.round(input.number(1)); withTableAt(input.point(0), (hit) => { if (mode === 0) { hit.table.deleteColumn(hit.column); return '열을 지웠습니다.'; } hit.table.insertColumn(mode === 2 ? hit.column + 1 : hit.column); return '열을 넣었습니다.'; }); }, }); export const tableMergeToolStateMachine = createSequenceTool({ tool: Tool.TABLEMERGE, steps: [ { kind: 'point', instructions: '병합할 범위의 왼쪽 위 칸을 지정하십시오.' }, { kind: 'point', instructions: '병합할 범위의 오른쪽 아래 칸을 지정하십시오.' }, ], commit: (input) => { const end = findTableCell(input.point(1)); withTableAt(input.point(0), (hit) => { if (!end || end.table.id !== hit.table.id) { toast.info('같은 표 안에서 두 칸을 지정하십시오.'); return null; } const rowSpan = Math.abs(end.row - hit.row) + 1; const colSpan = Math.abs(end.column - hit.column) + 1; hit.table.mergeCells( Math.min(hit.row, end.row), Math.min(hit.column, end.column), colSpan, rowSpan ); return `칸 ${rowSpan}×${colSpan}을 합쳤습니다.`; }); }, }); export const tableUnmergeToolStateMachine = createSequenceTool({ tool: Tool.TABLEUNMERGE, steps: [{ kind: 'point', instructions: '병합을 풀 칸을 지정하십시오.' }], commit: (input) => { withTableAt(input.point(0), (hit) => { hit.table.unmergeCells(hit.row, hit.column); return '병합을 풀었습니다.'; }); }, });