# -*- coding: utf-8 -*- """조달청 JDL TEXT 묶음(JDL VERSION 3.3) — 줄 형식 · 열 이름 · 검사기 열 나눔 · 줄 만들기. 근거(지식DB STmate/REA 분석/37_파일입출력_잔여_작업자료_20261114 · 34 §17.9 바로잡음): 검사기 JDLCHK.Tab_33.dll(sha256 c5052736…) · 0x5051e0 = 고정폭 자리에 탭 넣기 — 0x505218 jmp 가 고리를 건너뜀(죽은 표) → 줄은 손대지 않음 · 0x504d20 = 줄을 첫 \\x05 에서 둘로 — 앞쪽 + " " 를 탭(#9)으로 나눠 열마다 Trim(≤#32 걷음) = 열 배열, 뒤쪽 + " " 를 \\x07 로 나눔(STmate 덧붙임 칸) · 열 개수 = 앞쪽 배열 길이(0x405bfc 뒤 cmp) · 열 → 필드: 단가 목록 8종 FieldDefs 차례(M 0x4ea818) · MainPart 0x4f7890 · UnitPart 0x4fa7fc · JPart 0x4fdd10 만드는 쪽 ST_U_Convert.CPM Conv_ST8_Jodal 0x4b9f2c → 코드 줄 0x4b0904(FUN_004a7cf0 = 값 TrimRight · 숫자는 FloatToStr+EXTTOSTR · dl=1 이면 뒤에 탭) · 덧붙임 칸 앞 \\x05 · 사이 \\x07 """ from __future__ import annotations from decimal import Decimal EXT = "\x05" # 열 끝 · STmate 덧붙임 칸 시작 SUB = "\x07" # 덧붙임 칸 사이 TAB = "\t" # 검사기 열 개수(0x405bfc 뒤 cmp) · 그 밖 오류 번호는 L.Txt 203002 만 확인 COUNTS: dict[str, int] = { "Main.Txt": 3, "M.Txt": 19, "L.Txt": 7, "E.Txt": 9, "W.Txt": 9, "J.Txt": 9, "GongJong.Txt": 4, "Unit.Txt": 10, "JUnit.Txt": 10, "Danga.Txt": 10, "MainPart.Txt": 31, "UnitPart.Txt": 28, "JPart.Txt": 19, } ERR_CODE = {"L.Txt": "203002"} _PRICE = ["F_CODE", "F_DES", "F_SIZE", "F_UNIT", "F_MPRICE", "F_LPRICE", "F_EPRICE", "F_SCODE"] _LINK = [f"F_{k}{i}" for i in range(1, 6) for k in ("LCODE", "LQTY")] # 열 차례 → 필드 · None = 검사기가 읽지 않거나 짝 미확인 COLUMNS: dict[str, list[str | None]] = { "M.Txt": ["F_CODE", "F_DES", "F_SIZE", "F_UNIT", "F_MPRICE", "F_SCODE"] + [f"F_{k}{i}" for i in range(1, 7) for k in ("PRICE", "PAGE")] + ["F_BIGO"], "L.Txt": ["F_CODE", "F_DES", "F_SIZE", "F_UNIT", "F_LPRICE", "F_SCODE", "F_BIGO"], "E.Txt": _PRICE + ["F_BIGO"], "W.Txt": _PRICE + ["F_BIGO"], "J.Txt": _PRICE + ["F_BIGO"], "Unit.Txt": _PRICE + ["F_TFNO", "F_BIGO"], "JUnit.Txt": _PRICE + ["F_TFNO", "F_BIGO"], "Danga.Txt": _PRICE + ["F_TFNO", "F_BIGO"], # 세부 3종(Ghidra 0x4f7890 · 0x4fa7fc · 0x4fdd10) — F_CODE1 = 앞 열 묶음 + 1열(조건부) "MainPart.Txt": [ "F_CODE", "F_CODE1", "F_DES", "F_SIZE", "F_UNIT", "F_QQTY", "F_MPRICE", "F_LPRICE", ] + ["F_EPRICE", None, "F_SPROPER", "F_LOADBM", "F_SAVEBM", "F_SRATE", "F_KWAN", "F_SCOST"] + ["F_SGUBUN", "F_SCODE", "F_SANQTY"] + _LINK + ["F_LRATE", "F_SAN"], "UnitPart.Txt": [ "F_CODE", "F_CODE1", "F_DES", "F_SIZE", "F_UNIT", "F_QQTY", "F_MPRICE", "F_LPRICE", ] + ["F_EPRICE", None, "F_SPROPER", "F_LOADBM", "F_SAVEBM", "F_SRATE", "F_KWAN", "F_SANQTY"] + _LINK + ["F_LRATE", "F_SAN"], "JPart.Txt": ["F_CODE", "F_CODE1", None, "F_DES", "F_SIZE", "F_UNIT", "F_QQTY", "F_MPRICE"] + ["F_LPRICE", "F_EPRICE", None, "F_SPROPER", "F_LOADBM", "F_SAVEBM", "F_SRATE", "F_KWAN"] + [None, None, None], } # 검사기가 따로 채우는 필드 — F_SEQ(줄 번호 10자리 0 채움) · F_ORICODE(0열) · F_CLASS(MainPart·UnitPart) # 묶음 파일 차례(검사기 파일 목록 0x4e6a20~0x4e6b58) · 설정 = Global.Ini FILES = [ "M.Txt", "L.Txt", "E.Txt", "W.Txt", "J.Txt", "GongJong.Txt", "Unit.Txt", "Danga.Txt", "MainPart.Txt", "UnitPart.Txt", "JPart.Txt", "Main.Txt", "JUnit.Txt", "CostAccount.Txt", ] CODE_CHARS = set("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_/^") def dtrim(s: str) -> str: """Delphi Trim — 양끝 #0~#32 걷음.""" i, j = 0, len(s) while i < j and ord(s[i]) <= 32: i += 1 while j > i and ord(s[j - 1]) <= 32: j -= 1 return s[i:j] def num_text(v) -> str: """FloatToStr(유효 15자리) → EXTTOSTR(DP_Common 0x453e4c · 지수꼴을 풀어 씀).""" x = float(v or 0) if x == 0: return "0" d = Decimal(f"{x:.15g}") s = format(d, "f") if "." in s: s = s.rstrip("0").rstrip(".") return s class Raw(str): """값 글 손질(TrimRight) 없이 그대로 넣는 조각(낱글 리터럴).""" def val_text(v) -> str: """FUN_004a7cf0 값 글 — 숫자는 num_text · 참거짓은 Delphi 6 Variant 글(조건부) · 글은 TrimRight.""" if isinstance(v, Raw): return str(v) if isinstance(v, bool): return "True" if v else "False" if isinstance(v, (int, float)): return num_text(v) return ("" if v is None else str(v)).rstrip(" ") def split_line(line: bytes | str) -> tuple[list[str], list[str]]: """검사기 0x504d20 꼴 — (열 배열, 덧붙임 칸 배열).""" s = line.decode("cp949", "replace") if isinstance(line, bytes) else line s = s.rstrip("\r\n") p = s.find(EXT) left, right = (s, "") if p < 0 else (s[:p], s[p + 1 :]) def cut(t: str, sep: str) -> list[str]: t += " " out = [] while t: k = t.find(sep) if k < 0: k = len(t) - 1 # 덧붙인 " " 앞까지 out.append(dtrim(t[:k])) break out.append(dtrim(t[:k])) t = t[k + 1 :] return out return cut(left, TAB), (cut(right, SUB) if p >= 0 else []) def build_line(cols: list, ext: list | None = None) -> bytes: """열 → 탭 줄 · ext 가 있으면 \\x05 뒤 \\x07 사이 · cp949.""" s = TAB.join(val_text(c) for c in cols) if ext is not None: s += EXT + SUB.join(val_text(c) for c in ext) return s.encode("cp949", "replace") def columns_of(name: str, line: bytes | str) -> dict: """열 배열 → {필드: 값}(이름 모르는 열은 #n).""" cols, ext = split_line(line) names = COLUMNS.get(name, []) out = {(names[i] if i < len(names) and names[i] else f"#{i}"): v for i, v in enumerate(cols)} if ext: out["_EXT"] = ext return out def check_lines(name: str, text: bytes) -> list[str]: """검사기 꼴 — 줄마다 열 개수 · 0열 코드 글자(A~Z 0~9 - _ / ^).""" errs = [] want = COUNTS.get(name) for no, raw in enumerate(text.splitlines(), 1): if not raw.strip(): continue cols, _ = split_line(raw) if want is not None and len(cols) != want: code = ERR_CODE.get(name, "") errs.append(f"{code} {name} - Line : {no} 열 개수 {len(cols)}(필요 {want})".strip()) names = COLUMNS.get(name) if names and names[0] == "F_CODE" and cols: bad = set(cols[0]) - CODE_CHARS if bad: errs.append(f"{name} - Line : {no} 코드 글자 {''.join(sorted(bad))}") return errs