- DBF 고친 칸만 다시 씀(한글 깨짐 · 2칸을 1칸으로 · 3칸 87.745% 덮어쓰기 없앰) · 단가 → 내역 행 → 소계 → 직공비 다시 셈 · 행 끼우기·지우기 - SIxMemo(COSTN.SMT · PRJLST.SMT) 블록 구조 유지 · 넘치면 뒤에 붙이고 가리키는 값 갱신 - STC 다시 묶기(같은 파일은 원본 바이트) · 검사 · 자동 백업·복원 · 새 공사 · 복사등록(차수 인계) · 해당분 압축 - BDQTY 역변환(쓰기 준비) · 모듈 dbf_rw · dbf_memo · dbf_crypt · stc_archive · stc_save · stc_read · stc_project · api_file · scr_file - 지킴 시험 test_stmate_file_roundtrip · file_project · file_payload — 실무 STC 7건 왕복 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
3.9 KiB
Python
109 lines
3.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""STMATE 프로젝트 원천 읽기 — STC/폴더 → (대문자 이름 → 바이트) · 보조 표(KONGJONG·COSTA·QCODE) · 설정 전수.
|
|
|
|
근거: STC_컨테이너_구조.md §2·§5 · 02_데이터셋과_파일형식.md §3 · 25_설정키_전수와_식템플릿.md §1 ·
|
|
20_설정파일과_보고서_카탈로그.md §2(Start8x.Ini)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
|
|
from dbf_memo import SixMemo, ini_items
|
|
from dbf_rw import DbfTable
|
|
from stc_archive import files_of, read_entries
|
|
|
|
|
|
class Source:
|
|
"""원천 한 곳 — files(대문자 이름 → 바이트) · names(대문자 → 원래 이름) · entries(STC 머리 값)."""
|
|
|
|
def __init__(self, path: str):
|
|
p = pathlib.Path(path)
|
|
if not p.exists():
|
|
raise FileNotFoundError(path)
|
|
self.path = p
|
|
self.entries: list[dict] = []
|
|
self.names: dict[str, str] = {}
|
|
if p.is_file() and p.suffix.upper() == ".STC":
|
|
self.entries = read_entries(p.read_bytes())
|
|
self.files = files_of(self.entries)
|
|
self.names = {pathlib.Path(e["name"]).name.upper(): e["name"] for e in self.entries}
|
|
elif p.is_dir():
|
|
self.files = {}
|
|
for fp in sorted(p.iterdir()):
|
|
if fp.is_file():
|
|
self.files[fp.name.upper()] = fp.read_bytes()
|
|
self.names[fp.name.upper()] = fp.name
|
|
else:
|
|
raise ValueError("지원하지 않는 경로 형식")
|
|
self.kind = "stc" if self.entries else "folder"
|
|
|
|
|
|
def table_rows(b: bytes) -> list[dict]:
|
|
"""평문 DBF 행(삭제 제외) — 암호 표(0x06)는 빈 목록."""
|
|
if not b or b[0] == 0x06:
|
|
return []
|
|
t = DbfTable(b)
|
|
return [dict(t.row(i), _recno=i) for i in range(len(t)) if not t.deleted(i)]
|
|
|
|
|
|
def ini_sections(text: str) -> dict[str, dict[str, str]]:
|
|
out: dict[str, dict[str, str]] = {}
|
|
for sec, k, v in ini_items(text):
|
|
out.setdefault(sec, {})[k] = v
|
|
return out
|
|
|
|
|
|
def prjlst_smt_all(files: dict[str, bytes]) -> list[dict]:
|
|
"""PRJLST 행마다 PRJ_MEMO 본문 전체(섹션별 키=값) — PCODE 로 BOQ1n 과 짝."""
|
|
if "PRJLST.DBF" not in files:
|
|
return []
|
|
t = DbfTable(files["PRJLST.DBF"])
|
|
memo = SixMemo(files.get("PRJLST.SMT"))
|
|
out = []
|
|
for i in range(len(t)):
|
|
ptr = t.raw(i, "PRJ_MEMO") if "PRJ_MEMO" in t.fields else b""
|
|
text = memo.read(ptr).decode("cp949", "replace")
|
|
out.append(
|
|
{
|
|
"recno": i,
|
|
"pcode": t.get(i, "PCODE") if "PCODE" in t.fields else "",
|
|
"sections": ini_sections(text),
|
|
"key_count": len(ini_items(text)),
|
|
}
|
|
)
|
|
return out
|
|
|
|
|
|
def qcode_rows(files: dict[str, bytes]) -> list[dict]:
|
|
"""QCODE.DBF 행 + IFORM·XCODE 메모 본문(QCODE.SMT)."""
|
|
b = files.get("QCODE.DBF")
|
|
if not b:
|
|
return []
|
|
t = DbfTable(b)
|
|
memo = SixMemo(files.get("QCODE.SMT"))
|
|
rows = []
|
|
for i in range(len(t)):
|
|
if t.deleted(i):
|
|
continue
|
|
r = dict(t.row(i), _recno=i)
|
|
for f in ("IFORM", "XCODE"):
|
|
if f in t.fields:
|
|
r[f] = memo.read(t.raw(i, f)).decode("cp949", "replace")
|
|
rows.append(r)
|
|
return rows
|
|
|
|
|
|
def extra_tables(files: dict[str, bytes]) -> dict:
|
|
"""api_load 덧붙임 — KONGJONG 공종 · COSTA(암호면 건수만) · QCODE · PRJLST.SMT 전수 · Start8x.Ini 섹션."""
|
|
costa = files.get("COSTA.DBF", b"")
|
|
ini = files.get("START8X.INI") or files.get("STINI.MEM") or b""
|
|
return {
|
|
"kongjong_rows": table_rows(files.get("KONGJONG.DBF", b"")),
|
|
"costa_rows": table_rows(costa),
|
|
"costa_count": len(DbfTable(costa)) if costa else 0,
|
|
"qcode_rows": qcode_rows(files),
|
|
"prjlst_smt_all": prjlst_smt_all(files),
|
|
"start_ini_sections": ini_sections(ini.decode("cp949", "replace")),
|
|
}
|