# -*- 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 '\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]]}