# -*- coding: utf-8 -*- """구버전(DOS·7.x) → Win8 자료 변환 — ST_U_Convert ST_ConvToWin8(0x459928) 표 옮기기. 근거(지식DB REA 분석/37 §2): 7.x 0x459e68 · DOS 0x45a8d4 · 표마다 원본·대상 양쪽에 있는 필드만 값 복사 COSTN 7.x 0x4882a4(63쌍) · DOS 0x490cb8(35쌍 + COSTN1 M 코드 덧값) · 행 = CODE 첫 글자 MLSWBD · 단위 첫 글자 LMST + 둘째 '%' 면 앞 5칸만 · GUN 'H…' → 'Z' · B 코드 + 단위(대문자·Trim) 'X' → 'X'+CODE[1:6] BDQTY 0x489448 · PRJLST 7.x 0x48c890 / DOS 0x49379c · KONGJONG 0x48cbf0 · BOQ 7.x 0x48d58c / DOS 0x493e78 · BQM 0x48dc38 · Start8x.Ini(conv_win8_ini) · 결과 BDQTY 는 암호 표(DbfEncrypt) 틀(Win8 프로젝트)의 표 구조·요율·서식·설정을 씀(stc_project.new_project) · 원본 폴더는 읽기만. """ from __future__ import annotations import re from conv_win8_ini import b_list, detect, ini_from_7x, ini_from_dos from dbf_crypt import covers from dbf_memo import BLANK_PTR, SixMemo from dbf_rw import DbfTable from stc_project import new_project from stc_save import SaveLog from stc_save_extra import save_bdqty _AMT = [("AMT", "TAMT1"), ("LAMT", "LAMT1"), ("MAMT", "MAMT1"), ("SAMT", "SAMT1")] _AMT += [("D_AMT", "TAMT2"), ("D_LAMT", "LAMT2"), ("D_MAMT", "MAMT2"), ("D_SAMT", "SAMT2")] _HEAD = [(k, k) for k in ("CODE", "NAME", "SIZE", "UNIT", "BQCODE")] _FLAGS = [(k, k) for k in ("NAGCD", "UC", "K_S", "GUN", "JOJAK", "BOKSA")] _BK = [(f"{c}AMT", f"{c}AMT1") for c in "BCDEFG"] + [("HAMT", "ZAMT1"), ("KAMT", "KAMT1")] _BK += [(f"D_{c}AMT", f"{c}AMT2") for c in "BCDEFG"] + [("D_HAMT", "ZAMT2"), ("D_KAMT", "KAMT2")] COSTN_DOS = _HEAD + _AMT + _FLAGS + _BK _AP = [(f"AMT_{n}D", f"AMT_{n}1") for n in range(1, 6)] + [ (f"PG_{n}D", f"PG_{n}1") for n in range(1, 6) ] _AP += [(f"AMT_{n}B", f"AMT_{n}2") for n in range(1, 6)] + [ (f"PG_{n}B", f"PG_{n}2") for n in range(1, 6) ] _AP += [ ("PG_J", "PG_J"), ("PG_D", "PG_1"), ("PG_B", "PG_2"), ("AMT_61", "AMT_1"), ("AMT_62", "AMT_2"), ] _AP += [("AMT_6D", "AMT_61"), ("AMT_6B", "AMT_62")] COSTN_7X = _HEAD + _AMT + _FLAGS + _AP + _BK + [("PP", "PP")] COSTN1_M = [("AMT_A", "AMT_12"), ("AMT_B", "AMT_22"), ("AMT_C", "AMT_32"), ("AMT_D", "AMT_11")] COSTN1_M += [ ("AMT_E", "AMT_21"), ("AMT_F", "AMT_31"), ("PG_A", "PG_12"), ("PG_B", "PG_22"), ("PG_C", "PG_32"), ] COSTN1_M += [("PG_D", "PG_11"), ("PG_E", "PG_21"), ("PG_F", "PG_31"), ("PG_J", "PG_J")] BDQTY_MAP = [("BDCODE", "BDCODE"), ("CODE", "CODE"), ("T_NAME", "NAME1"), ("T_SIZE", "SIZE1")] BDQTY_MAP += [("QTY", "QTY1"), ("BIMOK", "BIMOK"), ("DEVD", "DEVD1")] _COST = ["MCOST", "LCOST", "SCOST"] PRJ_DOS = [(k, k) for k in ("PNUM", "PNAME", "PCODE", "PCLASS")] PRJ_DOS += [(c, f"{c}1") for c in _COST] + [("T_COST", "TCOST1"), ("COST_W", "COSTW1")] PRJ_DOS += [(f"{c}1", f"{c}2") for c in _COST] + [("T_COST1", "TCOST2"), ("COST_WA", "COSTW2")] PRJ_DOS += [(f"{c}2", f"{c}3") for c in _COST] + [("T_COST2", "TCOST3")] PRJ_7X = PRJ_DOS[:4] + [("CCODE", "CCODE")] + PRJ_DOS[4:] PRJ_7X += [(f"{c}3", f"{c}4") for c in _COST] + [("T_COST3", "TCOST4")] PRJ_7X += [("XXXNM", "XX1NM"), ("IXXNM", "XX2NM"), ("JXXNM", "XX3NM"), ("KXXNM", "XX4NM")] PRJ_7X += [("CHAMT", "CHAMT"), ("K_ORD", "K_ORD")] _BOQ = [("QTY", "QTY1")] + [(a, b) for a, b in _AMT[:4]] _BOQ += [ ("T_AMOUNT", "T_AMT1"), ("L_AMOUNT", "L_AMT1"), ("M_AMOUNT", "M_AMT1"), ("S_AMOUNT", "S_AMT1"), ] _BOQ += [("BDCODE", "BDCODE1"), ("QTY1", "QTY2")] + _AMT[4:] _BOQ += [ ("DT_AMOUNT", "T_AMT2"), ("DL_AMOUNT", "L_AMT2"), ("DM_AMOUNT", "M_AMT2"), ("DS_AMOUNT", "S_AMT2"), ] _BOQ += [("QTY2", "QTY3"), ("T_AMOUNT2", "T_AMT3"), ("QTY3", "QTY4"), ("T_AMOUNT3", "T_AMT4")] _BOQ += [(k, k) for k in ("SORTCODE", "ENDCODE", "BQCODE")] BOQ_7X = [("ITNUM", "ITNUM"), ("NAME", "NAME1"), ("SIZE", "SIZE1"), ("UNIT", "UNIT1")] + _BOQ BOQ_7X += [(k, k) for k in ("RL", "RM", "RS", "UC")] BOQ_DOS = [("ITNUM", "ITNUM"), ("DESCRIP", "NAME1"), ("DESSIZE", "SIZE1"), ("UNIT", "UNIT1")] + _BOQ BQM_7X = [ ("ITNUM", "ITNUM"), ("NAME", "NAME1"), ("SIZE", "SIZE1"), ("UNIT", "UNIT1"), ("QTY", "QTY1"), ] BQM_7X += [("AMT", "TAMT1"), ("T_AMOUNT", "T_AMT1"), ("BDCODE", "BDCODE1")] BQM_7X += [(k, k) for k in ("RATE", "BIGO", "SORTCODE", "ENDCODE")] KONG_DOS = [ "토 공", "배 수 공", "구 조 물 공", "포 장 공", "조 경 공", ] KONG_DOS += ["부 대 공", "기 타 공 "] UNMAPPED = [ "간접비 설정 이관(PRJ_MEMO — 7.x 0x48abb0 · DOS 0x4922e4) → 틀 설정 유지", "RATE·RATE_RA·RATE_M 행 대응(KANJUB2 0x47a9f4 · JB_* 틀) → 틀 요율 유지", "SYSINFO(암호 표) → 틀 유지 · IFORM 산출식(FORM.DBF · Tran_SAN_Format · DOS 0x4905d0) · IMG.DB 그림", "원본 BDQTY 가 암호 표면 열쇠(전역 +0x12) 미확인 → 평문 BDQTY 만", ] def _table(files: dict, name: str) -> DbfTable | None: b = files.get(name.upper()) if not b: return None t = DbfTable(b) return None if b[0] == 0x06 else t def _rows(t: DbfTable | None) -> list[dict]: if t is None: return [] return [{n: t.get(i, n) for n in t.order} for i in range(len(t)) if not t.deleted(i)] def _copy(src: dict, pairs: list[tuple[str, str]], have: set[str]) -> dict: return {d: src[s] for s, d in pairs if s in src and d in have} def _build(template: bytes, rows: list[dict]) -> bytes: """틀 필드로 새 표 — 새 레코드 = 숫자 0 · 논리·글 빈칸 · 주어진 칸만(원본·대상 둘 다 있는 칸).""" t = DbfTable(template) zero = bytearray(b" " * t.rlen) for f in t.fields.values(): if f.type in ("N", "F"): zero[f.off : f.off + f.len] = t.encode_value(f.name, 0) t.recs, t.dirty = [], True for i, r in enumerate(rows): t.recs.append(bytearray(zero)) for k, v in r.items(): if k in t.fields and t.fields[k].type != "M" and v not in (None, ""): t.set_raw(i, k, t.encode_value(k, v)) return t.to_bytes() def _build_bdqty(template: bytes, rows: list[dict]) -> bytes: """암호 BDQTY — 틀 레코드를 옮겨(stc_save_extra.save_bdqty) 암호 표 안 칸은 AppendBlank 꼴(0·F·빈칸)로 채움 · 표 밖 칸(NAME2 등)은 옮겨 온 틀 레코드 바이트(조건부).""" t = DbfTable(template) blank = {} for f in t.fields.values(): if covers(f.off, f.len) and f.type != "M": blank[f.name] = 0 if f.type in ("N", "F") else ("F" if f.type == "L" else "") full = [blank | r for r in rows] if not full: t.recs, t.dirty = [], True return t.to_bytes() return save_bdqty(template, full, SaveLog(), strict=False) or template def _is_t(v) -> bool: return str(v).strip().upper() in ("T", "Y", "1", "TRUE") def conv_costn( src: list[dict], pairs, have: set[str], m_extra: dict[str, dict] ) -> tuple[list[dict], dict]: """COSTN 행 옮기기 — (행, 옛 B → 새 X).""" out, b2x = [], {} for r in src: code = r.get("CODE", "").strip() if not code or code[0] not in "MLSWBD": continue unit = r.get("UNIT", "") if unit[:1] in "LMST" and unit[1:2] == "%": row = _copy(r, _HEAD, have) else: row = _copy(r, pairs, have) if str(r.get("GUN", "")).startswith("H") and "GUN" in have: row["GUN"] = "Z" if code[0] == "M" and code in m_extra: row |= _copy(m_extra[code], COSTN1_M, have) if code[0] == "B" and unit.strip().upper() == "X": b2x[code] = "X" + code[1:6] row["CODE"] = b2x[code] out.append(row) return out, b2x def conv_bdqty(src: list[dict], costn_codes: set[str], b2x: dict, have: set[str]) -> list[dict]: out = [] for r in src: bd, c = r.get("BDCODE", "").strip(), r.get("CODE", "").strip() if bd[:1] not in ("B", "D", "X") or bd not in costn_codes: continue if not (c in costn_codes or (c[:1] in "TEC" and c[1:6].strip().isdigit())): continue row = _copy(r, BDQTY_MAP, have) row["BDCODE"], row["CODE"] = b2x.get(bd, bd), b2x.get(c, c) if row["CODE"][:1] in "BS": row |= {k: v for k, v in (("X_QTY1", r.get("QTY", 0)), ("X_HAL1", 100)) if k in have} row |= {k: 100 for k in ("X_HAL2", "X_HAL3") if k in have} if _is_t(r.get("T_EXIST", "")) and "EXIST1" in have: row["EXIST1"] = 1 out.append(row) return out def conv_prjlst(src: list[dict], pairs, have: set[str], dos: bool) -> list[dict]: out = [] for r in src: row = _copy(r, pairs, have) pc = int(float(r.get("PCLASS") or 0)) if not dos and "K_ORD" in have: sc = int(float(r.get("SCLASS") or 0)) if pc not in (6, 7) and not (pc == 4 and sc == 1): row["K_ORD"] = 0 out.append(row) return out def conv_boq(src: list[dict], pairs, have: set[str], b2x: dict, dos: bool) -> list[dict]: out = [] for r in src: row = _copy(r, pairs, have) bd = r.get("BDCODE", "").strip() if bd in b2x and "BDCODE1" in have: row["BDCODE1"] = b2x[bd] if _is_t(r.get("EXIST", "")) and "EXIST1" in have: row["EXIST1"] = 1 if not dos: row |= {k: r[k] for k in ("DES1", "DES2") if k in r and k in have} if "UNIT1" in r and "UNIT2" in have: row["UNIT2"] = r["UNIT1"] out.append(row) return out def _fields(b: bytes) -> set[str]: return set(DbfTable(b).fields) def convert(old: dict, template: dict, pname: str = "") -> tuple[dict, dict]: """구버전 폴더 파일(대문자 이름 → 바이트) + Win8 틀 → (새 files, 보고).""" old = {k.upper(): v for k, v in old.items()} ver = detect(old) if 130 <= ver <= 149: raise ValueError("이미 STmate 2Y.m 자료로 변환된 데이타입니다") if not (110 <= ver <= 129): raise ValueError(f"구버전 STmate 자료가 아닙니다(판 {ver})") dos = ver < 120 prj_src = _rows(_table(old, "PRJLST.DBF")) name = pname or (prj_src[0].get("PNAME", "") if prj_src else "") or "변환 공사" out = new_project(template, name) notes: list[str] = [] conld = b_list(_rows(_table(old, "CONLD.DBF"))) contd = b_list(_rows(_table(old, "CONTD.DBF"))) m_extra = ( {r.get("CODE", "").strip(): r for r in _rows(_table(old, "COSTN1.DBF"))} if dos else {} ) have = _fields(template["COSTN.DBF"]) costn_src = _rows(_table(old, "COSTN.DBF")) costn, b2x = conv_costn(costn_src, COSTN_DOS if dos else COSTN_7X, have, m_extra) out["COSTN.DBF"] = _build(template["COSTN.DBF"], costn) codes = {r.get("CODE", "").strip() for r in costn_src} bd_old = _table(old, "BDQTY.DBF") if old.get("BDQTY.DBF") and bd_old is None: notes.append("원본 BDQTY 암호 표 — 열쇠 미확인으로 구성 행 비움") if "BDQTY.DBF" in template: rows = conv_bdqty(_rows(bd_old), codes, b2x, set(DbfTable(template["BDQTY.DBF"]).fields)) rows += [ {"BDCODE": r.get("DCODE", "").strip(), "CODE": r.get("ICODE", "").strip()} for r in _rows(_table(old, "IMGINFO.DBF")) if r.get("DCODE", "").startswith("D") ] out["BDQTY.DBF"] = _build_bdqty(template["BDQTY.DBF"], rows) prj = conv_prjlst(prj_src, PRJ_DOS if dos else PRJ_7X, _fields(template["PRJLST.DBF"]), dos) pt = DbfTable(out["PRJLST.DBF"]) has_memo = "PRJ_MEMO" in pt.fields text = SixMemo(out.get("PRJLST.SMT")).read(pt.raw(0, "PRJ_MEMO")) if has_memo else b"" if prj: pt = DbfTable(_build(out["PRJLST.DBF"], prj)) if has_memo: # 틀 설정을 공사마다 따로(간접비 이관 미확인) memo = SixMemo(None) for i in range(len(pt)): pt.set_raw(i, "PRJ_MEMO", memo.write(BLANK_PTR, text)) out["PRJLST.SMT"] = memo.to_bytes() out["PRJLST.DBF"] = pt.to_bytes() if "KONGJONG.DBF" in template: if dos: kong = [{"CODE": f" {i}", "NAME": n} for i, n in enumerate(KONG_DOS, 1)] else: src_k = _rows(_table(old, "KONGJONG.DBF")) kong = [ {"CODE": str(i), "NAME": r.get("DESCRIP", ""), "SIZE": r.get("DESSIZE", "")} for i, r in enumerate(src_k, 1) ] out["KONGJONG.DBF"] = _build(template["KONGJONG.DBF"], kong) boq_tpl = template.get("BOQ11.DBF") for k in [k for k in out if re.match(r"^BOQ\d+\.DBF$", k)]: del out[k] boq_names = [] for r in prj_src if boq_tpl else []: pc = str(r.get("PCODE", "")).strip() src_b = _table(old, f"BOQ{pc}.DBF") if src_b is None: notes.append(f"BOQ{pc}.DBF 없음 — 건너뜀") continue rows = conv_boq(_rows(src_b), BOQ_DOS if dos else BOQ_7X, _fields(boq_tpl), b2x, dos) out[f"BOQ{pc}.DBF"] = _build(boq_tpl, rows) boq_names.append(f"BOQ{pc}.DBF") if not dos and _table(old, f"BQM{pc}.DBF") is not None: notes.append(f"BQM{pc}.DBF 있음 — 틀에 BQM 구조가 없어 옮기지 않음") if b2x: conld = ( "".join(b2x.get(conld[i : i + 6], conld[i : i + 6]) for i in range(0, len(conld), 6)) if not dos else conld ) contd = ( "".join(b2x.get(contd[i : i + 6], contd[i : i + 6]) for i in range(0, len(contd), 6)) if not dos else contd ) if dos: ini, n2 = ini_from_dos(old.get("START.MEM", b""), conld, contd) notes += n2 else: ini = ini_from_7x(old.get("START.INI", b""), conld, contd) out["STINI.MEM"] = ini if "MEMO.INI" in old: out["MEMO.INI"] = old["MEMO.INI"] out["START8X.INI"] = ini report = { "version": ver, "kind": "DOS" if dos else "7.x", "pname": name, "costn_count": len(costn), "bdqty_count": len(DbfTable(out["BDQTY.DBF"])) if "BDQTY.DBF" in out else 0, "prj_count": len(prj), "boq_tables": boq_names, "b_to_x": b2x, "notes": notes, "unmapped": UNMAPPED, } return out, report