feat(일위대가실무): 324-2 맞춤 — 엑셀 구성 줄 → 돈 로직 · 우리 자재 · 규격 입력값 · 환산 · 층 기억

- M02_Hopyo_Match: 정규화(master_material.plain · plain_spec · plain_unit) · 차례 = 층 기억(같은 층 → 위 층) → 이름 같음 · 일위대가조합.json 메모 씨앗(읽기만) → 정리 이름 · 후보 5(두 글자 묶음 + 단위 + 산림품셈 · 규격 같은 자재 덤) · 후보는 확정 뒤에만 들어감
- 규격 → 입력값: 고르기 글(품명 괄호 포함 · 낱말 머리) · 기계 고르기이름 · D= H= H: B= T= L= L3= · 천 단위 쉼표 · 못 읽음 표시
- 환산: 같은 단위 1 · 단위표 · ㎡→㎥ T · m→㎡ B · m→㎥ B×T = 추정 · 두 단위 로직 · 그 밖 입력
- 길: GET /hopyo/{키}(호표마다 맞춤 n/m) · GET /hopyo/{키}/{번}(줄 맞춤 · 후보) · POST …/guess(미리 채움) · POST /hopyo-memory(확정 · 빼기 · 지움 = 층 쓰기)
- 시험 test_m02_hopyo_match 15 통과(+ 324-1 15)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
2026-10-14 22:12:00 +09:00
co-authored by Claude Opus 5.5
parent 63ba44a717
commit 5e33dda958
3 changed files with 710 additions and 2 deletions
+167
View File
@@ -0,0 +1,167 @@
"""일위대가 실무자료 맞춤(PLAN 324-2 · `M02_Hopyo_Match`) — 규격 → 입력값 · 단위 · 환산 ·
맞춤 차례 · 기억 우선 · 길(보기 · 미리 채움 · 확정) · master_data 원본 그대로. 층은 임시 폴더.
"""
import pytest
from M02_MasterTemplete import M02_Hopyo_Match as hm
from M02_MasterTemplete import M02_Template_Layers as layers
# 올리기 길 시험의 `client` 마당을 같이 씀
# ruff: noqa: F811
from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401
mm = hm.mm
# ── 규격 · 단위 · 환산 ─────────────────────────────────────────────────
def test_규격_이름_붙은_수():
assert hm.numbers("D=1,000mm,일반형") == {"D": (1000, "mm")}
assert hm.numbers("H:1.2m") == {"H": (hm.Decimal("1.2"), "m")}
assert {k: float(v) for k, (v, _) in hm.numbers("B=2.3,H=0.4,야면석").items()} == {
"B": 2.3,
"H": 0.4,
}
assert hm.numbers("L3=35cm적용,장비사용") == {"L3": (35, "cm")}
assert hm.numbers("Φ40×200") == {} and mm.plain_spec("Φ40×200") == "ø40x200"
def test_단위_정규화():
got = [mm.plain_unit(u) for u in ("㎥", "M", "Ton", "TON", "EA", "㎡")]
assert got == ["m3", "m", "톤", "톤", "개", "m2"]
@pytest.mark.parametrize(
("have", "want", "spec", "times", "kind"),
[
("㎥", "㎥", "", 1, "같음"),
("M", "m", "", 1, "같음"),
("kg", "ton", "", 0.001, "단위표"),
("㎡", "㎥", "T=150mm", 0.15, "추정"),
("M", "㎡", "B=2.3,H=0.4", 2.3, "추정"),
("M", "㎥", "B=2.3,T=0.2", 0.46, "추정"),
("개소", "㎥", "", None, "입력"),
("㎥", "㎡ · ㎥", "", 1, "두 단위"),
("M", "㎡ · ㎥", "", None, "입력"),
],
)
def test_환산(have, want, spec, times, kind):
assert hm.factor(have, want, spec) == {"배": times, "갈래": kind}
SPECS = [
{"이름": "뒷길이", "단위": "㎝", "고르기": [25, 35, 45], "자동값": None},
{"이름": "관경", "단위": "mm", "자동값": None},
{"이름": "설치높이", "단위": "m", "자동값": None},
{
"이름": "기계",
"고르기": ["0201-0020", "0201-0060"],
"고르기이름": {
"0201-0020": "굴착기(무한궤도) 0.2㎥",
"0201-0060": "굴착기(무한궤도) 0.6㎥",
},
"자동값": None,
},
{"이름": "횟수", "고르기": ["3회사용시", "4회사용시"], "자동값": None},
{"이름": "단위당", "고르기": ["㎡당", "㎥당"], "자동값": None},
{"이름": "지역", "고르기": ["전국평균", "서울"], "자동값": "전국평균"},
{"이름": "보정작업", "고르기": ["해당 없음", "전석섞인토사"], "자동값": None},
]
def test_규격에서_입력값():
line = {"이름": "찰붙임(4회)", "규격": "L3=35cm적용,굴삭기0.6㎥,D=1,000mm,H:1.2m", "단위": "㎥"}
values, unread = hm.spec_inputs(line, SPECS)
assert values == {
"뒷길이": 35,
"관경": 1000,
"설치높이": 1.2,
"기계": "0201-0060",
"횟수": "4회사용시",
"단위당": "㎥당",
}
assert unread == ["보정작업"] # 지역 = 자동값 · 못 읽은 것만 표시
other = {"이름": "찰붙임", "규격": "L3=40cm", "단위": "㎡"}
values, unread = hm.spec_inputs(other, SPECS[:1])
assert values == {} and unread == ["뒷길이"] # 고르기 밖 수 = 빈 값 + 표시
# ── 맞춤 차례 ────────────────────────────────────────────────────────
def _hopyo(client, no, layer="personal"):
res = client.get(f"{URL.format(layer)}/HP-000001/{no}")
assert res.status_code == 200, res.text
return {one["이름"]: one["맞춤"] for one in res.json()["호표"]["줄"]}
def test_이름_같음_후보_호표_퍼센트(client):
before = master_hash()
assert _post(client).status_code == 200
first = _hopyo(client, 1)
plywood = first["합판거푸집"]
assert plywood["상태"] == "같음" and plywood["대상"]["로직"] == "GF000160"
assert plywood["입력값"] == {"횟수": "4회사용시"} and plywood["들어감"] is True
remicon = first["레미콘(25-21-120)"]
assert remicon["상태"] == "후보" and remicon["들어감"] is False
assert all(c["규격"] == "25-21-120" for c in remicon["후보"][:3]) # 규격 같은 자재 먼저
dig = _hopyo(client, 3)["구조물터파기(인력100%)"]
assert dig["상태"] == "후보" and len(dig["후보"]) == 5
assert dig["후보"][0]["이름"].startswith("구조물터파기")
ref = _hopyo(client, 45)["고화매트재료비"]
assert ref["상태"] == "호표" and ref["대상"] == {"호표": 46, "단위": "㎡"} and ref["들어감"]
pct = _hopyo(client, 46)["잡재료비"]
assert pct["상태"] == "%" and pct["들어감"] is False
whole = client.get(f"{URL.format('personal')}/HP-000001").json()
assert whole["맞춤"][1] == 415 and 0 < whole["맞춤"][0] < 415
assert next(h for h in whole["호표"] if h["번"] == 61)["맞춤"] == [0, 0]
assert master_hash() == before
def test_미리_채움_확정_기억_우선(client):
assert _post(client).status_code == 200
url = f"{URL.format('personal')}/HP-000001/3/guess"
got = client.post(url, json={"차례": 1, "대상": {"로직": "GF000101"}}).json()
assert got["환산"] == {"배": 1, "갈래": "같음"} and got["대상"]["단위"] == "㎥"
assert client.post(url, json={"차례": 1, "대상": {"로직": "XX"}}).status_code == 400
key = _hopyo(client, 3)["구조물터파기(인력100%)"]["열쇠"]
assert key == "구조물터파기(인력100%)|토사|m3"
body = {"줄": {key: {"로직": "GF000101", "입력값": {"지역": "서울"}, "환산": 1}}}
assert client.post("/api/m02/layers/personal/hopyo-memory", json=body).status_code == 200
dig = _hopyo(client, 3)["구조물터파기(인력100%)"]
assert (dig["상태"], dig["대상"]["로직"], dig["입력값"], dig["들어감"]) == (
"기억",
"GF000101",
{"지역": "서울"},
True,
)
# 이름 같음보다 기억이 앞섬 · 빼기
plywood = _hopyo(client, 1)["합판거푸집"]["열쇠"]
out = {"줄": {plywood: {"빼기": True}}}
assert client.post("/api/m02/layers/personal/hopyo-memory", json=out).status_code == 200
assert _hopyo(client, 1)["합판거푸집"]["상태"] == "빼기"
clear = {"줄": {plywood: None}}
assert client.post("/api/m02/layers/personal/hopyo-memory", json=clear).status_code == 200
assert _hopyo(client, 1)["합판거푸집"]["상태"] == "같음"
def test_확정_검사와_위_층_기억(client):
bad = [
{"로직": "GC000422", "환산": 1}, # 돈 아닌 로직
{"로직": "GF000101", "환산": 0},
{"로직": "GF000101", "자재": "MT004258", "환산": 1},
{"환산": 1},
]
for one in bad:
res = client.post("/api/m02/layers/personal/hopyo-memory", json={"줄": {"a|b|c": one}})
assert res.status_code == 400, one
assert client.post("/api/m02/layers/system/hopyo-memory", json={"줄": {}}).status_code == 403
client.who.update(role="SYSTEM_ADMIN")
key = "구조물터파기(인력100%)|토사|m3"
body = {"줄": {key: {"로직": "GF000103", "환산": 1}}}
assert client.post("/api/m02/layers/system/hopyo-memory", json=body).status_code == 200
client.who.update(role="USER")
assert _post(client).status_code == 200
assert _hopyo(client, 3)["구조물터파기(인력100%)"]["대상"]["로직"] == "GF000103" # 위 층
mine = {"줄": {key: {"로직": "GF000101", "환산": 1}}}
assert client.post("/api/m02/layers/personal/hopyo-memory", json=mine).status_code == 200
assert _hopyo(client, 3)["구조물터파기(인력100%)"]["대상"]["로직"] == "GF000101" # 같은 층 먼저
assert hm.chain(layers.personal_dir(7, 42))[1] == layers.company_dir(7)