feat(est-test): 계약심사 심사평가결과·주요심사내용 자동입력

- 원본 처리 근거대로 설계·심사·절감액·절감율 네 줄과 표별 조정 목록 생성
- 심사 내역 사본에 설계 원가 금액 보존

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-11 20:50:58 +09:00
co-authored by Claude Opus 5.5
parent 100f05a9a3
commit a80101826c
8 changed files with 133 additions and 3 deletions
+3
View File
@@ -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방향 합계, 제출용 전체 인쇄.
+2
View File
@@ -24,6 +24,8 @@
`편집 → 선택 행 자재단가 등록`·`견적단가 등록`은 원본처럼 내역·일위대가 행을 새 자재(M)·견적(W) 단가로 등록하고 그 행을 새 코드에 연결합니다. 같은 품명·규격이 있으면 거부하고, 일위대가 호표 제목 줄은 등록하지 않습니다. `품명정리`는 고른 일위대가 호표의 품명을 커서 자리에서 나눠 뒤쪽을 규격 앞에 붙이고, 단위가 비어 있으면 채웁니다.
계약심사 화면의 `심사평가결과 자동입력`은 원본처럼 원가계산서의 총공사비 행에서 설계(심사 전)·심사 금액, 절감액, 절감율(소수 둘째 자리 버림)을 적고, `주요심사내용 자동입력`은 노임·자재·표준시장·견적·기계경비·단가산출·일위대가 중 새로 바뀐 자료를 「○○ 조정 : 이름(규격) 외N 종」으로 적습니다. 심사 내역 사본은 설계 원가 금액을 함께 남깁니다.
`파일 → 조달청 XML 검사·자리수 자동 수정`은 원본 XML 변환 전 검사처럼 단가산출 비목 값·재료/노임 연결 줄의 행별 자리수 지정, 단가 표 코드 중복·소문자, 기계경비 코드 17자리 아님을 목록으로 보입니다. `자리수 자동 수정`은 첫 항목의 자리수 지정만 풀고 다시 계산합니다. 기존 XML 내보내기는 막지 않습니다.
`파일 → 견적서 작성`은 원본처럼 제목·공사명·발주처·견적일자와 공급자(등록번호·상호·대표자·주소·업태·종목·전화)를 넣고, 설계내역 레벨(0 = 전체, N = N 이상 공종과 순공사비·비율 줄)·첫/나머지 페이지 줄수·정렬·비고란을 정해 미리보기·인쇄합니다. 합계금액은 원가계산서 아래에서 위로 첫 체크된 금액을 `일금:…원정 (₩…원정)`으로 적습니다. 공급자정보는 원본과 같은 `.einf`(INI) 파일로 저장·불러오기 합니다. 설정은 공사마다 저장됩니다.
+1
View File
@@ -55,6 +55,7 @@
<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-review-auto.js"></script>
<script defer src="native-actions.js"></script>
<script defer src="native-business-actions.js"></script>
<script defer src="native-app.js"></script>
+2 -1
View File
@@ -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") {
+69
View File
@@ -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 };
})();
+13 -2
View File
@@ -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 =
`<div class="business-tools"><b>계약심사</b><button data-review-copy>심사 내역 사본 열기</button><button data-review-clear>심사 입력 초기화</button><label>보기 <select id="review-filter"><option value="all" ${state.reviewFilter === "all" || !state.reviewFilter ? "selected" : ""}>전체 항목</option><option value="changed" ${state.reviewFilter === "changed" ? "selected" : ""}>변경 항목만</option><option value="has-reason" ${state.reviewFilter === "has-reason" ? "selected" : ""}>사유 작성 항목</option><option value="no-reason" ${state.reviewFilter === "no-reason" ? "selected" : ""}>사유 미작성 변경항목</option></select></label><button data-review-batch-reason>선택/표시 행 사유 일괄적용</button><span>기존 ${number(output.before)} · 심사 ${number(output.after)} · 차액 ${number(output.after - output.before)}</span></div><p>심사 수량과 비목별 단가를 직접 입력합니다. 고정금액 행은 비목별 금액을 입력합니다. 원래 내역은 유지되며 심사 사본에서 변경됩니다.</p><label>주요심사 내용<textarea data-review-summary rows="4" maxlength="100000" style="width:100%">${esc(project.documents?.["173"] || "")}</textarea></label><p class="business-note">${esc(output.errors.join(" / "))}</p><table class="sheet"><thead><tr>${["선택", "코드", "품명", "기존 수량", "심사 수량", "노무", "재료", "경비", "실적", "기존 금액", "심사 금액", "차액", "심사 사유", "작업"].map((value) => `<th>${value}</th>`).join("")}</tr></thead><tbody>${rows
`<div class="business-tools"><b>계약심사</b><button data-review-copy>심사 내역 사본 열기</button><button data-review-clear>심사 입력 초기화</button><label>보기 <select id="review-filter"><option value="all" ${state.reviewFilter === "all" || !state.reviewFilter ? "selected" : ""}>전체 항목</option><option value="changed" ${state.reviewFilter === "changed" ? "selected" : ""}>변경 항목만</option><option value="has-reason" ${state.reviewFilter === "has-reason" ? "selected" : ""}>사유 작성 항목</option><option value="no-reason" ${state.reviewFilter === "no-reason" ? "selected" : ""}>사유 미작성 변경항목</option></select></label><button data-review-batch-reason>선택/표시 행 사유 일괄적용</button><span>기존 ${number(output.before)} · 심사 ${number(output.after)} · 차액 ${number(output.after - output.before)}</span></div><p>심사 수량과 비목별 단가를 직접 입력합니다. 고정금액 행은 비목별 금액을 입력합니다. 원래 내역은 유지되며 심사 사본에서 변경됩니다.</p><button data-action="review-auto-result">심사평가결과 자동입력</button><button data-action="review-auto-content">주요심사내용 자동입력</button><label>심사평가결과<textarea data-review-result rows="4" maxlength="100000" style="width:100%;font-family:monospace">${esc(project.documents?.["173-result"] || "")}</textarea></label><label>주요심사 내용<textarea data-review-summary rows="4" maxlength="100000" style="width:100%">${esc(project.documents?.["173"] || "")}</textarea></label><p class="business-note">${esc(output.errors.join(" / "))}</p><table class="sheet"><thead><tr>${["선택", "코드", "품명", "기존 수량", "심사 수량", "노무", "재료", "경비", "실적", "기존 금액", "심사 금액", "차액", "심사 사유", "작업"].map((value) => `<th>${value}</th>`).join("")}</tr></thead><tbody>${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;
+1
View File
@@ -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",