Merge remote-tracking branches 'origin/sub_laptop_1' and 'origin/sub_laptop_3' into main_laptop_1

This commit is contained in:
2026-11-04 18:12:47 +09:00
20 changed files with 679 additions and 76 deletions
+87 -3
View File
@@ -1,11 +1,40 @@
/* 778-5 D-5 마법사 단가산출 줄 고름 헬퍼 — `logic/Hopyo_SkPick`(순수) · `Hopyo_SkApi`(몸) 를 트랜스파일해 Node 로 돌림 ·
* fetch 대역(부른 길 · 몸 모음) · stdin = {blocks} · stdout = JSON. */
* fetch 대역(부른 길 · 몸 모음) · stdin = {blocks} · stdout = JSON.
* 780-2 — stdin `{sheetSk: {...}}` = 엑셀 보기 오른쪽 판 바꾸기(`Hopyo_SheetSk`) · 단가산출 칸 · 시트 막대 · 판은 가짜. */
const fs = require("fs");
const path = require("path");
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
const ROOT = path.join(__dirname, "..", "..");
const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} } };
const made = []; // 가짜 단가산출 칸 — 지은 인자 · 강조
const fakeEl = () => ({
hidden: false,
attrs: {},
kids: [],
setAttribute(k, v) {
this.attrs[k] = v;
},
append(x) {
this.kids.push(x);
},
});
const STUBS = {
"@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} },
"@ui/ui_template_elements": {
el: (tag, o = {}) => ({ ...fakeEl(), tag, className: o.className }),
},
"./M02_MasterTemplete_Hopyo_WizardSk": {
sangeunStep: (scope, key, file, onChange, o) => {
const step = {
args: { scope, key, file, o: { ...o, onRow: !!o.onRow } },
marks: [],
onRow: o.onRow,
};
made.push(step);
return { root: fakeEl(), mark: (n) => step.marks.push(n) };
},
},
};
const stub = new Proxy({}, { get: () => () => {} });
const { loadTs } = makeLoader({
root: ROOT,
@@ -19,7 +48,9 @@ const { loadTs } = makeLoader({
global.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {} };
(async () => {
const { blocks } = JSON.parse(fs.readFileSync(0, "utf8"));
const input = JSON.parse(fs.readFileSync(0, "utf8"));
if (input.sheetSk) return process.stdout.write(JSON.stringify(sheetSk(input.sheetSk)));
const { blocks } = input;
const calls = [];
global.fetch = async (url, init = {}) => {
calls.push({
@@ -77,8 +108,61 @@ global.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: ()
await api.judgeSangeun({ layer: "personal", owner: null }, "HP-000001");
await api.judgeSangeun({ layer: "personal", owner: null }, "HP-000001", out.body);
out.calls = calls;
// 780-2 일위대가 안 · 밖 · 줄 0 블록 — 거르기 · 판정 · 확인 막음 · 끝내기 글 재료
const more = [
{ ...a, 부름: [3, 7] },
{ ...b, 부름: [] },
{ ...b, 번: 9, 부름: [], 줄: [] },
{ ...c, 부름: [12], 만든: "SK-000001" },
];
const e = more[2];
let d2 = sk.setSure(sk.emptyDraft(), e, true);
out.empty = {
판정: sk.verdictOf(e, {}),
켜짐: sk.canConfirm(e, {}),
확인: d2.확인,
몸: sk.sureBody({ 고름: {}, 확인: [9] }, more),
};
d2 = sk.emptyDraft();
out.filter = Object.fromEntries(
["all", "in", "out", "open"].map((f) => [f, sk.filterBlocks(more, d2, f).map((x) => x.번)]),
);
out.old = sk.filterBlocks(blocks, d2, "out").length; // 옛 서버(부름 없음) = 모두 안
out.wait2 = sk.waiting(d2, more);
process.stdout.write(JSON.stringify(out));
})().catch((error) => {
process.stderr.write(String(error?.stack ?? error));
process.exit(1);
});
/** 780-2 판 바꾸기 — 탭(역할) 차례로 고름 · 판 숨김 · 칸 지음 한 번 · 강조 · 줄 누름 뛰기 */
function sheetSk({ same, canWrite, picks }) {
const M02 = path.join(ROOT, "M02_MasterTemplete");
const { createSheetSk } = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_SheetSk.ts"));
const roles = { s1: "일위대가표", s2: "단가산출표", s3: "단가산출상세" };
const jumps = [];
const nav = {
role: (sheet) => roles[sheet],
blockAt: (sheet, row) =>
sheet === "s2" ? row - 4 : sheet === "s3" ? Math.floor(row / 10) : null,
spotOf: (no, role) => ({ 시트: role === "단가산출표" ? "s2" : "s3", 행: [no, no + 1] }),
jumpTo: (sheet, rows) => jumps.push([sheet, rows]),
};
const toggled = [];
const side = {
hidden: false,
after: () => {},
parentElement: { classList: { toggle: (c, on) => toggled.push([c, on]) } },
};
const scope = { layer: "personal", owner: null };
const target = same ? scope : { layer: "company", owner: null };
const p = createSheetSk({ scope, key: "HP-000001", 파일: "a.xls", target, canWrite, nav, side });
const seen = [];
for (const [sheet, cell] of picks) {
p.at({ 시트: sheet, 칸: cell });
seen.push({ 호표판숨김: side.hidden, 단가판숨김: p.root.hidden, 지음: made.length });
if (sheet === "s2" && made[0]) made[0].onRow(8); // 목록 줄 누름(단가산출표 탭)
if (sheet === "s3" && made[0]) made[0].onRow(8); // 상세 탭
}
return { seen, toggled, made: made.map((m) => ({ args: m.args, marks: m.marks })), jumps };
}
+99
View File
@@ -0,0 +1,99 @@
"""780-2 지킴 — 엑셀 보기 단가산출표 · 상세 탭 = 오른쪽 단가산출 목록(요구 627).
서버 없이 잡음: 탭(역할) 따라 판 바꿈(일위대가표 = 호표 목록 · 단가산출 탭 = 단가산출 목록 · 칸은 처음 한 번만 지음) ·
시트 행 → 목록 강조 · 목록 줄 → 지금 탭의 블록 자리 · 만들 층 ≠ 파일 층 = 보기만 · 거르기(전체 · 안 · 밖 · 고를 줄 있음) ·
줄 0 블록 = 「줄 없음」 확인 막음 · 마법사 칸과 같은 캐시 열쇠(두 벌 고름 맞춤) · [끝내기] = 막지 않고 물음창 글만 ·
화면 = 부품 상자만 · 700 줄.
"""
import json
import os
import re
import subprocess
from pathlib import Path
HERE = os.path.dirname(__file__)
M02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete"
from test_778_5_sk_pick_screen import BLOCKS as sys_blocks # noqa: E402
def _node(payload: dict) -> dict:
proc = subprocess.run(
["node", os.path.join(HERE, "helper_778_5_sk_pick.cjs")],
input=json.dumps(payload, ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
check=False,
)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
PICKS = [["s1", "B5"], ["s2", "B12"], ["s1", "C3"], ["s3", "A85"], ["s3", "A2"]]
def test_판_바꿈_목록_지음_강조_뛰기():
got = _node({"sheetSk": {"same": True, "canWrite": True, "picks": PICKS}})
assert got["seen"] == [
{"호표판숨김": False, "단가판숨김": True, "지음": 0}, # 일위대가표 = 호표 목록 · 칸 안 지음
{"호표판숨김": True, "단가판숨김": False, "지음": 1}, # 단가산출표 = 단가산출 목록
{"호표판숨김": False, "단가판숨김": True, "지음": 1},
{"호표판숨김": True, "단가판숨김": False, "지음": 1}, # 다시 와도 한 번만 지음
{"호표판숨김": True, "단가판숨김": False, "지음": 1},
]
assert [on for _, on in got["toggled"]] == [False, True, False, True, True]
(step,) = got["made"]
assert step["args"]["o"] == {"narrow": True, "onRow": True, "readOnly": False}
assert step["args"]["key"] == "HP-000001"
assert step["marks"] == [8, 8, 0] # 목록 행 12 → 단산8 · 상세 85 행 → 단산8 · 2 행 → 블록 0
# 목록 줄 누름 = 지금 탭의 자리(단가산출표 = 목록 행 · 상세 = 블록 범위)
assert got["jumps"] == [["s2", [8, 9]], ["s3", [8, 9]], ["s3", [8, 9]]]
def test_다른_층_권한_없음_보기만():
for same, can in ((False, True), (True, False)):
got = _node({"sheetSk": {"same": same, "canWrite": can, "picks": [["s2", "B5"]]}})
assert got["made"][0]["args"]["o"]["readOnly"] is True
def test_거르기_줄0_막음_끝내기_글():
got = _node({"blocks": sys_blocks})
assert got["empty"] == {"판정": "줄 없음", "켜짐": False, "확인": [], "몸": []}
assert got["filter"] == {"all": [2, 5, 9, 7], "in": [2, 7], "out": [5, 9], "open": [2, 9]}
assert got["old"] == 0 # 옛 서버(부름 없음) = 모두 일위대가 안
assert got["wait2"] == {"블록": 3, "밖": 2, "줄": 3} # 만든 블록 7 뺌 · 줄 0 블록 = 고를 줄 1
def _src(name: str) -> str:
return (M02 / f"M02_MasterTemplete_Hopyo_{name}.ts").read_text(encoding="utf-8")
def test_캐시_공유_알림_칠_다시():
wiz = _src("WizardSk")
assert wiz.count("skDraftKey(scope, key)") == 1 # 두 벌 모두 같은 열쇠(층 · 주인 · 파일 키)
assert "SK_SYNC" in wiz and "tell({ got: next })" in wiz # 옮김 = 받은 판정 그대로 다른 벌에
assert wiz.count("tell();") == 2 # [확인] · 줄 고름 바뀜
assert "new Event(HOPYO_BUILT)" in wiz # 옮김 뒤 열린 엑셀 보기 칠 다시
sheet = _src("Sheet")
assert "createSheetSk(" in sheet and "sk?.at(s);" in sheet
side = _src("SheetSk")
assert "sangeunStep(opt.scope, opt.key" in side # 마법사와 같은 파일 층 · 키
nav = _src("SheetNav")
assert "blockAt(sheet: string, row: number)" in nav and "spotOf(no: number, role: Role)" in nav
wizard = _src("Wizard")
assert 'L("M02_SkUnmoved")' in wizard and "finish.disabled = !!n?.줄;" in wizard # 밖 = 안 막음
tone = (M02 / "logic" / "M02_MasterTemplete_Hopyo_SheetMap.ts").read_text(encoding="utf-8")
assert '글: "일위대가 밖"' in tone
def test_부품_상자만_700줄():
for name in ("SheetSk", "Sheet", "SheetNav", "WizardSk", "Wizard"):
src = _src(name)
assert len(src.splitlines()) <= 700, name
assert not re.search(r"#[0-9a-fA-F]{3,6}\b|rgb\(", src), name
for name in ("SheetSk", "WizardSk"):
assert not re.search(r'el\("(button|input|select)"|<button|<input', _src(name)), name
css = (M02 / "M02_MasterTemplete_Hopyo.css").read_text(encoding="utf-8")
block = css.split(".m02-hopyo-sheet.is-sk", 1)[1].split("/*", 1)[0]
assert not re.search(r"#[0-9a-fA-F]{3,6}\b|rgb\(", block)
@@ -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]['고를줄']}"
@@ -92,7 +92,11 @@ def test_몸_꼴_이음_줄_자동_확인_없음():
assert first["method"] == "POST" and first["body"] == {"확인": []} # 판정만
assert first["url"].endswith("/layers/personal/hopyo/HP-000001/sangeun")
assert second["body"] == {"확인": got["body"]}
assert got["wait"] == {"블록": 3, "줄": 1} # 안 옮긴 블록 셋 · 블록 7 하위 줄
assert got["wait"] == {
"블록": 3,
"밖": 0,
"줄": 1,
} # 안 옮긴 블록 셋 · 블록 7 하위 줄 · 옛 서버 = 밖 없음
assert got["tidy"] == [5] # 고름 지우면 그 블록 확인 빠짐
assert got["round"]["확인"] == [2, 5] and got["bad"] == {"고름": {}, "확인": []}
+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)