From dc365e214039f8f9a2a5771ada94d0c8bb3f212e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 11 Nov 2026 17:12:15 +0900 Subject: [PATCH] =?UTF-8?q?feat(est-test):=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80=EB=A1=9C=20=EB=8B=A4=EB=A5=B8=20?= =?UTF-8?q?=EA=B3=B5=EC=82=AC=20=EB=82=B4=EC=97=AD=20=ED=95=A9=EC=B9=98?= =?UTF-8?q?=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/test_est_transfer.py | 26 +++++++++++++++ tools/est-test/PLAN.md | 5 +-- tools/est-test/README.md | 2 ++ tools/est-test/index.html | 1 + tools/est-test/native-transfer.js | 47 +++++++++++++++++++++++++-- 5 files changed, 77 insertions(+), 4 deletions(-) diff --git a/resources/tester/test_est_transfer.py b/resources/tester/test_est_transfer.py index 1d5fc9f7d..d581fc95d 100644 --- a/resources/tester/test_est_transfer.py +++ b/resources/tester/test_est_transfer.py @@ -100,3 +100,29 @@ def test_same_code_in_target_is_kept_not_overwritten(): })()""" ) assert result == {"added": 0, "kept": True, "criteria": True, "heads": True, "labor": True} + + +def test_project_add_merges_rows_above_net_cost(): + result = run_js( + """(() => { + const calc = (p) => EstNative.calculate(EstModel.validate(p)); + const target = calc(structuredClone(source)), other = calc(structuredClone(source)); + const net = (p) => p.tables.EstMainDat.find((r) => Number(r.LinkIndex) === 54); + const before = Number(net(target).TC), count = target.tables.EstMainDat.length; + const boundary = target.tables.EstMainDat.findIndex((r) => Number(r.LinkIndex) === 54); + const out = EstTransfer.addProject(target, other); + const done = calc(target); + let empty = ''; + try { EstTransfer.addProject(target, { tables: { EstMainDat: [{ LinkIndex: 54 }] } }); } catch (e) { empty = e.message; } + return { rows: out.rows === boundary, added: out.added.length, grew: done.tables.EstMainDat.length - count === boundary, + doubled: Math.abs(Number(net(done).TC) - 2 * before) < 2, errors: done.calculation.errors.length, empty: empty.slice(0, 8) }; + })()""" + ) + assert result == { + "rows": True, + "added": 0, + "grew": True, + "doubled": True, + "errors": 0, + "empty": "추가할 공사에 ", + } diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 0a284af62..4dc94f846 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -446,12 +446,13 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - 남음: 일위대가·단가산출·표준·견적·자재·노임 탭, 원본 「셀부호 밀기·당기기」. - [x] 연결 자료와 함께 복사·붙여넣기(원본 TFRMOTHERLOAD) — 내역 행의 자재·노임·실적·견적 행과 일위대가·단가산출·기계경비 호표를 하위까지 모아 다른 공사에 붙여넣기. 같은 코드는 붙여넣을 공사 자료 유지(덮어쓰지 않음), 기계경비 적용기준은 붙여넣을 공사에 없을 때만 옮김. - 검증완료: `test_est_transfer.py` 2개(빈 공사로 옮겨 연결 오류 0·금액 동일·하위 기계경비 호표 포함·PkID 중복 없음, 같은 코드·적용기준 덮어쓰지 않음), 관련 시험 통과. ORCA 브라우저에서 샘플 내역 3행 복사 → 새 내역 붙여넣기 → 연결 자료 12건 추가·금액 1,285,046·417,024·680,414 원본과 동일·계산 오류 0, 샘플 복구. - - 남음: 프로젝트 추가(다른 파일의 내역 전체 합치기), 두 공사 나란히 보기. + - 남음: 두 공사 나란히 보기. + - [x] 프로젝트 추가(원본 mnuFileLoadProjectAs) — 다른 공사 파일(ESTX·JSON)의 순공사비 위 내역 전체를 하위 자료와 함께 이 공사 순공사비 위에 합침. 같은 코드 자료 유지. + - 검증완료: `test_est_transfer.py` 3개 통과(합친 뒤 순공사비 2배·계산 오류 0·빈 공사 거부). ORCA 브라우저에서 메뉴 확인, 샘플에 샘플 합치기 → 35행·같은 코드 46건 유지·순공사비 45,293,066 → 90,586,132, 샘플 복구. 파일 고르기 창 자체는 자동 조작 불가로 기존 가져오기 경로(`/api/import`)를 그대로 씀. - 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133. - [ ] 남은 핵심(실무 빈도순): 1. 통합검색 — 저장된 다른 작업에서 단가·호표 찾기. 3. 수량산출서 — 누계·4방향 합계, 제출용 전체 인쇄. - 5. 프로젝트 추가(다른 파일의 내역 합치기). 6. 낙찰률 전체·범위·공종별 적용. 7. 무관자료 삭제, 데이타 정리(정렬·번호), 품명정리. 8. 원가계산서 — 정산원가계산서, 기타경비·요율 대화, 서식 저장·불러오기, 기성 원가 편집. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 67e697f16..1cccddaf5 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -22,6 +22,8 @@ 내역서·일위대가 화면의 `편집 → 단가 고르기`는 자재·노임·기계경비·일위대가·단가산출·견적·실적 자료를 검색어로 찾아 선택 행 아래에 넣거나, 선택 행 하나의 연결을 바꿉니다. 여러 개를 골라 한 번에 넣을 수 있고, 바꿀 때는 변경 전·후를 확인한 뒤 같은 코드를 쓰는 이 표의 다른 행도 함께 바꿀지 고릅니다. 넣거나 바꾼 행만 원천 단가를 다시 읽습니다. 단가산출 산출근거 행은 대상이 아닙니다. +`파일 → 프로젝트 추가`는 다른 공사 파일(ESTX·웹 JSON)의 순공사비 위 내역 행 전체를 하위 자료와 함께 이 공사의 순공사비 위에 합칩니다. 같은 코드 자료는 이 공사의 것을 그대로 씁니다. + `편집 → 내역 행 연결 자료와 함께 복사`는 고른 내역 행이 쓰는 자재·노임·실적·견적 자료와 일위대가·단가산출·기계경비 호표를 하위까지 모아 복사합니다. 다른 공사를 연 뒤 `연결 자료와 함께 붙여넣기`를 누르면 없는 자료만 추가하고, 같은 코드가 이미 있으면 그 공사의 자료를 그대로 씁니다. 붙여넣을 공사에 기계경비 적용기준이 비어 있을 때만 적용기준도 함께 옮깁니다. `파일 → Excel 내역 열 맞춰 가져오기`는 원본 엑셀→EST 변환처럼 다른 프로그램이나 발주처의 Excel 내역을 읽습니다. 시트·시작행·끝행과 번호·품명·규격·단위·수량·단가 등 항목별 Excel 열 문자를 적고 미리보기로 확인한 뒤, 순공사비 줄 위에 추가하거나 그 위 행을 바꿉니다. 단위와 수량이 없는 줄은 공종 제목이 되고, 금액은 단가×수량으로 다시 계산합니다. 수식 셀은 Excel이 저장해 둔 값을 씁니다. 합계단가만 있고 노무·재료·경비로 나눈 단가가 없으면 비목을 정할 수 없어 가져오지 않습니다. diff --git a/tools/est-test/index.html b/tools/est-test/index.html index d00944f8e..99d20fea9 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -74,6 +74,7 @@ + diff --git a/tools/est-test/native-transfer.js b/tools/est-test/native-transfer.js index 962002e9d..0a575a1e3 100644 --- a/tools/est-test/native-transfer.js +++ b/tools/est-test/native-transfer.js @@ -118,6 +118,48 @@ const EstTransfer = (() => { return copies.length; } + // 프로젝트 추가: 다른 공사의 순공사비 위 내역 행 전체를 하위 자료와 함께 이 공사의 순공사비 위에 붙인다. + function addProject(target, source) { + const list = source.tables?.EstMainDat || []; + let end = list.findIndex((row) => Number(row.LinkIndex) === 54); + if (end < 0) end = list.length; + const rows = list.slice(0, end); + if (!rows.length) throw Error("추가할 공사에 순공사비 위 내역 행이 없습니다."); + const result = merge(target, collect(source, rows)); + let at = target.tables.EstMainDat.findIndex((row) => Number(row.LinkIndex) === 54); + if (at < 0) at = target.tables.EstMainDat.length; + paste(target, rows, at); + return { ...result, rows: rows.length }; + } + + function chooseProject(state) { + if (state.view !== "EstMainDat") throw Error("내역서 화면에서 프로젝트를 추가하세요."); + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".estx,.ebkx,.json"; + input.addEventListener("change", async () => { + try { + const file = input.files[0]; + if (!file) return; + const source = /\.(estx|ebkx)$/i.test(file.name) + ? await EstApp.api(`/api/import?name=${encodeURIComponent(file.name)}`, { + method: "POST", + body: await file.arrayBuffer(), + }) + : JSON.parse(await file.text()); + if (!confirm(`「${source.name || file.name}」의 내역을 이 공사의 순공사비 위에 합칩니다.`)) + return; + let result; + EstApp.change((project) => (result = addProject(project, source))); + state.notice = `내역 ${result.rows}행을 합쳤습니다. 연결 자료 ${result.added.length}건 추가${result.kept.length ? `, 같은 코드 ${result.kept.length}건은 이 공사 자료를 그대로 씀` : ""}.`; + } catch (error) { + state.notice = error.message; + } + EstApp.render(); + }); + input.click(); + } + // 화면 let clipboard = null; function handle(name, state) { @@ -146,9 +188,10 @@ const EstTransfer = (() => { paste(project, clipboard.rows, at); }); state.notice = `내역 ${clipboard.rows.length}행을 붙여넣었습니다. 연결 자료 ${result.added.length}건 추가${result.kept.length ? `, 같은 코드 ${result.kept.length}건은 이 공사 자료를 그대로 씀` : ""}${result.criteria ? ", 비어 있던 기계경비 적용기준도 함께 옮김" : ""}.`; - } else return false; + } else if (name === "project-add") chooseProject(state); + else return false; return true; } - return { collect, merge, paste, handle }; + return { collect, merge, paste, addProject, handle }; })();