feat(est-test): 조달청 XML 검사와 자리수 자동 수정
- 원본 변환 전 검사 1·8·10·11 규칙으로 목록 표시 - 비목값 자리수 지정만 풀고 다시 계산 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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]
|
||||
@@ -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방향 합계, 제출용 전체 인쇄.
|
||||
|
||||
@@ -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 저장을 거부합니다.
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
<script defer src="native-docs.js"></script>
|
||||
<script defer src="native-wonga-tools.js"></script>
|
||||
<script defer src="native-estimate-print.js"></script>
|
||||
<script defer src="native-xml-check.js"></script>
|
||||
<script defer src="native-actions.js"></script>
|
||||
<script defer src="native-business-actions.js"></script>
|
||||
<script defer src="native-app.js"></script>
|
||||
@@ -81,6 +82,7 @@
|
||||
<button data-action="export-xlsx">전체 자료 Excel 내보내기</button>
|
||||
<button data-action="import-xlsx">Excel 표 불러오기</button>
|
||||
<button data-action="excel-map-open">Excel 내역 열 맞춰 가져오기</button>
|
||||
<button data-action="xml-check">조달청 XML 검사·자리수 자동 수정</button>
|
||||
<button data-action="export-xml">조달청 XML 내보내기</button>
|
||||
<button data-action="import-xml">조달청 XML 불러오기</button>
|
||||
<button data-action="export-estx">원본 구조 유지 ESTX 수정 저장</button>
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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 검사",
|
||||
`<p>${counts.join(" · ")}</p><div style="max-height:360px;overflow:auto"><table class="sheet" style="font-size:12px"><thead><tr><th>구분</th><th>표</th><th>코드</th><th>품명</th><th>내용</th></tr></thead><tbody>${list
|
||||
.slice(0, 300)
|
||||
.map(
|
||||
(e) =>
|
||||
`<tr><td>${esc(explain[e.kind])}</td><td>${esc(EstApp.titles?.[e.table] || e.table)}</td><td>${esc(e.code)}</td><td>${esc(e.name)}</td><td style="text-align:left">${esc(e.text)}</td></tr>`,
|
||||
)
|
||||
.join(
|
||||
"",
|
||||
)}</tbody></table></div>${list.some((e) => e.kind === 1) ? '<button data-action="xml-diget-fix">자리수 자동 수정</button>' : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
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 };
|
||||
})();
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user