162 lines
6.5 KiB
Python
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]]}
|