From 32043f3c012a3cdba8d5a36dadcd7f8d51fef778 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 10 Nov 2026 06:40:12 +0900 Subject: [PATCH] fix(est-test): reject duplicate JDL tables --- resources/tester/test_est_jdl_special.py | 14 ++++++++++++++ tools/est-test/PLAN.md | 6 ++++++ tools/est-test/README.md | 1 + tools/est-test/jdl.cjs | 7 ++++++- 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/resources/tester/test_est_jdl_special.py b/resources/tester/test_est_jdl_special.py index 65e498f80..f299c67cd 100644 --- a/resources/tester/test_est_jdl_special.py +++ b/resources/tester/test_est_jdl_special.py @@ -181,3 +181,17 @@ def test_jdl_estimate_preserves_components_when_total_matches(): ) assert result["GC"] == 0 assert result["NC"] == 20 + + +def test_jdl_rejects_case_insensitive_duplicate_tables(): + project = json.loads((ROOT / "tools/est-test/sample.json").read_text(encoding="utf-8")) + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'),a=require('./tools/est-test/archive.cjs'); + const p=JSON.parse(fs.readFileSync(0,'utf8')); + const files=new Map([['Global.ini',Buffer.from('[BASIC]')],['Main.txt',Buffer.from('0\\tfirst\\t02\\x05\\r\\n')], + ['MAIN.TXT',Buffer.from('0\\tsecond\\t02\\x05\\r\\n')]]); + try { j.read(a.pack(files),p); process.stdout.write('accepted'); } + catch(error) { process.stdout.write(error.message); }""" + result = subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT, text=False + ).decode("utf-8") + assert "중복" in result diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 67c763d4d..845435a9b 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -423,6 +423,9 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - 백그라운드 상태 감시 및 자동 알림: `tools/est-test/monitor.cjs`를 통해 15초 주기로 `orca.exe` CLI(`shell: false`, timeout 4~5s)를 호출하여 CODEX 워크트리의 안티그래비티 작업 상태와 EST 5175 서버 헬스를 조회하고, 상태 변경 시에만 `monitor.log`에 기록(최대 512KB 순환). 안티그래비티 완료/대기 전이 시 Codex Orchestration Run(`run_cdb1558b9280`)으로 단발성 자동 알림(`status`/`messageId=msg_2c7bca748f91`)이 실제 전달됨을 확인(중복 폭주 방지 플래그 적용, 민감 환경변수 미기록, 재시작/자동지시 없음). ## Codex · EST 독립 테스트 앱 (2026-11-04) +- [x] JDL 파일에 대소문자만 다른 중복 표가 있으면 가져오기를 중단하고 정상 파일 호환성을 확인한다. + - 자체 검증: `Main.txt`와 `MAIN.TXT`의 서로 다른 내용 중 하나가 조용히 선택되던 문제 재현 후 중복 오류 확인. JDL·업무 시험 50개 통과. + - [x] JDL 내역 출력 시 총액이 같아도 비목 금액이 다르면 저장 금액을 보존하고 왕복을 검증한다. - 자체 검증: 총액은 20으로 같지만 단가가 재료비 20·저장 금액이 노무비 20인 행에서 기존에 재료비로 잘못 출력되는 실패를 재현. 수정 후 노무비 20을 유지하고 JDL·업무 회귀 49개 통과. 설치 다리공사 파일 132개 내역을 JDL 왕복하여 재료비 2,129,366,589·노무비 4,411,679,576·경비 1,448,805,997원 각각 유지. JDL 검사 DLL 인증은 미확인. - 추가 검증: 사용자 지정 임도·통영 ESTX도 JDL 왕복 시 내역 73·91건과 재료비·노무비·경비 각각 일치. 임도 원본 산식 오류 1건은 그대로 남아 있어 오류 없는 자료로 판정하지 않음. @@ -518,6 +521,7 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - [ ] 설치된 JDL 검사 DLL의 호출 규약과 결과를 조사해 생성 파일 검사에 활용한다. - 진행: 32비트 DLL의 `GetDLLVersion` 호출은 1.03을 반환. `IsCertifiedJDL`은 디렉터리 인자를 받는 경로로 확인했으며, 기본 내보내기 파일을 임시 폴더에 풀어 호출한 결과 하위 바이트 0. `JDLCheck`는 한 문자열 인자의 stdcall 경로로 확인했으나 임시 자료 호출이 15초 안에 끝나지 않아 별도 프로세스를 종료했다. 반환 의미와 화면 없이 완료하는 방법은 미확인이며 인증 실패나 검사 성공으로 단정하지 않는다. - 추가 조사(2026-11-10): DLL의 `JDLCheck`(0x4e4ae8)는 검사 시작 시 `GetJDLAnnouncement`(0x4e58f8)를 호출하며, 이 함수에 `http://www.g2b.go.kr:8081/emptyFile/jdldll.html` 주소가 들어 있다. 해당 주소는 이 PC에서 5초 제한 요청 시 시간 초과됐다. 이전 검사 지연의 유력 원인이지만 호출 중단 위치를 확인하지 못해 확정하지 않는다. 원본 DLL 수정이나 우회 없이 검사 결과를 얻는 방법은 미확인. + - 인증 구분: `IsCertifiedJDL`(0x4e5628)은 대상 폴더의 `Global.Ini`에서 `[BASIC]`의 `MAKER`·`VERSION`과 `[CERTIFICATION]`의 `KEY`·`DLL_VERSION`을 읽고 길이 16인 키를 내부 계산 결과와 비교하는 경로가 확인된다(0x4e5727~0x4e57f8). 현재 생성 파일에는 인증 섹션이 없으므로, 검사 함수가 파일을 검사하는 것과 인증 키 검증은 구분한다. 키 생성 조건·인가 및 원본 검사기의 실제 판정은 확인하지 못했다. - [x] 기존 수량산출서 이름을 원본 제목 표에 저장한다. 기존 문서·목록·인덱스 보존과 실제 파일 재읽기를 검증한다. - 자체 검증: 설치 main.estx에서 이름 변경 후 재읽기와 문서·목록·인덱스 불변 확인. 제목 표가 따로 수정된 충돌을 거부하는 시험과 숨겨진 연결 보호 시험 통과. 원본 EST 실행 검증은 하지 못함. - [x] 원본 수량 목록이 비어 있는 기존 산출서에도 항목 추가를 지원하고 실제 파일로 재읽기·기존 서식 보존을 검증한다. @@ -801,6 +805,7 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - [x] 원본 결과와 기존 계산이 일치하는 수량산출서의 결과 표지·수량 목록 재계산 저장. - 자체 검증(2026-11-06): main.estx 수량산출서 폭 9→10 변경 시 표지 및 목록 수량 104→116으로 기록·재읽기, 다른 문서·인덱스·CAD 보존. EST 전용 102개 통과(70.78초). 계산 오류·기존 결과 불일치 문서는 저장을 거부하며 원본 EST 실행 검증은 미실시. - [ ] 서식 구간을 넘는 수량산출서 편집과 목록·연결 변경 지원; 계산 오류나 기존 결과 불일치 문서의 처리 확인. + - 추가 조사(2026-11-10): 설치 프로젝트 ESTX 24개와 기술정보 DB의 ESTX에서 `MulCode`와 `MullangIndex`가 함께 있는 내역을 검색했으나 발견하지 못했다. 신규 수량 연결의 원본 저장은 실제 연결 자료 없이 임의 확정하지 않는다. - 진행 및 미완료 범위(2026-11-08): 1. 서식 편집 지원: 수량산출서 편집기에서 위첨자(`^(2)` 등) 및 복합 서식 태그 경계를 가로지르는 텍스트 수정(`abc^(2)def` -> `abc^(3)xyz`) 시 분할된 서식 런을 유지하며 인라인 패치 적용 확인 (`test_quantity_document_edit_across_styles_and_superscript_preserves_structure` 통과). 2. 불일치 문서 처리: 원본 저장 수량과 산식 계산 결과가 다른 불일치 문서(예: `N0000000083` 원본 286 vs 산식 100.1)의 경우, 품명/규격/단위(Unite) 편집 시 기존 원본 수량(286)을 보존해 ESTX 패치를 허용하며, 산식을 수정해 결과가 변경되는 경우 "수량산출서 원본 결과와 재계산 값이 다릅니다. JSON으로 저장해 확인하세요"로 ESTX 덮어쓰기를 안전 차단함을 확인. @@ -862,6 +867,7 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD 2. 무결성 검증 제약: `exportEstx`는 동일 행 수 및 비인덱스 필드(수치, 일반 텍스트, BLOB 데이터 등)의 수정 저장을 안전하게 지원하지만, 행 추가/삭제나 인덱스 키(`PkID`, `Code`, `Index`, `RecNo` 등)를 변경하면 `.idx`의 B-Tree 구조와 불일치가 발생함. 원본 Delphi/DBISAM 엔진 외부에서 인덱스 재생성을 구현할 수 있는 바이너리 규격이나 소스 근거가 부재함. 3. 구현 범위 및 처리: 따라서 ESTX 내 행 추가/삭제 및 인덱스 키 변경은 기존처럼 명확한 예외 메시지(`{key}: 행 추가/삭제는 인덱스 재생성이 필요합니다`, `{key}.{name}: 인덱스 키 변경은 지원하지 않습니다`)로 안전 차단하고, 신규 파일/행 증감/자유로운 편집이 필요한 작업은 JSON 저장소(`project.tables`)에서 완전 무결하게 지원·보존하도록 범위를 명확히 유지함. - [ ] 남은 완전 호환 범위: ESTX 신규 파일/행 증감·키 변경의 인덱스 재생성, WPTools 편집 저장, 기성/연차 원본 별도 업무 파일; JDL 확장 산식/기성·물가 자료와 원본 검사 DLL 인증; 원본과 같은 인쇄 도형/배치 및 기존 산식 잔여 오류. CAD만 제외한 전체 완료로 판정하지 않음. + - 추가 조사(2026-11-10): 설치 프로젝트와 기술정보 DB의 ESTX 압축 항목을 확인했으나 REA가 기성 파일 표로 지목한 `AccomMainTable.Dat` 또는 연차 관련 이름의 파일은 찾지 못했다. 웹앱 자체 기성·연차 저장과 별도로 원본 업무 파일의 필드·저장 검증은 계속 미확인이다. - [x] 전용 회귀·포맷·700줄 제한·커밋/push 마무리. 검증 서버 종료 완료. - 자체 검증: EST 전용 전체 99 통과(48.88초). 변경 파일만 prettier/ruff format, git diff --check와 모든 소스 700줄 이하 확인. a5157dd3a 커밋을 CODEX에 push, origin/CODEX와 차이 0·작업 트리 깨끗함 확인. 검증 서버 5175 종료, 이번 일회 UI/분석 스크립트·출력물·전용 시험 저장본 정리. 기존 원본 자료와 앱 data는 수정하지 않음. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 7d83c1eab..049a4f4f4 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -46,6 +46,7 @@ - **JDL 기본 표**: 원본 검사 DLL의 필드 정의와 EST 처리기의 TAB/ENQ·CP949 표 구조에 따라 ZIP 안의 Global.ini·Main.txt·자원/공종/내역/원가 표를 생성·읽습니다. 일위대가 상세는 기본 플래그와 연결 자원이 확인된 행을 불러오고 미변환 구성은 저장 금액으로 보존합니다. 기계경비와 단가산출은 저장된 비목별 금액을 편집 가능한 호표로 가져오며 상세 산식은 원본 JDL에 보존합니다. 자재·노무로 구성된 기본 일위대가는 상세표로 내보냅니다. 가져온 작업을 변경하지 않았다면 원본 JDL을 바이트 그대로 내보내 확장 자료를 보존합니다. 변경된 작업의 특수 일위·산근·기계는 저장 금액의 고정 단가로 전개하며 제외 행은 생략 사실을 알립니다. 확장 산식·기성/물가 파일의 완전 변환과 원본 검사 DLL 인증은 미확인입니다. - 중복 자원 코드로 연결 대상을 구분할 수 없는 JDL 출력은 해당 내역의 저장 금액을 독립 단가로 기록하고 경고합니다. 자원 행 자체는 각각 보존합니다. - 내역의 퍼센트 수량을 반영하며, 단가로 계산한 비목별 금액이 저장 금액과 다르면 저장 금액을 독립 항목으로 보존합니다. +- 새로 생성한 JDL에는 원본 검사기의 인증 키가 포함되지 않습니다. 원본 검사기의 인증 판정과 파일 내용 검사는 서로 별개이며, 검사 결과는 확인하지 못했습니다. - JDL 기계 상세의 수량과 단가를 곱한 금액이 비목별 저장 합계와 맞는 경우 각 상세 금액을 편집할 수 있습니다. 상세 산식과 원천 단가 연결은 원본에 보존하며, 수량 해석이 모호하면 합계만 가져옵니다. - 가져온 JDL 기계 상세의 편집 금액과 호표 합계가 맞으면 JDL로 다시 저장할 때 각 상세를 고정 단가로 기록합니다. 원래의 상세 산식과 수량 관계는 복원하지 않습니다. - 일위대가에 연결된 기계·실적·견적·단가산출 자료도 일반 상세 조건을 만족하면 JDL로 출력합니다. 기계 상세가 변환되지 않거나 그 외 복합 원천은 고정 단가로 전개합니다. diff --git a/tools/est-test/jdl.cjs b/tools/est-test/jdl.cjs index 97de8be89..8ddca792a 100644 --- a/tools/est-test/jdl.cjs +++ b/tools/est-test/jdl.cjs @@ -297,7 +297,12 @@ function output(project) { return { bytes: pack(files), warnings }; } function read(bytes, project) { - const files = new Map([...unpack(bytes)].map(([k, v]) => [k.toLowerCase(), v])); + const files = new Map(); + for (const [name, data] of unpack(bytes)) { + const key = name.toLowerCase(); + if (files.has(key)) throw Error(`JDL 파일명 중복: ${name}`); + files.set(key, data); + } if (!files.has("global.ini") || !files.has("main.txt")) throw Error("Global.ini/Main.txt 없는 JDL입니다."); const main = decoder