"""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바이트 ). """ from __future__ import annotations import struct 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_text(hex_ptr: str, smt: bytes) -> str: 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 "" _tag, length, block = struct.unpack("= len(smt): return "" return smt[start : start + length].rstrip(b"\x00").decode("cp949", "replace") def read_qcode(files: dict[str, bytes]) -> dict: """STC 파일 묶음 → {fields, rows} · 메모 칸(IFORM · XCODE)은 본문 글자로.""" 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} for m in MEMO_FIELDS: if m in row: row[m] = _memo_text(r.get(f"{m}_HEX", ""), smt) 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: 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("