352 lines
14 KiB
Python
352 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""642-6(PLAN 785-6) 판정 화면 지킴 — 줄 판 · 블록 판 · 고름 캐시(설계 642-A 5-3 다섯째 줄).
|
||
|
||
- 부품 상자만(날 `<button>` · `<input>` 0 · 색은 공통 값) · 700 줄.
|
||
- 고름 캐시 순수 셈(`logic/Hopyo_Picks` · cjs 도우미) — 북부청 실제 줄 판(785-3 `Row.rows`) +
|
||
블록 풀이(785-2 `Solve.block`)를 그대로 넣어 봄: 제안 = 미리 체크만(빈 캐시 = 옮길 것 0) ·
|
||
[호표 제안 모두 확인] = 다 풀린 제안만 · 예시 1(단산10 GC001140 입력 필요 0) · 예시 2(단산47 기초 둘 ·
|
||
구분 엇갈림 → 토목) · 예시 3(단산24 GC000211 높이감률 · 구멍땜 입력 필요 → 0) · 옮기기 몸 = 확인한 것만 ·
|
||
막는 칸 남은 줄 막음 · `필요` 걷음 · 배 얹음.
|
||
읽기만(엑셀 · M01) · storage 안 씀.
|
||
"""
|
||
|
||
import json
|
||
import re
|
||
import subprocess
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from M02_MasterTemplete import M02_Hopyo_Bind as hb
|
||
from M02_MasterTemplete import M02_Hopyo_Clause as hc
|
||
from M02_MasterTemplete import M02_Hopyo_Excel as hx
|
||
from M02_MasterTemplete import M02_Hopyo_Row as hr
|
||
from M02_MasterTemplete import M02_Hopyo_Solve as hs
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
HERE = Path(__file__).resolve().parent
|
||
M02 = ROOT / "M02_MasterTemplete"
|
||
NORTH = (
|
||
ROOT
|
||
/ "resources/knowledge/original/실무문서/2026_실무일위대가 모음"
|
||
/ "2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls"
|
||
)
|
||
NEW = [
|
||
M02 / "M02_MasterTemplete_Hopyo_Rows.ts",
|
||
M02 / "M02_MasterTemplete_Hopyo_Block.ts",
|
||
M02 / "M02_MasterTemplete_Hopyo_JudgeApi.ts",
|
||
M02 / "logic/M02_MasterTemplete_Hopyo_Picks.ts",
|
||
]
|
||
FILE = {"layer": "personal", "owner": None}
|
||
|
||
|
||
def _run(steps: list[dict]) -> list:
|
||
proc = subprocess.run(
|
||
["node", str(HERE / "helper_642_6_picks.cjs")],
|
||
input=json.dumps({"steps": steps}, ensure_ascii=False),
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
check=False,
|
||
)
|
||
assert proc.returncode == 0, proc.stderr
|
||
return json.loads(proc.stdout)
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def judged():
|
||
"""호표 1 · 2 줄 판 + 단산 줄 풀이(예시 셋) — 785-5 `hopyo-judge/{키}/{번}` 꼴."""
|
||
book = hx.read_book(NORTH.name, NORTH.read_bytes())
|
||
master = hb.m01.cm.master()
|
||
rows = hr.rows(book, None)
|
||
blocks = {b["번"]: b for b in book["단산"]}
|
||
out = {}
|
||
for no in (1, 2):
|
||
hopyo = next(h for h in book["호표"] if h["번"] == no)
|
||
for row in rows[no]:
|
||
if row.get("번") not in (10, 24, 47):
|
||
continue
|
||
blk = blocks[row["번"]]
|
||
cands = [{"키": c["키"], "절": c.get("절")} for c in hc.basis(blk)["후보"]]
|
||
line = next(x for x in hopyo["줄"] if (x.get("참조") or {}).get("번") == row["번"])
|
||
same = [x for x in hopyo["줄"] if x is not line]
|
||
row["풀이"] = hs.block(master, blk, hs.parts(blk, cands), {"줄": line, "같은줄": same})
|
||
out[no] = rows[no]
|
||
return out
|
||
|
||
|
||
def _row(judged, no, sk):
|
||
return next(r for r in judged[no] if r.get("번") == sk)
|
||
|
||
|
||
def test_부품상자만_700줄():
|
||
for f in NEW:
|
||
text = f.read_text(encoding="utf-8")
|
||
assert len(text.splitlines()) <= 700, f.name
|
||
assert not re.search(r"""createElement\(\s*["'](button|input)["']""", text), f.name
|
||
assert not re.search(r"""\bel\(\s*["'](button|input|select)["']""", text), f.name
|
||
assert "<button" not in text and "<input" not in text, f.name
|
||
css = (M02 / "M02_MasterTemplete_Hopyo_Judge.css").read_text(encoding="utf-8")
|
||
assert not re.search(r"#[0-9a-fA-F]{3,8}\b|rgba?\(", css)
|
||
picks = NEW[3].read_text(encoding="utf-8")
|
||
assert not re.search(r"sessionStorage|fetch\(|document\.", picks.split("*/", 1)[1])
|
||
|
||
|
||
def test_제안은_미리체크만_자동확인없음(judged):
|
||
rows = judged[1]
|
||
got = _run(
|
||
[
|
||
{"fn": "emptyDraft", "args": []},
|
||
{"fn": "moveBody", "args": [{"$": 0}, FILE]},
|
||
*[{"fn": "rowDefaults", "args": [r]} for r in rows],
|
||
]
|
||
)
|
||
assert got[1]["수"] == 0 and got[1]["body"]["호표"] == {}
|
||
defaults = dict(zip([r["차례"] for r in rows], got[2:]))
|
||
first = rows[0]
|
||
assert first["판정"] == "제안 있음" and defaults[0][0]["키"] == first["후보"][0]["키"]
|
||
assert defaults[4][0]["갈래"] == "고정" # 후보 없음 = 고정 금액 제안
|
||
assert [p["키"] for p in defaults[5]] == ["GC000206", "GC000207"] # 예시 2 절마다 기초
|
||
|
||
|
||
def test_호표_제안_모두_확인은_다풀린_제안만(judged):
|
||
rows = judged[1]
|
||
got = _run(
|
||
[
|
||
{"fn": "emptyDraft", "args": []},
|
||
{"fn": "confirmSuggested", "args": [{"$": 0}, 1, rows]},
|
||
{"fn": "moveBody", "args": [{"$": 1}, FILE]},
|
||
]
|
||
)
|
||
on = {int(k) for k, v in got[1]["호표"]["1"].items() if v["확인"]}
|
||
assert on == {
|
||
0
|
||
} # 레미콘 별도계상 — 단산24 · 47 = 입력 필요 · 엇갈림 · 46 · 33 = 풀이 없음 · 면목 = 후보 없음
|
||
assert got[2]["수"] == 1 and list(got[2]["body"]["호표"]["1"]) == ["0"]
|
||
|
||
|
||
def test_별도계상_줄은_고름없이_확인_옮김(judged):
|
||
"""786-1 — 별도계상 줄 = 고정 줄 하나 · 막는 칸 없음 · [확인] 하면 옮기기 몸에 들어감."""
|
||
row = judged[1][0] # 레미콘 별도계상
|
||
got = _run(
|
||
[
|
||
{"fn": "rowDefaults", "args": [row]},
|
||
{"fn": "suggestReady", "args": [row]},
|
||
{"fn": "confirmable", "args": [{"$": 0}]},
|
||
{"fn": "blockedOf", "args": [{"$": 0}]},
|
||
{"fn": "apartPick", "args": [row["후보"][0]]},
|
||
{"fn": "emptyDraft", "args": []},
|
||
{"fn": "putLine", "args": [{"$": 5}, 1, 0, {"고른": {"$": 0}, "확인": True}]},
|
||
{"fn": "moveBody", "args": [{"$": 6}, FILE]},
|
||
]
|
||
)
|
||
(p,) = got[0]
|
||
assert p["갈래"] == "고정" and p["별도계상"] is True and p["품목"] == "프로젝트에서"
|
||
assert got[1] is True and got[2] is True and got[3] == [] and got[4] is True
|
||
assert got[7]["막힘"] == [] and got[7]["수"] == 1
|
||
(moved,) = got[7]["body"]["호표"]["1"]["0"]
|
||
assert moved["갈래"] == "고정" and moved["별도계상"] is True and "필요" not in moved
|
||
|
||
|
||
def test_예시1_단산10_입력필요0(judged):
|
||
row = _row(judged, 2, 10)
|
||
got = _run(
|
||
[
|
||
{"fn": "rowDefaults", "args": [row]},
|
||
{"fn": "suggestReady", "args": [row]},
|
||
{"fn": "confirmable", "args": [{"$": 0}]},
|
||
]
|
||
)
|
||
(p,) = got[0]
|
||
assert p["키"] == "GC001140" and p["필요"] == []
|
||
assert p["입력값"] == {
|
||
"운반거리": 6,
|
||
"경사각도": 35,
|
||
"운반회수": 42,
|
||
"운반무게": 575,
|
||
"회당운반량": 25,
|
||
}
|
||
assert {k: v["길"] for k, v in p["출처"].items()}["운반무게"] == "B4"
|
||
assert got[1] is True and got[2] is True
|
||
money = row["풀이"]["절"][0]["후보"][0]["금액"]["계"]
|
||
assert int(money) == 94227
|
||
|
||
|
||
def test_예시2_단산47_기초둘_구분엇갈림(judged):
|
||
row = _row(judged, 1, 47)
|
||
got = _run(
|
||
[
|
||
{"fn": "rowDefaults", "args": [row]},
|
||
{"fn": "blockedOf", "args": [{"$": 0}]},
|
||
]
|
||
)
|
||
assert [p["키"] for p in got[0]] == ["GC000206", "GC000207"]
|
||
assert got[1] == ["구분"]
|
||
fixed = _run([{"fn": "setInput", "args": [got[0][1], "구분", "토목", "B5"]}])[0]
|
||
assert fixed["입력값"]["구분"] == "토목" and fixed["출처"]["구분"] == {"길": "B5"}
|
||
assert _run([{"fn": "confirmable", "args": [[got[0][0], fixed]]}])[0] is True
|
||
secs = [s["제목"] for s in row["풀이"]["절"]]
|
||
assert len(secs) == 2 # 맡은 절 둘
|
||
|
||
|
||
def test_예시3_단산24_입력필요_0넣으면_풀림(judged):
|
||
row = _row(judged, 1, 24)
|
||
(p,) = _run([{"fn": "rowDefaults", "args": [row]}])[0]
|
||
assert p["키"] == "GC000211" and set(p["필요"]) == {"높이감률", "구멍땜"}
|
||
got = _run(
|
||
[
|
||
{"fn": "missing", "args": [p]},
|
||
{"fn": "setInput", "args": [p, "높이감률", "0"]},
|
||
{"fn": "setInput", "args": [{"$": 1}, "구멍땜", "0"]},
|
||
{"fn": "missing", "args": [{"$": 2}]},
|
||
]
|
||
)
|
||
assert set(got[0]) == {"높이감률", "구멍땜"} and got[3] == []
|
||
assert got[2]["입력값"]["높이감률"] == 0 and got[2]["출처"]["구멍땜"] == {"길": "사용자"}
|
||
assert got[2]["입력값"]["유형"] == "보통" and got[2]["입력값"]["사용횟수"] == "4회"
|
||
|
||
|
||
def test_옮기기몸_확인한것만_막힘_필요걷음_배(judged):
|
||
r24, r47 = _row(judged, 1, 24), _row(judged, 1, 47)
|
||
steps = [
|
||
{"fn": "emptyDraft", "args": []},
|
||
{"fn": "rowDefaults", "args": [r24]},
|
||
# 단산24 = 막는 칸 남은 채 확인 → 막힘
|
||
{"fn": "putLine", "args": [{"$": 0}, 1, r24["차례"], {"고른": {"$": 1}, "확인": True}]},
|
||
{"fn": "rowDefaults", "args": [r47]},
|
||
{"fn": "setInput", "args": [{"$": 3}, "구분", "토목", "B5"]},
|
||
# 단산47 은 확인 안 함 → 몸에 없음
|
||
{"fn": "putLine", "args": [{"$": 2}, 1, r47["차례"], {"고른": [], "확인": False}]},
|
||
{"fn": "rowDefaults", "args": [judged[1][0]]},
|
||
{"fn": "putLine", "args": [{"$": 5}, 1, 0, {"고른": {"$": 6}, "확인": True, "배": 1.5}]},
|
||
{"fn": "moveBody", "args": [{"$": 7}, {"layer": "personal", "owner": "u1"}]},
|
||
]
|
||
body = _run(steps)[-1]
|
||
assert body["수"] == 1
|
||
assert body["body"]["층"] == "personal" and body["body"]["주인"] == "u1"
|
||
(moved,) = body["body"]["호표"]["1"]["0"]
|
||
assert moved["배"] == 1.5 and "필요" not in moved
|
||
assert body["막힘"] == [
|
||
{"호표": 1, "차례": r24["차례"], "까닭": "입력 필요", "칸": ["높이감률", "구멍땜"]}
|
||
]
|
||
|
||
|
||
def test_고름바꾸면_확인풀림_칠(judged):
|
||
row = judged[1][0]
|
||
got = _run(
|
||
[
|
||
{"fn": "emptyDraft", "args": []},
|
||
{"fn": "rowDefaults", "args": [row]},
|
||
{"fn": "putLine", "args": [{"$": 0}, 1, 0, {"고른": {"$": 1}, "확인": True}]},
|
||
{"fn": "rowTone", "args": [row, {"고른": {"$": 1}, "확인": True}]},
|
||
{"fn": "editLine", "args": [{"$": 2}, 1, row, []]},
|
||
{"fn": "rowTone", "args": [judged[1][4], None]},
|
||
{"fn": "rowTone", "args": [judged[1][1], None]},
|
||
]
|
||
)
|
||
assert got[3] == "success"
|
||
assert got[4]["호표"]["1"]["0"]["확인"] is False
|
||
assert got[5] == "danger" and got[6] == "info"
|
||
|
||
|
||
def test_블록칠_새판정(monkeypatch):
|
||
"""SheetMap 블록 칠 = 새 판정(제안 칠) · Marks = `Judge.judged` 저장본 › 셈 중이면 옛 판정."""
|
||
from M02_MasterTemplete import M02_Hopyo_Marks as mk
|
||
from resources.tester.test_779_4_sheet_map import _node
|
||
|
||
def mark(v, n=None, call=(1,)):
|
||
return {
|
||
"번": 1,
|
||
"판정": v,
|
||
"고를수": n,
|
||
"부름": list(call),
|
||
"SK": None,
|
||
"갈래": None,
|
||
"못옮김": [],
|
||
}
|
||
|
||
got = _node(
|
||
{
|
||
"blockTones": [
|
||
mark("기초로 풂"),
|
||
mark("고를 것", 3),
|
||
mark("줄 조합", call=()),
|
||
{**mark("고를 것", 2), "SK": "SK-1", "갈래": "기초"},
|
||
mark("맞음"),
|
||
]
|
||
}
|
||
)["blocks"]
|
||
assert [b["tone"] for b in got] == ["success", "info", "info", "success", "success"]
|
||
assert got[1]["글"] == "제안 — 고를 것 3" and got[2]["글"] == "제안 — 줄 조합 · 일위대가 밖"
|
||
src = Path(mk.__file__).read_text(
|
||
encoding="utf-8"
|
||
) # 칠 = 블록 풀이 저장본만(785-7 옛 맞춤셈 걷음)
|
||
assert "hjudge.judged(folder, key)" in src and "Hopyo_Fit" not in src
|
||
|
||
|
||
def test_엑셀보기_이음_가운데페이지없음():
|
||
"""[수정] = 모달 「확인 — 제N호표」(786-2) · [옮기기] = 오른쪽 판 아래 · 단가산출 목록 = 새 블록 판정."""
|
||
assert not (M02 / "M02_MasterTemplete_Hopyo_Wizard.ts").exists()
|
||
sheet = (M02 / "M02_MasterTemplete_Hopyo_Sheet.ts").read_text(encoding="utf-8")
|
||
assert "openHopyoCheck({" in sheet and "createJudgeMove({" in sheet
|
||
right = sheet.split('className: "m02-hopyo-sheet__side"')[1].split("});")[0]
|
||
assert "move.root" in right
|
||
sk = (M02 / "M02_MasterTemplete_Hopyo_SheetSk.ts").read_text(encoding="utf-8")
|
||
assert "createJudgeBlock({" in sk and "sangeunStep" not in sk
|
||
|
||
|
||
def test_장부짝_줄은_고른것_모두(judged):
|
||
row = {**_row(judged, 1, 47), "판정": "제안 있음", "장부": {"갈래": "짝"}}
|
||
row["후보"] = [
|
||
{
|
||
"갈래": "기초",
|
||
"키": "GC000206",
|
||
"이름": "가공",
|
||
"입력값": {"Type": "Type-Ⅰ"},
|
||
"출처": {},
|
||
"몫": None,
|
||
},
|
||
{
|
||
"갈래": "기초",
|
||
"키": "GC000207",
|
||
"이름": "조립",
|
||
"입력값": {"구분": "토목"},
|
||
"출처": {},
|
||
"몫": None,
|
||
},
|
||
]
|
||
got = _run([{"fn": "rowDefaults", "args": [row]}, {"fn": "suggestReady", "args": [row]}])
|
||
assert [p["키"] for p in got[0]] == ["GC000206", "GC000207"] and got[1] is True
|
||
|
||
|
||
def test_줄칠_확인함_만듦_초록():
|
||
grid = {
|
||
"호표": [{"번": 1, "행": [5, 8]}, {"번": 2, "행": [9, 10]}],
|
||
"단산": [],
|
||
"보기": {"일위대가표": "s1"},
|
||
"줄": [
|
||
{"번": 1, "차례": 0, "행": 6},
|
||
{"번": 1, "차례": 1, "행": 7},
|
||
{"번": 1, "차례": 2, "행": 8},
|
||
{"번": 2, "차례": 0, "행": 10},
|
||
],
|
||
}
|
||
briefs = [
|
||
{"번": 1, "줄상태": [[0, "후보", False], [1, "없음", False], [2, "%", False]]},
|
||
{"번": 2, "줄상태": [[0, "후보", False]], "만든": "SC-1"},
|
||
]
|
||
got = _run(
|
||
[
|
||
{"fn": "indexOf", "args": [grid], "mod": "SheetMap"},
|
||
{
|
||
"fn": "paintOf",
|
||
"args": [{"$": 0}, briefs, [], {"$none": 1}, {"$set": [[1, 0], [1, 2]]}],
|
||
"mod": "SheetMap",
|
||
},
|
||
]
|
||
)[1]["s1"]
|
||
assert got["6"]["tone"] == "success" and "확인함" in got["6"]["글"] # 판정 화면 확인
|
||
assert got["7"]["tone"] == "danger" # 확인 안 한 못 이음 그대로
|
||
assert got["8"]["tone"] == "muted" # 밖 줄 = 확인해도 흐림
|
||
assert got["10"]["tone"] == "success" and "만듦" in got["10"]["글"] # 만든 SC 호표
|