diff --git a/resources/master_data/로직_산림품셈_03장_작업장_관리.json b/resources/master_data/로직_산림품셈_03장_작업장_관리.json index 01bdd935b..8224c9c56 100644 --- a/resources/master_data/로직_산림품셈_03장_작업장_관리.json +++ b/resources/master_data/로직_산림품셈_03장_작업장_관리.json @@ -25,11 +25,9 @@ "결과단위": "원/km", "출처": "산림품셈 3-3", "소유": "공용", - "비고": "할인·할증 못 넣음 — 1-4-5 경사도(값 칸 「할인·할증률」) · 1-4-9 제거대상 식생(조건 「제거대상 식생」 빈칸) — 칸 이름 · 범위규칙 문법 · 체인톱 가격 · 체인오일 단가 원문에 없음(시중가격) — 설계자 입력 · 상층목은 솎아베기 본수에 포함(주③⑥)", + "비고": "할인·할증 못 넣음 — 1-4-5 경사도(값 칸 「할인·할증률」) · 1-4-9 제거대상 식생(조건 「제거대상 식생」 빈칸) — 칸 이름 · 범위규칙 문법 · 체인톱 가격 = 자재품목 「동력체인톱」(기본 = 첫 규격 · 고를 수 있음) · 체인오일 = 자재품목 「작동유」 · 상층목은 솎아베기 본수에 포함(주③⑥)", "입력": [ { "이름": "구분", "고르기": ["소작업로", "대작업로"] }, - { "이름": "체인톱가격", "단위": "원" }, - { "이름": "체인오일단가", "단위": "원/ℓ" }, { "이름": "지역", "고르기": [ @@ -86,22 +84,33 @@ "수량": "체인톱대수 * 찾기(QF000001, 기계명='체인톱', 재료명='보통휘발유(주연료)').주연료", "비목": "재료비", "출처": "산림품셈 2-1-1 주③" + }, + { + "종류": "재료", + "요소": { "구분": "기계·설비", "상세구분": "기계류", "검색어": "작동유" }, + "이름": "체인오일", + "단위": "ℓ", + "수량": "체인톱대수 * 찾기(QF000001, 기계명='체인톱', 재료명='체인오일(일반오일)').주연료", + "비목": "재료비", + "출처": "산림품셈 2-1-1 주④" + }, + { + "종류": "재료", + "요소": { "구분": "기계·설비", "상세구분": "기계류", "검색어": "동력체인톱" }, + "이름": "체인톱 손료", + "단위": "대", + "수량": "체인톱대수 * EQ000620.손료계수", + "비목": "경비", + "출처": "산림품셈 2-2-1" } ], "덧줄": [ { "이름": "잡품", - "식": "재료비 * 찾기(QF000001, 기계명='체인톱', 재료명='보통휘발유(주연료)').잡품 / 100", + "식": "줄.'휘발유(체인톱)' * 찾기(QF000001, 기계명='체인톱', 재료명='보통휘발유(주연료)').잡품 / 100", "비목": "재료비", "출처": "산림품셈 2-1-1 주⑤" - }, - { - "이름": "체인오일", - "식": "체인톱대수 * 찾기(QF000001, 기계명='체인톱', 재료명='체인오일(일반오일)').주연료 * 체인오일단가", - "비목": "재료비", - "출처": "산림품셈 2-1-1 주④" - }, - { "이름": "체인톱 손료", "식": "체인톱대수 * EQ000620.손료계수 * 체인톱가격", "비목": "경비", "출처": "산림품셈 2-2-1" } + } ], "끝수": null }, diff --git a/resources/master_data/로직_산림품셈_06장_생육보조.json b/resources/master_data/로직_산림품셈_06장_생육보조.json index 5ed9f9d87..73b0c4f7e 100644 --- a/resources/master_data/로직_산림품셈_06장_생육보조.json +++ b/resources/master_data/로직_산림품셈_06장_생육보조.json @@ -363,12 +363,10 @@ "결과단위": "원/ha", "출처": "산림품셈 6-5", "소유": "공용", - "비고": "소작업로 = ha당 소작업로 길이(없으면 0 · 인력구성 특별 1.0 · 보통 1.0 — 소요인력 산출 1) · 해석: 체인톱 대수 = 특별인부 100%(2-1-1 비고) · 체인톱 가격 · 체인오일 단가 원문에 없음(시중가격) — 설계자 입력 · 덩굴걷기 추가는 6-4-1 따로(주③) · 할인·할증 못 넣음 — 1-4-1 작업시기(범위규칙이 글) · 1-4-3 집단화정도(조건 「개소당 평균면적」 빈칸 · 범위규칙이 글) · 1-4-5 경사도(값 칸 「할인·할증률」) · 1-4-12 제거대상 피복도(조건 빈칸 · 값 칸 「할인·할증률」) — 칸 이름 · 범위규칙 문법", + "비고": "소작업로 = ha당 소작업로 길이(없으면 0 · 인력구성 특별 1.0 · 보통 1.0 — 소요인력 산출 1) · 해석: 체인톱 대수 = 특별인부 100%(2-1-1 비고) · 체인톱 가격 = 자재품목 「동력체인톱」(기본 = 첫 규격 · 고를 수 있음) · 체인오일 = 자재품목 「작동유」 · 덩굴걷기 추가는 6-4-1 따로(주③) · 할인·할증 못 넣음 — 1-4-1 작업시기(범위규칙이 글) · 1-4-3 집단화정도(조건 「개소당 평균면적」 빈칸 · 범위규칙이 글) · 1-4-5 경사도(값 칸 「할인·할증률」) · 1-4-12 제거대상 피복도(조건 빈칸 · 값 칸 「할인·할증률」) — 칸 이름 · 범위규칙 문법", "입력": [ { "이름": "대상지유형", "고르기": ["치수림 단계", "유령림 단계"] }, { "이름": "소작업로", "단위": "km/ha" }, - { "이름": "체인톱가격", "단위": "원" }, - { "이름": "체인오일단가", "단위": "원/ℓ" }, { "이름": "지역", "고르기": [ @@ -425,22 +423,33 @@ "수량": "특별 * 찾기(QF000001, 기계명='체인톱', 재료명='보통휘발유(주연료)').주연료", "비목": "재료비", "출처": "산림품셈 2-1-1 주③" + }, + { + "종류": "재료", + "요소": { "구분": "기계·설비", "상세구분": "기계류", "검색어": "작동유" }, + "이름": "체인오일", + "단위": "ℓ", + "수량": "특별 * 찾기(QF000001, 기계명='체인톱', 재료명='체인오일(일반오일)').주연료", + "비목": "재료비", + "출처": "산림품셈 2-1-1 주④" + }, + { + "종류": "재료", + "요소": { "구분": "기계·설비", "상세구분": "기계류", "검색어": "동력체인톱" }, + "이름": "체인톱 손료", + "단위": "대", + "수량": "특별 * EQ000620.손료계수", + "비목": "경비", + "출처": "산림품셈 2-2-1" } ], "덧줄": [ { "이름": "잡품", - "식": "재료비 * 찾기(QF000001, 기계명='체인톱', 재료명='보통휘발유(주연료)').잡품 / 100", + "식": "줄.'휘발유(체인톱)' * 찾기(QF000001, 기계명='체인톱', 재료명='보통휘발유(주연료)').잡품 / 100", "비목": "재료비", "출처": "산림품셈 2-1-1 주⑤" - }, - { - "이름": "체인오일", - "식": "특별 * 찾기(QF000001, 기계명='체인톱', 재료명='체인오일(일반오일)').주연료 * 체인오일단가", - "비목": "재료비", - "출처": "산림품셈 2-1-1 주④" - }, - { "이름": "체인톱 손료", "식": "특별 * EQ000620.손료계수 * 체인톱가격", "비목": "경비", "출처": "산림품셈 2-2-1" } + } ], "끝수": null }, diff --git a/resources/master_data/로직_산림품셈_08장_방제.json b/resources/master_data/로직_산림품셈_08장_방제.json index b11e1447c..cb445ac50 100644 --- a/resources/master_data/로직_산림품셈_08장_방제.json +++ b/resources/master_data/로직_산림품셈_08장_방제.json @@ -50,7 +50,7 @@ "결과단위": "원/㎥", "출처": "산림품셈 8-3", "소유": "공용", - "비고": "경급 52㎝ 이상은 50(주③) · 해석: 체인톱 대수 = 벌목부 100%(2-1-1 비고) · 체인톱 가격 · 체인오일 단가 원문에 없음(시중가격) — 설계자 입력 · 할인·할증 못 넣음 — 1-4-8 장애물의 정도(조건 「장애물의 정도」 빈칸) · 1-4-17 방제대상목 분포(조건 빈칸 · 값 칸 「할인·할증률」) — 칸 이름 · 범위규칙 문법", + "비고": "경급 52㎝ 이상은 50(주③) · 해석: 체인톱 대수 = 벌목부 100%(2-1-1 비고) · 체인톱 가격 = 자재품목 「동력체인톱」(기본 = 첫 규격 · 고를 수 있음) · 체인오일 = 자재품목 「작동유」 · 할인·할증 못 넣음 — 1-4-8 장애물의 정도(조건 「장애물의 정도」 빈칸) · 1-4-17 방제대상목 분포(조건 빈칸 · 값 칸 「할인·할증률」) — 칸 이름 · 범위규칙 문법", "입력": [ { "이름": "경급", @@ -59,8 +59,6 @@ }, { "이름": "소군락", "고르기": ["소군락 모두베기", "해당 없음"] }, { "이름": "방제수종", "고르기": ["잣나무", "소나무, 해송"] }, - { "이름": "체인톱가격", "단위": "원" }, - { "이름": "체인오일단가", "단위": "원/ℓ" }, { "이름": "지역", "고르기": [ @@ -122,22 +120,33 @@ "수량": "벌목부수 * 찾기(QF000001, 기계명='체인톱', 재료명='보통휘발유(주연료)').주연료", "비목": "재료비", "출처": "산림품셈 2-1-1 주③" + }, + { + "종류": "재료", + "요소": { "구분": "기계·설비", "상세구분": "기계류", "검색어": "작동유" }, + "이름": "체인오일", + "단위": "ℓ", + "수량": "벌목부수 * 찾기(QF000001, 기계명='체인톱', 재료명='체인오일(일반오일)').주연료", + "비목": "재료비", + "출처": "산림품셈 2-1-1 주④" + }, + { + "종류": "재료", + "요소": { "구분": "기계·설비", "상세구분": "기계류", "검색어": "동력체인톱" }, + "이름": "체인톱 손료", + "단위": "대", + "수량": "벌목부수 * EQ000620.손료계수", + "비목": "경비", + "출처": "산림품셈 2-2-1" } ], "덧줄": [ { "이름": "잡품", - "식": "재료비 * 찾기(QF000001, 기계명='체인톱', 재료명='보통휘발유(주연료)').잡품 / 100", + "식": "줄.'휘발유(체인톱)' * 찾기(QF000001, 기계명='체인톱', 재료명='보통휘발유(주연료)').잡품 / 100", "비목": "재료비", "출처": "산림품셈 2-1-1 주⑤" - }, - { - "이름": "체인오일", - "식": "벌목부수 * 찾기(QF000001, 기계명='체인톱', 재료명='체인오일(일반오일)').주연료 * 체인오일단가", - "비목": "재료비", - "출처": "산림품셈 2-1-1 주④" - }, - { "이름": "체인톱 손료", "식": "벌목부수 * EQ000620.손료계수 * 체인톱가격", "비목": "경비", "출처": "산림품셈 2-2-1" } + } ], "끝수": null }, diff --git a/resources/tester/test_m01_auto.py b/resources/tester/test_m01_auto.py index 8dd35e5b2..ff389021d 100644 --- a/resources/tester/test_m01_auto.py +++ b/resources/tester/test_m01_auto.py @@ -98,15 +98,15 @@ def test_자동값으로_로직_전부_멈춤_0() -> None: def test_원_단위_입력은_자동값_null(client) -> None: - got = _auto(client, "GF000056") # 체인톱가격(원) · 체인오일단가(원/ℓ) - assert got["체인톱가격"] is None and got["체인오일단가"] is None - assert got["소작업로"] == 0 # 원 아닌 수는 그대로 0 + got = _auto(client, "GF000051") # 예취기가격(원) + assert got["예취기가격"] is None + assert got["조림목본수"] == 0 # 원 아닌 수는 그대로 0 def test_빈_입력을_쓰는_줄만_비움(client) -> None: - got = _calc(client, "GF000056") + got = _calc(client, "GF000051") blank = {one["이름"]: one for one in got["lines"] if one.get("까닭")} - assert set(blank) == {"체인오일", "체인톱 손료"} + assert set(blank) == {"예취기 손료"} for one in blank.values(): assert one["단가"] is None and one["금액"] is None and "비어 있음" in one["까닭"] rest = sum(Decimal(str(one["금액"])) for one in got["lines"] if one["금액"] is not None) @@ -115,16 +115,35 @@ def test_빈_입력을_쓰는_줄만_비움(client) -> None: def test_빠진_원_입력도_null_처럼_줄만_비움(client) -> None: - given = { - k: v - for k, v in _auto(client, "GF000056").items() - if k not in ("체인톱가격", "체인오일단가") - } + given = {k: v for k, v in _auto(client, "GF000051").items() if k != "예취기가격"} for path in ("calc", "text"): - got = client.post(f"/api/m01/{path}", json={"key": "GF000056", "inputs": given}).json() + got = client.post(f"/api/m01/{path}", json={"key": "GF000051", "inputs": given}).json() assert got["ok"] is True, (path, got) - lines = client.post("/api/m01/calc", json={"key": "GF000056", "inputs": given}).json()["lines"] - assert {o["이름"] for o in lines if o.get("까닭")} == {"체인오일", "체인톱 손료"} + lines = client.post("/api/m01/calc", json={"key": "GF000051", "inputs": given}).json()["lines"] + assert {o["이름"] for o in lines if o.get("까닭")} == {"예취기 손료"} + + +@pytest.mark.parametrize("key", ["GF000002", "GF000056", "GF000076"]) +def test_체인톱_가격_오일은_자재품목(client, key) -> None: + """44-5 — 체인톱 가격 = 「동력체인톱」(기본 첫 규격) · 체인오일 = 「작동유」 · 손 입력 없음.""" + assert not {"체인톱가격", "체인오일단가"} & set(_auto(client, key)) + lines = {o["이름"]: o for o in _calc(client, key)["lines"]} + assert lines["체인톱 손료"]["품목"]["키"] == "MT016587" # 400mm · 530,000원 + assert lines["체인톱 손료"]["단가"] == 530000 and "경비" in lines["체인톱 손료"]["비목"] + assert lines["체인오일"]["품목"]["키"] == "MT000583" # 기어작동유 · 4,612원/ℓ + assert lines["체인오일"]["단가"] == 4612 + gas = Decimal(str(lines["휘발유(체인톱)"]["금액"])) + assert abs(Decimal(str(lines["잡품"]["금액"])) - gas * Decimal("0.4")) < Decimal( + "0.001" + ) # 잡품 = 휘발유값 40% 그대로 + + +def test_체인톱_규격은_고를_수_있음(client) -> None: + given = _auto(client, "GF000056") + body = {"key": "GF000056", "inputs": given, "고름": {"4": "MT016588"}} # 4 = 체인톱 손료 줄 + got = client.post("/api/m01/calc", json=body).json() + saw = next(o for o in got["lines"] if o["이름"] == "체인톱 손료") + assert saw["품목"]["키"] == "MT016588" and saw["단가"] == 900000 def test_표_찾기_실패는_그_줄만_비우고_까닭은_날글_없이(client) -> None: diff --git a/resources/tester/test_masterdata_check.py b/resources/tester/test_masterdata_check.py index 5eb01763c..ca4b58b44 100644 --- a/resources/tester/test_masterdata_check.py +++ b/resources/tester/test_masterdata_check.py @@ -247,8 +247,6 @@ def test_방제_벌목조재_감_할증(whole): "경급": 6, "소군락": "소군락 모두베기", "방제수종": "잣나무", - "체인톱가격": 1200000, - "체인오일단가": 5000, "지역": "전국평균", } got = mf.run(whole, "GF000076", given) diff --git a/resources/tester/ui_checks/44-5_체인톱_자재.js b/resources/tester/ui_checks/44-5_체인톱_자재.js new file mode 100644 index 000000000..79bab6c96 --- /dev/null +++ b/resources/tester/ui_checks/44-5_체인톱_자재.js @@ -0,0 +1,57 @@ +// 44-5 체인톱 가격 · 오일 = 자재품목 — `orca eval --expression "$(cat 이 파일)"` 한 번에 돎(async 식 · JSON 돌려줌). +// 시작 화면 = M01 마스터 데이터(#/m01-master-data) · 좌측 「단가산출 로직」 목록(1쪽에 GF000002 보임). +// 하는 일 = GF000002 작업로 설치 상세 → 시험 계산 입력 · 재료 줄 품목 읽음 → [계산] → 소계 · 계 읽음(저장 안 함). +// 기대 = inputs 에 체인톱가격 · 체인오일단가 없음 · mats = 3:MT000583(기어작동유) · 4:MT016587(동력체인톱 400mm) · +// 재료 소계 24,293 · 경비 소계 4,452(체인톱 손료 0.0084 × 530,000) · 계 454,056. +(async () => { + const wait = (ms) => new Promise((r) => setTimeout(r, ms)); + const until = async (fn, ms = 8000) => { + for (let t = 0; t < ms; t += 150) { + const got = fn(); + if (got) return got; + await wait(150); + } + return null; + }; + const back = [...document.querySelectorAll("button,a")].find((b) => + b.textContent.includes("로직 목록"), + ); + back?.click(); + const tr = await until(() => + [...document.querySelectorAll("tr")].find((t) => t.textContent.startsWith("GF000002")), + ); + if (!tr) return JSON.stringify({ error: "목록에 GF000002 없음" }); + tr.click(); + const calc = await until(() => + document.querySelector('[data-mat-idx="4"]') + ? [...document.querySelectorAll("button")].find((b) => b.textContent.trim() === "계산") + : null, + ); + if (!calc) return JSON.stringify({ error: "시험 계산 재료 줄 없음" }); + const box = calc.closest("section,div"); + const labels = [...document.querySelectorAll(".m01-logic__field > span:first-child")].map((s) => + s.textContent.trim(), + ); + const mats = [...document.querySelectorAll("[data-mat-idx]")].map( + (s) => `${s.dataset.matIdx}:${s.dataset.ref}:${s.textContent.trim()}`, + ); + calc.click(); + const sub = (name) => + [...document.querySelectorAll("tr")] + .find((t) => t.textContent.trim().startsWith(name)) + ?.innerText.replace(/\s+/g, " ") + .trim(); + const done = await until(() => (sub("경비 소계") ?? "").match(/\d/)); + const total = [...document.querySelectorAll("*")] + .filter((x) => x.children.length === 0 && /^계 [\d,]+$/.test(x.textContent.trim())) + .map((x) => x.textContent.trim()); + return JSON.stringify({ + ok: !!done && !!box, + noHandInput: !labels.some((l) => /체인톱가격|체인오일단가/.test(l)), + labels, + mats, + material: sub("재료 소계"), + expense: sub("경비 소계"), + total: total[0], + }); +})();