From 100f05a9a34334e96fe23a33292c73c14535f639 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 11 Nov 2026 20:27:01 +0900 Subject: [PATCH] =?UTF-8?q?feat(est-test):=20=EC=A1=B0=EB=8B=AC=EC=B2=AD?= =?UTF-8?q?=20XML=20=EA=B2=80=EC=82=AC=EC=99=80=20=EC=9E=90=EB=A6=AC?= =?UTF-8?q?=EC=88=98=20=EC=9E=90=EB=8F=99=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 원본 변환 전 검사 1·8·10·11 규칙으로 목록 표시 - 비목값 자리수 지정만 풀고 다시 계산 Co-Authored-By: Claude Opus 5.5 --- resources/tester/test_est_xml_check.py | 42 +++++++ tools/est-test/PLAN.md | 2 + tools/est-test/README.md | 2 + tools/est-test/index.html | 2 + tools/est-test/native-business-actions.js | 3 +- tools/est-test/native-xml-check.js | 127 ++++++++++++++++++++++ tools/est-test/server.cjs | 1 + 7 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 resources/tester/test_est_xml_check.py create mode 100644 tools/est-test/native-xml-check.js diff --git a/resources/tester/test_est_xml_check.py b/resources/tester/test_est_xml_check.py new file mode 100644 index 000000000..42639ba64 --- /dev/null +++ b/resources/tester/test_est_xml_check.py @@ -0,0 +1,42 @@ +"""EST 테스트 앱 조달청 XML 검사·자리수자동수정(원본 TfrmJDLConvert 검사 1·8·10·11, TfrmXMLDigetCheck).""" + +import json +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + + +def test_rules_and_fix(): + script = """ + const fs = require('fs'), vm = require('vm'); + const ctx = vm.createContext({ structuredClone }); + for (const f of ['native-math.js', 'native-formula.js', 'native-text.js', 'native-xml-check.js']) + vm.runInContext(fs.readFileSync('tools/est-test/' + f, 'utf8'), ctx); + ctx.p = { tables: { + CostMainDat: [ + { Index: 1, Code: 'D1', Name: '터파기' }, + { SCheck: -1, Diget: 2, LineData: '노무' }, + { SCheck: 0, LinkIndex: 5, Diget: 1, LineData: '플래그 전 재료' }, + { Index: -20 }, + { LinkIndex: 6, DigetIndex: 1, LineData: '노임 연결' }, + { LinkIndex: 61 }, + { LinkIndex: 5, Diget: 3, LineData: '소계 뒤 재료' } ], + StuffcDat: [{ Code: 'M1', Index: 1, Name: '가', Size: '규' }, { Code: 'M1', Index: 2, Name: '나' }, { Code: 'm2', Index: 3 }], + MachTitleDat: [{ Code: 'E0000000233' }, { Code: 'E1234567890123456' }] } }; + const list = vm.runInContext('EstXmlCheck.check(p)', ctx).map((e) => [e.kind, e.table, e.index, e.text]); + const fixed = vm.runInContext('EstXmlCheck.fix(p)', ctx); + const after = vm.runInContext('EstXmlCheck.check(p).filter((e) => e.kind === 1).length', ctx); + const kept = vm.runInContext('[p.tables.CostMainDat[2].Diget, p.tables.CostMainDat[6].Diget, p.tables.CostMainDat[1].Diget]', ctx); + process.stdout.write(JSON.stringify({ list, fixed, after, kept })); + """ + result = json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT).decode("utf-8")) + assert result["list"] == [ + [1, "CostMainDat", 1, "노무"], + [1, "CostMainDat", 4, "노임 연결"], + [8, "StuffcDat", 1, "1.가(규) 와 코드 중복"], + [10, "StuffcDat", 2, "코드에 소문자사용"], + [11, "MachTitleDat", 0, "장비코드 길이가틀림(17자리)"], + ] + assert result["fixed"] == 2 and result["after"] == 0 + assert result["kept"] == [1, 3, 0] diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 046f4bf24..f125c7831 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -487,6 +487,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - [x] 견적서 작성(원본 mnuEstiMatePrn → TfrmEstiMatePrn 0x10c1994·그리기 0x10c4768 — 디스어셈블 근거) — 공사별 설정(`project.estimatePrint`), 원본 `.einf` INI 세 구역 저장·두 구역 불러오기, 레벨 행 선택, 7열, 1쪽 머리·공급자·합계금액(아래에서 위로 Checked 금액, 한글), 첫/나머지 쪽 줄수, 미리보기·인쇄. 수탁공사변환·XML 역변환 비교는 원본에서 숨긴 개발용 메뉴라 범위 밖. - 검증완료: `test_est_estimate_print.py`(전체/레벨 2 행, 쪽 27·36·7, 합계 「일금:구천칠백일십구만구천원정 (₩97,199,000원정)」, INI 왕복, 미리보기 글) 통과. ORCA 브라우저에서 상호·레벨 2 → 2쪽·28줄·합계·공급자 표, 공사 설정 저장 확인 후 샘플 복구. - 남음: 견적서 Excel 변환, 글꼴·크기·여백 세부값. + - [x] 조달청 XML 검사·자리수자동수정(원본 TfrmJDLConvert 0xe7d4bc 검사 1·8·10·11, TfrmXMLDigetCheck btnOk 0xcc848c — 디스어셈블 근거) — 비목값·−20~소계 사이 재료/노임 연결 줄 자리수, 7개 표 코드 중복·소문자, 기계 코드 17자리, 수정은 오류 1 줄 Diget·DigetIndex 0 후 재계산. 원본은 오류가 있으면 변환을 멈추나 웹은 기존 내보내기를 막지 않고 검사 창으로 알림(샘플 기계 코드가 11자리라 모두 막히는 것을 피함). + - 검증완료: `test_est_xml_check.py`(규칙 1의 −20/61 구간·SCheck, 중복 문구, 소문자, 17자리, 수정 대상만 풀림) 통과. ORCA 브라우저에서 샘플 검사 → 자리수 0·중복 0·소문자 0·장비코드 8건. - 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133. - [ ] 남은 핵심(실무 빈도순): 3. 수량산출서 — 누계·4방향 합계, 제출용 전체 인쇄. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 090161cfb..edb4c2b13 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -24,6 +24,8 @@ `편집 → 선택 행 자재단가 등록`·`견적단가 등록`은 원본처럼 내역·일위대가 행을 새 자재(M)·견적(W) 단가로 등록하고 그 행을 새 코드에 연결합니다. 같은 품명·규격이 있으면 거부하고, 일위대가 호표 제목 줄은 등록하지 않습니다. `품명정리`는 고른 일위대가 호표의 품명을 커서 자리에서 나눠 뒤쪽을 규격 앞에 붙이고, 단위가 비어 있으면 채웁니다. +`파일 → 조달청 XML 검사·자리수 자동 수정`은 원본 XML 변환 전 검사처럼 단가산출 비목 값·재료/노임 연결 줄의 행별 자리수 지정, 단가 표 코드 중복·소문자, 기계경비 코드 17자리 아님을 목록으로 보입니다. `자리수 자동 수정`은 첫 항목의 자리수 지정만 풀고 다시 계산합니다. 기존 XML 내보내기는 막지 않습니다. + `파일 → 견적서 작성`은 원본처럼 제목·공사명·발주처·견적일자와 공급자(등록번호·상호·대표자·주소·업태·종목·전화)를 넣고, 설계내역 레벨(0 = 전체, N = N 이상 공종과 순공사비·비율 줄)·첫/나머지 페이지 줄수·정렬·비고란을 정해 미리보기·인쇄합니다. 합계금액은 원가계산서 아래에서 위로 첫 체크된 금액을 `일금:…원정 (₩…원정)`으로 적습니다. 공급자정보는 원본과 같은 `.einf`(INI) 파일로 저장·불러오기 합니다. 설정은 공사마다 저장됩니다. `파일 → 갑지·표지·공사설명서·시방서 편집`은 원본 ESTX의 문서 레코드(DatGabgi·DatCover·DatExplean·DatSiBang)를 읽어 공사명·공사위치·발주처·설계일자·공사개요·공사금액·결재란, 표지의 부제·설계년도를 고칩니다. 공사명은 공사 이름과 함께 바뀝니다. `공사금액 만들기`는 원가계산서에서 고른 비목을 `금 일천오백만원(₩15,000,000)` 또는 `금15,000,000원(금일천오백만원)` 방식으로 적습니다. 설명서·시방서에서는 갑지의 공사명·위치·개요·금액을 커서 자리에 붙입니다. 인쇄 서식 8·9·10·11 미리보기는 이 내용을 씁니다. 서식·여백·글꼴은 다루지 않고, 문서를 고친 작업은 ESTX 저장을 거부합니다. diff --git a/tools/est-test/index.html b/tools/est-test/index.html index b8b0443fa..0d4ddb0f3 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -54,6 +54,7 @@ + @@ -81,6 +82,7 @@ + diff --git a/tools/est-test/native-business-actions.js b/tools/est-test/native-business-actions.js index 5d2d313b5..461cc8cb1 100644 --- a/tools/est-test/native-business-actions.js +++ b/tools/est-test/native-business-actions.js @@ -614,7 +614,8 @@ const EstBusinessActions = (() => { (typeof EstMultiSearch !== "undefined" && EstMultiSearch.handle(name, state)) || (typeof EstDocs !== "undefined" && EstDocs.handle(name, state, node)) || (typeof EstWongaTools !== "undefined" && EstWongaTools.handle(name, state)) || - (typeof EstEstimatePrint !== "undefined" && EstEstimatePrint.handle(name, state)) + (typeof EstEstimatePrint !== "undefined" && EstEstimatePrint.handle(name, state)) || + (typeof EstXmlCheck !== "undefined" && EstXmlCheck.handle(name, state)) ) { EstApp.render(); } else if (name === "load-contract" || name === "load-bid") { diff --git a/tools/est-test/native-xml-check.js b/tools/est-test/native-xml-check.js new file mode 100644 index 000000000..457e7b399 --- /dev/null +++ b/tools/est-test/native-xml-check.js @@ -0,0 +1,127 @@ +// 원본 조달청 XML 내보내기 전 검사(TfrmJDLConvert 0xe7d4bc)와 자리수자동수정(TfrmXMLDigetCheck btnOk 0xcc848c). +// 오류 1: 단가산출에서 비목 지정 값(SCheck −5~−1) 또는 −20 행부터 소계(61)까지의 재료·노임 연결(5·6) 행에 행별 자리수. +// 오류 8: 7개 단가 표 코드 중복 · 10: 코드에 영문 소문자 · 11: 기계경비 코드 길이 ≠ 17. +// 자리수자동수정은 오류 1 행의 Diget·DigetIndex 만 0 으로 풀고 다시 계산한다(특정 자리로 고치지 않음). +// 웹은 기존 XML 내보내기를 막지 않고 별도 검사 창으로 알린다. +const EstXmlCheck = (() => { + const num = (value) => Number(value) || 0; + const tables = [ + "LaborcDat", + "StuffcDat", + "EstiMateDat", + "SilJukDat", + "MachTitleDat", + "CostTitleDat", + "HeadTitleDat", + ]; + const explain = { + 1: "비목값에 자리수지정", + 8: "코드 중복", + 10: "코드에 소문자사용", + 11: "장비코드 길이가틀림(17자리)", + }; + + function check(project) { + const out = []; + let head = null, + flag = false; + (project.tables.CostMainDat || []).forEach((row, index) => { + if (num(row.Index) > 0) head = row; + if (num(row.Index) === -20) flag = true; + if (num(row.LinkIndex) === 61) flag = false; + const digit = num(row.DigetIndex) || num(row.Diget), + sc = num(row.SCheck); + if (digit && ((sc >= -5 && sc <= -1) || (flag && [5, 6].includes(num(row.LinkIndex))))) + out.push({ + kind: 1, + table: "CostMainDat", + index, + code: head?.Code ?? "", + name: head?.Name ?? "", + text: EstText.clean(row.LineData || ""), + }); + }); + for (const table of tables) { + const seen = new Map(); + (project.tables[table] || []).forEach((row, index) => { + if (!row.Code) return; + const first = seen.get(row.Code); + if (first) + out.push({ + kind: 8, + table, + index, + code: row.Code, + name: row.Name ?? "", + text: `${num(first.Index)}.${first.Name ?? ""}(${first.Size ?? ""}) 와 코드 중복`, + }); + else seen.set(row.Code, row); + if (/[a-z]/.test(row.Code)) + out.push({ + kind: 10, + table, + index, + code: row.Code, + name: row.Name ?? "", + text: explain[10], + }); + if (table === "MachTitleDat" && String(row.Code).length !== 17) + out.push({ + kind: 11, + table, + index, + code: row.Code, + name: row.Name ?? "", + text: explain[11], + }); + }); + } + return out; + } + + function fix(project) { + const rows = project.tables.CostMainDat || []; + const targets = check(project).filter((e) => e.kind === 1); + for (const e of targets) Object.assign(rows[e.index], { Diget: 0, DigetIndex: 0 }); + return targets.length; + } + + function show(state) { + const list = check(state.project), + { esc } = EstApp, + counts = [1, 8, 10, 11].map( + (k) => `${explain[k]} ${list.filter((e) => e.kind === k).length}건`, + ); + EstApp.showDialog( + "조달청 XML 검사", + `

${counts.join(" · ")}

${list + .slice(0, 300) + .map( + (e) => + ``, + ) + .join( + "", + )}
구분표코드품명내용
${esc(explain[e.kind])}${esc(EstApp.titles?.[e.table] || e.table)}${esc(e.code)}${esc(e.name)}${esc(e.text)}
${list.some((e) => e.kind === 1) ? '' : ""}`, + ); + } + + function handle(name, state) { + if (name === "xml-check") show(state); + else if (name === "xml-diget-fix") { + if ( + !confirm( + "자리수 수정후 재계산 시키므로공사금액이 변경될수 있습니다.자리수를 수정하시겠습니까?", + ) + ) + return true; + let count; + EstApp.change((project) => (count = fix(project))); + state.notice = `단가산출 ${count}줄의 자리수 지정을 풀고 다시 계산했습니다.`; + show(state); + } else return false; + return true; + } + + return { check, fix, handle }; +})(); diff --git a/tools/est-test/server.cjs b/tools/est-test/server.cjs index 43aa2757f..9c7fec6d5 100644 --- a/tools/est-test/server.cjs +++ b/tools/est-test/server.cjs @@ -76,6 +76,7 @@ const allowed = new Set([ "native-docs.js", "native-wonga-tools.js", "native-estimate-print.js", + "native-xml-check.js", "native-issues.js", "native-attachments.js", "native-standard.js",