# -*- coding: utf-8 -*- """코드 일괄 변경(ST_U_ChgCode Chg_Code_Sel · Exec_Change_Code) — 옛 코드 → 새 코드를 프로젝트 전체에 바꿈. 근거: 10_System_186개_파일분석장부.md(ST_U_ChgCode — 코드 일괄·선택 변경 · Excel 왕복 · CH_CODE 임시표) · 23번(COSTN.SMT 식 참조 &코드[LMS]&) · STC_난독화_상태.md(BDQTY 자리별 변환) · 02번(BOQ BDCODE1~4) 바꾸는 자리: COSTN.CODE · BOQ1n.BDCODE1~5 · BDQTY.BDCODE·CODE(암호 그대로 다시 숨김) · COSTN.SMT 식의 &코드…& · QCODE.BQCODE · BOQ1n.CODEA 규칙: 새 코드는 6자 영숫자 · 이미 있는 코드로는 못 바꿈 · 같은 길이라 메모는 제자리 덮어씀 """ from __future__ import annotations import re from dbf_crypt import BdqtyCodec from dbf_memo import SixMemo from dbf_rw import DbfTable from stc_save import SaveLog CODE_RE = re.compile(r"^[A-Z][0-9A-Z]{5}$") def _check(files: dict, mapping: dict[str, str]) -> dict[str, str]: ct = DbfTable(files["COSTN.DBF"]) have = {ct.get(i, "CODE") for i in range(len(ct))} m = { str(a).strip().upper(): str(b).strip().upper() for a, b in mapping.items() if str(b).strip() } m = {a: b for a, b in m.items() if a != b} for a, b in m.items(): if a not in have: raise ValueError(f"COSTN 에 없는 코드: {a}") if not CODE_RE.match(b): raise ValueError(f"새 코드 형식 오류(영문 1자 + 5자): {b}") if b in have and b not in m: raise ValueError(f"이미 있는 코드로 바꿀 수 없음: {a} → {b}") if len(set(m.values())) != len(m): raise ValueError("새 코드가 겹침") return m def _table( files: dict, out: dict, name: str, cols: tuple[str, ...], m: dict, log: SaveLog, codec=None ): if name not in files: return t = DbfTable(files[name], codec=codec) for i in range(len(t)): for c in cols: if c in t.fields: v = t.get(i, c) if v in m and t.set_raw(i, c, t.encode_value(c, m[v])): log.add(name, i, c, v, m[v]) if t.dirty: out[name] = t.to_bytes() def change_codes(files: dict, mapping: dict[str, str]) -> tuple[dict, list]: """{옛: 새} → 새 files · 바뀐 자리 목록.""" m = _check(files, mapping) out, log = dict(files), SaveLog() if not m: return out, [] _table(files, out, "COSTN.DBF", ("CODE",), m, log) for name in sorted(k for k in files if re.match(r"^BOQ\d*\.DBF$", k)): _table(files, out, name, tuple(f"BDCODE{n}" for n in range(1, 6)) + ("CODEA",), m, log) _table(files, out, "BDQTY.DBF", ("BDCODE", "CODE"), m, log, codec=BdqtyCodec()) _table(files, out, "QCODE.DBF", ("BQCODE",), m, log) if "COSTN.SMT" in files and "IFORM" in DbfTable(out["COSTN.DBF"]).fields: ct = DbfTable(out["COSTN.DBF"]) memo = SixMemo(files["COSTN.SMT"]) pat = re.compile(r"&(" + "|".join(map(re.escape, m)) + r")([LMS]?)&") for i in range(len(ct)): ptr = ct.raw(i, "IFORM") body = memo.read(ptr) if not body: continue text = body.decode("cp949", "replace") new = pat.sub(lambda x: f"&{m[x.group(1)]}{x.group(2)}&", text) if new != text: new_ptr = memo.write(ptr, new.encode("cp949")) if ct.set_raw(i, "IFORM", new_ptr): out["COSTN.DBF"] = ct.to_bytes() log.add("COSTN.SMT", i, ct.get(i, "CODE"), "", f"{len(pat.findall(text))}곳") if memo.dirty: out["COSTN.SMT"] = memo.to_bytes() return out, log.items