Files
Aislo/tools/stmate-test/api_qty.py
T
eomsangdonandClaude Opus 5.5 3d8fddfc34 feat(stmate-test): 보고서·제잡비·식언어·수량산출·화면 묶음 합침 · 재계산 후 저장 무변경
- 보고서: 원본 STMATE 엑셀과 셀 대조(목록표·자재단가대비·중기·일위대가·산근 목록·설계내역서·원가계산서) · 조달청 XML 공식 XSD 검증 · PM_Prt 화면
- 제잡비: 원본 계산 순서 4형식(T/S/TA/SA) · 실무 7건 저장 결과키 전수 1원 · 요율표 조회 · 선택지 계산 · 제잡비·제비율 화면
- 식언어·수량: 함수·조건식·금액변수·문법 검사·수량변수·QCODE · 산근·구성·중기공식 편집 화면 · 순환 알림
- 화면: 원본 메뉴 체계·단축키 · 설계내역서 편집(행 넣기·지우기·이동·레벨·보관함) · 초기값 · 단가목록표
- 저장: 초기값(Start8x.Ini) 바뀐 키 저장 · 노임 환산 직종 = 초기값 CONLD · 산근·구성 결과에 적용단가 칸 채우지 않음(원본 0)
- DBF 글 칸 앞 빈칸 보존 · 시험 도우미 표준입력 utf8(한글 조각 깨짐 원인)
- 실무 5건 재계산 후 저장 = 바뀐 칸 0 · 시험 332건 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 19:12:04 +09:00

125 lines
4.2 KiB
Python

# -*- 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)}
# 표준중기공식(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)}