From ae719f22b2f75e0dcc29c802ce7759ed79ef5e69 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 6 Sep 2026 14:16:21 +0900 Subject: [PATCH] =?UTF-8?q?feat(B07):=20DWG=20=EB=B3=80=ED=99=98=EA=B8=B0?= =?UTF-8?q?=EB=A5=BC=20LibreDWG=20=EB=A1=9C=20=EA=B5=90=EC=B2=B4=C2=B7?= =?UTF-8?q?=EC=A7=80=EC=9B=90=20=EB=B2=84=EC=A0=84=20=EC=95=88=EB=82=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ODA File Converter 제거 — 비회원 무료 사용이 비상업 용도로 제한되어 상용 서비스에 부적합 - LibreDWG dwg2dxf 를 별도 프로세스로 호출 (GPL 의무가 본 프로그램에 미치지 않는 방식), 경로는 .env LIBREDWG_DWG2DXF_PATH 또는 PATH 자동 탐색 - DWG 머리글로 형식 판정 — R2018 이후·미지원 형식은 변환 시도 없이 「2018 DWG 또는 DXF 로 저장」 안내 - 변환 실패·변환기 미설치도 같은 안내로 통일 - 형식 판정·안내 경로 테스트 추가 (tmp/tests, git 추적 제외) Co-Authored-By: Claude Opus 5 (1M context) --- .../B07_DesignDetail_Engine_Frame_Import.py | 82 +++++++++++++------ 1 file changed, 58 insertions(+), 24 deletions(-) diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Frame_Import.py b/B07_DesignDetail/B07_DesignDetail_Engine_Frame_Import.py index ccdd1843..252d27e9 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Frame_Import.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Frame_Import.py @@ -1,8 +1,14 @@ """B07 외부 도각 파일 불러오기 — DXF(및 변환된 DWG)를 도각 JSON 으로 바꾼다. 고객이 내는 도각은 DWG 일 확률이 높지만 DWG 는 비공개 형식이라 파이썬이 바로 못 읽는다. -`.env` 의 `ODA_CONVERTER_PATH` 에 무료 변환기(ODA File Converter) 경로를 넣어 두면 DWG 를 -DXF 로 바꿔 읽고, 없으면 「DXF 로 저장해 올려 달라」는 안내로 떨어진다 (2026-09-06 사용자 확정). +**LibreDWG 의 `dwg2dxf`** 로 바꿔 읽는다 (2026-09-06 사용자 확정). ODA File Converter 는 +비회원 무료 사용이 **비상업 용도로 제한**돼 이 프로그램에는 쓰지 않는다. + +LibreDWG 는 별도 실행 파일로만 부른다 — 라이브러리로 끌어안으면 GPL 이 이 프로그램까지 +번진다. 별도 프로세스 호출은 그 의무가 생기지 않는다. + +읽는 범위는 **R2018(AC1032) 까지**다. 그보다 새 형식이나 변환 실패는 「R2018 이하 또는 +DXF 로 저장해 달라」는 안내로 떨어진다. 프로그램 값이 들어갈 자리는 여기서 알아맞히지 않는다 — 불러온 뒤 사용자가 자리표를 직접 놓는다. 좌표는 파일에 있는 그대로 쓴다(도각은 실치수 1:1 mm 로 그린다). @@ -188,30 +194,58 @@ def dxf_to_entities(path: Path) -> list[dict[str, Any]]: return entities +# DWG 머리글의 형식 표시(앞 6바이트) — 읽을 수 있는 것과 사람이 읽을 이름. +_DWG_VERSIONS: dict[str, str] = { + "AC1014": "R14", + "AC1015": "2000", + "AC1018": "2004", + "AC1021": "2007", + "AC1024": "2010", + "AC1027": "2013", + "AC1032": "2018", +} +_SAVE_AS_GUIDE = ( + "캐드에서 「다른 이름으로 저장」으로 AutoCAD 2018 DWG 또는 DXF 를 골라 저장한 뒤" + " 올려 주십시오." +) + + +def dwg_version(data: bytes) -> str | None: + """DWG 머리글에서 형식 이름을 읽는다. 우리가 아는 형식이 아니면 None.""" + return _DWG_VERSIONS.get(data[:6].decode("ascii", "ignore")) + + +def _dwg2dxf_path() -> str | None: + """LibreDWG 변환기(dwg2dxf) 자리. `.env` 값이 먼저고, 없으면 PATH 에서 찾는다.""" + configured = os.getenv("LIBREDWG_DWG2DXF_PATH", "").strip() + if configured: + return configured if Path(configured).is_file() else None + return shutil.which("dwg2dxf") + + def _dwg_to_dxf(source: Path, work_dir: Path) -> Path: - """ODA File Converter 로 DWG 를 DXF 로 바꾼다. 변환기가 없으면 안내와 함께 실패.""" - converter = os.getenv("ODA_CONVERTER_PATH", "").strip() - if not converter or not Path(converter).is_file(): - raise ValueError( - "DWG 는 그대로 읽지 못합니다. 캐드에서 DXF 로 저장해 올려 주십시오." - " (서버에 ODA File Converter 를 두면 DWG 도 바로 읽습니다)" + """LibreDWG 로 DWG 를 DXF 로 바꾼다. 못 읽는 형식·변환기 없음은 안내와 함께 실패.""" + with source.open("rb") as handle: + version = dwg_version(handle.read(6)) + if version is None: + raise ValueError(f"이 DWG 는 R2018 이후이거나 알 수 없는 형식입니다. {_SAVE_AS_GUIDE}") + converter = _dwg2dxf_path() + if not converter: + raise ValueError(f"이 서버는 아직 DWG 를 바로 읽지 못합니다. {_SAVE_AS_GUIDE}") + target = work_dir / "converted.dxf" + try: + subprocess.run( + [converter, "-o", str(target), str(source)], + check=True, + timeout=180, + capture_output=True, ) - in_dir = work_dir / "in" - out_dir = work_dir / "out" - in_dir.mkdir(parents=True, exist_ok=True) - out_dir.mkdir(parents=True, exist_ok=True) - shutil.copy(source, in_dir / source.name) - # ODA 인자: 입력폴더 출력폴더 출력버전 출력형식 재귀 감사 - subprocess.run( - [converter, str(in_dir), str(out_dir), "ACAD2018", "DXF", "0", "1"], - check=True, - timeout=180, - capture_output=True, - ) - converted = sorted(out_dir.glob("*.dxf")) - if not converted: - raise ValueError("DWG 를 DXF 로 바꾸지 못했습니다. 캐드에서 DXF 로 저장해 올려 주십시오.") - return converted[0] + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc: + logger.info("B07 도각 DWG 변환 실패(%s): %s", version, exc) + raise ValueError(f"DWG({version}) 를 바꾸지 못했습니다. {_SAVE_AS_GUIDE}") from exc + if not target.is_file() or target.stat().st_size == 0: + raise ValueError(f"DWG({version}) 를 바꾸지 못했습니다. {_SAVE_AS_GUIDE}") + return target def import_frame_file(filename: str, data: bytes) -> list[dict[str, Any]]: