From 5520e3c5759b8948e858059bf77ce7ef3af42ce0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 10 Nov 2026 06:33:45 +0900 Subject: [PATCH] fix(est-test): preserve JDL cost components --- resources/tester/test_est_jdl_special.py | 18 ++++++++++++++++++ tools/est-test/PLAN.md | 4 ++++ tools/est-test/README.md | 3 ++- tools/est-test/jdl-mainpart.cjs | 9 +++++++++ tools/est-test/jdl.cjs | 4 ++-- 5 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 tools/est-test/jdl-mainpart.cjs diff --git a/resources/tester/test_est_jdl_special.py b/resources/tester/test_est_jdl_special.py index 15e762296..65e498f80 100644 --- a/resources/tester/test_est_jdl_special.py +++ b/resources/tester/test_est_jdl_special.py @@ -163,3 +163,21 @@ def test_jdl_estimate_percent_quantity_and_fixed_amount_round_trip(): ) assert [part["F_QQTY"] for part in result["parts"]] == [10, 100] assert [row["TC"] for row in result["rows"] if not row.get("Level")] == [20, 30, 50] + + +def test_jdl_estimate_preserves_components_when_total_matches(): + project = json.loads((ROOT / "tools/est-test/sample.json").read_text(encoding="utf-8")) + for table in project["tables"]: + project["tables"][table] = [] + script = """const fs=require('fs'),j=require('./tools/est-test/jdl.cjs'); + const p=JSON.parse(fs.readFileSync(0,'utf8')),blank=structuredClone(p); + p.tables.StuffcDat=[{Code:'M',Name:'material',GC:10,TC:10}]; + p.tables.EstMainDat=[{Code:'M',Name:'adjusted',LinkIndex:5,GN:2,GU:10,GC:0,NC:20,TC:20},{LinkIndex:54}]; + const q=j.read(j.output(p).bytes,blank);process.stdout.write(JSON.stringify(q.tables.EstMainDat.at(-1)));""" + result = json.loads( + subprocess.check_output( + ["node", "-e", script], input=json.dumps(project).encode(), cwd=ROOT + ) + ) + assert result["GC"] == 0 + assert result["NC"] == 20 diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index aed8999ee..67c763d4d 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -423,6 +423,10 @@ 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 내역 출력 시 총액이 같아도 비목 금액이 다르면 저장 금액을 보존하고 왕복을 검증한다. + - 자체 검증: 총액은 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건은 그대로 남아 있어 오류 없는 자료로 판정하지 않음. + - [x] JDL 내역의 퍼센트 수량을 가져오기·내보내기에서 같은 기준으로 계산하고 고정 금액 왕복을 검증한다. - 자체 검증: 일반 내역 10%×200=20, 고정 내역 100%×30=30을 출력·재가져와 합계 50 유지. JDL·업무·오류 검사 61개 통과. 원본 JDL 검사기 통과 여부는 미확인. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index e4a5161c7..7d83c1eab 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -4,7 +4,7 @@ 파일 열기에서 ESTX 작업 파일과 EBKX 백업 파일을 읽을 수 있습니다. EBKX를 연 작업도 JSON으로 보관하며 원본 파일을 덮어쓰지 않습니다. -내역·일위대가·수식 단가산출·기계경비·기초 단가·원가계산서를 원본 테이블 구조로 편집합니다. 수량 연결, 설계변경 당초 보존, 낙찰률, 기성 회차와 사정 누계, 품목조정, 연차 배분, 소요 자원 전개, 예정공정표 화면을 제공합니다. 실행 취소·다시 실행, 검색, 행 편집, JSON 저장·열기, CSV·Excel·조달청 XML·JDL 기본 표 입출력과 브라우저 인쇄를 사용할 수 있습니다. +내역·일위대가·수식 단가산출·기계경비·기초 단가·원가계산서를 원본 테이블 구조로 편집합니다. 수량 연결, 설계변경 당초 보존, 낙찰률, 기성 회차와 사정 누계, 품목조정, 연차 배분, 소요 자원 전개, 예정공정표 화면을 제공합니다. 실행 취소·다시 실행, 검색, 행 편집, JSON 저장·열기, CSV 내보내기, Excel·조달청 XML·JDL 기본 표 입출력과 브라우저 인쇄를 사용할 수 있습니다. 설계변경·기성·물가변동 내역은 100행씩 표시하며 코드와 품명으로 검색할 수 있습니다. 기성·물가변동은 규격과 단위도 검색합니다. 페이지와 검색은 화면 표시만 바꾸며 전체 내역 계산에는 영향을 주지 않습니다. @@ -45,6 +45,7 @@ - **조달청 XML**: DB에 보관된 공사원가호환규정 XSD 1.0.5의 요소 순서와 필드를 사용합니다. 작성업체·공사구분을 입력하며 코드/문구 길이와 연결 자료를 검사합니다. 내역·일위·산근·기계·기초 단가·원가를 내보내고 다시 읽습니다. 연결 없는 고정 금액·기계 상세는 독립 단가로 전개하며 변환 전에 알립니다. 가져온 저장 금액과 원문을 보존하고 자동계산을 끕니다. 이는 제출기관 인증/등록 검사나 최신 규정 적합 판정을 뜻하지 않습니다. - **JDL 기본 표**: 원본 검사 DLL의 필드 정의와 EST 처리기의 TAB/ENQ·CP949 표 구조에 따라 ZIP 안의 Global.ini·Main.txt·자원/공종/내역/원가 표를 생성·읽습니다. 일위대가 상세는 기본 플래그와 연결 자원이 확인된 행을 불러오고 미변환 구성은 저장 금액으로 보존합니다. 기계경비와 단가산출은 저장된 비목별 금액을 편집 가능한 호표로 가져오며 상세 산식은 원본 JDL에 보존합니다. 자재·노무로 구성된 기본 일위대가는 상세표로 내보냅니다. 가져온 작업을 변경하지 않았다면 원본 JDL을 바이트 그대로 내보내 확장 자료를 보존합니다. 변경된 작업의 특수 일위·산근·기계는 저장 금액의 고정 단가로 전개하며 제외 행은 생략 사실을 알립니다. 확장 산식·기성/물가 파일의 완전 변환과 원본 검사 DLL 인증은 미확인입니다. - 중복 자원 코드로 연결 대상을 구분할 수 없는 JDL 출력은 해당 내역의 저장 금액을 독립 단가로 기록하고 경고합니다. 자원 행 자체는 각각 보존합니다. +- 내역의 퍼센트 수량을 반영하며, 단가로 계산한 비목별 금액이 저장 금액과 다르면 저장 금액을 독립 항목으로 보존합니다. - JDL 기계 상세의 수량과 단가를 곱한 금액이 비목별 저장 합계와 맞는 경우 각 상세 금액을 편집할 수 있습니다. 상세 산식과 원천 단가 연결은 원본에 보존하며, 수량 해석이 모호하면 합계만 가져옵니다. - 가져온 JDL 기계 상세의 편집 금액과 호표 합계가 맞으면 JDL로 다시 저장할 때 각 상세를 고정 단가로 기록합니다. 원래의 상세 산식과 수량 관계는 복원하지 않습니다. - 일위대가에 연결된 기계·실적·견적·단가산출 자료도 일반 상세 조건을 만족하면 JDL로 출력합니다. 기계 상세가 변환되지 않거나 그 외 복합 원천은 고정 단가로 전개합니다. diff --git a/tools/est-test/jdl-mainpart.cjs b/tools/est-test/jdl-mainpart.cjs new file mode 100644 index 000000000..0adaca8ac --- /dev/null +++ b/tools/est-test/jdl-mainpart.cjs @@ -0,0 +1,9 @@ +function matchesAmounts(row, quantity, prices, num) { + const factor = quantity / (["%", "%"].includes(row.Unite) ? 100 : 1); + const amounts = [num(row.GC), num(row.NC), num(row.JC) + num(row.SC)]; + return prices.every( + (price, index) => Math.abs(Math.trunc(factor * price) - amounts[index]) <= 0.000001, + ); +} + +module.exports = { matchesAmounts }; diff --git a/tools/est-test/jdl.cjs b/tools/est-test/jdl.cjs index c65ebb3cb..97de8be89 100644 --- a/tools/est-test/jdl.cjs +++ b/tools/est-test/jdl.cjs @@ -8,6 +8,7 @@ const { orderUnits } = require("./jdl-unit-order.cjs"); const { isFixedPart, exportUnitPart } = require("./jdl-unit-part.cjs"); const { canPreserveSpecial, fixedSpecialPart } = require("./jdl-special-unit.cjs"); const { ordinaryMachinePart, machineQuantity } = require("./jdl-machine-parts.cjs"); +const { matchesAmounts } = require("./jdl-mainpart.cjs"); const decoder = new TextDecoder("euc-kr", { fatal: true }); function fingerprint(project) { const { originalJdlState, id, ...content } = project; @@ -225,8 +226,7 @@ function output(project) { !key || num(row.CalcIndex) === 1 || num(row.CalcIndex) === 3 || - Math.abs((gn / (["%", "%"].includes(row.Unite) ? 100 : 1)) * (m + l + e) - num(row.TC)) > - 0.000001 + !matchesAmounts(row, gn, [m, l, e], num) ) { key = source(0, { Code: "LITERAL" + ++serial,