feat(est-test): JDLExcel과 단가비교
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
"""EST 테스트 앱 JDLExcel과 단가비교(원본 ActionJDLCompare — Excel 코드 칸이 코드를 품으면 같은 행 합계와 TC 비교)."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_compare_contains_code_limit_and_xls_tables():
|
||||
code = r"""
|
||||
const fs = require('fs'), vm = require('vm');
|
||||
const ctx = vm.createContext({});
|
||||
vm.runInContext(fs.readFileSync('tools/est-test/native-jdl-compare.js', 'utf8'), ctx);
|
||||
const J = vm.runInContext('EstJdlCompare', ctx);
|
||||
const rows = [{ Code: 'U001', Name: '가', TC: 1000 }, { Code: 'U002', Name: '나', TC: 500 },
|
||||
{ Code: 'U003', Name: '다', TC: 7 }, { Code: '', Name: '머리' }];
|
||||
const table = [['코드', '', '합계'], ['[U001] 가', '', '1,000'], ['U002', '', 499]];
|
||||
const out = J.compare(rows, table, 0, 2).map((r) => [r.Code, r.OTC, r.GN, r.Checked]);
|
||||
const far = Array.from({ length: 60 }, () => ['x']); far.push(['U009', '', 5]);
|
||||
const limit = J.compare([{ Code: 'U009', TC: 5 }], far, 0, 2)[0].OTC;
|
||||
const xls = J.xlsTables([{ name: 'S1', cells: { '0,0': { value: 'U001' }, '0,7': { value: 1000 }, '2,1': { value: 'b' } } }]);
|
||||
process.stdout.write(JSON.stringify({ out, limit, xls: xls.S1 }));
|
||||
"""
|
||||
out = json.loads(subprocess.check_output(["node", "-e", code], cwd=ROOT).decode("utf-8"))
|
||||
assert out["out"] == [
|
||||
["U001", 1000, 0, False],
|
||||
["U002", 499, 1, True],
|
||||
["U003", None, None, True],
|
||||
]
|
||||
assert out["limit"] is None
|
||||
assert out["xls"][0][0] == "U001" and out["xls"][0][7] == 1000 and out["xls"][2][1] == "b"
|
||||
@@ -489,6 +489,9 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- 남음: 구성비·요율 전환(CalcBase), 약어 표시(SName), 제비율 기준 문서 보기(원본 한글 파일).
|
||||
- [x] 수량산출서 검색·전체보기(원본 TFRMMULLANG btnSerch·btnAllDisplay — 빈칸·|·괄호 무시 포함 검색) — 고르기 줄을 `native-quantity-sheets.js` 로 옮겨 700줄 제한 유지.
|
||||
- 검증완료: `test_est_quantity_sheets.py` 검색 시험 포함 수량 시험 22개 통과. ORCA 브라우저에서 「집수」 검색 → 항목에 집수정이 있는 「배수」만·선택 이동, 전체보기 → 3개 복귀, 샘플 복구.
|
||||
- [x] JDLExcel과 단가비교(원본 ActionJDLCompare, 숨은 Ctrl+Alt+J → TfrmJDLExcelCompare) — 일위대가·단가산출·기계경비 탭, 시트번호·코드칼럼(A)·합계셀칼럼(H), Excel(.xlsx·.xls — `/api/read-xls`) 1행~(호표 수+50)행에서 코드를 품은 칸의 같은 행 합계와 TC 비교, 차이·없음 표시. 보기 전용. 웹은 메뉴 단추로 엶.
|
||||
- 검증완료: `test_est_jdl_compare.py`(같음·차이 1·없음·+50행 한계·xls 셀 변환) 통과. ORCA 브라우저에서 일위대가 8개 중 같음 1·차이 10·없음 6 표시.
|
||||
- 제외: TfrmMultiEditor(인쇄 부품의 설계용 편집기 — 실행 파일에 클래스 없음), Est Plus(구버전 .est) 단가비교·내보내기(숨은 단축키, 394바이트 레코드 형식 — 구버전 불러오기와 함께 남음).
|
||||
- [x] 검색 결과 근거자료보기(원본 TfrmFoundDetail btnClose 0xefa7e8) — 통합 검색 결과의 일위·단산·기계 줄 「근거」: 그 작업의 호표 세부 줄(단산은 산출근거 글)과 총액·노무·재료·경비, 「적용」은 그 결과를 고르고 검색 창으로(가져오기는 검색 창에서). 고른 결과는 창을 오가도 유지.
|
||||
- 검증완료: 바인딩·통합 검색 시험 통과. ORCA 브라우저에서 「거푸집」 일위 → 「[합판거푸집]의 근거자료보기」 총액 53,029·노무 38,289·재료 14,740·세부 6줄, 적용 → 그 결과 체크.
|
||||
- [x] ES 정보(원본 mnuESInfo → TfrmESInfo 0x10b838c) — 기준시점·비교시점·최종기성일·선급금지급일(기본 오늘)·선급금지급액을 `project.esInfo` 에 기록(원본 [EscInfo] SDATE·CDATE·ACDATE·PreDATE·PreCost). 원본도 ES 제출 파일 작성에만 씀 — 웹 물가변동 계산과 잇지 않음, ESTX 에는 남지 않음.
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
<script defer src="native-cell-clip.js"></script>
|
||||
<script defer src="native-row-links.js"></script>
|
||||
<script defer src="native-es-info.js"></script>
|
||||
<script defer src="native-jdl-compare.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>
|
||||
@@ -247,6 +248,7 @@
|
||||
><button data-view="change">설계변경</button><button data-view="progress">기성고</button
|
||||
><button data-view="escalation">물가변동</button
|
||||
><button data-action="es-info">ES 정보</button
|
||||
><button data-action="jdl-compare">JDL엑셀과 단가비교</button
|
||||
><button data-view="annual">연차공사</button><button data-view="review">계약심사</button
|
||||
><button data-view="resources">동원인원·자재·장비</button>
|
||||
<button data-view="schedule">예정공정표</button>
|
||||
|
||||
@@ -633,7 +633,8 @@ const EstBusinessActions = (() => {
|
||||
(typeof EstDisplay !== "undefined" && EstDisplay.handle(name, state)) ||
|
||||
(typeof EstCellClip !== "undefined" && EstCellClip.handle(name, state)) ||
|
||||
(typeof EstRowLinks !== "undefined" && EstRowLinks.handle(name, state)) ||
|
||||
(typeof EstEsInfo !== "undefined" && EstEsInfo.handle(name, state))
|
||||
(typeof EstEsInfo !== "undefined" && EstEsInfo.handle(name, state)) ||
|
||||
(typeof EstJdlCompare !== "undefined" && EstJdlCompare.handle(name, state, node))
|
||||
) {
|
||||
EstApp.render();
|
||||
} else if (name === "insert-annual") {
|
||||
|
||||
@@ -299,5 +299,16 @@ const EstExcelMap = (() => {
|
||||
return true;
|
||||
}
|
||||
|
||||
return { fields, columnIndex, columnName, records, rows, insert, load, shift, handle };
|
||||
return {
|
||||
fields,
|
||||
columnIndex,
|
||||
columnName,
|
||||
records,
|
||||
rows,
|
||||
insert,
|
||||
load,
|
||||
shift,
|
||||
sheetTables,
|
||||
handle,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// 원본 「JDLExcel과 단가비교」(ActionJDLCompare, 숨은 단축키 Ctrl+Alt+J → TfrmJDLExcelCompare) — 일위대가·단가산출·기계경비 호표마다
|
||||
// Excel 의 코드 열에서 그 코드를 품은 셀(Pos(코드, 셀) > 0)을 1행~(호표 수+50)행 안에서 찾아 같은 행 합계 열 값과 TC 를 비교한다.
|
||||
// 차이(|TC − Excel|)가 있으면 표시, 못 찾으면 「없음」. 자료는 바꾸지 않는다(보기 전용). Excel 은 .xlsx·.xls 모두.
|
||||
const EstJdlCompare = (() => {
|
||||
const TABLES = [
|
||||
["HeadTitleDat", "일위대가"],
|
||||
["CostTitleDat", "단가산출"],
|
||||
["MachTitleDat", "기계경비"],
|
||||
];
|
||||
function compare(rows, table, codeCol, totalCol) {
|
||||
const limit = rows.length + 50;
|
||||
return rows
|
||||
.filter((r) => String(r.Code ?? "").trim())
|
||||
.map((r) => {
|
||||
const code = String(r.Code).trim();
|
||||
let found = null;
|
||||
for (let i = 0; i < Math.min(limit, table.length); i++) {
|
||||
const cell = table[i]?.[codeCol];
|
||||
if (cell != null && String(cell).includes(code)) {
|
||||
found = Number(String(table[i][totalCol] ?? "").replace(/,/g, "")) || 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const tc = Number(r.TC) || 0;
|
||||
return {
|
||||
Code: code,
|
||||
Name: r.Name ?? "",
|
||||
Size: r.Size ?? "",
|
||||
Unite: r.Unite ?? "",
|
||||
TC: tc,
|
||||
OTC: found,
|
||||
GN: found == null ? null : Math.abs(tc - found),
|
||||
Checked: found == null || Math.abs(tc - found) > 1e-9,
|
||||
};
|
||||
});
|
||||
}
|
||||
// .xls(BIFF) 셀 → 행 배열
|
||||
function xlsTables(sheets) {
|
||||
const out = {};
|
||||
for (const s of sheets) {
|
||||
const table = [];
|
||||
for (const [k, c] of Object.entries(s.cells)) {
|
||||
const [r, col] = k.split(",").map(Number);
|
||||
(table[r] ||= [])[col] = c.value;
|
||||
}
|
||||
out[s.name] = table;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function form(state) {
|
||||
const c = (state.jdlCompare ||= {
|
||||
tab: 0,
|
||||
sheet: 1,
|
||||
code: "A",
|
||||
total: "H",
|
||||
tables: null,
|
||||
result: [],
|
||||
}),
|
||||
{ esc, number } = EstApp,
|
||||
diff = c.result.filter((r) => r.Checked).length;
|
||||
EstApp.showDialog(
|
||||
"JDL엑셀 단가 비교",
|
||||
`<div class="business-tools">${TABLES.map(([, l], i) => `<button data-action="jdl-compare-tab" data-i="${i}"${i === c.tab ? ' style="font-weight:bold"' : ""}>${l}</button>`).join("")}</div><div class="form-fields"><label>시트번호<input name="jc-sheet" type="number" min="1" value="${c.sheet}"></label><label>코드칼럼<input name="jc-code" value="${esc(c.code)}" size="3"></label><label>합계셀칼럼<input name="jc-total" value="${esc(c.total)}" size="3"></label></div><button data-action="jdl-compare-open">엑셀파일열기</button> <button data-action="jdl-compare-run">검 색</button> ${c.tables ? `시트 ${Object.keys(c.tables).length}개` : "Excel 을 여세요"} · 다른 금액·없음 ${diff}건<div style="max-height:380px;overflow:auto"><table class="sheet"><thead><tr><th>코드</th><th>품명</th><th>규격</th><th>단위</th><th>EST 금액</th><th>Excel 금액</th><th>차이</th></tr></thead><tbody>${c.result.map((r) => `<tr${r.Checked ? ' style="background:#fde2e2"' : ""}><td>${esc(r.Code)}</td><td style="text-align:left">${esc(r.Name)}</td><td>${esc(r.Size)}</td><td>${esc(r.Unite)}</td><td>${number(r.TC)}</td><td>${r.OTC == null ? "없음" : number(r.OTC)}</td><td>${r.GN == null ? "" : number(r.GN)}</td></tr>`).join("")}</tbody></table></div>`,
|
||||
);
|
||||
}
|
||||
function read(c) {
|
||||
const v = (n) => document.querySelector(`#dialog-content [name="${n}"]`)?.value;
|
||||
c.sheet = Math.max(1, Number(v("jc-sheet")) || 1);
|
||||
c.code = String(v("jc-code") || "A").toUpperCase();
|
||||
c.total = String(v("jc-total") || "H").toUpperCase();
|
||||
}
|
||||
async function run(name, state, node) {
|
||||
if (name === "jdl-compare") return form(state);
|
||||
const c = state.jdlCompare;
|
||||
read(c);
|
||||
if (name === "jdl-compare-tab") {
|
||||
c.tab = Number(node.dataset.i);
|
||||
c.result = [];
|
||||
} else if (name === "jdl-compare-open") {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".xlsx,.xls";
|
||||
input.addEventListener("change", async () => {
|
||||
try {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
const body = await file.arrayBuffer();
|
||||
c.tables = /\.xls$/i.test(file.name)
|
||||
? xlsTables(await EstApp.api("/api/read-xls", { method: "POST", body }))
|
||||
: EstExcelMap.sheetTables(
|
||||
await EstApp.api("/api/import-xlsx", { method: "POST", body }),
|
||||
);
|
||||
} catch (error) {
|
||||
state.notice = error.message;
|
||||
}
|
||||
form(state);
|
||||
});
|
||||
return input.click();
|
||||
} else if (name === "jdl-compare-run") {
|
||||
if (!c.tables) throw Error("엑셀파일을 먼저 여세요.");
|
||||
const table = Object.values(c.tables)[c.sheet - 1];
|
||||
if (!table) throw Error(`시트 ${c.sheet} 이 없습니다.`);
|
||||
const col = (x) => EstExcelMap.columnIndex(x);
|
||||
c.result = compare(
|
||||
state.project.tables[TABLES[c.tab][0]] || [],
|
||||
table,
|
||||
col(c.code),
|
||||
col(c.total),
|
||||
);
|
||||
}
|
||||
form(state);
|
||||
}
|
||||
const names = new Set(["jdl-compare", "jdl-compare-tab", "jdl-compare-open", "jdl-compare-run"]);
|
||||
function handle(name, state, node) {
|
||||
if (!names.has(name)) return false;
|
||||
run(name, state, node).catch((error) => {
|
||||
state.notice = error.message;
|
||||
EstApp.render();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return { compare, xlsTables, handle };
|
||||
})();
|
||||
@@ -5,6 +5,7 @@ const http = require("node:http"),
|
||||
vm = require("node:vm"),
|
||||
crypto = require("node:crypto");
|
||||
const { importEstx, table: dbisamTable } = require("./import-estx.cjs");
|
||||
const { readXls } = require("./xls-read.cjs");
|
||||
const exchange = require("./exchange.cjs");
|
||||
const { exportEstx } = require("./export-estx.cjs");
|
||||
const jdl = require("./jdl.cjs");
|
||||
@@ -83,6 +84,7 @@ const allowed = new Set([
|
||||
"native-cell-clip.js",
|
||||
"native-row-links.js",
|
||||
"native-es-info.js",
|
||||
"native-jdl-compare.js",
|
||||
"native-wonga-tools.js",
|
||||
"native-estimate-print.js",
|
||||
"native-xml-check.js",
|
||||
@@ -245,6 +247,10 @@ const server = http.createServer(async (req, res) => {
|
||||
reply(res, 200, importEstx(bytes, name));
|
||||
return;
|
||||
}
|
||||
if (req.method === "POST" && url.pathname === "/api/read-xls") {
|
||||
reply(res, 200, readXls(await body(req)));
|
||||
return;
|
||||
}
|
||||
if (req.method === "POST" && url.pathname === "/api/pps-table") {
|
||||
// 조달청 코드 목록(원본 System\MainSys\PPS_STUFFC.dat · PPS_HEAD.dat, DBISAM) — 코드·명칭·규격·단위만
|
||||
const rows = dbisamTable(await body(req), null, []);
|
||||
|
||||
Reference in New Issue
Block a user