# -*- coding: utf-8 -*- """변환·가져오기 끼워 넣기 API — 조달청 XML 가져오기·변환 설정 · 엑셀 내역/역등록 · 기준 BOQ·자재·코드 엑셀 왕복 · 분리형 변경 STC · JB_* 기준 제비율표. 근거: 28번 §3(ST_U_Convert) · 30번(호환규정 XSD) · 24번 §3(ST_P_XLS CONV_XLS_2_ST · XLS_2_BDREC · MK_BSCBOQ_EXL) · 16번 §4.6(차수 인계) · 10번 §4(JB_* ↔ RATE*) · 10번(ST_U_ChgCode) 쓰기는 tools/stmate-test/work_dbf/ 아래만(server.WORK_DBF_ROOT) · 엑셀·XML 원본은 읽기만. 파일 받기: 경로(서버 PC) 또는 b64(화면에서 고른 파일). """ from __future__ import annotations import base64 import pathlib from conv_jb import JB_DIR, list_jb, match_project, read_jb from conv_xls_apply import import_boq, register from conv_xls_out import export_boq_xlsx, export_code_xlsx, export_jaje_xlsx, import_code_xlsx from conv_xls_out import import_jaje_xlsx from conv_xml import xml_to_project from conv_xml_cfg import apply_settings, options, validate from fastapi import APIRouter, HTTPException from fastapi.responses import Response from pydantic import BaseModel from stc_change import split_change from stc_codechg import change_codes from stc_project import safe_name, write_output from stc_read import Source router = APIRouter() XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" def _root() -> pathlib.Path: import server return server.WORK_DBF_ROOT def _src(path: str) -> Source: try: return Source(path) except FileNotFoundError: raise HTTPException(404, f"경로 없음: {path}") except ValueError as e: raise HTTPException(400, str(e)) def _blob(path: str = "", b64: str = "", text: str = ""): """경로 · base64 · 글 중 하나 → bytes/str.""" if text: return text if b64: return base64.b64decode(b64.split(",", 1)[-1]) p = pathlib.Path(path or "") if not path or not p.is_file(): raise HTTPException(404, f"파일 없음: {path}") return p.read_bytes() def _guard(fn, *a, **k): try: return fn(*a, **k) except (ValueError, KeyError) as e: raise HTTPException(400, str(e)) def _xlsx(data: bytes, name: str) -> Response: from urllib.parse import quote disp = f"attachment; filename*=UTF-8''{quote(name)}" return Response(content=data, media_type=XLSX, headers={"Content-Disposition": disp}) class FileIn(BaseModel): path: str = "" b64: str = "" text: str = "" class XmlImportReq(BaseModel): template_path: str xml: FileIn pname: str = "" target_folder_name: str = "" class XmlCfgReq(BaseModel): xml_text: str settings: dict = {} class XlsBoqReq(BaseModel): source_path: str xlsx: FileIn table: str = "" kind: str = "내역서" target_folder_name: str = "" class XlsRegReq(BaseModel): template_path: str xlsx: FileIn pname: str = "" target_folder_name: str = "" class SrcReq(BaseModel): source_path: str table: str = "" class JajeImportReq(BaseModel): source_path: str xlsx: FileIn class CodeChgReq(BaseModel): source_path: str mapping: dict[str, str] = {} xlsx: FileIn | None = None target_folder_name: str = "" class SplitReq(BaseModel): source_path: str pcode: str = "" target_folder_name: str = "" class JbReq(BaseModel): path: str def _save(target: str, files: dict, s: Source, extra: dict) -> dict: res = write_output(_root(), safe_name(target), files, s.names, s.entries) return {**res, **extra} # ── 조달청 XML ── @router.post("/api/conv/xml-import") def api_xml_import(req: XmlImportReq): """XML → 새 공사(틀 = template_path 의 요율·서식·설정) — work_dbf/<이름>/ 와 STC.""" t = _src(req.template_path) files, rep = _guard(xml_to_project, t.files, _blob(**req.xml.model_dump()), req.pname) return _save( req.target_folder_name or rep["info"]["pname"] or "XML가져옴", files, t, {"report": rep} ) @router.get("/api/conv/xml-options") def api_xml_options(): return options() @router.post("/api/conv/xml-settings") def api_xml_settings(req: XmlCfgReq): """내보낸 XML 에 공사유형·건축 사업내용 반영 + XSD 검증.""" xml = _guard(apply_settings, req.xml_text, req.settings) return {"xml": xml, "validation": validate(xml)} # ── 엑셀 → 내역 · 역등록 ── @router.post("/api/conv/xls-boq") def api_xls_boq(req: XlsBoqReq): """STMATE 엑셀 내역서 → 지금 공사의 BOQ 표를 바꿔 작업본으로 씀(wM_From_XLS).""" s = _src(req.source_path) files, rep = _guard(import_boq, s.files, _blob(**req.xlsx.model_dump()), req.table, req.kind) return _save(req.target_folder_name or f"{s.path.stem}_엑셀내역", files, s, {"report": rep}) @router.post("/api/conv/xls-register") def api_xls_register(req: XlsRegReq): """STMATE 엑셀 한 권 → 새 공사(CONV_XLS_2_ST · XLS_2_BDREC).""" t = _src(req.template_path) files, rep = _guard(register, t.files, _blob(**req.xlsx.model_dump()), req.pname) return _save(req.target_folder_name or rep["pname"], files, t, {"report": rep}) # ── 엑셀 내보내기 짝 ── @router.post("/api/conv/boq-xlsx") def api_boq_xlsx(req: SrcReq): s = _src(req.source_path) return _xlsx(_guard(export_boq_xlsx, s.files, req.table), f"{s.path.stem}_기준BOQ.xlsx") @router.post("/api/conv/jaje-xlsx") def api_jaje_xlsx(req: SrcReq): s = _src(req.source_path) return _xlsx(_guard(export_jaje_xlsx, s.files), f"{s.path.stem}_자재기준자료.xlsx") @router.post("/api/conv/jaje-import") def api_jaje_import(req: JajeImportReq): """자재 기준자료 엑셀 → costn_updates(바뀐 칸만) — 화면이 /api/save 로 넘김.""" s = _src(req.source_path) ups = _guard(import_jaje_xlsx, s.files, _blob(**req.xlsx.model_dump())) return {"costn_updates": ups, "count": len(ups)} @router.post("/api/conv/code-xlsx") def api_code_xlsx(req: SrcReq): s = _src(req.source_path) return _xlsx(_guard(export_code_xlsx, s.files), f"{s.path.stem}_코드변경표.xlsx") @router.post("/api/conv/code-change") def api_code_change(req: CodeChgReq): """코드 일괄 변경 — mapping 또는 코드 변경표 엑셀(새 코드 칸).""" s = _src(req.source_path) mapping = dict(req.mapping) if req.xlsx and (req.xlsx.path or req.xlsx.b64): mapping.update(_guard(import_code_xlsx, _blob(**req.xlsx.model_dump()))) files, log = _guard(change_codes, s.files, mapping) return _save( req.target_folder_name or f"{s.path.stem}_코드변경", files, s, {"mapping": mapping, "change_count": len(log)}, ) # ── 분리형 변경 STC ── @router.post("/api/conv/split-change") def api_split_change(req: SplitReq): """앞 파일의 현재안 → 다음 변경 차수 STC(BOQ11 차수1 = 현재안 · 차수2 = 출발점).""" s = _src(req.source_path) files, rep = _guard(split_change, s.files, req.pcode) return _save( req.target_folder_name or f"{s.path.stem}_변경{rep['k_ord']}차", files, s, {"report": rep} ) # ── JB_* 기준 제비율표(읽기 전용) ── @router.get("/api/conv/jb-list") def api_jb_list(): return {"dir": str(JB_DIR), "files": list_jb()} @router.post("/api/conv/jb-read") def api_jb_read(req: JbReq): p = pathlib.Path(req.path).resolve() if JB_DIR.resolve() not in p.parents or not p.is_file(): raise HTTPException(400, "설치 폴더 ST_BSC 의 JB_* 만 읽음") return read_jb(p) @router.post("/api/conv/jb-match") def api_jb_match(req: SrcReq): """프로젝트 RATE* 표 ↔ 기준 JB 파일(셀 모두 같은 것 · 아니면 가장 가까운 것).""" return match_project(_src(req.source_path).files)