diff --git a/resources/tester/test_est_review_workflow.py b/resources/tester/test_est_review_workflow.py index 9aa01c231..39e2cf319 100644 --- a/resources/tester/test_est_review_workflow.py +++ b/resources/tester/test_est_review_workflow.py @@ -62,3 +62,21 @@ def test_review_summary_print_contains_totals_and_item_differences(): assert result["rows"][1]["Name"] == "item" assert result["rows"][1]["difference"] == -120 assert result["rows"][1]["Reason"] == "quantity reviewed" + + +def test_review_reset_one_row_keeps_other_inputs(): + result = run( + "math", + """(()=>{ + const handlers={},p=EstNative.empty(); + p.contractReview={edits:{'row:1':{GU:80,Reason:'first'},'row:2':{GN:3,Reason:'second'}}}; + globalThis.document={addEventListener:(name,fn)=>handlers[name]=fn}; + globalThis.EstApp={state:{project:p},change:fn=>fn(),render:()=>{}}; + EstReview.install(); + const click=key=>handlers.click({target:{closest:s=>s==='[data-review-reset]'?{dataset:{reviewReset:key}}:null}}); + click('row:1');const after=structuredClone(p.contractReview);click('row:2'); + return {after,removed:!Object.hasOwn(p,'contractReview')}; + })()""", + ) + assert result["after"]["edits"] == {"row:2": {"GN": 3, "Reason": "second"}} + assert result["removed"] diff --git a/tools/est-test/README.md b/tools/est-test/README.md index f825e3d41..780a61b09 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -16,7 +16,7 @@ - **원본 구조 유지 ESTX 수정 저장**: 가져온 파일의 공사명·계산 옵션과 행 수 및 인덱스 키가 유지된 단가·수량·문구·금액, 기존 BLOB과 원가 서식을 기록하고 재읽기로 검산합니다. 수량산출서는 기존 서식 구간 안의 텍스트 편집을 저장하며 표지와 문서 구조는 보존합니다. 한 줄에서 표지로 구분된 여러 구간을 함께 수정하거나 연속 글자 서식에 걸친 텍스트를 수정할 수도 있습니다. 새 텍스트에는 변경 시작 위치의 서식이 적용됩니다. 원본 결과가 기존 계산과 모두 일치하면 수정된 계산 결과 표지와 수량 목록을 함께 갱신합니다. 불일치나 계산 오류가 있으면 저장을 거부합니다. 기존 BLOB은 공간이 부족하면 블록을 추가합니다. 원본 파일을 덮어쓰지 않으며 `.idx`와 CAD 바이너리를 보존합니다. 새 문서, 행 추가·삭제·인덱스 키 변경, 표지나 위첨자 등 복잡한 구조를 가로지르는 편집과 목록·연결 변경, 웹 기성·연차·당초 사본은 지원하지 않아 저장을 거부합니다. 이 범위는 JSON으로 보관합니다. 원본 EST 실행 검증은 못했습니다. - **예정공정표**: 내역 행을 공정으로 일괄 추가하고 내역 금액으로 가중치를 갱신할 수 있습니다. 코드가 없거나 같은 코드가 반복된 행은 내역의 행 식별자로 구분합니다. 기존 공정의 기간·진도·이름은 유지합니다. 공정별 기간 막대와 입력 진도의 가중평균을 표시하며, 날짜 역전·잘못된 진도를 차단합니다. 공휴일/공기 산정 기준을 임의 적용하지 않습니다. - **공종별 동원 출력**: 한 번의 원천 계산에서 공종별 인원·자재·장비·실적·견적을 나누며 전체 합계와 금액/수량을 대조합니다. 문서 본문과 인쇄 설정도 저장·복원·취소 대상입니다. -- **계약심사**: 작업모드의 계약심사 화면에서 심사 수량과 비목별 단가·심사 사유를 직접 입력하고 기존액·심사액·차액을 비교합니다. 고정금액 행은 비목별 금액을 입력하며, 원가계산서도 다시 계산합니다. 심사 입력은 JSON에 저장하고, 확인한 결과는 원래 작업과 별도인 심사 내역 사본으로 열 수 있습니다. 심사결과요약서에는 내역별 비교·사유와 도급액 차액을 출력합니다. 심사율이나 법적 기준은 자동으로 정하지 않습니다. +- **계약심사**: 작업모드의 계약심사 화면에서 심사 수량과 비목별 단가·심사 사유를 직접 입력하고 기존액·심사액·차액을 비교합니다. 고정금액 행은 비목별 금액을 입력하며, 원가계산서도 다시 계산합니다. 항목별 입력을 초기화하거나 실행 취소할 수 있습니다. 심사 입력은 JSON에 저장하고, 확인한 결과는 원래 작업과 별도인 심사 내역 사본으로 열 수 있습니다. 심사결과요약서에는 내역별 비교·사유와 도급액 차액을 출력합니다. 심사율이나 법적 기준은 자동으로 정하지 않습니다. Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요청은 순서대로 처리하며, 저장 대기 중의 추가 입력이나 새로 연 문서를 이전 저장 응답으로 덮어쓰지 않습니다. 실행 취소는 현재 작업 파일의 연결을 유지합니다. diff --git a/tools/est-test/native-review.js b/tools/est-test/native-review.js index a18a516be..9a6c26d5a 100644 --- a/tools/est-test/native-review.js +++ b/tools/est-test/native-review.js @@ -101,7 +101,7 @@ const EstReview = (() => { ({ row }) => editable(row) && (row.Name || row.Code), ); byId("work-content").innerHTML = - `
계약심사기존 ${number(output.before)} · 심사 ${number(output.after)} · 차액 ${number(output.after - output.before)}

심사 수량과 비목별 단가를 직접 입력합니다. 고정금액 행은 비목별 금액을 입력합니다. 원래 내역은 유지되며 심사 사본에서 변경됩니다.

${esc(output.errors.join(" / "))}

${["코드", "품명", "기존 수량", "심사 수량", "노무", "재료", "경비", "실적", "기존 금액", "심사 금액", "차액", "심사 사유"].map((value) => ``).join("")}${rows + `
계약심사기존 ${number(output.before)} · 심사 ${number(output.after)} · 차액 ${number(output.after - output.before)}

심사 수량과 비목별 단가를 직접 입력합니다. 고정금액 행은 비목별 금액을 입력합니다. 원래 내역은 유지되며 심사 사본에서 변경됩니다.

${esc(output.errors.join(" / "))}

${value}
${["코드", "품명", "기존 수량", "심사 수량", "노무", "재료", "경비", "실적", "기존 금액", "심사 금액", "차액", "심사 사유", "작업"].map((value) => ``).join("")}${rows .map(({ row, index }) => { const key = rowKey(row, index), input = edits[key] || {}, @@ -115,7 +115,7 @@ const EstReview = (() => { .map((field) => ``) .join( "", - )}`; + )}`; }) .join("")}
${value}
${cell(field)}${number(row.TC)}${number(changed.TC)}${number(EstMath.num(changed.TC) - EstMath.num(row.TC))}
${number(row.TC)}${number(changed.TC)}${number(EstMath.num(changed.TC) - EstMath.num(row.TC))}
`; } @@ -143,7 +143,15 @@ const EstReview = (() => { document.addEventListener("click", (event) => { const project = EstApp.state.project; try { - if (event.target.closest("[data-review-copy]")) { + const reset = event.target.closest("[data-review-reset]"); + if (reset && project.contractReview?.edits) { + const key = reset.dataset.reviewReset; + if (Object.hasOwn(project.contractReview.edits, key)) + EstApp.change(() => { + delete project.contractReview.edits[key]; + if (!Object.keys(project.contractReview.edits).length) delete project.contractReview; + }, false); + } else if (event.target.closest("[data-review-copy]")) { const next = copy(project); if (confirm("현재 작업 대신 심사 내역 사본을 여시겠습니까?")) { EstApp.setProject(next);