"""STMATE 수량코드(QCODE.DBF · QCODE.SMT) 읽기·쓰기 — 수량산출서 기본 수량변수 원장. 근거: 지식DB STmate/old/20_분석 02(QCODE 15필드 CODE·NAME·BQCODE·QTY0~9·IFORM·XCODE) · 28 §2(기본수량변수 ↔ 확장수량변수) · REA 30 quantity_variable_resolution(QTY0~9 직접 · XCODE 이름=값) · SIxMemo_구조(메모 칸 10바이트 ) · REA 35(IFORM = 수량산출서 파일 바이트 VTS·XLS 그대로 · XCODE = 「이름=값」CRLF 글). """ from __future__ import annotations import base64 import struct from qty_sheetfile import kind_of from smt_parser import build_dbf_bytes, parse_dbf_bytes MEMO_FIELDS = ("IFORM", "XCODE") QTY_FIELDS = tuple(f"QTY{i}" for i in range(10)) # 표본 STC 의 QCODE.DBF 필드 정의(빈 표 · 새 파일을 만들 때의 틀) DEFAULT_FIELDS = [ {"name": "CODE", "type": "C", "len": 6, "dec": 0}, {"name": "NAME", "type": "C", "len": 60, "dec": 0}, {"name": "BQCODE", "type": "C", "len": 16, "dec": 0}, *({"name": f, "type": "N", "len": 12, "dec": 4} for f in QTY_FIELDS), {"name": "IFORM", "type": "M", "len": 10, "dec": 0}, {"name": "XCODE", "type": "M", "len": 10, "dec": 0}, ] def _memo_bytes(hex_ptr: str, smt: bytes) -> bytes: raw = bytes.fromhex(hex_ptr or "") if ( len(raw) < 10 or not smt or smt[8:15] != b"SIxMemo" or not any(b not in (0, 0x20) for b in raw) ): return b"" _tag, length, block = struct.unpack("= len(smt): return b"" return smt[start : start + length] def _memo_text(hex_ptr: str, smt: bytes) -> str: return _memo_bytes(hex_ptr, smt).rstrip(b"\x00").decode("cp949", "replace") def read_qcode(files: dict[str, bytes]) -> dict: """STC 파일 묶음 → {fields, rows} · XCODE 는 글자 · IFORM 은 파일 바이트(IFORM_B64 · 종류 · 크기).""" dbf = files.get("QCODE.DBF", b"") if len(dbf) < 32: return {"fields": DEFAULT_FIELDS, "rows": [], "present": False} fields, rows, _meta = parse_dbf_bytes(dbf) smt = files.get("QCODE.SMT", b"") out = [] for r in rows: row = {f["name"]: r.get(f["name"], "") for f in fields} if "XCODE" in row: row["XCODE"] = _memo_text(r.get("XCODE_HEX", ""), smt) if "IFORM" in row: # IFORM = 수량산출서 파일(VTS·XLS) 바이트 그대로 — 글자로 풀지 않음 data = _memo_bytes(r.get("IFORM_HEX", ""), smt) row["IFORM"] = "" row["IFORM_B64"] = base64.b64encode(data).decode("ascii") if data else "" row["IFORM_KIND"] = kind_of(data) row["IFORM_SIZE"] = len(data) out.append(row) return { "fields": [{k: f[k] for k in ("name", "type", "len", "dec")} for f in fields], "rows": out, "present": True, } def build_qcode(rows: list[dict], files: dict[str, bytes] | None = None) -> tuple[bytes, bytes]: """행 목록 → (QCODE.DBF, QCODE.SMT) 바이트 · 원본 필드 정의가 있으면 그대로 씀.""" files = files or {} fields, meta = DEFAULT_FIELDS, None if len(files.get("QCODE.DBF", b"")) >= 32: fields, _rows, meta = parse_dbf_bytes(files["QCODE.DBF"]) smt = bytearray(512) smt[8:15] = b"SIxMemo" recs = [] for r in rows: rec = {} for f in fields: name, val = f["name"], r.get(f["name"], "") if f.get("type") == "M" or name in MEMO_FIELDS: if name == "IFORM" and r.get("IFORM_B64"): data = base64.b64decode(r["IFORM_B64"]) # 수량산출서 파일 바이트 그대로 else: text = str(val or "").replace("\r\n", "\n").replace("\n", "\r\n") data = text.encode("cp949", "replace") if not data: rec[f"{name}_HEX"] = (b" " * int(f["len"])).hex() continue block = len(smt) // 64 smt.extend(data) smt.extend(b"\x00" * ((-len(smt)) % 64)) rec[f"{name}_HEX"] = struct.pack("