from test_est_rea_parity import run def test_estimate_changes_include_new_deleted_and_changed_link_types(): result = run( "math", """(()=>{ const p=EstNative.empty();p.tables.EstMainDat=[ {PkID:1,Code:'A',Name:'changed',LinkIndex:101,GN:2,TC:200}, {PkID:2,Code:'B',Name:'changed link',LinkIndex:5,GN:1,TC:50}, {PkID:4,Code:'D',Name:'new',LinkIndex:101,GN:1,TC:40}]; p.baseline={tables:{EstMainDat:[ {PkID:1,Code:'A',Name:'changed',LinkIndex:101,GN:1,TC:100}, {PkID:2,Code:'B',Name:'changed link',LinkIndex:101,GN:1,TC:50}, {PkID:3,Code:'C',Name:'deleted',LinkIndex:101,GN:3,TC:300}, {PkID:5,Code:'E',Name:'other',LinkIndex:5,GN:1,TC:10}]}}; return {rows:EstEstimate.changes(p),print:EstForms.data('estimate-change',p)}; })()""", ) rows = {row["Code"]: row for row in result["rows"]} assert set(rows) == {"A", "B", "C", "D"} assert rows["A"]["difference"] == 100 assert rows["B"]["status"] == "변경" assert rows["C"]["status"] == "삭제" and rows["C"]["index"] is None assert rows["D"]["status"] == "신규" assert result["print"]["rows"] == result["rows"] def test_estimate_usage_reports_applied_and_source_prices_without_mutation(): result = run( "math", """(()=>{ const p=EstNative.empty();p.tables.EstiMateDat=[ {Code:'A',Name:'source',InputTC:12000,InputJC:12000}, {Code:'B',InputTC:5000,InputJC:5000},{Code:'B',InputTC:6000,InputJC:6000} ]; p.tables.EstMainDat=[ {Code:'A',Name:'work',LinkIndex:101,GN:2,TU:10000,JC:20000,TC:20000}, {Code:'B',Name:'duplicate',LinkIndex:101,GN:1,TU:4500,JC:4500,TC:4500}, {Code:'C',Name:'missing',LinkIndex:101,GN:1,TU:2000,JC:2000,TC:2000}, {Code:'A',Name:'material',LinkIndex:5,GN:1,TU:1000} ]; const before=JSON.stringify(p), rows=EstEstimate.usage(p), print=EstForms.data('estimate-usage',p); return {rows,print,same:before===JSON.stringify(p)}; })()""", ) assert result["same"] assert [row["status"] for row in result["rows"]] == ["연결됨", "코드 중복", "연결 없음"] assert result["rows"][0]["sourcePrice"] == 12000 assert result["rows"][0]["appliedPrice"] == 10000 assert result["rows"][0]["difference"] == -2000 assert result["rows"][1]["sourcePrice"] is None assert result["print"]["rows"] == result["rows"] def test_estimate_apply_row_and_all_prices(): result = run( "math", """(()=>{ const p = EstNative.empty(); p.options.SourceRefresh = false; p.options.AutoCalc = false; p.tables.EstiMateDat = [ { Code: 'A', Name: 'estimate A', InputTC: 15000, InputGC: 5000, InputNC: 7000, InputJC: 3000, InputSC: 0 }, { Code: 'B', Name: 'estimate B', InputTC: 30000, InputJC: 30000 }, { Code: 'D', Name: 'dup', InputTC: 5000 }, { Code: 'D', Name: 'dup', InputTC: 6000 }, ]; p.tables.EstMainDat = [ { Code: 'A', Name: 'item A', LinkIndex: 101, GN: 2, TU: 10000, GU: 3000, NU: 5000, JU: 2000, GC: 6000, NC: 10000, JC: 4000, TC: 20000, SCheck: 1 }, { Code: 'B', Name: 'item B', LinkIndex: 101, GN: 3, TU: 20000, JU: 20000, JC: 60000, TC: 60000 }, { Code: 'C', Name: 'item C', LinkIndex: 101, GN: 1, TU: 5000, JU: 5000, JC: 5000, TC: 5000 }, { Code: 'D', Name: 'item D', LinkIndex: 101, GN: 1, TU: 4000, JU: 4000, JC: 4000, TC: 4000 }, { Code: 'OTHER', Name: 'other row', LinkIndex: 5, GN: 5, TU: 1000, GC: 5000, TC: 5000 }, ]; const initialUsage = EstEstimate.usage(p); const otherBefore = JSON.stringify(p.tables.EstMainDat[4]); // 행 0(SCheck=1: 재료비 몰아주기 설정)에 단가 적용: // SCheck=1이 보존되어 GU=15000, NU=0, JU=0, SU=0 및 GN=2 -> GC=30000, TC=30000 EstEstimate.applyRowPrice(p, 0); const afterRow0 = { row0_TU: p.tables.EstMainDat[0].TU, row0_GU: p.tables.EstMainDat[0].GU, row0_NU: p.tables.EstMainDat[0].NU, row0_JU: p.tables.EstMainDat[0].JU, row0_GC: p.tables.EstMainDat[0].GC, row0_TC: p.tables.EstMainDat[0].TC, row0_SCheck: p.tables.EstMainDat[0].SCheck, otherPreserved: JSON.stringify(p.tables.EstMainDat[4]) === otherBefore, sourceRefresh: p.options.SourceRefresh, autoCalc: p.options.AutoCalc, }; // SCheck를 0으로 초기화한 뒤 다시 적용하면 원천의 재료/노무/경비 비목 분할(5000/7000/3000)이 유지됨 p.tables.EstMainDat[0].SCheck = 0; EstEstimate.applyRowPrice(p, 0); const afterRow0_split = { row0_TU: p.tables.EstMainDat[0].TU, row0_GU: p.tables.EstMainDat[0].GU, row0_NU: p.tables.EstMainDat[0].NU, row0_JU: p.tables.EstMainDat[0].JU, row0_GC: p.tables.EstMainDat[0].GC, row0_NC: p.tables.EstMainDat[0].NC, row0_JC: p.tables.EstMainDat[0].JC, row0_TC: p.tables.EstMainDat[0].TC, }; let errMissing = false; try { EstEstimate.applyRowPrice(p, 2); } catch { errMissing = true; } let errDup = false; try { EstEstimate.applyRowPrice(p, 3); } catch { errDup = true; } // 남은 대상인 행 1(수량 3)에 대해 applyAllPrices 적용 const appliedCount = EstEstimate.applyAllPrices(p); const afterRow1 = { row1_TU: p.tables.EstMainDat[1].TU, row1_JU: p.tables.EstMainDat[1].JU, row1_JC: p.tables.EstMainDat[1].JC, row1_TC: p.tables.EstMainDat[1].TC, }; // AutoCalc=true 전환 후 전체 재계산: 금액이 수량(GN)을 반영해 갱신됨 p.options.AutoCalc = true; const calculated = EstNative.calculate(p); const afterRecalc = { row0_GC: calculated.tables.EstMainDat[0].GC, row0_NC: calculated.tables.EstMainDat[0].NC, row0_JC: calculated.tables.EstMainDat[0].JC, row0_TC: calculated.tables.EstMainDat[0].TC, row1_JC: calculated.tables.EstMainDat[1].JC, row1_TC: calculated.tables.EstMainDat[1].TC, }; return { initialCanApply: initialUsage.filter(u => u.canApply).length, afterRow0, afterRow0_split, errMissing, errDup, appliedCount, afterRow1, afterRecalc, }; })()""", ) assert result["initialCanApply"] == 2 # AutoCalc=false 시 단가는 GU=15000으로 갱신되나 기존 금액(GC=6000, TC=20000) 보존 assert result["afterRow0"]["row0_TU"] == 15000 assert result["afterRow0"]["row0_GU"] == 15000 assert result["afterRow0"]["row0_NU"] == 0 assert result["afterRow0"]["row0_JU"] == 0 assert result["afterRow0"]["row0_GC"] == 6000 assert result["afterRow0"]["row0_TC"] == 20000 assert result["afterRow0"]["row0_SCheck"] == 1 assert result["afterRow0"]["otherPreserved"] is True assert result["afterRow0"]["sourceRefresh"] is False assert result["afterRow0"]["autoCalc"] is False # SCheck=0 시 원천 비목 분할(5000/7000/3000) 유지 및 기존 금액(GC=6000, NC=10000, JC=4000, TC=20000) 보존 assert result["afterRow0_split"]["row0_TU"] == 15000 assert result["afterRow0_split"]["row0_GU"] == 5000 assert result["afterRow0_split"]["row0_NU"] == 7000 assert result["afterRow0_split"]["row0_JU"] == 3000 assert result["afterRow0_split"]["row0_GC"] == 6000 assert result["afterRow0_split"]["row0_NC"] == 10000 assert result["afterRow0_split"]["row0_JC"] == 4000 assert result["afterRow0_split"]["row0_TC"] == 20000 assert result["errMissing"] is True assert result["errDup"] is True assert result["appliedCount"] == 1 # 행 1: 단가 30000 갱신, 수동계산 상태이므로 기존 금액(JC=60000, TC=60000) 보존 assert result["afterRow1"]["row1_TU"] == 30000 assert result["afterRow1"]["row1_JU"] == 30000 assert result["afterRow1"]["row1_JC"] == 60000 assert result["afterRow1"]["row1_TC"] == 60000 # AutoCalc=true 재계산 후 GN(수량)을 반영한 금액으로 정상 계산 assert result["afterRecalc"]["row0_GC"] == 10000 assert result["afterRecalc"]["row0_NC"] == 14000 assert result["afterRecalc"]["row0_JC"] == 6000 assert result["afterRecalc"]["row0_TC"] == 30000 assert result["afterRecalc"]["row1_JC"] == 90000 assert result["afterRecalc"]["row1_TC"] == 90000 def test_edge_estimate_usage_apply_price_and_undo_when_server_running(): from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(channel="msedge", headless=True) page = browser.new_page() try: page.goto("http://100.65.141.15:5175", timeout=5000) page.wait_for_function("() => Boolean(EstApp.state.project)") except Exception: browser.close() import pytest pytest.skip("EST 5175 server not reachable") # 1. 테스트 프로젝트 데이터 세팅 (견적단가 및 내역 행 주입) page.evaluate( """(() => { const p = EstNative.empty(); p.options.SourceRefresh = false; p.tables.EstiMateDat = [ { Code: 'E001', Name: '견적비목 1', Size: '규격A', Unite: '식', InputTC: 50000, InputJC: 50000 }, { Code: 'E002', Name: '견적비목 2', Size: '규격B', Unite: '식', InputTC: 80000, InputJC: 80000 }, ]; p.tables.EstMainDat = [ { PkID: 201, Code: 'E001', Name: '견적비목 1', Size: '규격A', Unite: '식', LinkIndex: 101, GN: 10, TU: 40000, JU: 40000, JC: 400000, TC: 400000 }, { PkID: 202, Code: 'E002', Name: '견적비목 2', Size: '규격B', Unite: '식', LinkIndex: 101, GN: 5, TU: 70000, JU: 70000, JC: 350000, TC: 350000 }, ]; EstApp.setProject(p); EstApp.state.view = 'estimate-usage'; EstApp.render(); })()""" ) # 2. 견적단가 사용 내역 화면 확인 assert page.locator("#view-title:has-text('견적단가 사용 내역')").is_visible() btn_apply_all = page.locator("button[data-estimate-apply-all]") assert btn_apply_all.is_visible() assert "기초단가 일괄 적용 (2건)" in btn_apply_all.text_content() # 3. 개별 단가 적용 버튼 클릭 (1행 E001) btn_row_apply = page.locator("button[data-estimate-apply-row='0']") assert btn_row_apply.is_visible() btn_row_apply.click() # 1행이 50,000으로 갱신되었는지 확인 tu_row0 = page.evaluate("EstApp.state.project.tables.EstMainDat[0].TU") tc_row0 = page.evaluate("EstApp.state.project.tables.EstMainDat[0].TC") assert tu_row0 == 50000 assert tc_row0 == 500000 # 버튼 라벨이 (1건)으로 변경되었는지 확인 assert "기초단가 일괄 적용 (1건)" in btn_apply_all.text_content() # 4. 일괄 적용 버튼 클릭 (남은 1건 E002 적용) btn_apply_all.click() tu_row1 = page.evaluate("EstApp.state.project.tables.EstMainDat[1].TU") tc_row1 = page.evaluate("EstApp.state.project.tables.EstMainDat[1].TC") assert tu_row1 == 80000 assert tc_row1 == 400000 # 이제 일괄 적용 버튼은 disabled 상태인지 확인 assert btn_apply_all.is_disabled() # 5. Undo(실행 취소) 검증 page.locator(".toolbar button[data-action='undo']").click() # 1단계 취소 후 2행은 원래대로 70,000 복구 assert page.evaluate("EstApp.state.project.tables.EstMainDat[1].TU") == 70000 page.locator(".toolbar button[data-action='undo']").click() # 2단계 취소 후 1행도 원래대로 40,000 복구 assert page.evaluate("EstApp.state.project.tables.EstMainDat[0].TU") == 40000 browser.close()