feat(780-1): 일위대가 밖 단가산출도 판정 · 확인 · 칠 — 모든 블록 · 부름 칸 · 빈 블록 「줄 없음」(요구 627)

- Sangeun: 판정 · 확인 몸 = 모든 블록(북부청 임도 49 → 95) · 줄마다 `부름 [호표 번]`([] = 일위대가 밖) ·
  밖 블록 = 이름 · 단위 블록 것 · 근거 없음 · 없는 번만 404 · 하위 밖 블록(울진소광 492 → 83)은 확인 목록에 함께 = 하위 먼저
- SkUnit: 줄 0 블록 = 판정 「줄 없음」 고를줄 1 → 확인해도 못만듦(빈 SK 없음)
- Marks: 블록마다 `부름` · `줄` · `어긋남` = 모든 블록(밖 포함) · 맞춤셈 `판정` 밖 = None 그대로
- 시험 고침(낡은 기대 = 일위대가 안 블록만): 778-5 units 49 → 95(안 49 따로) · 779-2 어긋남 + 밖 4 · 44 · 48 · 77 · 78 ·
  779-5 줄 칠 모든 블록 · 778-5 excel misses 밖 블록 5 · 새 시험 test_780_1_sangeun_outside(밖 확인 · 빈 블록 · 울진소광 · 여덟 파일 459)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 18:08:50 +09:00
co-authored by Claude Opus 5.5
parent 34288c1980
commit 75db009175
8 changed files with 166 additions and 29 deletions
+7 -4
View File
@@ -1,7 +1,8 @@
"""M02 불러오기 엑셀 보기 하이라이트 재료(PLAN 779-2 · 요구서 621 3장 5) — 읽기만(파일 · storage 변화 0).
블록 `[{번, 판정, SK, 갈래, 못옮김}]` = 단가산출 만들기 결과(`Sangeun.made`) + 맞춤셈 캐시(`Fit.result`)를 읽기만.
블록 `[{번, 판정, SK, 갈래, 못옮김, 부름}]` = 단가산출 만들기 결과(`Sangeun.made`) + 맞춤셈 캐시(`Fit.result`)를 읽기만.
- `판정` = 그 블록을 부르는 첫 줄의 맞춤셈 판정 상태(맞음 · 다른 점 · 후보 …) · 안 불리거나 셈 전이면 None
- `부름` = 부르는 호표 번 [] = 일위대가 밖(780-1) · `어긋남` · `줄` = 모든 블록(밖 포함)
- `SK` = 만든 SK 키 · 없으면 None / `갈래` · `못옮김` = 그 SK 본뜬(출처별) · 없으면 None · []
줄 상태는 `Match.view` 호표마다 `줄상태 [[차례, 상태, 체크]]` · `만든`(SC 키)로 실려 감.
@@ -127,7 +128,8 @@ def blocks(folder: Path, key: str, to: Path | None = None) -> list[dict[str, Any
cache = (fit.result(folder, key) or {}).get("블록") or {}
have = sangeun.made(to, key, here)
all_, first = sangeun.calls(hp)
units = hunits.Units() if first else None
units = hunits.Units() if all_ else None
call = sangeun.callers(hp)
out = []
for no in sorted(all_):
line = first.get(no)
@@ -142,8 +144,9 @@ def blocks(folder: Path, key: str, to: Path | None = None) -> list[dict[str, Any
"SK": sk,
"갈래": origin.get("갈래"),
"못옮김": origin.get("못옮김") or [],
"어긋남": misses(all_[no]) if line else None,
"줄": lines(all_[no], units) if line and units else [],
"부름": call.get(no, []),
"어긋남": misses(all_[no]),
"줄": lines(all_[no], units) if units else [],
}
)
return out
+34 -10
View File
@@ -7,6 +7,8 @@
SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 ·
바탕 없음 · 본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래 「단위」, 근거 {로직, 이름}, 못옮김 []} ·
같은 본뜬(키 · 원본)이 이 층에 있으면 그 키에 덮음 · 만드는 층 = 부르는 층.
판정 · 확인 = 모든 블록(780-1 · 요구 627) — 일위대가가 부르는 블록 + 일위대가 밖(내역서 등이 부름) · 줄마다
`부름 [호표 번]`([] = 일위대가 밖) · 밖 블록 = 이름 · 단위 블록 것 · 근거 꼬리표 없음(맞춤셈 열쇠 = 부르는 줄).
같은 내용(733-1 · `M02_Library_Same` 지문) SK 가 이 층에 있으면 새로 안 만들고 그것을 씀 — 이름 = 먼저 만든 것 ·
뒤 출처 = 그 SK `본뜬.또 [그 블록 본뜬 + 이름]`(`made` · `origin_of` 가 읽음) · 다른 출처가 쓰는 SK 는 내용을
안 바꿈(바뀌면 떼고 다시 찾음) · 응답 `같은것씀 [{번, 키}]` · 위 층 같은 것 = `위층같음 [{번, 층, 주인, 키}]`(알림만).
@@ -89,6 +91,19 @@ def calls(hp: dict[str, Any]) -> tuple[dict[int, dict], dict[int, dict]]:
return blocks, first
def callers(hp: dict[str, Any]) -> dict[int, list]:
"""{단산 번: 부르는 호표 번 [차례대로 · 겹침 뺌]} — 없는 번 = 일위대가 밖."""
out: dict[int, list] = {}
for h in hp.get("호표") or []:
for line in h.get("줄") or []:
ref = line.get("참조") or {}
if ref.get("종류") == "단산" and ref.get("번"):
got = out.setdefault(int(ref["번"]), [])
if h.get("번") not in got:
got.append(h.get("번"))
return out
def _bad(text: str, status: int = 400) -> store.StoreError:
return store.StoreError(status, text)
@@ -155,6 +170,8 @@ class Maker:
def basis(self, no: int) -> dict[str, Any] | None:
"""근거 꼬리표 = 맞춤셈 1위 기초(참고)."""
if no not in self.first: # 일위대가 밖 = 맞춤셈 없음
return None
entry = self.cache.get(fit.unit_key(self.first[no])) or {}
top = (entry.get("차례") or [None])[0]
if not top:
@@ -175,7 +192,7 @@ class Maker:
return {"층": layer, "주인": owner, "키": got} if got else None
def judge(self, no: int, picks: dict | None = None) -> dict[str, Any]:
line, block = self.first[no], self.blocks[no]
line, block = self.first.get(no) or {}, self.blocks[no]
unit = str(line.get("단위") or block.get("단위") or "").strip()
try:
got = skunit.judge(block, self.units(), self.ref_of, picks, unit)
@@ -206,7 +223,7 @@ class Maker:
plan = self.plan(no, sure[no])
got = self.judged[no]
lost = [r for r in got["줄"] if r["상태"] == units.LINKED and r["차례"] not in got["담음"]]
name = self.blocks[no].get("이름") or self.first[no].get("이름")
name = self.blocks[no].get("이름") or (self.first.get(no) or {}).get("이름")
if plan["갈래"] == skpatch.WAIT or lost:
why = plan.get("까닭") or f"하위 「{lost[0]['이름']}」 — 그 블록 먼저 확인"
out["못만듦"].append({"번": no, "이름": name, "까닭": why})
@@ -217,7 +234,7 @@ class Maker:
def write(self, no: int, plan: dict, have: str | None) -> str:
"""그 블록 SK 를 씀 → 키 — 다른 출처(또)가 쓰는 SK 의 내용은 안 바꿈(바뀌면 떼고 다시 찾음)."""
doc = _doc(self.first[no], self.blocks[no], no, self.hp, plan, self.sure)
doc = _doc(self.first.get(no) or {}, self.blocks[no], no, self.hp, plan, self.sure)
root, mine = store.root(), doc["본뜬"]
self.above += [{"번": no, **r} for r in self.same.find_above(root, KIND, doc)]
if have:
@@ -267,24 +284,31 @@ class Maker:
def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[str, Any]:
"""[단가산출] — 블록마다 줄 판정 · 확인한 블록만 단위 SK → `{블록, 판정 [{번, 이름, 단위, 판정, 고를줄,
우리금액, 엑셀금액, 근거, 만든, 줄}], 만듦 [{번, 이름, 키, 갈래, 못옮김}], 못만듦 [{번, 이름, 까닭}], 그대로,
우리금액, 엑셀금액, 근거, 만든, 줄, 부름}], 만듦 [{번, 이름, 키, 갈래, 못옮김}], 못만듦 [{번, 이름, 까닭}], 그대로,
같은것씀, 위층같음}` · `picks` = 확인 목록."""
sure = _sure(picks)
maker = Maker(folder, key, who)
bad = sorted(set(sure) - set(maker.first))
bad = sorted(set(sure) - set(maker.blocks))
if bad:
raise _bad(f"없는 블록 · 부르는 줄 없음 「단산{bad[0]}」", 404)
raise _bad(f"없는 블록 「단산{bad[0]}」", 404)
if not maker.done:
fit.ensure(folder, key)
raise _bad("맞춤 셈 중 — 끝나면 다시 누름", 409)
out: dict[str, Any] = {"블록": len(maker.first), "만듦": [], "못만듦": []}
out: dict[str, Any] = {"블록": len(maker.blocks), "만듦": [], "못만듦": []}
for no in sorted(sure):
maker.make(no, sure, out)
rows = []
for no in sorted(maker.first):
line, block = maker.first[no], maker.blocks[no]
call = callers(maker.hp)
for no in sorted(maker.blocks):
line, block = maker.first.get(no), maker.blocks[no]
got = maker.judged.get(no) or maker.judge(no)
head = {"번": no, "이름": block.get("이름") or line.get("이름"), "단위": line.get("단위")}
name = block.get("이름") or (line or {}).get("이름")
head = {
"번": no,
"이름": name,
"단위": (line or block).get("단위"),
"부름": call.get(no, []),
}
keep = ("판정", "고를줄", "우리금액", "엑셀금액", "줄")
head |= {k: got[k] for k in keep} | {"근거": maker.basis(no)}
rows.append({**head, "만든": maker.keys.get(mark(key, no))})
+5 -2
View File
@@ -4,7 +4,7 @@
줄 판정 = `M02_Hopyo_Units` 잇기(상태 · 방법 · 단위) + 매개값(품 · 시간 · 수량 · 율) · 사용자 고름
`{차례: 단위 | {뺌: 까닭}}`(그 줄만 · 꼬리표 「내가 고름」) · 단가 = M01 지금 값으로 다시 셈(`우리금액`) ·
엑셀 금액과의 `차` = 참고(엑셀 = 과거 값 · 판정에 안 씀) · 셈 밖 줄 = 상태 「밖」(SK 에 안 들임).
블록 판정 = 줄 모음 — 모두 이어짐 = 「옮길 수 있음」 · 아니면 「고를 줄 n」.
블록 판정 = 줄 모음 — 모두 이어짐 = 「옮길 수 있음」 · 아니면 「고를 줄 n」 · 줄 0 = 「줄 없음」(고를줄 1 · 빈 SK 막음).
단위 SK 더한줄(바탕 없음 · 778-4 꼴) = 인력 `{요소 LB, 수량 품}` · 기계경비 로직 `{로직 GC, 입력값 {기계},
수량 시간 × 할증}` · 재료 `{요소 MT, 수량(손료율 든), 별도계상?}`(못 찾은 별도계상 = 0 원 고정 줄) ·
율 `{고정 {비율 {기준, 비목}}, 수량 = 율 %}`(줄 목록 = 그 행 줄 `add:#k`) · 하위 `{참조 SK · SC}` · 값 고정 줄.
@@ -24,6 +24,7 @@ from M02_MasterTemplete import M02_Sangeun_Doc as sd
from M02_MasterTemplete.M02_Hopyo_Words import mtypes
GOOD = "옮길 수 있음"
EMPTY = "줄 없음"
OUT, DROP = "밖", "뺌"
UNIT = "단위" # 본뜬 갈래
MINE = "내가 고름"
@@ -85,7 +86,9 @@ def lines(block: dict[str, Any], us: units.Units, picks: dict | None = None) ->
def verdict(rows: list[dict]) -> tuple[str, int]:
"""블록 판정 — (「옮길 수 있음」 | 「고를 줄 n」, n)."""
"""블록 판정 — (「옮길 수 있음」 | 「고를 줄 n」 | 「줄 없음」, n) · 줄 0 = 1(확인해도 못 만듦)."""
if not rows:
return EMPTY, 1
n = sum(1 for r in rows if r["상태"] not in (units.LINKED, OUT, DROP))
return (GOOD if not n else f"고를 줄 {n}"), n
@@ -1,6 +1,7 @@
"""778-5 D-6 엑셀 셈 어긋남(`M02_Hopyo_Marks.misses` · `SheetMap.missesOf`) — 불리는 블록의 재현 못 한
줄 · 덮음 실패 · 금액 차 · 까닭 · 상세 시트 행 차례. 북부청 임도 2 · 33 = 엑셀 합이 진동로울러 노무를 뺌 ·
81 = 이제 맞음 · 칠은 안 바꿈(블록 칠 같음) · `hopyo-marks` 길에 실림(읽기만)."""
81 = 이제 맞음 · 칠은 안 바꿈(블록 칠 같음) · `hopyo-marks` 길에 실림(읽기만) · 780-1 일위대가 밖 블록도
(북부청 임도 4 · 44 · 48 · 77 · 78 · 읽기 흠은 보이는 대로)."""
import warnings
@@ -26,9 +27,9 @@ def north():
return parsed, grid
def _misses(parsed) -> dict[int, dict]:
def _misses(parsed, outside: bool = False) -> dict[int, dict]:
blocks, first = sg.calls(parsed)
got = {n: mk.misses(blocks[n]) for n in first}
got = {n: mk.misses(blocks[n]) for n in (blocks if outside else first)}
return {n: m for n, m in got.items() if m}
@@ -49,6 +50,8 @@ def test_북부청_임도_어긋남(north):
assert all("못 읽음" in x["까닭"] for x in m["줄"])
blocks, _ = sg.calls(north[0])
assert mk.misses(blocks[81]) is None # 이제 맞음
out = {n: m for n, m in _misses(north[0], True).items() if n not in got}
assert sorted(out) == [4, 44, 48, 77, 78] and all(m["까닭"] for m in out.values())
def test_다른_파일_어긋남_수():
+3 -2
View File
@@ -120,8 +120,9 @@ def test_확인한_블록만_단위_SK_고름_재사용(client):
fit.run_job(folder, key)
got = client.post(url, json={}).json()
assert got["만듦"] == [] and not _sks(folder) # 확인 안 한 블록 = 안 만듦
assert len(got["판정"]) == got["블록"] == 49 # 불리는 블록
rows = {r["번"]: r for r in got["판정"]}
assert len(got["판정"]) == got["블록"] == 95 # 모든 블록(780-1 · 밖 포함)
rows = {r["번"]: r for r in got["판정"] if r["부름"]} # 일위대가 안 49
assert len(rows) == 49
good = [n for n, r in rows.items() if r["판정"] == skunit.GOOD and r["근거"]]
pick = next(n for n, r in rows.items() if r["고를줄"])
assert rows[pick]["판정"] == f"고를 줄 {rows[pick]['고를줄']}"
+2 -2
View File
@@ -64,8 +64,8 @@ def test_marks_읽기만_판정_캐시_같음_SK(client):
assert judged and all(r["판정"] in ("맞음", "다른 점", "후보", "없음") for r in judged)
assert {r["판정"] for r in judged} <= {e["판정"]["상태"] for e in cache.values()}
assert all(r["SK"] is None and r["갈래"] is None and r["못옮김"] == [] for r in rows)
# 778-5 D-6 — 엑셀 셈 어긋남 덧칸(불리는 블록) · 2 · 33 = 엑셀 합이 줄 뺌
assert {r["번"] for r in rows if r["어긋남"]} == {2, 33, 64, 95}
# 778-5 D-6 — 엑셀 셈 어긋남 덧칸(780-1 모든 블록) · 2 · 33 = 엑셀 합이 줄 뺌 · 4 … 78 = 일위대가 밖
assert {r["번"] for r in rows if r["어긋남"]} == {2, 33, 64, 95, 4, 44, 48, 77, 78}
# [단가산출 만들기] 뒤 = SK · 갈래 · 못옮김 실림(778-5 — 확인한 블록만)
rows = client.post(f"{base}/{KEY}/sangeun").json()["판정"]
sure = [{"번": r["번"]} for r in rows if not r["고를줄"]]
@@ -1,6 +1,7 @@
"""779-5 블록 안 줄 칠(`logic/M02_MasterTemplete_Hopyo_SheetMap.ts` · Node) — 블록 `줄 [[행, 상태, 차]]` →
출처 행만 칠(이어짐 초록 · 차 있으면 주황 · 후보 파랑 · 못 이음 빨강 · 밖 흐림) · 머리 행 = 가장 나쁜 칠 ·
목록 행 = 머리와 같음 · 줄 재료 없으면 옛 블록 칠(범위 통째) · 실물(북부청 임도 · 영월) 줄 칠 수 = 단위 줄 수 ·
목록 행 = 머리와 같음 · 줄 재료 없으면 옛 블록 칠(범위 통째) · 실물(북부청 임도 · 영월) 줄 칠 수 = 단위 줄 수
(780-1 모든 블록 · 일위대가 밖 포함) ·
상태 = 줄 잇기(D-2) · 참고 차 = STmate 옛 노임만."""
import warnings
@@ -63,21 +64,22 @@ def test_실물_줄_칠_수_단위_줄_수(tag, units):
grid = hg.read(path.name, data, parsed)
grid.pop("책")
blocks, first = sg.calls(parsed)
assert len(first) < len(blocks) # 일위대가 밖 블록 있음
marks, want = [], 0
for no in sorted(blocks):
lines = mk.lines(blocks[no], units) if no in first else []
lines = mk.lines(blocks[no], units)
rows = [u["행"] for u in blocks[no]["항목"] if u.get("행") is not None]
want += len(rows) if no in first else 0
assert [x[0] for x in lines] == (rows if no in first else [])
want += len(rows)
assert [x[0] for x in lines] == rows
assert all(s in ("이어짐", "고를 것", "못 이음", "밖") for _, s, _ in lines)
assert all(not d or s == "이어짐" for _, s, d in lines) # 차 = 이어진 줄만
marks.append(
{"번": no, "판정": "맞음", "SK": None, "갈래": None, "못옮김": [], "줄": lines}
)
detail = _node({"grid": grid, "blocks": marks})["paint"][grid["보기"]["단가산출상세"]]
heads = {b["행"][0] for b in grid["단산"] if b["번"] in first}
heads = {b["행"][0] for b in grid["단산"]}
unit_rows = {x[0] for m in marks for x in m["줄"]}
spans = [b["행"] for b in grid["단산"] if b["번"] in first]
spans = [b["행"] for b in grid["단산"] if any(x["번"] == b["번"] and x["줄"] for x in marks)]
painted = {int(r) for r in detail if any(x <= int(r) <= y for x, y in spans)} - heads
assert unit_rows - heads == painted and want == sum(len(m["줄"]) for m in marks)
diff = sum(d for m in marks for _, _, d in m["줄"])
@@ -0,0 +1,101 @@
"""780-1 일위대가 밖 단가산출도 잇기(요구 627) — 판정 · 확인 = 모든 블록 · `부름 [호표 번]`([] = 일위대가 밖) ·
밖 블록 확인 = 단위 SK(이름 · 단위 = 블록 것 · 근거 없음) · 빈 블록(줄 0) = 「줄 없음」 확인해도 못만듦 ·
하위 밖 블록(울진소광 492 → 83) = 확인 목록에 함께 넣으면 하위 먼저 만듦 · 칠 재료 `줄` · `어긋남` 모든 블록 ·
여덟 파일 블록 459 판정 오류 0(빈 블록은 모두 밖 = 일위대가 안 판정 그대로). 층 = 임시 폴더 · 마스터 읽기만.
"""
from __future__ import annotations
import json
from M02_MasterTemplete import M02_Hopyo_Fit as fit
from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun
from M02_MasterTemplete import M02_Hopyo_SkUnit as skunit
from M02_MasterTemplete import M02_Hopyo_Units as units
from M02_MasterTemplete import M02_Template_Layers as layers
from resources.tester import helper_m02_hopyo_golden as golden
from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401
BLOCKS = {"북부청임도": (95, 49), "북부청사방": (67, 45), "거창": (61, 13), "봉화": (65, 31)}
BLOCKS |= {"동부청영월": (69, 35), "울진대흥": (47, 14), "영덕계류": (15, 2), "울진소광": (40, 17)}
def _sks(folder) -> dict[str, dict]:
return {p.stem: json.loads(p.read_bytes()) for p in (folder / "sangeun").glob("SK-*.json")}
def _ready(client, tag="북부청임도"):
path = golden.FILES[tag]
key = _post(client, data=path.read_bytes(), name=path.name).json()["키"]
folder = layers.personal_dir(7, 42)
fit.run_job(folder, key)
return folder, key, f"{URL.format('personal')}/{key}/sangeun"
def _first_picks(row: dict) -> dict:
"""고를 줄마다 첫 후보(없으면 뺌)."""
picks = {}
for x in row["줄"]:
if x["상태"] in (units.PICK, units.NONE):
c = (x["후보"] or [{}])[0]
k = c.get("키") or c.get("자재")
field = {"L": "노임", "X": "기계", "M": "자재"}.get(x["묶음"])
picks[str(x["차례"])] = {field: k} if k and field else c or {"뺌": "현장"}
return picks
def test_북부청_임도_밖_블록_판정_확인_빈_블록_칠(client):
before = master_hash()
folder, key, url = _ready(client)
got = client.post(url, json={}).json()
rows = {r["번"]: r for r in got["판정"]}
assert got["블록"] == len(rows) == 95 and sum(bool(r["부름"]) for r in rows.values()) == 49
assert all(r["근거"] is None for r in rows.values() if not r["부름"]) # 밖 = 맞춤셈 없음
inside = next(r for r in rows.values() if r["부름"])
assert all(isinstance(n, int) for n in inside["부름"])
eight, empty = rows[8], rows[1]
assert eight["부름"] == [] and eight["판정"] == skunit.GOOD and eight["줄"]
assert (empty["판정"], empty["고를줄"], empty["줄"]) == (skunit.EMPTY, 1, [])
# 밖 블록 확인 = 단위 SK(이름 · 단위 = 블록) · 빈 블록 = 못만듦(빈 SK 없음)
made = client.post(url, json={"확인": [{"번": 8}, {"번": 1}]}).json()
assert [x["번"] for x in made["만듦"]] == [8]
assert made["못만듦"] == [{"번": 1, "이름": empty["이름"], "까닭": skunit.EMPTY}]
docs = _sks(folder)
assert list(docs) == [made["만듦"][0]["키"]]
doc = docs[made["만듦"][0]["키"]]
assert (doc["이름"], doc["단위"]) == (eight["이름"], eight["단위"])
assert doc["바탕"] is None and doc["더한줄"] and "근거" not in doc["본뜬"]
assert client.post(url, json={"확인": [{"번": 9999}]}).status_code == 404
# 칠 재료 — 밖 블록도 줄 · 어긋남 · 부름 · 만든 SK
marks = {r["번"]: r for r in client.get(f"{URL.format('personal')}-marks/{key}").json()["블록"]}
assert len(marks) == 95 and marks[8]["SK"] == doc["키"] and marks[8]["부름"] == []
assert marks[8]["판정"] is None and len(marks[8]["줄"]) == len(eight["줄"])
assert all(s == units.LINKED for _, s, _ in marks[8]["줄"])
assert {n for n, r in marks.items() if r["어긋남"]} == {2, 33, 64, 95, 4, 44, 48, 77, 78}
assert master_hash() == before
def test_울진소광_하위_밖_블록_함께(client):
folder, key, url = _ready(client, "울진소광")
rows = {r["번"]: r for r in client.post(url, json={}).json()["판정"]}
assert rows[83]["부름"] == [] and rows[492]["부름"] and rows[918]["부름"]
sure = [{"번": n, "고름": _first_picks(rows[n])} for n in (492, 83)]
made = client.post(url, json={"확인": sure}).json()
assert [x["번"] for x in made["만듦"]] == [83, 492] and made["못만듦"] == []
sub = made["만듦"][0]["키"]
top = _sks(folder)[made["만듦"][1]["키"]]
assert any((x.get("본뜬") or {}).get("키") == sub for x in top["더한줄"]) # 하위 SK 줄 펼침
def test_여덟_파일_모든_블록_판정():
us = units.Units()
for tag, (n, called) in BLOCKS.items():
hp = golden.read(tag)
blocks, first = sangeun.calls(hp)
call = sangeun.callers(hp)
assert (len(blocks), len(first)) == (n, called), tag
assert {b for b in blocks if call.get(b)} == set(first), tag # 부름 = 첫 줄과 같은 블록
for no, block in blocks.items():
got = skunit.judge(block, us, lambda sub: None, None, str(block.get("단위") or ""))
if not block.get("항목"): # 빈 블록 = 모두 밖
assert no not in first and got["판정"] == skunit.EMPTY, (tag, no)