feat(B07 CAD): Ctrl+V 로 오토캐드 DXF 붙여넣기 · 오른쪽 위 WEBCLIP 안내

Ctrl+V 기본 동작을 막지 않아 paste 이벤트로 시스템 클립보드 글을 읽음 — DXF 면 서버에서
도형으로 바꿔 원좌표 그대로 붙이고 고른 상태로 둠(같은 이름 층은 기존 층 · 없는 층은 DXF
층 색으로 새로 · id 는 모두 새로). DXF 가 아니면 앱 안 클립보드 붙여넣기(PASTECLIP).
볼 수만 있는 도면은 안 붙임. 캔버스 오른쪽 위 흐린 안내 「오토캐드에서 WEBCLIP → 여기서
Ctrl+V」 · 누르면 설치 한 줄(보기 전용이면 숨김 · B07 · M02 양식 둘 다).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K7JS47SacjPxZ718QsZKyr
This commit is contained in:
2026-09-28 13:44:48 +09:00
co-authored by Claude Opus 5.5
parent be4f3d33ac
commit e3a9cb65d0
8 changed files with 225 additions and 3 deletions
@@ -0,0 +1,65 @@
/**
* Ctrl+V — 시스템 클립보드 글이 DXF(오토캐드 WEBCLIP)면 서버에서 도형으로 바꿔 현재 도면에
* 원좌표 그대로 붙이고 고른 상태로 둔다 · DXF 가 아니면 앱 안 클립보드 붙여넣기(PASTECLIP).
* 키 처리(`input-controller.ts`)는 Ctrl+V 의 기본 동작을 막지 않아 브라우저가 `paste` 를 쏜다 —
* `paste` 이벤트의 글은 권한 없이 읽힘(iframe 안에서도).
*/
import { toast } from 'react-toastify';
import { runCommandInput } from '../commands/run-command';
import { hasClipboardContent } from '../helpers/cad-clipboard';
import { getEntitiesAndLayersFromJsonObject } from '../helpers/import-export-handlers/import-entities-from-json';
import {
addEntities,
getLayers,
isDrawingReadOnly,
setLayers,
setSelectedEntityIds,
} from '../state';
import type { JsonEntity } from '../entities/Entity';
import { type ClipLayer, isDxfText, prepareClip } from './dxf-clip';
const DXF_CLIP_URL = '/api/projects/dxf-clip';
export async function pasteDxfText(text: string): Promise<number> {
const response = await fetch(DXF_CLIP_URL, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
const payload = (await response.json().catch(() => ({}))) as {
entities?: JsonEntity[];
layers?: ClipLayer[];
message?: string;
};
if (!response.ok || !payload.entities) {
throw new Error(payload.message ?? `HTTP ${response.status}`);
}
const { entities, newLayers } = prepareClip(payload.entities, payload.layers ?? [], getLayers());
const drawing = await getEntitiesAndLayersFromJsonObject({ entities, layers: getLayers() });
if (newLayers.length) setLayers([...getLayers(), ...newLayers]);
addEntities(drawing.entities, true);
setSelectedEntityIds(drawing.entities.map((entity) => entity.id));
return drawing.entities.length;
}
export function registerDxfClipPaste(): void {
document.addEventListener('paste', (event: ClipboardEvent) => {
if ((event.target as HTMLElement | null)?.closest?.('input, textarea, select')) return;
const text = event.clipboardData?.getData('text/plain') ?? '';
event.preventDefault();
if (!isDxfText(text)) {
if (hasClipboardContent()) runCommandInput('PASTECLIP');
return;
}
if (isDrawingReadOnly()) {
toast.info('볼 수만 있는 도면입니다 — 붙여넣을 수 없습니다.');
return;
}
pasteDxfText(text)
.then((count) => toast.success(`오토캐드 도형 ${count}개를 붙여넣었습니다.`))
.catch((error: unknown) =>
toast.error(error instanceof Error ? error.message : 'DXF 를 붙여넣지 못했습니다.')
);
});
}
@@ -0,0 +1,49 @@
/**
* 오토캐드 → 웹 CAD 붙여넣기(WEBCLIP) — 순수 부분.
* 오토캐드 쪽 `resources/tools/autocad/aislo_webclip.lsp` 가 고른 개체를 DXF 글로 클립보드에 넣고,
* 여기서 그 글을 알아보고 서버(`/api/projects/dxf-clip`)가 돌려준 JSON 을 현재 도면에 맞춘다.
* DOM · 상태를 안 만지는 것만 둔다(시험이 바로 부름) — 잇기는 `dxf-clip-paste.ts`.
*/
import type { Layer } from '../App.types.ts';
import type { JsonEntity } from '../entities/Entity';
/** DXF 글인가 — 그룹 코드 0 다음 줄이 SECTION 으로 시작(앞 공백 · CRLF 허용). */
export function isDxfText(text: string): boolean {
return /^\s*0\s*\r?\n\s*SECTION\b/.test(text);
}
export interface ClipLayer {
name: string;
color: string | null;
}
/**
* 서버 엔티티(layerId = DXF 도면층 이름)를 현재 도면에 붙일 모양으로 —
* 같은 이름 도면층이 있으면 그 층 · 없으면 새 층(DXF 층 색) · id 는 모두 새로(두 번 붙여도 안 겹침).
*/
export function prepareClip(
entities: JsonEntity[],
clipLayers: ClipLayer[],
existing: Layer[]
): { entities: JsonEntity[]; newLayers: Layer[] } {
const idByName = new Map(existing.map((layer) => [layer.name, layer.id]));
const newLayers: Layer[] = [];
const colorByName = new Map(clipLayers.map((layer) => [layer.name, layer.color]));
const layerIdOf = (name: string): string => {
let id = idByName.get(name);
if (!id) {
id = crypto.randomUUID();
idByName.set(name, id);
const color = colorByName.get(name);
newLayers.push({ id, name, isVisible: true, isLocked: false, ...(color ? { color } : {}) });
}
return id;
};
const renew = (entity: JsonEntity): JsonEntity => ({
...entity,
id: crypto.randomUUID(),
layerId: layerIdOf(String(entity.layerId ?? '0')),
...(entity.children ? { children: entity.children.map(renew) } : {}),
});
return { entities: entities.map(renew), newLayers };
}