From 188b50fa2f27f0ccaa95e06e45ec0443cb5caaf6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 12 Nov 2026 19:16:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(est-test):=20=EC=97=B0=EC=B0=A8=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=83=80=20=EB=B3=B5=EC=82=AC=20=EC=98=B5?= =?UTF-8?q?=EC=85=98(=EB=8D=AE=EC=96=B4=EC=93=B0=EA=B8=B0=C2=B7=EC=97=B4?= =?UTF-8?q?=20=EC=82=BD=EC=9E=85,=20=EB=B3=B5=EC=82=AC=EB=82=B4=EC=9A=A9?= =?UTF-8?q?=20=EA=B3=A0=EB=A5=B4=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 고른 내역 행을 차수 하나의 놓을 행부터 복사 - 수량·단가금액·명칭규격 고르기, 설계변경 상단/하단, 행 금액 다시 셈 - 내역서 도구줄과 연차 화면에 단추 Co-Authored-By: Claude Opus 5.5 --- resources/tester/helper_est_native.cjs | 1 + resources/tester/test_est_annual_copy.py | 46 +++++++++ tools/est-test/PLAN.md | 2 + tools/est-test/index.html | 2 + tools/est-test/native-actions-more.js | 1 + tools/est-test/native-annual-copy.js | 113 +++++++++++++++++++++++ tools/est-test/native-extra.js | 4 +- tools/est-test/server.cjs | 1 + 8 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 resources/tester/test_est_annual_copy.py create mode 100644 tools/est-test/native-annual-copy.js diff --git a/resources/tester/helper_est_native.cjs b/resources/tester/helper_est_native.cjs index 6208af505..c84b717cb 100644 --- a/resources/tester/helper_est_native.cjs +++ b/resources/tester/helper_est_native.cjs @@ -36,6 +36,7 @@ for (const file of [ "native-extra.js", "native-applied-cost.js", "native-progress-view.js", + "native-annual-copy.js", "native-business-actions.js", "native-forms.js", "native-excel-to-est.js", diff --git a/resources/tester/test_est_annual_copy.py b/resources/tester/test_est_annual_copy.py new file mode 100644 index 000000000..33e1ffd30 --- /dev/null +++ b/resources/tester/test_est_annual_copy.py @@ -0,0 +1,46 @@ +"""연차 데이타 복사 옵션: 덮어쓰기·열 삽입·복사내용 게이트·상단(당초) 값·끝 행 넘침.""" + +from test_est_rea_parity import run + + +def test_overwrite_copies_chosen_groups_and_recalculates_amounts(): + result = run( + "math", + """(()=>{ + const p=EstNative.empty(); + p.tables.EstMainDat=[{Code:'A',Name:'a',Size:'s',GN:2,NU:10,GU:5},{Code:'B',Name:'b',GN:3,NU:1,GU:1}, + {Code:'C',Name:'c',GN:1,NU:7,GU:0,GN_:4,Name_:'c0'}]; + p.annual=[{name:'1차',formula:'[기준]'},{name:'2차',formula:'[기준]',rows:[]}]; + const n=EstAnnualCopy.apply(p,{sources:[0,2],target:1,year:0,insert:false,name:false,gn:true,cost:false}); + const r=p.annual[0].rows; + const q=structuredClone(p); + const m=EstAnnualCopy.apply(q,{sources:[2],target:2,year:1,insert:false,name:true,gn:true,cost:true,top:true}); + const over=EstAnnualCopy.apply(structuredClone(p),{sources:[0,1,2],target:2,year:0,name:true,gn:true,cost:true}); + return {n,len:r.length,first:r[0],row1:[r[1].Name,r[1].GN,r[1].NC,r[1].GC,r[1].TC],row2:[r[2].Name,r[2].GN,r[2].NC], + top:[q.annual[1].rows[2].Name,q.annual[1].rows[2].GN,q.annual[1].rows[2].NC],m,over, + parse:EstAnnualCopy.parse('1-3, 5')}; + })()""", + ) + assert result["n"] == 2 + assert result["first"] is None + assert result["row1"] == ["b", 2, 2, 2, 4] + assert result["row2"] == ["c", 1, 7] + assert result["top"] == ["c0", 4, 28] + assert result["m"] == 1 + assert result["over"] == 1 + assert result["parse"] == [0, 1, 2, 4] + + +def test_insert_mode_shifts_main_and_every_loaded_year(): + result = run( + "math", + """(()=>{ + const p=EstNative.empty(); + p.tables.EstMainDat=[{Name:'a',GN:1,NU:1},{Name:'b',GN:1,NU:1}]; + p.annual=[{name:'1차',formula:'[기준]'},{name:'2차',formula:'[기준]',rows:[{Name:'x'},{Name:'y'}]}]; + EstAnnualCopy.apply(p,{sources:[1],target:1,year:0,insert:true,name:true,gn:true,cost:true}); + return {main:p.tables.EstMainDat.map(r=>r.Name??null),y1:p.annual[0].rows.map(r=>r?.Name??null), + y2:p.annual[1].rows.map(r=>r?.Name??null)}; + })()""", + ) + assert result == {"main": ["a", None, "b"], "y1": [None, "b"], "y2": ["x", None, "y"]} diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 67371cb6d..c6cd3ca51 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -499,6 +499,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - 검증완료: 시험 1개 통과(당초 수량 4·변경 10). ORCA 브라우저에서 고르기 창·회차 loadMethod 확인, 원복. - [x] 근거자료보기 고치기·붙여넣기(TfrmGunguEdit popPaste · spec_pc §8 · spec_sort_memo §3) — 근거(메모)를 문단·굵게·표·그림(자리표)으로 펼쳐 고치고, 붙여넣기(서식 있는 글·표)·저장. 저장은 원본 WPTools 글 형식(굴림체 보통·굵게, 표 기본 테두리, 그림 원문 그대로, zlib). 문단 간격·들여쓰기·글꼴 크기는 버림. 원래 메모가 있던 행은 ESTX 저장 가능, 새 메모는 JSON 저장만(새 BLOB 할당 미지원). - 검증완료: 메모 시험 2개·정리 시험 2개 통과. 설치 샘플 메모 201건 글자 201/201 일치·그림 71/71 보존·되쓰기 188/201 동일(나머지는 칸 안 빈 줄만 빠짐), 고친 메모 ESTX 저장 → 다시 읽기 일치. ORCA 브라우저에서 굵게·표 저장 후 다시 열기 확인, 원복. + - [x] 연차 데이타 복사 옵션(TfrmMultiDataCopyMethod · spec_pb2 §1) — 원본의 끌어 놓기 대신 단추: 고른 내역 행(번호 범위)을 차수 하나의 놓을 행부터. 덮어쓰기 / 열 삽입후 내용복사(빈 행 넣고 모든 차수 한 줄 밀림), 복사내용 수량·단가금액·명칭규격(마지막 선택 기억), 설계변경이면 상단(당초 `_`)·하단(변경). 행 금액은 수량×단가로 다시 셈, 끝 행 넘침은 버림. 원본의 집계형 격자(Tag 18 Total*) 끌기는 웹에 그 격자가 없어 뺌. + - 검증완료: 시험 2개·연차 시험 통과. ORCA 브라우저에서 내역 3·4행 → 1차 10행 복사, 차수 계산 오류 없음 확인, 원복. - [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 2a79d4141..1517848e0 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -24,6 +24,7 @@ + @@ -170,6 +171,7 @@ > diff --git a/tools/est-test/native-actions-more.js b/tools/est-test/native-actions-more.js index 1abf375ef..0e0080054 100644 --- a/tools/est-test/native-actions-more.js +++ b/tools/est-test/native-actions-more.js @@ -9,6 +9,7 @@ const EstActionsMore = (() => { typeof EstOtherLoad !== "undefined" && EstOtherLoad, typeof EstAppliedCost !== "undefined" && EstAppliedCost, typeof EstProgressView !== "undefined" && EstProgressView, + typeof EstAnnualCopy !== "undefined" && EstAnnualCopy, ].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-annual-copy.js b/tools/est-test/native-annual-copy.js new file mode 100644 index 000000000..87ac805a0 --- /dev/null +++ b/tools/est-test/native-annual-copy.js @@ -0,0 +1,113 @@ +// 원본 연차공사 「데이타 복사 옵션」(TfrmMultiDataCopyMethod btnApply 0xedaba4 → TDataCopyMulti eedc54 — spec_pb2 §1): +// 내역서 행을 골라 차수 하나의 놓을 행부터 차례로 옮긴다. 복사방법 = 덮어쓰기 · 열 삽입후 내용복사(놓을 자리에 빈 행을 넣어 +// 모든 차수가 한 줄 밀림). 복사내용 = 명칭·규격(Name·Size·Unite) · 수량(GN) · 단가·금액(TU·TC·NU·NC·GU·GC·JU·JC·SU·SC). +// 설계변경 내역이면 상단(당초 = 「_」 짝 칸) / 하단(변경 = 지금 값). 고른 차수 줄이 없으면 지금 내역 줄을 바탕으로 하고, +// 행마다 금액을 수량×단가로 다시 셈(f1a078). 끝 행을 넘는 나머지는 버림. 코드·연결 등 다른 칸은 그대로. +const EstAnnualCopy = (() => { + const num = (v) => Number(v) || 0; + const NAME = ["Name", "Size", "Unite"], + COST = ["TU", "TC", "NU", "NC", "GU", "GC", "JU", "JC", "SU", "SC"]; + const KEY = "est-annual-copy"; + + function apply(project, { sources, target, year, insert, name, gn, cost, top }) { + const main = project.tables.EstMainDat, + chosen = project.annual?.[year]; + if (!chosen) throw Error("입력할 차수를 선택하세요."); + if (!(target >= 0 && target <= main.length)) throw Error("놓을 행이 없습니다."); + const picked = sources.map((i) => structuredClone(main[i])).filter(Boolean); + if (insert) { + main.splice(target, 0, {}); + for (const y of project.annual) if (Array.isArray(y.rows)) y.rows.splice(target, 0, null); + } + chosen.rows ||= []; + let count = 0; + picked.forEach((src, k) => { + const t = target + k; + if (t >= main.length) return; + const read = (f) => (top && src[f + "_"] != null ? src[f + "_"] : src[f]); + const row = chosen.rows[t] ? { ...chosen.rows[t] } : structuredClone(main[t]); + if (name) for (const f of NAME) row[f] = read(f) ?? ""; + if (gn) row.GN = num(read("GN")); + if (cost) for (const f of COST) row[f] = num(read(f)); + // 행 금액 계산: 비목 금액 = 수량 × 비목 단가, 합계 = 비목 합 + for (const p of ["N", "G", "J", "S"]) + row[p + "C"] = Math.trunc(num(row.GN) * num(row[p + "U"])); + row.TC = ["NC", "GC", "JC", "SC"].reduce((s, f) => s + num(row[f]), 0); + row.TU = ["NU", "GU", "JU", "SU"].reduce((s, f) => s + num(row[f]), 0); + while (chosen.rows.length < t) chosen.rows.push(null); + chosen.rows[t] = row; + count++; + }); + return count; + } + + // 번호 목록 "1-3,5" → 0부터 차례 + function parse(text) { + const out = []; + for (const part of String(text) + .split(/[,\s]+/) + .filter(Boolean)) { + const [a, b] = part.split("-").map(Number); + if (!Number.isInteger(a)) throw Error(`행 번호 오류: ${part}`); + for (let i = a; i <= (Number.isInteger(b) ? b : a); i++) out.push(i - 1); + } + return out; + } + + function dialog(state) { + const project = state.project, + { esc } = EstApp; + if (!project.annual?.length) throw Error("차수를 먼저 추가하세요."); + const saved = JSON.parse(localStorage.getItem(KEY) || "{}"), + o = { insert: false, gn: true, cost: true, name: true, ...saved }; + const picked = state.view === "EstMainDat" ? [...state.selection].sort((a, b) => a - b) : []; + const range = picked.length ? picked.map((i) => i + 1).join(",") : "1"; + const change = project.options.Mode === "설계변경"; + const check = (k, label) => + ``; + EstApp.showDialog( + "데이타 복사 옵션", + `
` + + `` + + `
복사내용${check("gn", "수량")}${check("cost", "단가, 금액")}${check("name", "명칭, 규격")}
` + + (change + ? `` + : "") + + `
복사위치${project.annual.map((y, i) => ``).join("")}
` + + ``, + ); + } + function run(state) { + const box = document.getElementById("dialog-content"), + get = (k) => box.querySelector(`[data-ac="${k}"]`); + const year = box.querySelector('[name="ac-year"]:checked'); + if (!year) throw Error("입력할 차수를 선택하세요."); + const opts = { + insert: get("insert").value === "1", + gn: get("gn").checked, + cost: get("cost").checked, + name: get("name").checked, + }; + localStorage.setItem(KEY, JSON.stringify(opts)); + let count = 0; + EstApp.change( + (project) => + (count = apply(project, { + ...opts, + sources: parse(get("rows").value), + target: Number(get("target").value) - 1, + year: Number(year.value), + top: get("top")?.value === "1", + })), + ); + EstApp.byId("dialog").close(); + state.notice = `${count}행을 복사했습니다.`; + } + function handle(name, state) { + if (name === "annual-copy") dialog(state); + else if (name === "annual-copy-apply") run(state); + else return false; + return true; + } + return { apply, parse, handle }; +})(); diff --git a/tools/est-test/native-extra.js b/tools/est-test/native-extra.js index d78e2ac23..ab5a97725 100644 --- a/tools/est-test/native-extra.js +++ b/tools/est-test/native-extra.js @@ -134,7 +134,9 @@ const EstExtra = (() => { state.business = output; html = box( "연차공사", - button("add-annual", "차수 추가") + button("reset-annual", "연차 차수 초기화(웹보조)"), + button("add-annual", "차수 추가") + + button("reset-annual", "연차 차수 초기화(웹보조)") + + button("annual-copy", "데이타 복사(내역 행 → 차수)"), table( ["차수", "수량·금액 배분 수식", "순공사비", "도급액", "작업", ""], project.annual.map((year, index) => [ diff --git a/tools/est-test/server.cjs b/tools/est-test/server.cjs index 88cd00962..bbb291925 100644 --- a/tools/est-test/server.cjs +++ b/tools/est-test/server.cjs @@ -107,6 +107,7 @@ const allowed = new Set([ "native-other-load.js", "native-applied-cost.js", "native-progress-view.js", + "native-annual-copy.js", "native-actions-more.js", "native-wonga-tools.js", "native-estimate-print.js",