252 lines
7.7 KiB
Python
252 lines
7.7 KiB
Python
# -*- 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)
|