# -*- 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