Files
eomsangdonandClaude Opus 5 efaf2403a0 feat(b09): 301 유로폼 사용수량 12-38-2 — 임대료 또는 손료를 「자재 단가」 탭에서 설계자가 넣은 쪽으로 세움
· 손료 = 자재가 × 표 수량 곧장(패널 0.089 · 내부 패널 0.003매/㎡) + 부자재 24/52/79% · 소모자재 5%(주자재비 = 패널 + 내부 패널) — 봉화 2024 호표 재료비 보통 4,500 · 간단 3,697 과 일치
· 임대료 = 「유로폼 임대료 ㎡당」 한 줄 · 부자재 % 는 원문이 안 적어 안 걺
· 둘 다 · 한쪽만 반쯤 · 아무것도 — 안 세우고 사유(부모 12-38 까지 그대로)
· 12-38-1 잔존율은 안 곱함 — 표 수량이 이미 12회·25% 몫으로 보임(우리 역산 · 차 2.5% 는 추정이라 비고에 밝힘)
· 이중계상 ③ 가드에서 12-38-02 만 뺌(패널이 자재총괄에 줄 없음) · 유로폼이 자재총괄로 가면 빨강 시험
· 합산형 부모 — 갈래 벌이 같으면 여러 단계 갈래끼리 합산(12-38 = 사용수량 + 설치·해체)
· 작업조 표 7 은 밑수 없음 목록에서 뺌(1단위당 = 인원 ÷ 시공량) — 12-38-3 설치·해체가 막혀 있던 자리
· 마스터 재생성 — 12-38 · 12-38-02 갈래 키만 바뀜

기본 조립 제목 금액 변화 0 · 프로젝트 내역 변화 0 · 잴 시험 일곱 빨강→초록 · 끄기 넷 빨강 · 전체 시험 1677 + 318 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
2026-09-14 21:29:04 +09:00

125 lines
5.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""유로폼 사용수량 12-38-2 — 2026-09-14 브레인 301 판정 ①~⑥ · ③′.
① 산림 12-38-2 정본 ② 「임대료 또는 손료」 설계자 선택 · 제안값 없음
③′ 손료 = 자재가 × 표 수량 곧장(실무 넷 — 봉화 「31,500 × 0.89 / 10」) · 12-38-1 잔존율은 안 곱함
④ 이중계상 ③ 가드에서 12-38-02 만 뺌 — 패널이 B08 자재총괄에 줄이 없어서. 되살아날 자리를 시험으로 걺
⑤ 고르는 자리 = 「자재 단가」 탭(넣은 쪽으로 섬 · 둘 다면 안 섬) ⑥ 임대료 갈래엔 % 안 걺
"""
from __future__ import annotations
from decimal import Decimal
from B09_Estimation.B09_Estimation_UnitPrice import cached_build, detail_of
PANEL = "AR-M-10aba455"
INNER = "AR-M-5b89b294"
RENT = "AR-M-d00a6b1a"
LOSS = ((PANEL, "31500", "봉화 2024"), (INNER, "21000", "봉화 2024"))
def _titles(build, code: str) -> list[str]:
return [t for t in build.book.titles if t == f"B-{code}" or t.startswith(f"B-{code}#")]
def test_아무것도_안_넣으면_안_서고_설계자_선택_사유() -> None:
build = cached_build()
assert not _titles(build, "FP-12-38-02")
reason = build.component_gaps.get("FP-12-38-02", "")
assert "임대료 또는 손료" in reason, reason
assert "임대료 또는 손료" in build.component_gaps.get("FP-12-38", "") # 부모까지 까닭이 올라감
for material in (PANEL, INNER, RENT):
assert "FP-12-38-02" in build.material_uses.get(material, []), material
def test_손료는_표_수량_곧장_부자재_소모자재는_주자재비의_퍼센트() -> None:
"""봉화 2024 호표 「유로폼 설치 및 해체」 재료비 보통 4,500 · 간단 3,697 과 같은 값 — 실무 실증."""
build = cached_build(material_prices=LOSS)
assert sorted(_titles(build, "FP-12-38-02")) == [
"B-FP-12-38-02#간단",
"B-FP-12-38-02#보통",
"B-FP-12-38-02#복잡",
]
detail = detail_of(build, "B-FP-12-38-02#보통")["rows"]
rows = {r["ref_code"]: Decimal(r["quantity"]) for r in detail if r.get("ref_code")}
assert rows[PANEL] == Decimal("0.089") and rows[INNER] == Decimal("0.003"), rows
assert build.book.resolve("B-FP-12-38-02#보통").material == Decimal(4500)
assert build.book.resolve("B-FP-12-38-02#간단").material == Decimal(3697) # 봉화 간단 3,697
# 부모 12-38 = 사용수량 + 설치 및 해체 — 두 단계 갈래가 같으면 갈래끼리 합산
parent = {r["ref_code"] for r in detail_of(build, "B-FP-12-38#보통")["rows"]}
assert parent == {"B-FP-12-38-02#보통", "B-FP-12-38-03#보통"}, parent
def test_작업조_표는_밑수가_시공량이라_밑수_없음이_아님() -> None:
"""12-38-3 「(단위: 일 당) 형틀목공 4인 · 시공량 35㎡」 — 1㎡당 = 인원 ÷ 시공량(CrewOutput).
밑수 「N㎡당」 문구가 없다고 `basis_missing` 에 들어 설치·해체가 통째로 막혀 있었음(작업조 표 넷).
"""
build = cached_build()
crew = ("FP-05-28-02", "FP-12-07-02", "FP-12-38-03", "FP-13-16-02")
assert not [code for code in crew if code in build.basis_missing]
rows = {
r["name"]: Decimal(r["quantity"]) for r in detail_of(build, "B-FP-12-38-03#보통")["rows"]
}
assert rows["형틀목공"] == Decimal(4) / Decimal(35), rows
def test_잔존율은_안_곱하고_역산은_추정이라_밝힘() -> None:
build = cached_build(material_prices=LOSS)
note = next(
r["note"]
for r in detail_of(build, "B-FP-12-38-02#보통")["rows"]
if r.get("ref_code") == PANEL
)
assert "우리 역산" in note and "추정" in note and "25회" in note, note
def test_임대료는_한_줄만_퍼센트_안_걺() -> None:
build = cached_build(material_prices=((RENT, "5000", "견적"),))
assert _titles(build, "FP-12-38-02") == ["B-FP-12-38-02"]
rows = detail_of(build, "B-FP-12-38-02")["rows"]
assert [r["ref_code"] for r in rows] == [RENT], rows
assert build.book.resolve("B-FP-12-38-02").material == Decimal(5000)
assert "B-FP-12-38#보통" in build.book.titles
def test_둘_다_넣거나_한쪽만_반쯤_넣으면_안_섬() -> None:
both = cached_build(material_prices=(*LOSS, (RENT, "5000", "견적")))
assert not _titles(both, "FP-12-38-02")
assert "둘 다" in both.component_gaps["FP-12-38-02"]
half = cached_build(material_prices=(LOSS[0],))
assert not _titles(half, "FP-12-38-02")
assert "내부 패널" in half.component_gaps["FP-12-38-02"]
def test_유로폼이_자재총괄에_줄로_서면_가드를_되살릴_것() -> None:
"""④ 12-38-02 를 가드에서 뺀 전제 — 패널·유로폼이 B08 자재총괄(`material`)에 줄이 없음.
누가 유로폼을 자재총괄로 보내는 날 할증이 두 번 붙음 → 여기서 빨강 · 가드에 12-38-02 를 되돌릴 것.
"""
import json
from pathlib import Path
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import DESTINATION
from B09_Estimation.B09_Estimation_Guards import SURCHARGE_INCLUDED_ITEMS
assert "FP-12-38-02" not in SURCHARGE_INCLUDED_ITEMS
words = ("유로폼", "패널")
assert all(DESTINATION[n] != "material" for n in DESTINATION if any(w in n for w in words))
root = Path(__file__).resolve().parents[2] / "resources" / "data_structure_unit"
for path in root.glob("*.json"):
text = path.read_text(encoding="utf-8")
def walk(node):
if isinstance(node, dict):
name = str(node.get("name") or "")
if any(w in name for w in words):
assert node.get("destination") != "material", (path.name, node)
for value in node.values():
walk(value)
elif isinstance(node, list):
for value in node:
walk(value)
walk(json.loads(text))