185 lines
6.8 KiB
Python
185 lines
6.8 KiB
Python
# -*- 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
|