Files
Aislo/tools/stmate-test/conv_xml_cfg.py
T

162 lines
6.5 KiB
Python

# -*- coding: utf-8 -*-
"""XML 변환 설정 (uM_XML_Info · uM_ConstType) — 공사유형 · 건축공사 사업내용(공사현장 지역 포함) → 조달청 XML 에 반영.
근거: 30_조달청_호환규정_XSD.md §5·§6(주공사 9종 · 지역코드 · 건축 사업내용) · 34번 §2.7(uM_XML_Info 공사유형 8) ·
old/30_원자료/ui_form_catalog.txt(uM_XML_Info ed_PrjTyp · uM_ConstType cn_Cn_Code1 시도 · cn_* 선택지)
선택지·칸 차례·열거값은 XSD 에서 읽음(코드에 박지 않음) · 화면 공사유형 8 = uM_XML_Info ed_PrjTyp(문화재수리 제외).
반영: 공사정보/주공사 · 단일공사정보/공사구분 = 공사유형 · 건축이면 작성정보/건축공사사업내용작성여부 = true 와
공사정보/건축공사사업내용(XSD 차례 · 빈 칸 = 숫자 0 · 열거 첫 값 · 글 빈칸) · 끝에 XSD 검증 결과.
"""
from __future__ import annotations
import pathlib
import re
import xml.etree.ElementTree as ET
HERE = pathlib.Path(__file__).resolve().parent
KB = HERE.parent.parent / "resources/knowledge/original/경쟁사 프로그램/STmate/old/30_원자료"
XSD = KB / "조달청_공사원가호환규정.xsd"
CATALOG = KB / "ui_form_catalog.txt"
XS = "{http://www.w3.org/2001/XMLSchema}"
def _xsd():
return ET.parse(XSD).getroot()
def _enum(el) -> list[str]:
return [e.get("value") for e in el.iter(f"{XS}enumeration")]
def _ui_list(form: str, ctl: str) -> list[str]:
"""ui_form_catalog 의 폼·컨트롤 선택지(쉼표 구분)."""
if not CATALOG.exists():
return []
cur = ""
for ln in CATALOG.read_text(encoding="utf-8").splitlines():
m = re.match(r"^■\s+(\S+)", ln)
if m:
cur = m.group(1)
elif cur == form and ln.strip().startswith(ctl):
return [x.strip() for x in ln.strip()[len(ctl) :].split(",") if x.strip()]
return []
def building_fields() -> list[dict]:
"""건축공사사업내용형식 칸 — {name, type(double|enum|text), enum, max}."""
ct = next(c for c in _xsd().iter(f"{XS}complexType") if c.get("name") == "건축공사사업내용형식")
out = []
for el in ct.find(f"{XS}sequence").findall(f"{XS}element"):
en = _enum(el)
mx = el.find(f".//{XS}maxLength")
kind = "double" if el.get("type") == "xsd:double" else ("enum" if en else "text")
out.append(
{
"name": el.get("name"),
"type": kind,
"enum": en,
"max": int(mx.get("value")) if mx is not None else 0,
}
)
return out
def options() -> dict:
"""설정 화면 선택지 — 공사유형(화면 8 · XSD 9) · 시도(지역코드 앞 두 자리 · 이름은 화면 목록) · 건축 칸."""
root = _xsd()
main = next(e for e in root.iter(f"{XS}element") if e.get("name") == "주공사")
names = {}
for item in _ui_list("uM_ConstType", "cn_Cn_Code1"):
m = re.match(r"^(.+);\s*(\d+)$", item)
if m:
names[m.group(2)] = m.group(1)
fields = building_fields()
codes = next((f["enum"] for f in fields if f["name"] == "공사현장지역코드"), [])
sido = sorted({c[:2] for c in codes})
return {
"prj_types": _ui_list("uM_XML_Info", "ed_PrjTyp") or _enum(main),
"prj_types_xsd": _enum(main),
"sido": [
{"code": s, "name": names.get(s, ""), "regions": [c for c in codes if c[:2] == s]}
for s in sido
],
"building": fields,
}
def _set(parent, tag: str, text: str, after: tuple[str, ...] = ()):
"""parent 아래 tag 를 text 로(없으면 after 칸들 뒤에 끼움)."""
el = parent.find(tag)
if el is None:
el = ET.Element(tag)
kids = list(parent)
at = max((i + 1 for i, k in enumerate(kids) if k.tag in after), default=len(kids))
parent.insert(at, el)
el.text = text
return el
def _fmt(v) -> str:
try:
f = float(v)
return str(int(f)) if f.is_integer() else repr(f)
except (TypeError, ValueError):
return str(v or "")
def apply_settings(xml: str, cfg: dict) -> str:
"""내보낸 XML 글 + 설정 {prj_type, building: {칸: 값}} → 새 XML 글."""
root = ET.fromstring(xml.encode("utf-8"))
ptype = (cfg or {}).get("prj_type") or ""
allowed = options()["prj_types_xsd"]
if ptype and ptype not in allowed:
raise ValueError(f"공사유형 '{ptype}' 은 호환규정 주공사에 없음")
if ptype:
info = root.find("공사정보")
if info is not None:
_set(info, "주공사", ptype)
for el in root.iter("단일공사정보"):
_set(el, "공사구분", ptype)
bld = (cfg or {}).get("building")
if ptype == "건축공사" and bld is not None:
writer = root.find("작성정보")
_set(writer, "건축공사사업내용작성여부", "true", ("파일검증여부", "파일제작일"))
info = root.find("공사정보")
old = info.find("건축공사사업내용")
if old is not None:
info.remove(old)
box = ET.Element("건축공사사업내용")
for f in building_fields():
v = bld.get(f["name"])
if f["type"] == "double":
text = _fmt(v if v not in (None, "") else 0)
elif f["type"] == "enum":
if v not in (None, "") and str(v) not in f["enum"]:
raise ValueError(f"{f['name']} 값 '{v}' 은 호환규정 열거에 없음")
text = str(v) if v not in (None, "") else f["enum"][0]
else:
text = re.sub(r"[\r\n\t]+", " ", str(v or "")).strip()
text = text[: f["max"]] if f["max"] else text
ET.SubElement(box, f["name"]).text = text
kids = list(info)
at = max(
(i + 1 for i, k in enumerate(kids) if k.tag in ("비고", "기계경비산출")),
default=len(kids),
)
info.insert(at, box)
body = ET.tostring(root, encoding="unicode")
return '<?xml version="1.0" encoding="UTF-8"?>\n' + body
def validate(xml: str) -> dict:
"""XSD 검증 — {ok, errors[]} (lxml 없으면 ok=None)."""
try:
from lxml import etree
except ImportError:
return {"ok": None, "errors": ["lxml 없음 — 검증 생략"]}
schema = etree.XMLSchema(etree.parse(str(XSD)))
doc = etree.fromstring(xml.encode("utf-8"))
ok = schema.validate(doc)
return {"ok": bool(ok), "errors": [str(e) for e in list(schema.error_log)[:10]]}