# -*- coding: utf-8 -*- """수량산출서·수량코드·표준중기공식 끼워 넣기 API. 근거: 지식DB STmate/old/20_분석 28 §1.1(중기공식 등록·수정·삭제) · §2(수량산출서 · 기본/확장 수량변수) · 02(QCODE 15필드) · REA 28 TWQTYLIST · REA 30 TWM_EDIT_EQ(명칭 · 적용 공식/공식+코드 · EQSYNC). 쓰기는 tools/stmate-test/work_dbf/ 아래만. """ from __future__ import annotations import json import pathlib import re import zipfile from fastapi import APIRouter, HTTPException from pydantic import BaseModel from qty_qcode import build_qcode, read_qcode router = APIRouter() HERE = pathlib.Path(__file__).resolve().parent def _work_root() -> pathlib.Path: import sys srv = sys.modules.get("server") # 실행 중 서버가 있으면 그 작업 폴더(시험은 임시 폴더로 바꿈) return pathlib.Path(getattr(srv, "WORK_DBF_ROOT", HERE / "work_dbf")) def _read_files(source_path: str) -> dict[str, bytes]: p = pathlib.Path(source_path) if not p.exists(): raise HTTPException(status_code=404, detail=f"경로를 찾을 수 없음: {source_path}") if p.is_file() and p.suffix.upper() == ".STC": with zipfile.ZipFile(p) as zf: return {pathlib.Path(n).name.upper(): zf.read(n) for n in zf.namelist()} if p.is_dir(): return {fp.name.upper(): fp.read_bytes() for fp in p.iterdir() if fp.is_file()} raise HTTPException(status_code=400, detail="지원하지 않는 경로 형식") class QcodeReq(BaseModel): source_path: str class QcodeSaveReq(BaseModel): source_path: str target_folder_name: str = "stmate_edited" rows: list[dict] = [] @router.post("/api/qty/qcode") def api_qty_qcode(req: QcodeReq): """프로젝트의 수량코드(QCODE) 행 — 메모(IFORM · XCODE)는 글자로.""" return read_qcode(_read_files(req.source_path)) @router.post("/api/qty/qcode-save") def api_qty_qcode_save(req: QcodeSaveReq): """수량코드 저장 — work_dbf/<이름>/QCODE.DBF·SMT 와 같은 이름 STC 안의 두 파일을 바꿈.""" bad = [ r.get("CODE") for r in req.rows if not re.fullmatch(r"Q\d{5}", str(r.get("CODE", "")).strip()) ] if bad: raise HTTPException(status_code=400, detail=f"수량코드 형식 오류: {bad[:5]}") files = _read_files(req.source_path) dbf, smt = build_qcode(req.rows, files) safe = ( re.sub(r"[^\w\-가-힣]", "_", req.target_folder_name or "stmate_edited").strip("_") or "stmate_edited" ) root = _work_root() out_dir = root / safe out_dir.mkdir(parents=True, exist_ok=True) (out_dir / "QCODE.DBF").write_bytes(dbf) (out_dir / "QCODE.SMT").write_bytes(smt) stc = root / f"{safe}.STC" base = _read_files(str(stc)) if stc.exists() else files base.update({"QCODE.DBF": dbf, "QCODE.SMT": smt}) with zipfile.ZipFile(stc, "w", compression=zipfile.ZIP_DEFLATED) as zf: for name, content in base.items(): zf.writestr(name, content) return {"ok": True, "saved_dir": str(out_dir), "saved_stc": str(stc), "count": len(req.rows)} # 수량산출서 파일(IFORM) — 원본 형식(VTS·XLS) 바이트 그대로 · REA 35 STmateQty8 불러오기·저장·모두 내보내기 class SheetFileReq(BaseModel): filename: str data_b64: str @router.post("/api/qty/sheet-file") def api_qty_sheet_file(req: SheetFileReq): """수량산출파일 불러오기 — 확장자·크기 검사(원본 알림 글) · 엑셀이면 셀·이름 해독 · IFORM_B64 돌려줌.""" import base64 from qty_sheetfile import check_upload, describe data = base64.b64decode(req.data_b64 or "") msg = check_upload(req.filename, data) if msg: raise HTTPException(status_code=400, detail=msg) out = describe(data) out["IFORM_B64"] = req.data_b64 return out class SheetExportReq(BaseModel): source_path: str target_folder_name: str = "stmate_edited" @router.post("/api/qty/sheet-export") def api_qty_sheet_export(req: SheetExportReq): """모든 수량산출서를 「수량산출서」 폴더에 .VTS 로 — 내용은 IFORM 그대로(확장자는 늘 .VTS).""" from qty_sheetfile import export_names rows = read_qcode(_read_files(req.source_path))["rows"] safe = ( re.sub(r"[^\w\-가-힣]", "_", req.target_folder_name or "stmate_edited").strip("_") or "stmate_edited" ) out_dir = _work_root() / safe / "수량산출서" files = export_names(rows) if files: out_dir.mkdir(parents=True, exist_ok=True) for name, data in files: (out_dir / name).write_bytes(data) return {"ok": True, "saved_dir": str(out_dir), "files": [n for n, _ in files]} # 표준중기공식(EQSYNC) — 설치 폴더 공용 등록부 · 여기서는 work_dbf/EQSYNC.json class EqItem(BaseModel): name: str text: str = "" ins_x: int = 0 # 0 공식 · 1 공식+코드 class EqSaveReq(BaseModel): items: list[EqItem] = [] def _eq_path() -> pathlib.Path: return _work_root() / "EQSYNC.json" @router.get("/api/qty/eq") def api_qty_eq(): p = _eq_path() items = json.loads(p.read_text(encoding="utf-8")) if p.exists() else [] return {"items": items} @router.post("/api/qty/eq") def api_qty_eq_save(req: EqSaveReq): names = [i.name.strip() for i in req.items] if any(not n for n in names): raise HTTPException(status_code=400, detail="중기공식 명칭이 비어 있음") if len(set(names)) != len(names): raise HTTPException(status_code=400, detail="같은 명칭의 중기공식이 둘 이상") p = _eq_path() p.parent.mkdir(parents=True, exist_ok=True) p.write_text( json.dumps([i.model_dump() for i in req.items], ensure_ascii=False, indent=1), encoding="utf-8", ) return {"ok": True, "count": len(req.items)}