Files
Aislo/tools/stmate-test/stc_read.py
T
eomsangdonandClaude Opus 5.5 e987602f24 feat(stmate-test): 무손실 저장 — 손대지 않은 저장 = 원본 STC 바이트 동일 · 고친 칸만 다시 씀
- 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>
2026-11-13 18:42:04 +09:00

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")),
}