feat(stmate-test): REA MCP(Ghidra) 변경 차수·잔여 재검증 — 변경 4차 = 계열 5 · 할증 계수 4바이트 실수 · 손료 계수 칸 걷음 · 강제금액 = JDL 변환 · JDL 고정폭 형식
- 변경 차수 상한 4차(K_ORD>4 거부) · 4차 = 5번째 칸(MCOST5·XX5NM·QTY5) - 할증 계수 float32 — 70%·115% 1원 적음 - 손료 E0~E3 = 조달청 XML 구분값뿐 · 계수 곱 없음 - jdl_format.py — 13종 자르는 자리 · 단가 목록 8종 열 이름 · 줄 만들기·읽기·검사 - 지식DB 34번 §17 · REA_MCP_보강 근거 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""조달청 JDL TEXT 묶음 — 고정폭 줄 형식(자르기 자리) · 열 개수 검사 · 줄 만들기/읽기
|
||||
|
||||
근거: 지식DB STmate/REA 분석/34_변경차수_잔여계산_작업자료_20261114 「REA MCP 보강」
|
||||
JDLCHK.Tab_33.dll(sha256 c5052736…) 파일마다 검사 함수
|
||||
— 줄을 바이트 자리 표(0x405dd8 SetLength 뒤 dword 표)로 잘라(0x5051e0) 열 배열 → 열 개수 비교(0x405bfc 뒤 cmp)
|
||||
— 열 → 필드 대입(FieldByName · TFieldDefs.Add 0x491dec 순서 · 크기)
|
||||
M.Txt 0x4ea818(Ghidra 확인) · L 0x4ecfab · E 0x4ee03f · W 0x4ef503 · J 0x4f09c8 · Unit 0x4f3885 · JUnit 0x4f4e79
|
||||
Danga 0x4f649c · GongJong 0x4f2788 · MainPart 0x4f7db4 · UnitPart 0x4fad23 · JPart 0x4fe0f4 · Main 0x4ea305
|
||||
자리는 cp949 바이트 기준 · 마지막 열 = 남은 전부 · F_ORICODE·F_MEMO 는 줄에 없음(검사기가 따로 채움)
|
||||
열 이름이 확인된 표 = 단가 목록 8종(M·L·E·W·J·Unit·JUnit·Danga) · 나머지(MainPart·UnitPart·JPart·GongJong·Main)는
|
||||
자르기 자리·열 개수만(열 이름 짝 조건부)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
CUTS: dict[str, list[int]] = {
|
||||
"Main.Txt": [11, 91],
|
||||
"M.Txt": [12, 42, 72, 78, 90, 102, 114, 120, 132, 138, 150, 156, 168, 174, 186, 192, 204, 210],
|
||||
"L.Txt": [12, 42, 72, 78, 90, 102],
|
||||
"E.Txt": [12, 42, 72, 78, 90, 102, 114, 126],
|
||||
"W.Txt": [12, 42, 72, 78, 90, 102, 114, 126],
|
||||
"J.Txt": [12, 42, 72, 78, 90, 102, 114, 126],
|
||||
"GongJong.Txt": [13, 43, 47],
|
||||
"Unit.Txt": [12, 42, 72, 78, 90, 102, 114, 126, 130],
|
||||
"JUnit.Txt": [12, 42, 72, 78, 90, 102, 114, 126, 130],
|
||||
"Danga.Txt": [12, 42, 72, 78, 90, 102, 114, 126, 130],
|
||||
"MainPart.Txt": [
|
||||
13,
|
||||
24,
|
||||
54,
|
||||
84,
|
||||
90,
|
||||
100,
|
||||
112,
|
||||
124,
|
||||
136,
|
||||
148,
|
||||
150,
|
||||
151,
|
||||
152,
|
||||
162,
|
||||
163,
|
||||
175,
|
||||
176,
|
||||
188,
|
||||
198,
|
||||
210,
|
||||
220,
|
||||
232,
|
||||
242,
|
||||
254,
|
||||
264,
|
||||
276,
|
||||
286,
|
||||
298,
|
||||
308,
|
||||
318,
|
||||
],
|
||||
"UnitPart.Txt": [
|
||||
12,
|
||||
23,
|
||||
53,
|
||||
83,
|
||||
89,
|
||||
99,
|
||||
111,
|
||||
123,
|
||||
135,
|
||||
147,
|
||||
149,
|
||||
150,
|
||||
151,
|
||||
161,
|
||||
162,
|
||||
172,
|
||||
184,
|
||||
194,
|
||||
206,
|
||||
216,
|
||||
228,
|
||||
238,
|
||||
250,
|
||||
260,
|
||||
272,
|
||||
282,
|
||||
292,
|
||||
],
|
||||
"JPart.Txt": [
|
||||
12,
|
||||
23,
|
||||
34,
|
||||
64,
|
||||
94,
|
||||
100,
|
||||
110,
|
||||
122,
|
||||
134,
|
||||
146,
|
||||
158,
|
||||
160,
|
||||
161,
|
||||
162,
|
||||
168,
|
||||
169,
|
||||
173,
|
||||
177,
|
||||
],
|
||||
}
|
||||
|
||||
_PRICE = ["F_CODE", "F_DES", "F_SIZE", "F_UNIT", "F_MPRICE", "F_LPRICE", "F_EPRICE", "F_SCODE"]
|
||||
COLUMNS: dict[str, list[str]] = {
|
||||
"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"],
|
||||
}
|
||||
# 묶음 파일 차례(검사기 파일 목록 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 widths(name: str) -> list[int | None]:
|
||||
"""열 너비(바이트) — 마지막 열 = None(남은 전부)."""
|
||||
c = [0] + CUTS[name]
|
||||
return [c[i + 1] - c[i] for i in range(len(c) - 1)] + [None]
|
||||
|
||||
|
||||
def split_line(name: str, line: bytes | str) -> list[str]:
|
||||
"""한 줄을 자르기 자리로 나눔(cp949 바이트) · 각 열 양끝 빈칸 걷음."""
|
||||
b = line.encode("cp949") if isinstance(line, str) else line
|
||||
b = b.rstrip(b"\r\n")
|
||||
# 줄이 자르기 자리보다 짧으면 그 뒤 열은 생기지 않음(열 개수 오류로 걸림 — 조건부)
|
||||
c = [0] + [x for x in CUTS[name] if x < len(b)] + [len(b)]
|
||||
return [b[c[i] : c[i + 1]].decode("cp949", "replace").strip() for i in range(len(c) - 1)]
|
||||
return out
|
||||
|
||||
|
||||
def build_line(name: str, values: list | dict) -> bytes:
|
||||
"""열 값 → 고정폭 줄(cp949) · 넘치면 자름(한글 반쪽 바이트는 빈칸으로) · dict 면 COLUMNS 이름 차례."""
|
||||
if isinstance(values, dict):
|
||||
values = [values.get(k, "") for k in COLUMNS[name]]
|
||||
ws = widths(name)
|
||||
if len(values) != len(ws):
|
||||
raise ValueError(f"{name} 열 {len(ws)}개 필요 — {len(values)}개")
|
||||
out = b""
|
||||
for v, w in zip(values, ws):
|
||||
s = "" if v is None else (f"{v}" if not isinstance(v, float) else f"{v:g}")
|
||||
b = s.encode("cp949")
|
||||
if w is not None:
|
||||
if len(b) > w:
|
||||
b = b[:w]
|
||||
while True:
|
||||
try:
|
||||
b.decode("cp949")
|
||||
break
|
||||
except UnicodeDecodeError:
|
||||
b = b[:-1]
|
||||
b = b.ljust(w, b" ")
|
||||
out += b
|
||||
return out
|
||||
|
||||
|
||||
def check_lines(name: str, text: bytes) -> list[str]:
|
||||
"""검사기 꼴 — 줄마다 열 개수(자르기 자리 + 1) · F_CODE 글자(A~Z 0~9 - _ / ^)."""
|
||||
errs = []
|
||||
for no, raw in enumerate(text.splitlines(), 1):
|
||||
if not raw.strip():
|
||||
continue
|
||||
cols = split_line(name, raw)
|
||||
if len(cols) != len(CUTS[name]) + 1:
|
||||
errs.append(f"{name} - Line : {no} 열 개수 {len(cols)}")
|
||||
if name in COLUMNS and COLUMNS[name][0] == "F_CODE":
|
||||
bad = set(cols[0]) - CODE_CHARS
|
||||
if bad:
|
||||
errs.append(f"{name} - Line : {no} 코드 글자 {''.join(sorted(bad))}")
|
||||
return errs
|
||||
Reference in New Issue
Block a user