Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -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
|
||||
},
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -247,8 +247,6 @@ def test_방제_벌목조재_감_할증(whole):
|
||||
"경급": 6,
|
||||
"소군락": "소군락 모두베기",
|
||||
"방제수종": "잣나무",
|
||||
"체인톱가격": 1200000,
|
||||
"체인오일단가": 5000,
|
||||
"지역": "전국평균",
|
||||
}
|
||||
got = mf.run(whole, "GF000076", given)
|
||||
|
||||
@@ -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],
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user