diff --git a/resources/tester/test_est_review_auto.py b/resources/tester/test_est_review_auto.py new file mode 100644 index 000000000..fb902261e --- /dev/null +++ b/resources/tester/test_est_review_auto.py @@ -0,0 +1,42 @@ +"""EST 테스트 앱 계약심사 자동입력(원본 TfrmInvest 심사평가결과·주요심사내용).""" + +import json +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + + +def test_result_and_content_text(): + script = """ + const fs = require('fs'), vm = require('vm'); + const ctx = vm.createContext({}); + vm.runInContext(fs.readFileSync('tools/est-test/native-review-auto.js', 'utf8'), ctx); + ctx.p = { tables: { + Wonga: [{ Name: '도 급 액', Cost: 1 }, { Name: '총 공 사 비', Cost_: 1000000, Cost: 876543.21 }], + StuffcDat: [{ Name: '레 미 콘', Size: '25-24|150', NewItem: true }, { Name: 'x' }, { Name: 'y', NewItem: true }, { NewItem: true }], + LaborcDat: [{ Name: '보통 인부', Size: '일반', NewItem: true }], + HeadTitleDat: [{ Name: '거푸집', Size: '합판' }] } }; + const r = vm.runInContext('EstReviewAuto.result(p)', ctx); + ctx.q = { tables: { Wonga: [{ Name: '총공사비', Cost_: 100, Cost: 150 }] } }; + const up = vm.runInContext('EstReviewAuto.result(q)', ctx); + const none = vm.runInContext('EstReviewAuto.result({ tables: { Wonga: [] } })', ctx); + const c = vm.runInContext('EstReviewAuto.content(p)', ctx); + process.stdout.write(JSON.stringify({ r, up, none, c })); + """ + out = json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT).decode("utf-8")) + assert out["r"].split("\n") == [ + f"o 설계금액 : {'1,000,000':>16} 원", + f"o 심사금액 : {'876,543.21':>16} 원", + f"o 절 감 액 : {'-123,456.79':>16} 원", + f"o 절 감 율 : {'-12.34':>16} %", + ] + assert out["up"].split("\n")[2:] == [ + f"o 절 감 액 : {'+50':>16} 원", + f"o 절 감 율 : {'+50':>16} %", + ] + assert out["none"] is None + assert out["c"].split("\n") == [ + "노임단가 조정 : 보통인부 외1 종", + "자재단가 조정 : 레미콘(25-24150) 외3 종", + ] diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index f125c7831..227eba1b4 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -489,6 +489,9 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - 남음: 견적서 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건. + - [x] 계약심사 자동입력(원본 TfrmInvest 심사평가결과·주요심사내용 0x10bfc98 — 디스어셈블 근거) — 총공사비 행 Cost_/Cost, 절감율 Trunc(×10000)×0.01, cp949 16바이트 맞춤·증액 「+」, 7개 표 NewItem 「○○ 조정 : 이름(규격) 외N 종」(N 원본 셈). 심사 사본에 설계 원가 Cost_ 보존. + - 검증완료: `test_est_review_auto.py`(네 줄 글·증액 부호·총공사비 없음·주요심사 두 줄) 통과, 계약심사 관련 시험 44개 통과. ORCA 브라우저에서 수량 0.9배 심사 → 사본 → 자동입력 「설계 97,199,324 · 심사 96,952,167 · 절감 −247,157 · −0.25 %」, 샘플 복구. + - 남음: 위치·사업개요·사업기간·기타사항 칸(Invest.Dat 10개 RTF). - 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133. - [ ] 남은 핵심(실무 빈도순): 3. 수량산출서 — 누계·4방향 합계, 제출용 전체 인쇄. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index edb4c2b13..b3547b757 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -24,6 +24,8 @@ `편집 → 선택 행 자재단가 등록`·`견적단가 등록`은 원본처럼 내역·일위대가 행을 새 자재(M)·견적(W) 단가로 등록하고 그 행을 새 코드에 연결합니다. 같은 품명·규격이 있으면 거부하고, 일위대가 호표 제목 줄은 등록하지 않습니다. `품명정리`는 고른 일위대가 호표의 품명을 커서 자리에서 나눠 뒤쪽을 규격 앞에 붙이고, 단위가 비어 있으면 채웁니다. +계약심사 화면의 `심사평가결과 자동입력`은 원본처럼 원가계산서의 총공사비 행에서 설계(심사 전)·심사 금액, 절감액, 절감율(소수 둘째 자리 버림)을 적고, `주요심사내용 자동입력`은 노임·자재·표준시장·견적·기계경비·단가산출·일위대가 중 새로 바뀐 자료를 「○○ 조정 : 이름(규격) 외N 종」으로 적습니다. 심사 내역 사본은 설계 원가 금액을 함께 남깁니다. + `파일 → 조달청 XML 검사·자리수 자동 수정`은 원본 XML 변환 전 검사처럼 단가산출 비목 값·재료/노임 연결 줄의 행별 자리수 지정, 단가 표 코드 중복·소문자, 기계경비 코드 17자리 아님을 목록으로 보입니다. `자리수 자동 수정`은 첫 항목의 자리수 지정만 풀고 다시 계산합니다. 기존 XML 내보내기는 막지 않습니다. `파일 → 견적서 작성`은 원본처럼 제목·공사명·발주처·견적일자와 공급자(등록번호·상호·대표자·주소·업태·종목·전화)를 넣고, 설계내역 레벨(0 = 전체, N = N 이상 공종과 순공사비·비율 줄)·첫/나머지 페이지 줄수·정렬·비고란을 정해 미리보기·인쇄합니다. 합계금액은 원가계산서 아래에서 위로 첫 체크된 금액을 `일금:…원정 (₩…원정)`으로 적습니다. 공급자정보는 원본과 같은 `.einf`(INI) 파일로 저장·불러오기 합니다. 설정은 공사마다 저장됩니다. diff --git a/tools/est-test/index.html b/tools/est-test/index.html index 0d4ddb0f3..0c09853d8 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -55,6 +55,7 @@ + diff --git a/tools/est-test/native-business-actions.js b/tools/est-test/native-business-actions.js index 461cc8cb1..363418f0a 100644 --- a/tools/est-test/native-business-actions.js +++ b/tools/est-test/native-business-actions.js @@ -615,7 +615,8 @@ const EstBusinessActions = (() => { (typeof EstDocs !== "undefined" && EstDocs.handle(name, state, node)) || (typeof EstWongaTools !== "undefined" && EstWongaTools.handle(name, state)) || (typeof EstEstimatePrint !== "undefined" && EstEstimatePrint.handle(name, state)) || - (typeof EstXmlCheck !== "undefined" && EstXmlCheck.handle(name, state)) + (typeof EstXmlCheck !== "undefined" && EstXmlCheck.handle(name, state)) || + (typeof EstReviewAuto !== "undefined" && EstReviewAuto.handle(name, state)) ) { EstApp.render(); } else if (name === "load-contract" || name === "load-bid") { diff --git a/tools/est-test/native-review-auto.js b/tools/est-test/native-review-auto.js new file mode 100644 index 000000000..8d7791b44 --- /dev/null +++ b/tools/est-test/native-review-auto.js @@ -0,0 +1,69 @@ +// 원본 주요심사내용 창(TfrmInvest) 자동입력 — 심사평가결과(0x10bf3xx)·주요심사내용(0x10bfc98). +// 심사평가결과: 원가계산서에서 이름(빈칸·| 제거)에 「총공사비」가 든 첫 행의 Cost_(설계)·Cost(심사), 절감액 = 심사 − 설계, +// 절감율 = Trunc(절감액 / 설계 × 10000) × 0.01 %(둘 다 0 아닐 때), 숫자는 cp949 바이트 16칸 오른쪽 맞춤, 증액에만 「+」. +// 주요심사내용: 노임·자재·표준시장·견적·기계경비·단가산출·일위대가 차례로 NewItem 행이 있으면 +// 「○○ 조정 : 이름(규격) 외N 종」(N = 첫 행을 포함한 전체 개수, 원본 셈 그대로) 한 줄씩. +const EstReviewAuto = (() => { + const num = (value) => Number(value) || 0; + const strip = (text) => String(text ?? "").replace(/[\s|]/g, ""); + const bytes = (text) => [...text].reduce((n, ch) => n + (ch.charCodeAt(0) > 0x7f ? 2 : 1), 0); + const right16 = (text) => " ".repeat(Math.max(0, 16 - bytes(text))) + text; + const fmt = (value) => + Number(value).toLocaleString("en-US", { maximumFractionDigits: 9, useGrouping: true }); + + function result(project) { + const row = (project.tables.Wonga || []).find((r) => strip(r.Name).includes("총공사비")); + if (!row) return null; + const design = num(row.Cost_), + review = num(row.Cost), + d = review - design, + sign = d > 0 ? "+" : ""; + const lines = [ + `o 설계금액 : ${right16(fmt(design))} 원`, + `o 심사금액 : ${right16(fmt(review))} 원`, + `o 절 감 액 : ${right16(sign + fmt(d))} 원`, + ]; + if (d && design) { + const r = Math.trunc((d / design) * 10000) * 0.01; + lines.push(`o 절 감 율 : ${right16(sign + fmt(Number(r.toFixed(2))))} %`); + } + return lines.join("\n"); + } + + const tables = [ + ["LaborcDat", "노임단가", false], + ["StuffcDat", "자재단가", true], + ["SilJukDat", "표준시장단가", true], + ["EstiMateDat", "견적단가", true], + ["MachTitleDat", "기계경비", true], + ["CostTitleDat", "단가산출", true], + ["HeadTitleDat", "일위대가", true], + ]; + function content(project) { + return tables + .map(([table, label, size]) => { + const rows = (project.tables[table] || []).filter((r) => r.NewItem); + if (!rows.length) return ""; + const first = rows[0]; + return `${label} 조정 : ${strip(first.Name)}${size ? `(${strip(first.Size)})` : ""} 외${rows.length} 종`; + }) + .filter(Boolean) + .join("\n"); + } + + function handle(name, state) { + if (name !== "review-auto-result" && name !== "review-auto-content") return false; + const key = name === "review-auto-result" ? "173-result" : "173", + text = name === "review-auto-result" ? result(state.project) : content(state.project); + if (text == null) throw Error("원가계산서에 총공사비 행이 없습니다."); + const old = state.project.documents?.[key]; + if (old && !confirm("기존 내용을 지우고 자동입력하시겠습니까?")) return true; + EstApp.change((project) => { + project.documents ||= {}; + project.documents[key] = text; + }, false); + return true; + } + + return { result, content, handle }; +})(); diff --git a/tools/est-test/native-review.js b/tools/est-test/native-review.js index 6e69c368a..d2c4e5191 100644 --- a/tools/est-test/native-review.js +++ b/tools/est-test/native-review.js @@ -101,7 +101,11 @@ const EstReview = (() => { delete next.contractReview; next.name = `${project.name} · 심사 내역`; next.tables.EstMainDat = output.reviewed.tables.EstMainDat; - next.tables.Wonga = output.reviewed.tables.Wonga; + // 원본 심사 자료처럼 설계(심사 전) 원가 금액을 Cost_ 에 남긴다(심사평가결과 설계금액). + next.tables.Wonga = output.reviewed.tables.Wonga.map((row, i) => ({ + ...row, + Cost_: EstMath.num(output.baseline.tables.Wonga?.[i]?.Cost), + })); next.options.SourceRefresh = false; next.calculation = output.reviewed.calculation; return next; @@ -115,7 +119,7 @@ const EstReview = (() => { ({ row }) => editable(row) && (row.Name || row.Code), ); byId("work-content").innerHTML = - `
계약심사기존 ${number(output.before)} · 심사 ${number(output.after)} · 차액 ${number(output.after - output.before)}

심사 수량과 비목별 단가를 직접 입력합니다. 고정금액 행은 비목별 금액을 입력합니다. 원래 내역은 유지되며 심사 사본에서 변경됩니다.

${esc(output.errors.join(" / "))}

${["선택", "코드", "품명", "기존 수량", "심사 수량", "노무", "재료", "경비", "실적", "기존 금액", "심사 금액", "차액", "심사 사유", "작업"].map((value) => ``).join("")}${rows + `
계약심사기존 ${number(output.before)} · 심사 ${number(output.after)} · 차액 ${number(output.after - output.before)}

심사 수량과 비목별 단가를 직접 입력합니다. 고정금액 행은 비목별 금액을 입력합니다. 원래 내역은 유지되며 심사 사본에서 변경됩니다.

${esc(output.errors.join(" / "))}

${value}
${["선택", "코드", "품명", "기존 수량", "심사 수량", "노무", "재료", "경비", "실적", "기존 금액", "심사 금액", "차액", "심사 사유", "작업"].map((value) => ``).join("")}${rows .filter(({ row, index }) => { const key = rowKey(row, index), input = edits[key] || {}, @@ -165,6 +169,13 @@ const EstReview = (() => { EstApp.render(); return; } + if (node.dataset.reviewResult !== undefined) { + EstApp.change( + () => ((EstApp.state.project.documents ||= {})["173-result"] = node.value), + false, + ); + return; + } if (node.dataset.reviewSummary !== undefined) { EstApp.change(() => { const project = EstApp.state.project; diff --git a/tools/est-test/server.cjs b/tools/est-test/server.cjs index 9c7fec6d5..cf5580b4f 100644 --- a/tools/est-test/server.cjs +++ b/tools/est-test/server.cjs @@ -77,6 +77,7 @@ const allowed = new Set([ "native-wonga-tools.js", "native-estimate-print.js", "native-xml-check.js", + "native-review-auto.js", "native-issues.js", "native-attachments.js", "native-standard.js",
${value}