from test_est_rea_parity import run def test_standard_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:21,GN:2,TC:200}, {PkID:2,Code:'B',Name:'changed link',LinkIndex:5,GN:1,TC:50}, {PkID:4,Code:'D',Name:'new',LinkIndex:21,GN:1,TC:40}]; p.baseline={tables:{EstMainDat:[ {PkID:1,Code:'A',Name:'changed',LinkIndex:21,GN:1,TC:100}, {PkID:2,Code:'B',Name:'changed link',LinkIndex:21,GN:1,TC:50}, {PkID:3,Code:'C',Name:'deleted',LinkIndex:21,GN:3,TC:300}, {PkID:5,Code:'E',Name:'other',LinkIndex:5,GN:1,TC:10}]}}; return {rows:EstStandard.changes(p),print:EstForms.data('standard-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_standard_usage_reports_applied_and_source_prices_without_mutation(): result = run( "math", """(()=>{ const p=EstNative.empty();p.tables.SilJukDat=[{Code:'A',Name:'source',ValueIndex:4,InputTC:120}, {Code:'B',ValueIndex:4,InputTC:50},{Code:'B',ValueIndex:4,InputTC:60}]; p.tables.EstMainDat=[{Code:'A',Name:'work',LinkIndex:21,GN:2,TU:100,TC:200}, {Code:'B',Name:'duplicate',LinkIndex:21,GN:1,TU:45,TC:45}, {Code:'C',Name:'missing',LinkIndex:21,GN:1,TU:20,TC:20}, {Code:'A',Name:'material',LinkIndex:5,GN:1,TU:10}]; const before=JSON.stringify(p), rows=EstStandard.usage(p), print=EstForms.data('standard-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"] == 120 assert result["rows"][0]["appliedPrice"] == 100 assert result["rows"][0]["difference"] == -20 assert result["rows"][1]["sourcePrice"] is None assert result["print"]["rows"] == result["rows"] def test_standard_apply_row_and_all_prices(): result = run( "math", """(()=>{ const p = EstNative.empty(); p.options.SourceRefresh = false; p.options.AutoCalc = false; p.tables.SilJukDat = [ { Code: 'A', Name: 'standard A', ValueIndex: 4, InputTC: 150, InputGC: 50, InputNC: 70, InputJC: 30, InputSC: 0 }, { Code: 'B', Name: 'standard B', ValueIndex: 4, InputTC: 300, InputSC: 300 }, { Code: 'D', Name: 'dup', ValueIndex: 4, InputTC: 50 }, { Code: 'D', Name: 'dup', ValueIndex: 4, InputTC: 60 }, ]; p.tables.EstMainDat = [ { Code: 'A', Name: 'item A', LinkIndex: 21, GN: 2, TU: 100, GU: 30, NU: 50, JU: 20, GC: 60, NC: 100, JC: 40, TC: 200, SCheck: 1 }, { Code: 'B', Name: 'item B', LinkIndex: 21, GN: 3, TU: 200, SU: 200, SC: 600, TC: 600 }, { Code: 'C', Name: 'item C', LinkIndex: 21, GN: 1, TU: 50, SU: 50, SC: 50, TC: 50 }, { Code: 'D', Name: 'item D', LinkIndex: 21, GN: 1, TU: 40, SU: 40, SC: 40, TC: 40 }, { Code: 'OTHER', Name: 'other row', LinkIndex: 5, GN: 5, TU: 10, GC: 50, TC: 50 }, ]; const initialUsage = EstStandard.usage(p); const otherBefore = JSON.stringify(p.tables.EstMainDat[4]); // 행 0(SCheck=1: 재료비 몰아주기 설정)에 단가 적용: // SCheck=1이 보존되어 GU=150, NU=0, JU=0, SU=0 및 GN=2 -> GC=300, TC=300 EstStandard.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으로 초기화한 뒤 다시 적용하면 원천의 재료/노무/경비 비목 분할(50/70/30)이 유지됨 p.tables.EstMainDat[0].SCheck = 0; EstStandard.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 { EstStandard.applyRowPrice(p, 2); } catch { errMissing = true; } let errDup = false; try { EstStandard.applyRowPrice(p, 3); } catch { errDup = true; } // 남은 대상인 행 1(수량 3)에 대해 applyAllPrices 적용 const appliedCount = EstStandard.applyAllPrices(p); const afterAll = { row1_TU: p.tables.EstMainDat[1].TU, row1_SU: p.tables.EstMainDat[1].SU, row1_SC: p.tables.EstMainDat[1].SC, row1_TC: p.tables.EstMainDat[1].TC, row2_TU: p.tables.EstMainDat[2].TU, row3_TU: p.tables.EstMainDat[3].TU, otherPreserved: JSON.stringify(p.tables.EstMainDat[4]) === otherBefore, }; const finalUsage = EstStandard.usage(p); // 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_SC: calculated.tables.EstMainDat[1].SC, row1_TC: calculated.tables.EstMainDat[1].TC, }; return { initialUsage, afterRow0, afterRow0_split, errMissing, errDup, appliedCount, afterAll, finalUsage, afterRecalc }; })()""", ) assert result["initialUsage"][0]["canApply"] is True assert result["initialUsage"][1]["canApply"] is True assert result["initialUsage"][2]["canApply"] is False assert result["initialUsage"][3]["canApply"] is False assert result["afterRow0"] == { "row0_TU": 150, "row0_GU": 150, "row0_NU": 0, "row0_JU": 0, "row0_GC": 60, "row0_TC": 200, "row0_SCheck": 1, "otherPreserved": True, "sourceRefresh": False, "autoCalc": False, } assert result["afterRow0_split"] == { "row0_TU": 150, "row0_GU": 50, "row0_NU": 70, "row0_JU": 30, "row0_GC": 60, "row0_NC": 100, "row0_JC": 40, "row0_TC": 200, } assert result["errMissing"] is True assert result["errDup"] is True assert result["appliedCount"] == 1 assert result["afterAll"] == { "row1_TU": 300, "row1_SU": 300, "row1_SC": 600, "row1_TC": 600, "row2_TU": 50, "row3_TU": 40, "otherPreserved": True, } assert result["afterRecalc"] == { "row0_GC": 100, "row0_NC": 140, "row0_JC": 60, "row0_TC": 300, "row1_SC": 900, "row1_TC": 900, } assert result["finalUsage"][0]["canApply"] is False assert result["finalUsage"][1]["canApply"] is False def test_edge_standard_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.SilJukDat = [ { Code: 'S001', Name: '표준토공', Size: '일반', Unite: 'm3', ValueIndex: 4, InputTC: 15000 }, { Code: 'S002', Name: '표준포장', Size: 't=10cm', Unite: 'm2', ValueIndex: 4, InputTC: 32000 }, ]; p.tables.EstMainDat = [ { PkID: 101, Code: 'S001', Name: '표준토공', Size: '일반', Unite: 'm3', LinkIndex: 21, GN: 10, TU: 12000, SU: 12000, SC: 120000, TC: 120000 }, { PkID: 102, Code: 'S002', Name: '표준포장', Size: 't=10cm', Unite: 'm2', LinkIndex: 21, GN: 5, TU: 30000, SU: 30000, SC: 150000, TC: 150000 }, ]; EstApp.setProject(p); EstApp.state.view = 'standard'; EstApp.render(); })()""" ) # 2. 표준시장단가 사용 내역 화면 확인 assert page.locator("#view-title:has-text('표준시장단가 사용 내역')").is_visible() btn_apply_all = page.locator("button[data-standard-apply-all]") assert btn_apply_all.is_visible() assert "기초단가 일괄 적용 (2건)" in btn_apply_all.text_content() # 3. 개별 단가 적용 버튼 클릭 (1행 S001) btn_row_apply = page.locator("button[data-standard-apply-row='0']") assert btn_row_apply.is_visible() btn_row_apply.click() # 1행이 15,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 == 15000 assert tc_row0 == 150000 # 버튼 라벨이 (1건)으로 변경되었는지 확인 assert "기초단가 일괄 적용 (1건)" in btn_apply_all.text_content() # 4. 일괄 적용 버튼 클릭 (남은 1건 S002 적용) 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 == 32000 assert tc_row1 == 160000 # 이제 일괄 적용 버튼이 disabled 상태인지 확인 assert btn_apply_all.is_disabled() # 5. Undo(실행 취소) 검증 page.locator(".toolbar button[data-action='undo']").click() # 1단계 취소 시 2행은 원래대로 30,000 복귀 assert page.evaluate("EstApp.state.project.tables.EstMainDat[1].TU") == 30000 page.locator(".toolbar button[data-action='undo']").click() # 2단계 취소 시 1행도 원래대로 12,000 복귀 assert page.evaluate("EstApp.state.project.tables.EstMainDat[0].TU") == 12000 browser.close() def test_standard_manual_mode_autocalc_false_and_recalc_switch(): result = run( "math", """(()=>{ const p = EstNative.empty(); p.options.SourceRefresh = false; p.options.AutoCalc = false; // 수동계산 모드 p.tables.SilJukDat = [ { Code: 'S10', Name: '표준단가 10', ValueIndex: 4, InputTC: 20000, InputSC: 20000 }, ]; p.tables.EstMainDat = [ { Level: 1, Code: '', Name: '공종1', TC: 50000 }, { Code: 'S10', Name: '품목 10', LinkIndex: 21, GN: 5, TU: 10000, SU: 10000, SC: 50000, TC: 50000 }, ]; // 1. 수동계산 상태에서 applyRowPrice 실행 // 해당 행(인덱스 1)의 단가(TU, SU)는 20,000으로 갱신되나, // AutoCalc=false이므로 자체 금액(SC=50000, TC=50000) 및 상위 공종 행(Level 1, TC=50000) 금액이 모두 보존됨 EstStandard.applyRowPrice(p, 1); const afterManual = { row1_TU: p.tables.EstMainDat[1].TU, row1_SC: p.tables.EstMainDat[1].SC, row1_TC: p.tables.EstMainDat[1].TC, row0_Level1_TC: p.tables.EstMainDat[0].TC, }; // 2. 전체 재계산(AutoCalc=true 전환 및 EstNative.calculate 실행) p.options.AutoCalc = true; const calculated = EstNative.calculate(p); const afterRecalc = { row1_TU: calculated.tables.EstMainDat[1].TU, row1_SC: calculated.tables.EstMainDat[1].SC, row1_TC: calculated.tables.EstMainDat[1].TC, row0_Level1_TC: calculated.tables.EstMainDat[0].TC, }; return { afterManual, afterRecalc }; })()""", ) assert result["afterManual"]["row1_TU"] == 20000 assert result["afterManual"]["row1_SC"] == 50000 assert result["afterManual"]["row1_TC"] == 50000 # 수동 상태에서는 자체 금액 및 상위 공종(Level 1) 금액 모두 보존됨 assert result["afterManual"]["row0_Level1_TC"] == 50000 # 전체 재계산 전환 후 수량(GN=5)을 반영해 자체 금액 및 상위 공종 정상 합산됨 assert result["afterRecalc"]["row1_TU"] == 20000 assert result["afterRecalc"]["row1_SC"] == 100000 assert result["afterRecalc"]["row1_TC"] == 100000 assert result["afterRecalc"]["row0_Level1_TC"] == 100000