auto: 2026-09-28 14:23 (ESD_LAPTOP)
This commit is contained in:
@@ -209,11 +209,19 @@ def _layer_color(document: Any, name: str) -> str | None:
|
||||
|
||||
|
||||
def dxf_text_to_entities(text: str) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
"""오토캐드 WEBCLIP 이 클립보드에 넣은 DXF 글 → (엔티티 목록, 쓰인 도면층 [{name, color}]).
|
||||
"""클립보드 DXF 글 → (엔티티 목록, 쓰인 도면층 [{name, color}])."""
|
||||
return _entities_with_layers(ezdxf.read(io.StringIO(text)))
|
||||
|
||||
엔티티 `layerId` 는 DXF 도면층 이름 그대로 — 웹 CAD 가 이름으로 자기 도면층에 잇는다.
|
||||
"""
|
||||
document = ezdxf.read(io.StringIO(text))
|
||||
|
||||
def drawing_file_to_entities(
|
||||
filename: str, data: bytes
|
||||
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
"""끌어다 놓은 DXF·DWG 파일 → (엔티티 목록, 쓰인 도면층 [{name, color}])."""
|
||||
return _entities_with_layers(_read_drawing_file(filename, data))
|
||||
|
||||
|
||||
def _entities_with_layers(document: Any) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
"""엔티티 `layerId` 는 DXF 도면층 이름 그대로 — 웹 CAD 가 이름으로 자기 도면층에 잇는다."""
|
||||
entities = _document_entities(document)
|
||||
names = sorted({str(entity["layerId"]) for entity in entities})
|
||||
return entities, [{"name": name, "color": _layer_color(document, name)} for name in names]
|
||||
@@ -291,6 +299,11 @@ def _dwg_to_dxf(source: Path, work_dir: Path) -> Path:
|
||||
|
||||
def import_frame_file(filename: str, data: bytes) -> list[dict[str, Any]]:
|
||||
"""올린 도각 파일(DXF·DWG)을 도각 엔티티 목록으로 바꾼다."""
|
||||
return _document_entities(_read_drawing_file(filename, data))
|
||||
|
||||
|
||||
def _read_drawing_file(filename: str, data: bytes) -> Any:
|
||||
"""DXF·DWG 파일 → ezdxf 문서(메모리에 다 읽음 · 임시 폴더는 바로 지움)."""
|
||||
suffix = Path(filename).suffix.lower()
|
||||
if suffix not in (".dxf", ".dwg"):
|
||||
raise ValueError("DXF 또는 DWG 파일만 올릴 수 있습니다.")
|
||||
@@ -300,6 +313,6 @@ def import_frame_file(filename: str, data: bytes) -> list[dict[str, Any]]:
|
||||
source.write_bytes(data)
|
||||
target = _dwg_to_dxf(source, work_dir) if suffix == ".dwg" else source
|
||||
try:
|
||||
return dxf_to_entities(target)
|
||||
return ezdxf.readfile(str(target))
|
||||
except ezdxf.DXFError as exc:
|
||||
raise ValueError(f"DXF 를 읽지 못했습니다: {exc}") from exc
|
||||
|
||||
@@ -19,6 +19,7 @@ from pydantic import BaseModel
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Export import export_drawing
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Frame_Import import (
|
||||
drawing_file_to_entities,
|
||||
dxf_text_to_entities,
|
||||
import_frame_file,
|
||||
)
|
||||
@@ -151,7 +152,7 @@ async def import_frame_template(
|
||||
|
||||
|
||||
class DxfClipRequest(BaseModel):
|
||||
"""오토캐드 WEBCLIP 이 클립보드에 넣은 DXF 글."""
|
||||
"""클립보드에 담긴 DXF 글."""
|
||||
|
||||
text: str
|
||||
|
||||
@@ -189,6 +190,35 @@ async def paste_dxf_clip(request: DxfClipRequest) -> JSONResponse:
|
||||
)
|
||||
|
||||
|
||||
@router.post("/dxf-clip/file")
|
||||
async def paste_drawing_file(file: UploadFile = File(...)) -> JSONResponse:
|
||||
"""끌어다 놓은 DXF·DWG 파일(오토캐드 WBLOCK·DXFOUT) → 붙일 엔티티 · 도면층 · 저장 안 함."""
|
||||
try:
|
||||
data = await file.read()
|
||||
if len(data) > _FRAME_IMPORT_MAX_BYTES:
|
||||
raise ValueError("도면 파일이 너무 큽니다(20MB 넘음).")
|
||||
entities, layers = await asyncio.to_thread(
|
||||
drawing_file_to_entities, file.filename or "", data
|
||||
)
|
||||
validate_template_entities(entities)
|
||||
return JSONResponse(
|
||||
{
|
||||
"status": "success",
|
||||
"entities": entities,
|
||||
"layers": layers,
|
||||
"entity_count": len(entities),
|
||||
}
|
||||
)
|
||||
except ValueError as exc:
|
||||
return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)})
|
||||
except Exception:
|
||||
logger.exception("B07 도면 파일 붙여넣기 실패: %s", file.filename)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"status": "error", "message": "도면 파일을 붙여넣지 못했습니다."},
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{project_id}/frame-template", response_model=FrameTemplateSaveResponse)
|
||||
async def put_frame_template(
|
||||
project_id: UUID, request: FrameTemplateSaveRequest
|
||||
|
||||
@@ -520,8 +520,8 @@ body > canvas[data-id="canvas"] {
|
||||
background: var(--cad-chrome-raised);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
/* 오토캐드 WEBCLIP 안내 — 캔버스 오른쪽 위 흐린 글씨(빠른 특성 창이 뜨면 그 아래로 가려짐) */
|
||||
.cad-webclip-hint {
|
||||
/* 오토캐드 파일 끌어다 놓기 안내 — 캔버스 오른쪽 위 흐린 글씨(빠른 특성 창이 뜨면 그 아래로 가려짐) */
|
||||
.cad-drop-hint {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
top: calc(var(--cad-title-height) + var(--cad-ribbon-height) + 8px);
|
||||
@@ -540,13 +540,27 @@ body > canvas[data-id="canvas"] {
|
||||
opacity: 0.55;
|
||||
cursor: pointer;
|
||||
}
|
||||
.cad-webclip-hint:hover {
|
||||
.cad-drop-hint:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.cad-webclip-hint__install {
|
||||
.cad-drop-hint__steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
color: var(--cad-text-dim);
|
||||
font-size: 10px;
|
||||
}
|
||||
/* 파일을 끄는 동안 — 화면 가장자리 흐린 테두리(`dxf-clip-paste.ts`) */
|
||||
body.cad-file-dragging::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
z-index: 50;
|
||||
inset: 6px;
|
||||
border: 2px dashed var(--cad-accent);
|
||||
border-radius: 6px;
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cad-view-controls button {
|
||||
width: 34px;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/** 화면 오른쪽 위 흐린 안내 — 오토캐드에서 저장한 파일을 끌어다 놓는 법 · 누르면 순서 (B07 · M02 양식 둘 다) */
|
||||
import { type FC, useState } from 'react';
|
||||
import { isDrawingReadOnly } from '../state';
|
||||
|
||||
export const DropHint: FC = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
if (isDrawingReadOnly()) return null;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="cad-drop-hint controls"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
title="오토캐드 도형을 이 화면에 붙이는 법"
|
||||
>
|
||||
오토캐드에서 WBLOCK(또는 DXFOUT)으로 저장 → 파일을 여기로 끌어다 놓기
|
||||
{open && (
|
||||
<span className="cad-drop-hint__steps">
|
||||
<span>① 오토캐드에서 도형을 고르고 WBLOCK(또는 DXFOUT) 입력 → 파일 이름 정해 저장</span>
|
||||
<span>② 저장한 파일(도면 파일 통째도 됨)을 이 화면으로 끌어다 놓기</span>
|
||||
<span>③ 원래 좌표 · 도면층 그대로 붙고 골라진 상태 — 되돌리기 한 번에 빠짐</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -16,7 +16,7 @@ import { isRibbonCollapsed } from './ui-state';
|
||||
import { getActiveToolActor } from '../state';
|
||||
import type { Tool } from '../tools';
|
||||
import { ViewControls } from './ViewControls';
|
||||
import { WebclipHint } from './WebclipHint';
|
||||
import { DropHint } from './DropHint';
|
||||
|
||||
export const Toolbar: FC = () => {
|
||||
useCadRefresh();
|
||||
@@ -57,7 +57,7 @@ export const Toolbar: FC = () => {
|
||||
onToggleCollapsed={() => setPanelCollapsed((value) => !value)}
|
||||
/>
|
||||
<ViewControls />
|
||||
<WebclipHint />
|
||||
<DropHint />
|
||||
<QuickProperties />
|
||||
<BlockLibraryPanel />
|
||||
{commandLineVisible && <CommandLine />}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/** 화면 오른쪽 위 흐린 안내 — 오토캐드 WEBCLIP → 여기서 Ctrl+V · 누르면 설치 한 줄 (B07 · M02 양식 둘 다) */
|
||||
import { type FC, useState } from 'react';
|
||||
import { isDrawingReadOnly } from '../state';
|
||||
|
||||
export const WebclipHint: FC = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
if (isDrawingReadOnly()) return null;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="cad-webclip-hint controls"
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
title="오토캐드 도형을 이 화면에 붙여넣는 법"
|
||||
>
|
||||
오토캐드에서 WEBCLIP → 여기서 Ctrl+V
|
||||
{open && (
|
||||
<span className="cad-webclip-hint__install">
|
||||
설치: 오토캐드 APPLOAD → aislo_webclip.lsp 불러오기(시작하기 세트에 넣으면 매번 자동)
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,11 @@
|
||||
/**
|
||||
* Ctrl+V — 시스템 클립보드 글이 DXF(오토캐드 WEBCLIP)면 서버에서 도형으로 바꿔 현재 도면에
|
||||
* 원좌표 그대로 붙이고 고른 상태로 둔다 · DXF 가 아니면 앱 안 클립보드 붙여넣기(PASTECLIP).
|
||||
* 키 처리(`input-controller.ts`)는 Ctrl+V 의 기본 동작을 막지 않아 브라우저가 `paste` 를 쏜다 —
|
||||
* `paste` 이벤트의 글은 권한 없이 읽힘(iframe 안에서도).
|
||||
* 오토캐드 도형 붙이기 — 두 길 모두 서버가 DXF 를 도형으로 바꿔 현재 도면에 원좌표 그대로 붙이고
|
||||
* 고른 상태로 둔다(되돌리기 한 번에 빠짐).
|
||||
* - 끌어다 놓기: 오토캐드 WBLOCK(DWG) · DXFOUT(DXF) 로 저장한 파일, 또는 도면 파일 통째.
|
||||
* 끄는 동안 `body.cad-file-dragging` 으로 흐린 테두리.
|
||||
* - Ctrl+V: 시스템 클립보드 글이 DXF 면 같은 길 · 아니면 앱 안 붙여넣기(PASTECLIP).
|
||||
* 키 처리(`input-controller.ts`)는 Ctrl+V 의 기본 동작을 막지 않아 브라우저가 `paste` 를 쏜다 —
|
||||
* `paste` 이벤트의 글은 권한 없이 읽힘(iframe 안에서도).
|
||||
*/
|
||||
import { toast } from 'react-toastify';
|
||||
import { runCommandInput } from '../commands/run-command';
|
||||
@@ -21,12 +24,25 @@ 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 }),
|
||||
});
|
||||
return pasteServerClip(
|
||||
await fetch(DXF_CLIP_URL, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text }),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function pasteDrawingFile(file: File): Promise<number> {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
return pasteServerClip(
|
||||
await fetch(`${DXF_CLIP_URL}/file`, { method: 'POST', credentials: 'include', body: form })
|
||||
);
|
||||
}
|
||||
|
||||
async function pasteServerClip(response: Response): Promise<number> {
|
||||
const payload = (await response.json().catch(() => ({}))) as {
|
||||
entities?: JsonEntity[];
|
||||
layers?: ClipLayer[];
|
||||
@@ -43,6 +59,20 @@ export async function pasteDxfText(text: string): Promise<number> {
|
||||
return drawing.entities.length;
|
||||
}
|
||||
|
||||
function reportPaste(job: Promise<number>): void {
|
||||
job
|
||||
.then((count) => toast.success(`오토캐드 도형 ${count}개를 붙여넣었습니다.`))
|
||||
.catch((error: unknown) =>
|
||||
toast.error(error instanceof Error ? error.message : '도형을 붙여넣지 못했습니다.')
|
||||
);
|
||||
}
|
||||
|
||||
function refuseReadOnly(): boolean {
|
||||
if (!isDrawingReadOnly()) return false;
|
||||
toast.info('볼 수만 있는 도면입니다 — 붙여넣을 수 없습니다.');
|
||||
return true;
|
||||
}
|
||||
|
||||
export function registerDxfClipPaste(): void {
|
||||
document.addEventListener('paste', (event: ClipboardEvent) => {
|
||||
if ((event.target as HTMLElement | null)?.closest?.('input, textarea, select')) return;
|
||||
@@ -52,14 +82,25 @@ export function registerDxfClipPaste(): void {
|
||||
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 를 붙여넣지 못했습니다.')
|
||||
);
|
||||
if (!refuseReadOnly()) reportPaste(pasteDxfText(text));
|
||||
});
|
||||
|
||||
// 파일을 끌 때만 — 앱 안 끌기(글 · 패널)는 건드리지 않음. 기본 동작(파일 열기로 화면 이동)은 막음.
|
||||
const dragging = (event: DragEvent) => event.dataTransfer?.types.includes('Files') ?? false;
|
||||
const setDragging = (on: boolean) => document.body.classList.toggle('cad-file-dragging', on);
|
||||
document.addEventListener('dragover', (event) => {
|
||||
if (!dragging(event)) return;
|
||||
event.preventDefault();
|
||||
setDragging(true);
|
||||
});
|
||||
document.addEventListener('dragleave', (event) => {
|
||||
if (!event.relatedTarget) setDragging(false);
|
||||
});
|
||||
document.addEventListener('drop', (event) => {
|
||||
if (!dragging(event)) return;
|
||||
event.preventDefault();
|
||||
setDragging(false);
|
||||
const file = event.dataTransfer?.files[0];
|
||||
if (file && !refuseReadOnly()) reportPaste(pasteDrawingFile(file));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 오토캐드 → 웹 CAD 붙여넣기(WEBCLIP) — 순수 부분.
|
||||
* 오토캐드 쪽 `resources/tools/autocad/aislo_webclip.lsp` 가 고른 개체를 DXF 글로 클립보드에 넣고,
|
||||
* 여기서 그 글을 알아보고 서버(`/api/projects/dxf-clip`)가 돌려준 JSON 을 현재 도면에 맞춘다.
|
||||
* 오토캐드 → 웹 CAD 붙여넣기 — 순수 부분.
|
||||
* 오토캐드 기본 명령(WBLOCK · DXFOUT)으로 저장한 파일을 끌어다 놓거나 DXF 글을 Ctrl+V 하면,
|
||||
* 서버(`/api/projects/dxf-clip` · `/file`)가 돌려준 JSON 을 여기서 현재 도면에 맞춘다.
|
||||
* DOM · 상태를 안 만지는 것만 둔다(시험이 바로 부름) — 잇기는 `dxf-clip-paste.ts`.
|
||||
*/
|
||||
import type { Layer } from '../App.types.ts';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 오토캐드 WEBCLIP 붙여넣기 순수 부분(`integration/dxf-clip.ts`) 검증.
|
||||
* 오토캐드 도형 붙이기 순수 부분(`integration/dxf-clip.ts`) 검증.
|
||||
* 실행: node --experimental-strip-types resources/tester/cad/dxf-clip.test.ts
|
||||
* (타입만 가져오는 모듈이라 vitest · jsdom 없이 바로 돎)
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""B07 DXF 붙여넣기 — 오토캐드 WEBCLIP 이 클립보드에 넣은 DXF 글 → 웹 CAD 엔티티 · 도면층."""
|
||||
"""B07 DXF 붙여넣기 — 클립보드 DXF 글 · 끌어다 놓은 DXF·DWG 파일 → 웹 CAD 엔티티 · 도면층."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
@@ -42,3 +42,38 @@ def test_route_rejects_broken_text():
|
||||
response = client.post("/api/projects/dxf-clip", json={"text": " 0\nSECTION\n 2\nENTITIES\n"})
|
||||
assert response.status_code == 400
|
||||
assert response.json()["status"] == "error"
|
||||
|
||||
|
||||
# 끌어다 놓기 — 오토캐드 WBLOCK(DWG) · DXFOUT(DXF) 로 저장한 파일
|
||||
DWG = Path("resources/knowledge/original/경쟁사 프로그램/오솔길/OSK_sym/REpyo.dwg")
|
||||
|
||||
|
||||
def test_file_route_reads_dxf():
|
||||
response = client.post(
|
||||
"/api/projects/dxf-clip/file",
|
||||
files={"file": ("클립.dxf", SAMPLE.encode("utf-8"), "application/octet-stream")},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["entity_count"] == 4
|
||||
assert [layer["name"] for layer in body["layers"]] == ["구조물", "치수글"]
|
||||
|
||||
|
||||
def test_file_route_reads_dwg():
|
||||
response = client.post(
|
||||
"/api/projects/dxf-clip/file",
|
||||
files={"file": (DWG.name, DWG.read_bytes(), "application/octet-stream")},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["entity_count"] > 0
|
||||
|
||||
|
||||
def test_file_route_explains_unreadable():
|
||||
wrong = client.post("/api/projects/dxf-clip/file", files={"file": ("a.pdf", b"%PDF", "x")})
|
||||
assert wrong.status_code == 400
|
||||
assert "DXF 또는 DWG" in wrong.json()["message"]
|
||||
newer = client.post(
|
||||
"/api/projects/dxf-clip/file", files={"file": ("a.dwg", b"AC1035" + b"\0" * 64, "x")}
|
||||
)
|
||||
assert newer.status_code == 400
|
||||
assert "AutoCAD 2018 DWG 또는 DXF" in newer.json()["message"]
|
||||
|
||||
Reference in New Issue
Block a user