diff --git a/resources/tester/helper_est_native.cjs b/resources/tester/helper_est_native.cjs index 9c387903f..587ebb92f 100644 --- a/resources/tester/helper_est_native.cjs +++ b/resources/tester/helper_est_native.cjs @@ -39,6 +39,7 @@ for (const file of [ "native-annual-copy.js", "native-print-range.js", "native-work-mode.js", + "native-dongwon-check.js", "native-business-actions.js", "native-forms.js", "native-excel-to-est.js", diff --git a/resources/tester/test_est_dongwon_check.py b/resources/tester/test_est_dongwon_check.py new file mode 100644 index 000000000..f64530cf4 --- /dev/null +++ b/resources/tester/test_est_dongwon_check.py @@ -0,0 +1,26 @@ +"""동원 확인 탭: 호표 1 단위 펼침 합과 호표 금액 대조(10원 넘으면 표시).""" + +from test_est_rea_parity import run + + +def test_head_check_flags_mismatch_over_ten_won(): + result = run( + "math", + """(()=>{ + const p=EstNative.empty();p.options.SourceRefresh=false; + p.tables.LaborcDat=[{Code:'L1',Name:'보통인부',TC:100,NC:100}]; + p.tables.StuffcDat=[{Code:'M1',Name:'자갈',TC:50,GC:50}]; + p.tables.HeadMainDat=[{Code:'H1',Index:1,Name:'A'},{Code:'L1',LinkIndex:6,GN:2,NU:100,NC:200}, + {Code:'M1',LinkIndex:5,GN:1,GU:50,GC:50}]; + const q=EstNative.calculate(p); + const a=EstDongwonCheck.check(q,103).rows[0]; + q.tables.HeadTitleDat[0].TC=a.TC+11; + const b=EstDongwonCheck.check(q,103).rows[0]; + return {tc:a.TC,sum:Math.trunc(a.sum),calc:[a.calc.NC,a.calc.GC],a:a.checked,b:b.checked, + items:a.items.map(r=>r.Code).sort()}; + })()""", + ) + assert result["tc"] == 250 and result["sum"] == 250 + assert result["calc"] == [200, 50] + assert result["a"] is False and result["b"] is True + assert result["items"] == ["L1", "M1"] diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 8d1c71edf..ef41f72bb 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -506,6 +506,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - [x] 작업모드 표준내역서·표준설계변경·최근 계약심사목록(spec_pc §4) — 작업모드 메뉴 내역서·설계변경·표준내역서·표준설계변경이 모드 값(CurrentWorkMode, ESTX 읽기·쓰기)을 바꿈. 일반↔표준 묶음이 바뀌면 지금 원가계산서를 옛 묶음 칸에 두고 새 묶음 것을 꺼냄(없으면 설치 양식목록 — 일반 WongaFileList·표준 SWongaFileList 각 30종 — 에서 고름, 번호 기준 계산). 원본은 모드를 바꿀 때 문서를 닫고 최근 목록을 띄우나 웹은 지금 작업 유지. 최근 계약심사목록 = 저장한 작업 중 계약심사 입력이 있는 것. - 미결: 표준 양식의 산업안전보건관리비 식 「A × 0.496」 의 A 가 무엇인지 미확인 — 웹은 「참조 항목 없음: A」 로 알림(원본 확인 필요). - 검증완료: 작업모드 시험 2개·ESTX 저장 시험 34개 통과. ORCA 브라우저에서 표준내역서로 바꿈 → 표준 양식 30종 → 열기, 내역서로 돌아오면 원래 원가계산서 31행 복원, 계약심사목록 창 확인, 원복. + - [x] 동원 확인·내역서 탭(TfrmDongWon Button1~4 · spec_part §4) — 동원 화면 단추 확인 장비·단산·일위: 호표마다 1 단위를 펼친 항목 금액 합과 호표 금액을 대조해 10원 넘게 어긋나면 표시(어긋난 것만 보기), 행을 고르면 당초값·계산값(노무·재료·경비·실적)·항목 목록. 내역서: 내역 줄마다 펼친 항목. 웹 동원 계산은 기계경비를 더 쪼개지 않아 장비 확인은 늘 맞음. + - 검증완료: 확인 시험 1개·동원 관련 시험 35개 통과. ORCA 브라우저에서 일위 8건 모두 일치·당초값=계산값, 항목 3개 표시. - [x] 다른 공사 불러오기 창(원본 barbtnSelfLoad → TfrmOtherLoad, 찾기 popSerch) — 저장된 다른 작업을 골라 같은 표를 띄우고 찾기(품명·규격, A&B 둘 다)·고른 줄을 하위 자료와 함께 가져오기(내역서는 고른 줄 아래에 줄, 나머지는 자료·호표 합침, 같은 코드는 이 공사 것). 검증완료: 바인딩 통과, ORCA 브라우저에서 산불진화임도 작업의 일위대가 3개 → 「취수장」 1개 → 가져오기 호표 8→9·하위 41건, 샘플 복구. - [x] 사용위치 검색 창(원본 TfrmUsesDataSerch btnSerch·btnAllDisplay) — 현재 표 항목을 품명·규격(빈칸·|·괄호 무시)으로 거르고 골라 사용위치 보기. 검증완료: 바인딩 통과, ORCA 브라우저에서 노임 9개 → 「보 통」 1개 → 보통인부 사용위치 11곳. - [x] 인쇄양식 불러오기·저장하기(원본 mnuPrintFormatLoad·Save 0x10ed318·0x10ed000 — spec_pc §3) — .pfot(인쇄정보표 MIDAS 이진) 읽기·쓰기 `print-format.cjs`(설명 BLOB·빈 칸 표지까지 보존, 원본 파일 그대로 다시 쓰면 바이트 같음). 불러오면 공사 인쇄표를 그 값으로, 저장은 지금 인쇄표를 원본 설치 main.pfot 필드 설명자 위에(`EST_PFOT_TEMPLATE`). .gfot(cxGrid 열 배치)는 웹 격자와 호환되지 않아 하지 않음. diff --git a/tools/est-test/index.html b/tools/est-test/index.html index 0f550c118..28897203e 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -27,6 +27,7 @@ + diff --git a/tools/est-test/native-actions-more.js b/tools/est-test/native-actions-more.js index b032c9f77..973a9b54d 100644 --- a/tools/est-test/native-actions-more.js +++ b/tools/est-test/native-actions-more.js @@ -12,6 +12,7 @@ const EstActionsMore = (() => { typeof EstAnnualCopy !== "undefined" && EstAnnualCopy, typeof EstPrintRange !== "undefined" && EstPrintRange, typeof EstWorkMode !== "undefined" && EstWorkMode, + typeof EstDongwonCheck !== "undefined" && EstDongwonCheck, ].filter(Boolean); function handle(name, state, node) { for (const module of modules()) if (module.handle(name, state, node)) return true; diff --git a/tools/est-test/native-dongwon-check.js b/tools/est-test/native-dongwon-check.js new file mode 100644 index 000000000..2b52e19d4 --- /dev/null +++ b/tools/est-test/native-dongwon-check.js @@ -0,0 +1,122 @@ +// 원본 동원인원·자재·장비 「확인」·「내역서」 탭(TfrmDongWon Button1~4 — spec_part §4): 동원 계산이 펼친 중간 결과 보기. +// 장비·단산·일위 = 호표마다 1 단위를 동원 항목으로 펼쳐 금액 합을 내고, 호표 금액(TC)과 10원 넘게 어긋나면 표시(1095cdc +// Checked := |TC − Trunc(Σ Cost)| > 10). 행을 고르면 당초값(호표 TC·NC·GC·JC)과 계산값(항목 금액을 노무·재료·경비·실적으로 +// 나눠 더한 값)·항목 목록. 내역서 = 내역 줄마다 펼친 항목(점검 없음). 웹 동원 계산은 기계경비를 더 쪼개지 않아 장비는 늘 맞음. +const EstDongwonCheck = (() => { + const num = (v) => Number(v) || 0; + const TITLE = { 103: "HeadTitleDat", 104: "CostTitleDat", 105: "MachTitleDat" }; + const KIND = { 5: "자재", 6: "노임", 21: "표준", 101: "견적", 105: "기계" }; + // 항목 → 비목: 노임·노무비 금액분 = 노무, 자재·견적·재료비 금액분 = 재료, 기계·경비 = 경비, 표준 = 실적 + const part = (item) => + item.Code === "LZZZZZZZZZZ" || item.kind === 6 + ? "NC" + : item.Code === "EZZZZZZZZZZ" || item.kind === 105 + ? "JC" + : item.Code === "SZZZZZZZZZZ" || item.kind === 21 + ? "SC" + : "GC"; + + function check(project, kind) { + const titles = (project.tables[TITLE[kind]] || []).filter((t) => t.Code); + const out = EstResources.calculate( + project, + true, + titles.map((t) => ({ kind, code: t.Code, WorkCode: t.Code, WorkName: t.Name })), + ); + const items = new Map(); + for (const row of out.rows) + (items.get(row.WorkCode) || items.set(row.WorkCode, []).get(row.WorkCode)).push(row); + return { + errors: out.errors, + rows: titles.map((t) => { + const list = items.get(t.Code) || [], + sum = list.reduce((s, r) => s + num(r.amount), 0), + calc = { NC: 0, GC: 0, JC: 0, SC: 0 }; + for (const r of list) calc[part(r)] += num(r.amount); + return { + Code: t.Code, + Name: t.Name, + Size: t.Size, + TC: num(t.TC), + NC: num(t.NC), + GC: num(t.GC), + JC: num(t.JC), + sum, + calc, + checked: Math.abs(num(t.TC) - Math.trunc(sum)) > 10, + items: list, + }; + }), + }; + } + function estimate(project) { + const out = EstResources.calculate(project, true), + groups = new Map(); + for (const row of out.rows) { + const key = `${row.WorkCode}\u0000${row.WorkName}`; + if (!groups.has(key)) groups.set(key, { Code: row.WorkCode, Name: row.WorkName, items: [] }); + groups.get(key).items.push(row); + } + return { errors: out.errors, rows: [...groups.values()] }; + } + + let shown = null; + function draw(state) { + const { esc, number } = EstApp, + { kind, data, pick, only } = shown; + const list = data.rows.map((r, i) => [r, i]).filter(([r]) => !only || r.checked); + const chosen = data.rows[pick]; + const head = + kind === "est" + ? "
당초값 계 ${number(chosen.TC)} · 노무 ${number(chosen.NC)} · 재료 ${number(chosen.GC)} · 경비 ${number(chosen.JC)}
계산값 계 ${number(Math.trunc(chosen.sum))} · 노무 ${number(Math.trunc(chosen.calc.NC))} · 재료 ${number(Math.trunc(chosen.calc.GC))} · 경비 ${number(Math.trunc(chosen.calc.JC))}${chosen.calc.SC ? ` · 실적 ${number(Math.trunc(chosen.calc.SC))}` : ""}
| 종류 | 코드 | 품명 | 규격 | 소요량 | 금액 |
|---|---|---|---|---|---|
| ${esc(KIND[r.kind] || r.kind)} | ${esc(r.Code)} | ${esc(r.Name)} | ${esc(r.Size ?? "")} | ${number(r.quantity)} | ${number(r.amount)} |
위에서 행을 고르세요.
"; + const title = { 105: "확인 — 장비", 104: "확인 — 단산", 103: "확인 — 일위", est: "내역서" }[ + kind + ]; + EstApp.showDialog( + `동원인원,자재,장비 · ${title}`, + `${kind === "est" ? "" : ``} ${data.rows.length}건${esc(data.errors.slice(0, 5).join(" / "))}
` : ""}`, + ); + } + function install() { + document.addEventListener("click", (event) => { + const tr = event.target.closest?.("[data-dw-pick]"); + if (!tr || !shown) return; + shown.pick = Number(tr.dataset.dwPick); + draw(EstApp.state); + }); + document.addEventListener("change", (event) => { + if (event.target.dataset?.dwOnly === undefined || !shown) return; + shown.only = event.target.checked; + draw(EstApp.state); + }); + } + if (typeof document !== "undefined" && document.addEventListener) install(); + + function handle(name, state, node) { + if (name !== "dongwon-check") return false; + const kind = node.dataset.kind === "est" ? "est" : Number(node.dataset.kind); + shown = { + kind, + data: kind === "est" ? estimate(state.project) : check(state.project, kind), + pick: -1, + only: false, + }; + draw(state); + return true; + } + return { check, estimate, handle }; +})(); diff --git a/tools/est-test/native-extra.js b/tools/est-test/native-extra.js index ab5a97725..10c8ead8e 100644 --- a/tools/est-test/native-extra.js +++ b/tools/est-test/native-extra.js @@ -220,7 +220,11 @@ const EstExtra = (() => { }); html = box( "동원인원·자재·장비", - `${kindSelect} ${byWorkCheck} (총 ${filtered.length}건)`, + `${kindSelect} ${byWorkCheck} (총 ${filtered.length}건)` + + button("dongwon-check", "확인 장비", 'data-kind="105"') + + button("dongwon-check", "확인 단산", 'data-kind="104"') + + button("dongwon-check", "확인 일위", 'data-kind="103"') + + button("dongwon-check", "내역서(줄별)", 'data-kind="est"'), table(headers, rows), esc(output.errors.join(" / ")), ); diff --git a/tools/est-test/native-resources.js b/tools/est-test/native-resources.js index 3189fe6a3..818741dbf 100644 --- a/tools/est-test/native-resources.js +++ b/tools/est-test/native-resources.js @@ -1,6 +1,7 @@ const EstResources = (() => { const { num, components } = EstMath; - function calculate(input, byWork = false) { + // roots: [{ kind, code, WorkCode, WorkName }] 를 주면 내역서 대신 그 호표마다 1 단위를 따로 펼친다(동원 확인 탭) + function calculate(input, byWork = false, roots = null) { const project = EstNative.calculate({ ...structuredClone(input), options: { ...input.options, ResourceTracing: true }, @@ -180,7 +181,12 @@ const EstResources = (() => { const source = tables[EstNative.sourceTables[kind]]?.find((row) => row.Code === code); record(kind, code, multiplier * weightedRate(source, rates)); } - for (const [index, row] of tables.EstMainDat.entries()) { + for (const [index, root] of (roots || []).entries()) { + workKey = `${index}:`; + work = { WorkCode: root.WorkCode, WorkName: root.WorkName }; + expand(root.kind, root.code, 1, []); + } + for (const [index, row] of (roots ? [] : tables.EstMainDat).entries()) { if (num(row.LinkIndex) === 54) break; if ( num(row.Level) > 0 || diff --git a/tools/est-test/server.cjs b/tools/est-test/server.cjs index 8784c7f9d..6a77b94a2 100644 --- a/tools/est-test/server.cjs +++ b/tools/est-test/server.cjs @@ -111,6 +111,7 @@ const allowed = new Set([ "native-annual-copy.js", "native-print-range.js", "native-work-mode.js", + "native-dongwon-check.js", "native-actions-more.js", "native-wonga-tools.js", "native-estimate-print.js",