# -*- coding: utf-8 -*- """저장 덧붙임 — COSTN 새 코드 행 · BDQTY 구성 고침(암호 그대로) · QCODE 수량코드 · 제잡비 설정(PRJLST.SMT). 근거: STC_왕복검증.md §3(알려진 칸만 갱신) · STC_난독화_상태.md(BDQTY 자리별 변환) · 29번 · 02번(QCODE 15필드) · 25번 §1(저장키) · kanjub_cfg.smt_updates(설정 → 저장키) 규칙: - 화면이 보낸 값이 불러온 값과 뜻으로 같으면 그 파일은 원래 바이트 그대로 - COSTN 새 코드: 같은 머리글자 마지막 행 뒤에 끼움 · 숫자 칸 0 · 메모 칸 빈 포인터 - BDQTY: 바뀐 호표(BDCODE 묶음)만 다시 늘어놓음 · 원래 행은 원래 바이트에서 바뀐 칸만 암호로 씀 · 새 행 틀 = 같은 구성 코드 행(이름·규격 암호 칸을 그대로 옮김) > 같은 호표 첫 행 > 첫 행 · 복호가 깨진 값(U+FFFD 포함)은 쓰지 않음 — 틀 행의 원래 바이트 유지 - QCODE: 행이 바뀌었을 때만 qty_qcode.build_qcode 로 두 파일을 다시 씀 - 제잡비: 차수별 설정 → smt_updates · 원래 값과 같은 키는 버림 · 없던 키는 빈 값·0 이 아닐 때만 """ from __future__ import annotations import re from bdqty import decode_bdqty from dbf_crypt import BdqtyCodec, covers from dbf_memo import _same_ini_value, ini_items from dbf_rw import DbfTable from kanjub_cfg import smt_updates from qty_qcode import build_qcode, read_qcode CODE_RE = re.compile(r"^[A-Z][0-9A-Z]{5}$") def _num(v) -> float | None: try: return float(str(v).strip().replace(",", "")) except (TypeError, ValueError): return None def same_val(a, b) -> bool: """뜻 비교 — 둘 다 숫자로 읽히면 숫자로 · 아니면 앞뒤 공백·줄바꿈 꼴 무시.""" na, nb = _num(a), _num(b) if na is not None and nb is not None: return abs(na - nb) < 1e-9 norm = lambda x: str("" if x is None else x).replace("\r\n", "\n").strip() # noqa: E731 return norm(a) == norm(b) def same_row(old: dict, new: dict) -> bool: """old 의 칸 중 new 에 있는 칸이 모두 같은가(new 에 없는 칸은 안 바뀐 것으로 봄).""" return all(same_val(v, new[k]) for k, v in old.items() if k in new and not k.startswith("_")) # ── COSTN 새 코드 행 ── def insert_costn_rows(ct: DbfTable, ups: list[dict], log) -> list[str]: """요청 중 COSTN 에 없는 코드를 새 행으로 끼움 — 끼운 코드 목록.""" have = {ct.get(i, "CODE") for i in range(len(ct))} added = [] for u in ups: code = str(u.get("CODE", "")).strip().upper() if code in have or not CODE_RE.match(code): continue last = [i for i in range(len(ct)) if ct.get(i, "CODE")[:1] == code[:1]] pos = (last[-1] + 1) if last else len(ct) rec = bytearray(b" " * ct.rlen) for f in ct.fields.values(): if f.type in ("N", "F"): rec[f.off : f.off + f.len] = ct.encode_value(f.name, 0) ct.recs.insert(pos, rec) ct.dirty = True vals = {k: v for k, v in u.items() if not k.startswith("_")} vals["CODE"] = code for k, v in vals.items(): if k in ct.fields and ct.fields[k].type != "M": ct.set_raw(pos, k, ct.encode_value(k, v)) log.add("COSTN.DBF", pos, "_insert", "", code) have.add(code) added.append(code) return added # ── BDQTY 구성 ── def _groups(rows: list[dict]) -> dict[str, list[int]]: out: dict[str, list[int]] = {} for i, r in enumerate(rows): out.setdefault(str(r.get("BDCODE", "")).strip(), []).append(i) return out def _lsame(t: DbfTable, k: str, a, b) -> bool: """칸 꼴을 아는 비교 — 논리 칸은 빈칸 = F.""" f = t.fields.get(k) if f is not None and f.type == "L": n = lambda x: str(x or "").strip().upper()[:1] or "F" # noqa: E731 return n(a) == n(b) return same_val(a, b) def save_bdqty(b: bytes, new_rows: list[dict], log, strict: bool = True) -> bytes | None: """화면 구성행(decode_bdqty 꼴) → 새 BDQTY 바이트 · 바뀐 것이 없으면 None. strict=False(엑셀 역등록) — 암호 표 밖 칸은 오류 대신 건너뜀.""" old = decode_bdqty(b) t = DbfTable(b, codec=BdqtyCodec()) def same(o: dict, n: dict) -> bool: return all(_lsame(t, k, v, n[k]) for k, v in o.items() if k in n and not k.startswith("_")) if len(old) == len(new_rows) and all(same(o, n) for o, n in zip(old, new_rows)): return None g_old, g_new = _groups(old), _groups(new_rows) by_comp: dict[str, int] = {} for i, r in enumerate(old): by_comp.setdefault(str(r.get("CODE", "")).strip(), i) recs, plan = [], [] # plan = (새 자리, 비교 기준 옛 행, 새 행) for code, idx in g_new.items(): olds = g_old.get(code, []) news = [new_rows[i] for i in idx] if len(olds) == len(news) and all(same(old[o], n) for o, n in zip(olds, news)): recs += [bytearray(t.recs[o]) for o in olds] continue for k, n in enumerate(news): if k < len(olds): base = olds[k] else: comp = str(n.get("CODE", "")).strip() base = by_comp.get(comp, olds[0] if olds else 0) log.add("BDQTY.DBF", len(recs), "_insert", old[base].get("CODE", ""), comp) plan.append((len(recs), base, n)) recs.append(bytearray(t.recs[base])) for code in g_old: if code not in g_new: log.add("BDQTY.DBF", g_old[code][0], "_delete", code, "") t.recs, t.dirty = recs, True for i, base, n in plan: ref = old[base] for k, v in n.items(): if k.startswith("_") or k not in t.fields or _lsame(t, k, ref.get(k, ""), v): continue if "�" in str(v): # 복호가 깨진 칸(표 밖 글자)에서 온 값 — 옮겨 온 원래 바이트 유지 continue f = t.fields[k] if f.type in ("N", "F") and _num(v) is None and str(v).strip(): continue # 숫자로 읽히지 않는 복호값(자리 표가 덜 맞는 칸)도 같음 if not covers(f.off, f.len): if strict: raise ValueError(f"BDQTY {k} 칸은 암호 표 밖이라 쓸 수 없음") continue t.set_raw(i, k, t.encode_value(k, v)) log.add("BDQTY.DBF", i, k, str(ref.get(k, "")), str(v)) return t.to_bytes() # ── QCODE ── def save_qcode(files: dict[str, bytes], rows: list[dict], log) -> dict[str, bytes]: """수량코드 행이 바뀌었을 때만 {QCODE.DBF, QCODE.SMT}.""" cur = read_qcode(files)["rows"] if len(cur) == len(rows) and all(same_row(o, n) for o, n in zip(cur, rows)): return {} bad = [ r.get("CODE") for r in rows if not re.fullmatch(r"Q\d{5}", str(r.get("CODE", "")).strip()) ] if bad: raise ValueError(f"수량코드 형식 오류: {bad[:5]}") clean = [{k: v for k, v in r.items() if not k.startswith("_")} for r in rows] dbf, smt = build_qcode(clean, files) log.add("QCODE.DBF", 0, "_rows", str(len(cur)), str(len(rows))) return {"QCODE.DBF": dbf, "QCODE.SMT": smt} # ── 제잡비 설정 → PRJLST.SMT 키 ── def kanjub_smt(text: str, stages: dict, explicit: dict) -> dict: """차수별 설정(kanjubStages)과 화면이 직접 준 키를 합친 갱신 사전 — 원래 값과 같은 키는 뺌.""" orig = {k: v for _s, k, v in ini_items(text)} derived: dict = {} for slot, cfg in sorted((stages or {}).items(), key=lambda x: int(x[0])): if not isinstance(cfg, dict): continue upd = smt_updates(cfg, int(slot)) if int(slot) != 1: upd = {k: v for k, v in upd.items() if not k.startswith("ADD_")} derived.update(upd) keep = { k: v for k, v in derived.items() if (k in orig and not _same_ini_value(orig[k], v)) or (k not in orig and str(v).strip() not in ("", "0")) } out = dict(explicit or {}) for k, v in keep.items(): if k not in out or (k in orig and _same_ini_value(orig[k], out[k])): out[k] = v return out