feat(B07): CAD 명령 TOAREA 면적산출로 보내기 · 부모 요청 export-table 메시지(83-2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
This commit is contained in:
@@ -4,6 +4,7 @@ import { clearRecovery, restoreRecovery } from '../helpers/autosave';
|
||||
import { exportEntitiesToJsonFile } from '../helpers/import-export-handlers/export-entities-to-json';
|
||||
import { exportEntitiesToLocalStorage } from '../helpers/import-export-handlers/export-entities-to-local-storage';
|
||||
import { requestDrawingExport, requestHostSave } from '../integration/aislo-drawing-bridge';
|
||||
import { runSendSelectionTable } from '../integration/aislo-table-export';
|
||||
import { exportEntitiesToPngFile } from '../helpers/import-export-handlers/export-entities-to-png';
|
||||
import { exportEntitiesToSvgFile } from '../helpers/import-export-handlers/export-entities-to-svg';
|
||||
import { isHostSave, redo, undo } from '../state';
|
||||
@@ -87,6 +88,14 @@ export const FILE_COMMANDS: CadCommand[] = [
|
||||
return 'PNG 내보내기';
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'TOAREA',
|
||||
label: '면적산출로 보내기',
|
||||
glyph: '▦',
|
||||
hint: '고른 범위(표 · 선 표 · 글)를 면적산출 새 시트로 한 번 옮긴다',
|
||||
needsSelection: true,
|
||||
run: runSendSelectionTable,
|
||||
},
|
||||
{
|
||||
id: 'UNDO',
|
||||
label: '실행 취소',
|
||||
|
||||
@@ -89,6 +89,7 @@ const VIEW_ONLY_COMMAND_IDS: ReadonlySet<string> = new Set([
|
||||
'EXPORTPNG',
|
||||
'COPYCLIP',
|
||||
'COPYBASE',
|
||||
'TOAREA', // 고른 범위 → 면적산출 표(M02 83장)
|
||||
]);
|
||||
|
||||
/** 확정한 도면에서 이 명령을 실행해도 되는가. */
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* CAD → 부모 화면 표 보내기(M02 83장 · 도면 → 면적산출 한 번 옮기기 · 연동 아님). 짝 = `M02_MasterTemplete_AreaDrawing.ts`.
|
||||
* export-table-request 부모 [면적산출로 보내기] 단추 → 고른 범위를 표 자료로
|
||||
* export-table{table} 표 자료(`helpers/table-from-drawing.ts` 꼴) → 부모가 새 시트로
|
||||
* CAD 명령 TOAREA(「면적산출로 보내기」)도 같은 길. 보기 전용 도면에서도 됨(도면을 안 바꿈).
|
||||
* 고른 것이 없거나 옮길 글이 없으면 부모에 안 보내고 안내만.
|
||||
*/
|
||||
import { toast } from 'react-toastify';
|
||||
import type { DrawingEntityJson } from '../helpers/table-from-drawing.ts';
|
||||
import { type DrawingTable, tableFromDrawing } from '../helpers/table-from-drawing.ts';
|
||||
import { getSelectedEntities } from '../state.ts';
|
||||
|
||||
export const AISLO_EXPORT_TABLE_REQUEST_MESSAGE = 'aislo:b08:export-table-request';
|
||||
export const AISLO_EXPORT_TABLE_MESSAGE = 'aislo:b08:export-table';
|
||||
|
||||
const TEXT = {
|
||||
empty: '면적산출로 보낼 범위를 먼저 고르세요(누름 · 끌기).',
|
||||
noText: '고른 범위에 글이 없어 표로 옮길 것이 없습니다.',
|
||||
standalone: '면적산출 화면에서 연 도면에서만 보낼 수 있습니다.',
|
||||
};
|
||||
|
||||
type Notify = (type: string, payload: Record<string, unknown>) => void;
|
||||
|
||||
function notifyParent(type: string, payload: Record<string, unknown>): void {
|
||||
if (window.parent === window) return;
|
||||
window.parent.postMessage({ type, ...payload }, window.location.origin);
|
||||
}
|
||||
|
||||
/** 고른 범위 → 표 자료 · 고른 것이 없으면 'empty' · 글이 없으면 'no-text' */
|
||||
export async function selectionTable(): Promise<DrawingTable | 'empty' | 'no-text'> {
|
||||
const selected = getSelectedEntities();
|
||||
if (!selected.length) return 'empty';
|
||||
const json = (await Promise.all(selected.map((entity) => entity.toJson()))).filter(
|
||||
(entity): entity is NonNullable<typeof entity> => Boolean(entity)
|
||||
);
|
||||
return tableFromDrawing(json as DrawingEntityJson[]) ?? 'no-text';
|
||||
}
|
||||
|
||||
/** 고른 범위를 표로 만들어 부모에 보냄 — 보냈으면 true */
|
||||
export async function sendSelectionTable(notify: Notify = notifyParent): Promise<boolean> {
|
||||
const result = await selectionTable();
|
||||
if (result === 'empty') toast.info(TEXT.empty);
|
||||
else if (result === 'no-text') toast.info(TEXT.noText);
|
||||
else {
|
||||
notify(AISLO_EXPORT_TABLE_MESSAGE, { table: result });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** CAD 명령 TOAREA — 부모 없이 띄운 CAD(단독)면 안내만 */
|
||||
export function runSendSelectionTable(): string {
|
||||
if (window.parent === window) {
|
||||
toast.info(TEXT.standalone);
|
||||
return '부모 화면 없음';
|
||||
}
|
||||
void sendSelectionTable();
|
||||
return '면적산출로 보내기';
|
||||
}
|
||||
|
||||
/** 부모 요청 메시지면 처리하고 true */
|
||||
export async function handleTableExportMessage(
|
||||
data: unknown,
|
||||
notify: Notify = notifyParent
|
||||
): Promise<boolean> {
|
||||
const message = data as { type?: string } | null;
|
||||
if (
|
||||
!message ||
|
||||
typeof message !== 'object' ||
|
||||
message.type !== AISLO_EXPORT_TABLE_REQUEST_MESSAGE
|
||||
)
|
||||
return false;
|
||||
await sendSelectionTable(notify);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 부모 [면적산출로 보내기] 요청을 받음 — 같은 출처 · 부모 창만 */
|
||||
export function registerAisloTableExport(): void {
|
||||
window.addEventListener('message', (event: MessageEvent<unknown>) => {
|
||||
if (event.origin !== window.location.origin || event.source !== window.parent) return;
|
||||
void handleTableExportMessage(event.data);
|
||||
});
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { queryEntitiesNearPoint } from './helpers/spatial-index';
|
||||
import { trackHoveredSnapPoint } from './helpers/track-hovered-snap-points';
|
||||
import { InputController } from './inputController/input-controller.ts';
|
||||
import { registerAisloDrawingBridge } from './integration/aislo-drawing-bridge.ts';
|
||||
import { registerAisloTableExport } from './integration/aislo-table-export.ts';
|
||||
import { registerDxfClipPaste } from './integration/dxf-clip-paste.ts';
|
||||
import {
|
||||
getActiveToolActor,
|
||||
@@ -155,6 +156,7 @@ function initApplication() {
|
||||
setLayers(layers);
|
||||
setActiveLayerId(layers[0].id);
|
||||
registerAisloDrawingBridge();
|
||||
registerAisloTableExport(); // 고른 범위 → 면적산출 표(M02 83장)
|
||||
registerDxfClipPaste();
|
||||
registerCadDebugHook();
|
||||
registerAutoSave();
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* CAD → 부모 표 보내기(`integration/aislo-table-export.ts` · M02 83-2) 검증.
|
||||
* 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad table-export
|
||||
* (시험 파일은 바깥 라이브러리를 직접 부르지 않음 — 엔티티는 fromJson 으로 만듦)
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
getCommandById,
|
||||
isViewOnlyCommand,
|
||||
} from "../../../B07_DesignDetail/openwebcad/src/commands/registry.ts";
|
||||
import type { JsonEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/Entity.ts";
|
||||
import { LineEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/LineEntity.ts";
|
||||
import { TextEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/TextEntity.ts";
|
||||
import {
|
||||
AISLO_EXPORT_TABLE_MESSAGE,
|
||||
AISLO_EXPORT_TABLE_REQUEST_MESSAGE,
|
||||
handleTableExportMessage,
|
||||
selectionTable,
|
||||
} from "../../../B07_DesignDetail/openwebcad/src/integration/aislo-table-export.ts";
|
||||
import {
|
||||
getEntities,
|
||||
setEntities,
|
||||
setLayers,
|
||||
setSelectedEntityIds,
|
||||
} from "../../../B07_DesignDetail/openwebcad/src/state.ts";
|
||||
|
||||
const layer = { id: "layer-1", isLocked: false, isVisible: true, name: "작업" };
|
||||
|
||||
const line = (x1: number, y1: number, x2: number, y2: number) =>
|
||||
LineEntity.fromJson({
|
||||
id: crypto.randomUUID(),
|
||||
type: "Line",
|
||||
lineColor: "#fff",
|
||||
lineWidth: 1,
|
||||
layerId: layer.id,
|
||||
shapeData: { startPoint: { x: x1, y: y1 }, endPoint: { x: x2, y: y2 } },
|
||||
} as unknown as JsonEntity<never>);
|
||||
const text = (label: string, x: number, y: number) =>
|
||||
TextEntity.fromJson({
|
||||
id: crypto.randomUUID(),
|
||||
type: "Text",
|
||||
lineColor: "#fff",
|
||||
lineWidth: 1,
|
||||
layerId: layer.id,
|
||||
shapeData: {
|
||||
label,
|
||||
basePoint: { x, y },
|
||||
options: {
|
||||
textDirection: { x: 1, y: 0 },
|
||||
textAlign: "left",
|
||||
textColor: "#fff",
|
||||
fontSize: 2.5,
|
||||
fontFamily: "Noto Sans KR",
|
||||
},
|
||||
},
|
||||
} as unknown as JsonEntity<never>);
|
||||
|
||||
/** 2×2 선 표 + 글 넷 + 표 밖 선 하나(고르지 않음) */
|
||||
async function drawing() {
|
||||
const parts = await Promise.all([
|
||||
line(0, 20, 40, 20),
|
||||
line(0, 10, 40, 10),
|
||||
line(0, 0, 40, 0),
|
||||
line(0, 0, 0, 20),
|
||||
line(20, 0, 20, 20),
|
||||
line(40, 0, 40, 20),
|
||||
text("항목", 2, 14),
|
||||
text("수량", 22, 14),
|
||||
text("흙깎기", 2, 4),
|
||||
text("1,500", 22, 4),
|
||||
]);
|
||||
const outside = await line(100, 100, 200, 100);
|
||||
setEntities([...parts, outside]);
|
||||
return { parts, outside };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
setLayers([layer]);
|
||||
setEntities([]);
|
||||
setSelectedEntityIds([]);
|
||||
});
|
||||
|
||||
describe("고른 범위 → 표 자료", () => {
|
||||
it("고른 선 + 글만 표로 · 숫자 글은 숫자", async () => {
|
||||
const { parts } = await drawing();
|
||||
setSelectedEntityIds(parts.map((entity) => entity.id));
|
||||
const table = await selectionTable();
|
||||
expect(table).toMatchObject({
|
||||
kind: "grid",
|
||||
rows: [
|
||||
["항목", "수량"],
|
||||
["흙깎기", 1500],
|
||||
],
|
||||
colWidths: [20, 20],
|
||||
});
|
||||
});
|
||||
|
||||
it("고른 것 없음 = empty · 글 없는 선 = no-text", async () => {
|
||||
const { outside } = await drawing();
|
||||
expect(await selectionTable()).toBe("empty");
|
||||
setSelectedEntityIds([outside.id]);
|
||||
expect(await selectionTable()).toBe("no-text");
|
||||
});
|
||||
});
|
||||
|
||||
describe("부모 요청 메시지", () => {
|
||||
it("export-table-request = 고른 범위를 export-table{table} 로 회신 · 도면은 그대로", async () => {
|
||||
const { parts } = await drawing();
|
||||
setSelectedEntityIds(parts.map((entity) => entity.id));
|
||||
const before = getEntities();
|
||||
const notify = vi.fn();
|
||||
expect(
|
||||
await handleTableExportMessage({ type: AISLO_EXPORT_TABLE_REQUEST_MESSAGE }, notify),
|
||||
).toBe(true);
|
||||
expect(notify).toHaveBeenCalledTimes(1);
|
||||
const [type, payload] = notify.mock.calls[0];
|
||||
expect(type).toBe(AISLO_EXPORT_TABLE_MESSAGE);
|
||||
expect(payload.table.rows[1]).toEqual(["흙깎기", 1500]);
|
||||
expect(getEntities()).toBe(before);
|
||||
});
|
||||
|
||||
it("고른 것이 없으면 회신 안 함 · 다른 메시지는 false", async () => {
|
||||
await drawing();
|
||||
const notify = vi.fn();
|
||||
expect(
|
||||
await handleTableExportMessage({ type: AISLO_EXPORT_TABLE_REQUEST_MESSAGE }, notify),
|
||||
).toBe(true);
|
||||
expect(notify).not.toHaveBeenCalled();
|
||||
expect(await handleTableExportMessage({ type: "aislo:b08:save-request" }, notify)).toBe(false);
|
||||
expect(await handleTableExportMessage(null, notify)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CAD 명령 TOAREA", () => {
|
||||
it("고른 것이 있어야 하고 · 확정(보기 전용) 도면에서도 됨", () => {
|
||||
const command = getCommandById("TOAREA");
|
||||
expect(command?.label).toBe("면적산출로 보내기");
|
||||
expect(command?.needsSelection).toBe(true);
|
||||
expect(command && isViewOnlyCommand(command)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user