Merge remote-tracking branch 'origin/sub_laptop_1' into sub_laptop_3

This commit is contained in:
2026-11-08 19:24:40 +09:00
62 changed files with 769 additions and 8698 deletions
-34
View File
@@ -1,34 +0,0 @@
/* 642-6(PLAN 785-6) — `logic/M02_MasterTemplete_Hopyo_Picks.ts`(판정 화면 고름 캐시 순수 셈)를 트랜스파일해 Node 로 돌림.
* stdin = {steps: [{fn, args, mod?}]} — args 안 `{"$": i}` = i 번째 걸음 결과 · `{"$none": 1}` = () => null ·
* `{"$set": [[번, 차례]]}` = (번, 차례) => 그 짝 있음 · mod "SheetMap" = 칠 셈 · stdout = 걸음 결과 배열 JSON(Map → 객체). */
const fs = require("fs");
const path = require("path");
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
const ROOT = path.join(__dirname, "..", "..");
const { loadTs } = makeLoader({ root: ROOT });
const picks = loadTs(
path.join(ROOT, "M02_MasterTemplete", "logic", "M02_MasterTemplete_Hopyo_Picks.ts"),
);
const map = loadTs(
path.join(ROOT, "M02_MasterTemplete", "logic", "M02_MasterTemplete_Hopyo_SheetMap.ts"),
);
const input = JSON.parse(fs.readFileSync(0, "utf8"));
const out = [];
const ref = (v) =>
v && typeof v === "object"
? "$none" in v
? () => null
: "$set" in v
? (a, b) => v.$set.some(([x, y]) => x === a && y === b)
: "$" in v && Object.keys(v).length === 1
? out[v.$]
: Array.isArray(v)
? v.map(ref)
: Object.fromEntries(Object.entries(v).map(([k, x]) => [k, ref(x)]))
: v;
for (const s of input.steps)
out.push((s.mod === "SheetMap" ? map : picks)[s.fn](...ref(s.args)) ?? null);
process.stdout.write(
JSON.stringify(out, (_k, v) => (v instanceof Map ? Object.fromEntries(v) : v)),
);
+20 -60
View File
@@ -1,4 +1,4 @@
/* 780-2 · 785-6 엑셀 보기 오른쪽 판 바꾸기 헬퍼 — stdin `{sheetSk: {...}}` = `Hopyo_SheetSk` 를 트랜스파일해 Node 로 돌림 ·
/* 780-2 · 809-1 엑셀 보기 오른쪽 판 바꾸기 헬퍼 — stdin `{sheetSk: {...}}` = `Hopyo_SheetSk` 를 트랜스파일해 Node 로 돌림 ·
* 단가산출 칸 · 시트 막대 · 판은 가짜 · stdout = JSON. 옛 마법사 줄 고름(`SkPick` · `SkApi`)은 785-7 에서 지움. */
const fs = require("fs");
const path = require("path");
@@ -18,57 +18,31 @@ const fakeEl = () => ({
});
const list = []; // 그린 목록 줄 벌
const reads = []; // 목록 읽음
const opened = []; // [가져온 식으로 열기]
const BRIEFS = [
{
번: 8,
이름: "거푸집",
단위: "㎡",
부름: [1],
표지: null,
만든: null,
판정: "고를 것",
고를수: 2,
},
{
번: 9,
이름: "잡석",
단위: "㎥",
부름: [],
표지: "준용",
만든: null,
판정: "줄 조합",
고를수: 0,
},
{
번: 10,
이름: "운반",
단위: "㎡",
부름: [2],
표지: null,
만든: "SK-000001",
판정: "기초로 풂",
고를수: 0,
},
{ 번: 8, 이름: "거푸집", 단위: "㎡", 부름: [1], SK: null, 판정: "가져온 식" },
{ 번: 9, 이름: "잡석", 단위: "㎥", 부름: [], SK: null, 판정: null },
{ 번: 10, 이름: "운반", 단위: "㎡", 부름: [2], SK: "SK-000001", 판정: "가져온 식" },
];
const STUBS = {
"@ui/ui_template_locale": { t: (k) => (k === "M02_JudgeBlock" ? "단산{n}" : k) },
"@ui/ui_template_badge": { createBadge: (text, tone) => `${tone}:${text}` },
"@ui/ui_template_text": { createText: (text) => text },
"@ui/ui_template_list_row": { createListRow: (opts) => ({ opts, dataset: {} }) },
"./M02_MasterTemplete_Hopyo_JudgeApi": {
readJudge: async (scope, key, to) => {
"./M02_MasterTemplete_Hopyo_SheetNav": {
readHopyoMarks: async (scope, key, to) => {
reads.push({ key, to });
return { 키: key, 파일: "a.xls", 호표: [], 블록: BRIEFS };
return BRIEFS;
},
},
"./M02_MasterTemplete_Hopyo_Block": { createJudgeBlock: (o) => ({ ...o, destroy() {} }) },
"./M02_MasterTemplete_Hopyo_Api": { POLL_MS: 3000 },
"./M02_MasterTemplete_Hopyo_Rows": { createJudgeMove: () => ({ root: fakeEl(), destroy() {} }) },
"./M02_MasterTemplete_Hopyo_Imported": { openImported: (o) => opened.push(o) },
"@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} },
"@ui/ui_template_elements": {
createButton: (o) => ({ label: o.label, dataset: {}, hidden: false, click: () => o.onClick({ stopPropagation() {} }) }),
el: (tag, o = {}) => ({
...fakeEl(),
tag,
kids: o.children ?? [],
className: o.className,
replaceChildren(...kids) {
this.kids = kids;
@@ -97,7 +71,7 @@ global.sessionStorage = { getItem: () => null, setItem: () => {}, removeItem: ()
process.exit(1);
});
/** 780-2 · 785-6 판 바꾸기 — 탭(역할) 차례로 고름 · 판 숨김 · 목록 읽음 한 번 · 강조 · 줄 누름 = 뛰기 + 블록 판 */
/** 780-2 · 809-1 판 바꾸기 — 탭(역할) 차례로 고름 · 판 숨김 · 목록 읽음 한 번 · 강조 · 줄 누름 = 뛰기 · 줄 끝 = 가져온 식 모달 */
async function sheetSk({ same, canWrite, picks }) {
const M02 = path.join(ROOT, "M02_MasterTemplete");
const { createSheetSk } = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_SheetSk.ts"));
@@ -118,22 +92,7 @@ async function sheetSk({ same, canWrite, picks }) {
};
const scope = { layer: "personal", owner: null };
const target = same ? scope : { layer: "company", owner: null };
const state = {
get: () => ({ 호표: {}, 블록: { 8: { 고른: [], 확인: true } } }),
on: () => () => {},
};
const opened = [];
const p = createSheetSk({
scope,
key: "HP-000001",
target,
canWrite,
nav,
side,
state,
openLines: (make) => opened.push(make("host")),
closeLines: () => {},
});
const p = createSheetSk({ scope, key: "HP-000001", target, canWrite, nav, side });
const seen = [];
const rows = () => list.at(-1) ?? [];
for (const [sheet, cell] of picks) {
@@ -146,22 +105,23 @@ async function sheetSk({ same, canWrite, picks }) {
읽음: reads.length,
강조: active,
});
if (sheet !== "s1")
rows()
.find((r) => r.opts.label.startsWith("단산8 "))
?.opts.onClick();
if (sheet !== "s1") {
const row = rows().find((r) => r.opts.label.startsWith("단산8 "));
row?.opts.onClick();
row?.opts.trailing.kids.at(-2).click(); // [가져온 식으로 열기](만들 층)
}
}
return {
seen,
toggled,
jumps,
reads,
blocks: opened.map((o) => ({ no: o.no, readOnly: o.readOnly, onSheet: !!o.onSheet })),
opened: opened.map((o) => ({ no: o.no, canWrite: o.canWrite, to: o.target.layer })),
rows: rows().map((r) => ({
label: r.opts.label,
sub: r.opts.sub,
stripe: r.opts.stripe,
tail: r.opts.trailing,
tail: r.opts.trailing.kids.filter((k) => !k.hidden).map((k) => (typeof k === "string" ? k : k.label)),
})),
count: p.count(),
};
+21 -15
View File
@@ -1,7 +1,7 @@
"""780-2 지킴 — 엑셀 보기 단가산출표 · 상세 탭 = 오른쪽 단가산출 목록(요구 627).
서버 없이 잡음: 탭(역할) 따라 판 바꿈(일위대가표 = 호표 목록 · 단가산출 탭 = 단가산출 목록 · 칸은 처음 한 번만 지음) ·
시트 행 → 목록 강조 · 목록 줄 → 지금 탭의 블록 자리 · 만들 층 ≠ 파일 층 = 보기만 · 거르기(전체 · 안 · 밖 · 고를 줄 있음) ·
시트 행 → 목록 강조 · 목록 줄 → 지금 탭의 블록 자리 · 줄 끝 [가져온 식으로 열기](809-1 · 판정 블록 판 걷음) ·
[끝내기] = 막지 않고 물음창 글만 ·
화면 = 부품 상자만 · 700 줄.
"""
@@ -34,7 +34,7 @@ PICKS = [["s1", "B5"], ["s2", "B12"], ["s1", "C3"], ["s3", "A85"], ["s3", "A2"]]
def test_판_바꿈_목록_지음_강조_뛰기():
got = _node({"sheetSk": {"same": True, "canWrite": True, "picks": PICKS}})
# 785-6 — 단가산출 목록 = 새 블록 판정(hopyo-judge 목록) · 처음 단가산출 탭에 올 때 한 번 읽음
# 809-1 — 단가산출 목록 = 블록 칠 재료(hopyo-marks) · 처음 단가산출 탭에 올 때 한 번 읽음
hide = [(s["호표판숨김"], s["단가판숨김"], s["읽음"]) for s in got["seen"]]
assert hide == [
(False, True, 0),
@@ -53,21 +53,22 @@ def test_판_바꿈_목록_지음_강조_뛰기():
"단산8 거푸집",
None,
]
# 목록 줄 누름 = 지금 탭의 자리(단가산출표 = 목록 행 · 상세 = 블록 범위) + 시트 아래 블록 판
# 목록 줄 누름 = 지금 탭의 자리(단가산출표 = 목록 행 · 상세 = 블록 범위) · 줄 끝 = 가져온 식 모달
assert got["jumps"] == [["s2", [8, 9]], ["s3", [8, 9]], ["s3", [8, 9]]]
assert got["blocks"] == [{"no": 8, "readOnly": False, "onSheet": True}] * 3
assert got["opened"] == [{"no": 8, "canWrite": True, "to": "personal"}] * 3
rows = {r["label"]: r for r in got["rows"]}
assert rows["단산8 거푸집"]["sub"] == "㎡ · 고를 것 2 · M02_JudgeCalls 1"
assert rows["단산8 거푸집"]["tail"] == "accent:M02_JudgeConfirmed" # 캐시 확인
assert rows["단산9 잡석"]["sub"] == "㎥ · 줄 조합 · 준용 · M02_JudgeOutside"
assert rows["단산10 운반"]["tail"] == "success:M02_JudgeMade SK-000001"
assert got["count"] == {"블록": 2, "밖": 1, "줄": 2} # 만든 SK 뺌
assert rows["단산8 거푸집"]["sub"] == "㎡ · M02_JudgeCalls 1"
assert rows["단산8 거푸집"]["tail"] == [
"M02_ToMySk"
] # 만들 층 = 개인 → [가져온 식으로 열기] 숨김
assert rows["단산9 잡석"]["sub"] == "㎥ · M02_JudgeOutside"
assert rows["단산10 운반"]["tail"] == ["success:M02_JudgeMade SK-000001", "M02_ToMySk"]
assert got["count"] == {"블록": 2, "밖": 1, "줄": 0} # 만든 SK 뺌
def test_다른_층_권한_없음_보기만():
for same, can in ((False, True), (True, False)):
got = _node({"sheetSk": {"same": same, "canWrite": can, "picks": [["s2", "B5"]]}})
assert got["blocks"][0]["readOnly"] is True
def test_권한_그대로_넘김():
got = _node({"sheetSk": {"same": False, "canWrite": False, "picks": [["s2", "B5"]]}})
assert got["opened"][0] == {"no": 8, "canWrite": False, "to": "company"}
def _src(name: str) -> str:
@@ -78,11 +79,16 @@ def test_캐시_공유_알림_칠_다시():
sheet = _src("Sheet")
assert "createSheetSk(" in sheet and "sk?.at(s);" in sheet
side = _src("SheetSk")
assert "readJudge(opt.scope, opt.key, opt.target)" in side # 785-6 새 블록 판정 목록
assert "readHopyoMarks(opt.scope, opt.key, opt.target)" in side # 809-1 블록 칠 재료 목록
assert (
"createJudgeBlock" not in side and "createJudgeMove" not in side
) # 판정 블록 판 · 옮기기 걷음
nav = _src("SheetNav")
assert "blockAt(sheet: string, row: number)" in nav and "spotOf(no: number, role: Role)" in nav
wizard = _src("Import")
assert 'L("M02_SkUnmoved")' in wizard and "leftCount" not in wizard # 밖 = 안 막음 · 옛 남은 줄 막음 걷음
assert (
'L("M02_SkUnmoved")' in wizard and "leftCount" not in wizard
) # 밖 = 안 막음 · 옛 남은 줄 막음 걷음
tone = (M02 / "logic" / "M02_MasterTemplete_Hopyo_SheetMap.ts").read_text(encoding="utf-8")
assert '글: "일위대가 밖"' in tone
+11 -53
View File
@@ -1,5 +1,5 @@
"""785-1(642-1) 지킴 — 출처 풀이 · 조항 색인(`M02_Hopyo_Clause`) · 블록 글 → 절 나무 · 상수 ·
대입식 · 절 몫(`M02_Hopyo_Formula`) · 북부청 견본 엑셀(설계 642-A 5-3 첫 줄 기대).
"""785-1(642-1) 지킴 — 블록 글 → 절 나무 · 상수 · 대입식 · 절 몫(`M02_Hopyo_Formula`) · 북부청 견본 엑셀.
조항 색인 → 기초 후보(`Clause.basis` · `index`) 시험은 809-1 에서 지움(경쟁사 로직 인정 · 요구 703).
읽기만 · 서버 없음.
"""
@@ -16,7 +16,6 @@ ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from M02_MasterTemplete import M02_Hopyo_Clause as clause # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Formula as fm # noqa: E402
@@ -36,60 +35,19 @@ def blocks() -> dict[int, dict]:
return {b["번"]: b for b in book["단산"]}
@lru_cache(maxsize=None)
def got(no: int) -> dict:
return clause.basis(blocks()[no])
def keys(no: int) -> set[str]:
return {o["키"] for o in got(no)["후보"]}
# ── 조항 색인 → 후보 ──────────────────────────────────────────────────
@pytest.mark.parametrize(
"no, want",
[
(10, {"GC001140", "GC001141", "GC001142"}),
(46, {"GC000198"}),
(24, {"GC000211"}),
(47, {"GC000206", "GC000207"}),
(39, {"GF000224", "GF000225"}),
],
)
def test_후보(no, want):
assert keys(no) == want
assert got(no)["표지"] is None
def test_윗마디_절마다():
"""47 「6-2」 → 절 「1. 현장가공」 GC000206 · 「2. 조립」 GC000207 · 공장가공은 뺌."""
by = {o["키"]: (o["맞음"], o["절"]) for o in got(47)["후보"]}
assert by == {"GC000206": ("윗마디", "1"), "GC000207": ("윗마디", "2")}
assert {o["키"] for o in got(47)["뺌"]} >= {"GC000208"}
@pytest.mark.parametrize("no, mark", [(33, "준용"), (6, "옛 번호"), (3, "번호 없음")])
def test_표지(no, mark):
assert got(no)["표지"] == mark
def test_준용_결과단위_거름_절_번호():
"""33 「산림 12-12 준용」 → GF000177(원/식) 뺌 · 절 4 「표준품셈3-4-8」 → GC000110."""
r = got(33)
assert {o["키"]: o["까닭"][:7] for o in r["뺌"]} == {"GF000177": "결과단위 다름"}
assert [(o["키"], o["절"]) for o in r["후보"]] == [("GC000110", "4")]
def test_색인():
assert set(clause.index()[("건설품셈 공통", "1-2-7")]) == {"GC001140", "GC001141", "GC001142"}
assert clause.nature("원/10㎡") == "면적" and clause.nature("원/자재 단위") is None
# ── 절 나무 ───────────────────────────────────────────────────────────
def _works(no: int) -> list[str]:
nodes = blocks()[no]["절"]
ids = {id(w) for w in fm.works(nodes)}
return [p for p, one in clause._paths(nodes) if id(one) in ids]
return [p for p, one in _paths(nodes) if id(one) in ids]
def _paths(nodes: list[dict], up: str = "") -> list[tuple[str, dict]]:
out = []
for one in nodes:
path = f"{up}.{one['번']}" if up else one["번"]
out += [(path, one), *_paths(one["아래"], path)]
return out
def test_절_나무():
-194
View File
@@ -1,194 +0,0 @@
# -*- coding: utf-8 -*-
"""642-2(PLAN 785-2) 옵션값 읽기 · 검산 · 판정 — 북부청 임도 예시 1 · 2 · 3 + 단산46 · 39 기대표
(설계 642-A 2-10 · 5-3) · 출처 꼬리표 · 지어 넣은 값 0 · 입력 필요 · 품 같음 · master_auto.defaults.
읽기만(엑셀 · M01) · storage 안 씀."""
from decimal import Decimal
from pathlib import Path
import pytest
from M02_MasterTemplete import M02_Hopyo_Bind as hb
from M02_MasterTemplete import M02_Hopyo_Excel as hx
from M02_MasterTemplete import M02_Hopyo_Solve as hs
NORTH = (
Path(__file__).resolve().parents[1]
/ "knowledge/original/실무문서/2026_실무일위대가 모음"
/ "2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls"
)
PATHS = {"B1", "B2", "B3", "B4", "B5", "B6"}
# 후보 = 785-1 Clause 몫(조항 색인) — 여기선 설계 2-10 후보 열 그대로
CANDS = {
10: ["GC001140", "GC001141", "GC001142"],
24: ["GC000211"],
46: ["GC000198"],
47: [{"키": "GC000206", "절": "1"}, {"키": "GC000207", "절": "2"}],
39: ["GF000224", "GF000225"],
}
@pytest.fixture(scope="module")
def master():
return hb.m01.cm.master()
@pytest.fixture(scope="module")
def book():
return hx.read_book(NORTH.name, NORTH.read_bytes())
@pytest.fixture(scope="module")
def solved(master, book):
blocks = {b["번"]: b for b in book["단산"]}
out = {}
for no, cands in CANDS.items():
hopyo = next(
h for h in book["호표"] if any((x.get("참조") or {}).get("번") == no for x in h["줄"])
)
line = next(x for x in hopyo["줄"] if (x.get("참조") or {}).get("번") == no)
same = [x for x in hopyo["줄"] if x is not line]
blk = blocks[no]
out[no] = hs.block(master, blk, hs.parts(blk, cands), {"줄": line, "같은줄": same})
return out
def _cand(solved, no, key, sec=0):
return next(c for c in solved[no]["절"][sec]["후보"] if c["키"] == key)
def _where(cand):
return {k: v["길"] for k, v in cand["바인딩"]["출처"].items()}
def test_예시1_단산10_소운반(solved):
got = solved[10]
assert got["판정"] == "기초로 풂" and got["고를수"] == 0
assert got["절"][0]["고름"] == "GC001140"
c = _cand(solved, 10, "GC001140")
b = c["바인딩"]
assert b["입력값"] == {
"운반무게": 575,
"회당운반량": 25,
"운반거리": 6,
"경사각도": 35,
"운반회수": 42,
}
assert _where(c) == {
"운반무게": "B4",
"회당운반량": "B4",
"운반거리": "B3",
"경사각도": "B3",
"운반회수": "B3",
}
assert b["입력필요"] == [] and b["엇갈림"] == {}
assert set(b["안씀"]) == {"재료종별", "재료형상", "재료갈래", "운반로", "실작업시간"}
(row,) = c["줄"]
assert row["판정"] == "품 같음"
assert Decimal(str(row["우리"]["품"])).quantize(Decimal("0.000001")) == Decimal("0.547619")
assert round(c["금액"]["계"], 1) == 94227.7
def test_예시2_단산47_절_둘(solved):
got = solved[47]
one, two = got["절"]
assert (one["고름"], two["고름"]) == ("GC000206", "GC000207")
a = _cand(solved, 47, "GC000206", 0)["바인딩"]
assert a["입력값"] == {"Type": "Type-Ⅰ"} and a["출처"]["Type"]["길"] == "B1"
c = _cand(solved, 47, "GC000207", 1)
b = c["바인딩"]
assert b["입력값"] == {"Type": "Type-Ⅰ", "호": "Ⅰ-1", "소량감률": 50}
assert _where(c) == {"Type": "B1", "호": "B5", "소량감률": "B5"}
assert [x["값"] for x in b["엇갈림"]["구분"]] == ["토목", "건축"]
assert b["기본"]["구분"] == "토목"
assert [r["판정"] for r in c["줄"]] == ["품 같음"] * 4
assert got["판정"] == "고를 것" and got["고를수"] == 1 # 구분 하나만 사용자
def test_예시3_단산24_합판거푸집(solved, master, book):
c = _cand(solved, 24, "GC000211")
b = c["바인딩"]
assert b["입력값"] == {
"사용횟수": "4회",
"유형": "보통",
"지붕슬래브": "아님",
"폼타이": "아님",
}
assert _where(c) == {"사용횟수": "B1", "유형": "B2", "지붕슬래브": "B2", "폼타이": "B2"}
assert b["입력필요"] == ["높이감률", "구멍땜"]
assert b["기본"]["높이감률"] is None and b["기본"]["구멍땜"] is None # 규칙에 맞는 칸 없음
assert set(b["안씀"]) == {"폼타이규격", "측압"} and b["비움"] == ["자재지역"]
assert solved[24]["판정"] == "고를 것" and solved[24]["고를수"] == 2
# 사용자가 0 넣음 → 노무 줄 품 같음 · 노무비 38,291 = 엑셀
given = {**b["입력값"], "높이감률": 0, "구멍땜": 0}
items = [x for x in next(b for b in book["단산"] if b["번"] == 24)["항목"] if not x.get("밖")]
rows = hs.pair(hb.quantities(master, "GC000211", given), items)
assert {r["판정"] for r in rows if r["엑셀"]} == {"품 같음"}
fill = {n: b["기본"][n] for n in b["안씀"]} | {"자재지역": None}
money = hb.mf.run(master, "GC000211", {**fill, **given})
assert round(money["노무비"]) == 38291
def test_단산46_레미콘타설(solved):
got = solved[46]
c = _cand(solved, 46, "GC000198")
b = c["바인딩"]
assert b["입력값"] == {"타설방법": "장비사용타설", "구조물": "철근구조물", "소량감률": 50}
assert _where(c) == {"타설방법": "B1", "구조물": "B1", "소량감률": "B5"}
assert b["입력필요"] == ["굴착기"] and b["비움"] == ["지역"] # 굴착기 규격 = B3 · 사용자
mach = next(r for r in c["줄"] if r["엑셀"] and r["엑셀"]["묶음"] == "X")
assert mach["판정"] == "품 같음" and mach["표지"] == "기계 갈래 엇갈림"
assert [d["이름"] for d in got["덧"]] == ["레미콘"] # 별도계상 = 덧 줄
def test_단산39_찰붙임(solved):
sec = solved[39]["절"][0]
assert sec["고름"] == "GF000225" and solved[39]["판정"] == "기초로 풂"
c = _cand(solved, 39, "GF000225")
assert c["바인딩"]["입력값"] == {
"뒷길이": 35,
"굵은골재최대치수": 25,
"강도": 21,
"슬럼프": "120",
"채움콘크리트량": 0.12,
"보정작업": "해당 없음",
}
assert _where(c)["뒷길이"] == "B1" and _where(c)["채움콘크리트량"] == "B6"
assert _cand(solved, 39, "GF000224")["판정"] == "품 다름"
def test_지어_넣은_값_없음(solved):
for got in solved.values():
for sec in got["절"]:
for c in sec["후보"]:
b = c["바인딩"]
assert set(b["입력값"]) == set(b["출처"]) # 출처 없는 값 0
assert {v["길"] for v in b["출처"].values()} <= PATHS
@pytest.mark.parametrize(
"excel, ours, want",
[
(0.67, 0.6667, True),
(1.18, 1.1765, True),
(3.53, 3.5294, True),
(0.1111111111, 0.11111, True),
(0.67, 0.69, False),
(0.547619, 0.5476190476, True),
(None, 1, False),
],
)
def test_품_같음(excel, ours, want):
assert hs.same(excel, ours) is want
def test_defaults_규칙(master):
ma = hb.ma
jige = ma.defaults(master, master.logic("GC001140"))
assert jige["실작업시간"] == 0 and jige["운반무게"] == 0 # 가드
assert jige["경사각도"] == 0 and jige["운반거리"] is None # 고르기 첫째 · 못 정함
pour = ma.defaults(master, master.logic("GC000198"))
assert pour["소량감률"] == 0 and pour["타설방법"] == "인력운반타설" # 0 을 품은 범위
form = ma.defaults(master, master.logic("GC000211"))
assert form["높이감률"] is None and form["구멍땜"] is None
assert 1 not in [v for v in jige.values() if not isinstance(v, str)] # 지어 넣는 1 없음
-296
View File
@@ -1,296 +0,0 @@
"""642-3 1차 줄 판정 · 엑셀 사전 · 유가전력 MO 지킴 시험(PLAN 785-3 · 설계 642-A 5-3 셋째 줄).
- 북부청 임도 415 줄 1차 갈래 수 = 단산 268 · 일위 1 · 별도계상 64 · 재료(사전) 78 · 기계 3 · 율 1
(분석 642-C 3-1) · 판정 2차 필요 268 · 같은 EST 꼴 사방 201 · 거창 68 = 모든 줄 갈래 하나(100 %)
- 엑셀 사전 = 단가표 시트 갈래 수 · 노임 19 M01 LB 하나씩 · 경유 → MO
- 장부 짝 고른 것 꼴(새) · 옛 꼴 그대로 · 틀린 키 400 · 환산 옮김(Match.factor = Row.factor)
- 유가전력 MO — 고정 줄 이음 셈(재료비 = MO 값 · 묶음 재료 → 율 밑) · 자재 목록 MO · TS 짝 거울
엑셀은 읽기만 · 층 · storage 안 씀.
"""
import collections
import json
import os
import subprocess
from pathlib import Path
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Hopyo_Elem as elem
from M02_MasterTemplete import M02_Hopyo_Excel as ex
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Hopyo_Match as hm
from M02_MasterTemplete import M02_Hopyo_Row as row
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Fixed as fixed
HERE = Path(__file__).resolve().parent
DOCS = HERE.parent / "knowledge" / "original" / "실무문서"
BUKBU = DOCS / "2026_실무일위대가 모음" / "2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls"
SABANG = DOCS / "2026_실무일위대가 모음" / "2026년_북부지방산림청_사방사업(계류+댐)_설계내역서.xls"
GEOCHANG = (
DOCS
/ "01.산불거창(거창북상병곡)"
/ "01.내역서"
/ "00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx"
)
OIL = "MO000002" # 자동차용경유 전국평균
def _read(path: Path, keys: bool) -> dict:
data = path.read_bytes()
book = ex.read_book(path.name, data)
sheets, _ = ex._open(path.name, data, row.SHEETS)
return {**book, "사전": row.lexicon(sheets, keys)}
def _kinds(book: dict) -> collections.Counter:
"""줄마다 (갈래, 근거 첫 낱말) — 1차 ①~③ · ④ 사전."""
index = row._index(book["사전"])
out = collections.Counter()
for h in book["호표"]:
for line in h["줄"]:
kind, why, _ = row.first(line)
if kind is None:
hits = row.second(line, index)
kinds = sorted({x["갈래"] for x in hits}) or ["없음"]
kind, why = "/".join(kinds), "사전"
out[(kind, why.split(" ")[0])] += 1
return out
@pytest.fixture(scope="module")
def bukbu() -> dict:
return _read(BUKBU, True)
def test_북부청_415_줄_1차_갈래(bukbu):
got = _kinds(bukbu)
assert sum(got.values()) == 415
assert got == {
("단가산출", "비고"): 268,
("일위대가", "비고"): 1,
("재료", "별도계상"): 64,
("재료", "사전"): 78,
("기계", "단위"): 3,
("율", "단위"): 1,
}
def test_북부청_줄_판_판정과_후보(bukbu):
out = row.rows(bukbu)
lines = [x for v in out.values() for x in v]
assert len(out) == 68 and len(lines) == 415
verdict = collections.Counter(x["판정"] for x in lines)
assert verdict["2차 필요"] == 268 and set(verdict) <= set(row.VERDICTS)
assert all(set(x["갈래"]) <= set(row.KINDS) and x["후보"] is not None for x in lines)
first = {x["행"]: x for x in out[1]} # 제1호표(설계 2-10 기대표)
assert first[6]["갈래"] == ["재료"] and first[6]["근거"] == "별도계상"
assert first[6]["판정"] == "제안 있음" and len(first[6]["후보"]) == 1
apart = [x for x in lines if x["근거"] == "별도계상"] # 786-1 별도계상 64 줄 = 고정 줄 하나
assert len(apart) == 64 and all(
x["갈래"] == ["재료"] and x["판정"] == "제안 있음" for x in apart
)
for x in apart:
(p,) = x["후보"]
assert (p["갈래"], p["키"], p["별도계상"], p["품목"]) == (
"고정",
None,
True,
"프로젝트에서",
)
assert p["단위"] and p["단가"] == {"노무비": 0, "재료비": 0, "경비": 0} and "장부" not in x
assert all(
p["갈래"] != "고정" or "품목" not in p for x in lines for p in x["후보"] if x not in apart
)
assert first[7]["번"] == 46 and first[7]["판정"] == "2차 필요"
assert first[10]["갈래"] == ["재료"] and first[10]["근거"].startswith("사전 자재단가")
rate = next(x for x in lines if x["갈래"] == ["율"])
assert rate["판정"] == "제안 있음"
assert (
rate["후보"][0]["비목"] == "재료비" and rate["후보"][0]["기준"] == "재료비"
) # 재료비의 3 %
sub = next(x for x in lines if x["갈래"] == ["일위대가"])
assert sub["번"] == 46 and sub["후보"][0]["키"] == "제46호표" and sub["판정"] == "제안 있음"
for x in lines: # 고른 것 꼴(설계 2-1)
for p in x["후보"]:
assert {"갈래", "키", "이름", "단위", "입력값", "출처", "몫"} <= set(p)
def test_엑셀_사전_단가표_시트(bukbu):
lex = bukbu["사전"]
assert lex["꼴"] == "EST"
count = collections.Counter(x["갈래"] for x in lex["줄"])
assert count == {
"단가산출": 95,
"재료": 91,
"일위대가": 68,
"기계": 32,
"인력": 19,
"유가": 4, # 자재단가 경유 · 휘발유 + 기본재료비 둘
"환율": 3,
}
people = [x for x in lex["줄"] if x["갈래"] == "인력"]
assert all(len(x["후보"]) == 1 and x["후보"][0]["키"].startswith("LB") for x in people)
oil = next(x for x in lex["줄"] if x["갈래"] == "유가" and x["시트"] == "자재단가")
assert oil["후보"][0]["키"] == OIL
json.dumps(lex, ensure_ascii=False) # HP 에 둘 수 있는 꼴
@pytest.mark.parametrize("path, total", [(SABANG, 201), (GEOCHANG, 68)])
def test_같은_EST_꼴_모든_줄_갈래_하나(path, total):
got = _kinds(_read(path, False))
assert sum(got.values()) == total
assert not [k for k in got if "/" in k[0] or k[0] == "없음"]
# ── 장부 짝 고른 것 꼴 ─────────────────────────────────────────────────────
def test_장부_짝_고른_것_꼴과_옛_꼴():
known = ledger._known()
mat = sorted(known[1])[0]
new = ledger.check_pair(
{"갈래": "유가", "키": OIL, "배": 1, "이름": "경유", "단위": "L"}, "시험", known
)
assert new == {"갈래": "유가", "키": OIL, "입력값": {}, "배": 1, "몫": None, "이름": "경유",
"단위": "L"} # fmt: skip
rate = {"갈래": "율", "비목": "재료비", "기준": "재료", "배": 1}
assert ledger.check_pair(rate, "시험", known)["기준"] == "재료"
assert ledger.check_pair({"자재": mat, "배": 2}, "시험", known) == {
"자재": mat, "입력값": {}, "배": 2, "몫": None} # fmt: skip
for bad in (
{"갈래": "유가", "키": "MT000008"},
{"갈래": "고정", "키": "MO000002"},
{"갈래": "인력", "키": "LB999999"},
{"갈래": "재료", "키": mat, "입력값": {"x": 1}},
{"갈래": "율", "비목": "재료비"},
{"갈래": "없는갈래"},
):
with pytest.raises(ledger.store.StoreError) as got:
ledger.check_pair(bad, "시험", known)
assert got.value.status == 400
line = {"이름": "경유", "규격": "", "단위": "ℓ", "수량": 3, "단가": {"재료비": 1376}}
book = [("personal", "1/2", {ledger.key(line): {"갈래": "짝", "짝": [new]}})]
out = row.judge(line, {}, {}, book, folder="x")
assert out["판정"] == "제안 있음" and out["후보"][0]["키"] == OIL
assert out["후보"][0]["출처"] == {"장부": "personal"}
# 단산 줄 = 장부 짝이 있어도 늘 2차 블록 풀이(장부 = 1차 후보 힌트만 · 785-7)
sk = {
"이름": "야면석소운반",
"규격": "",
"단위": "㎡",
"수량": 1,
"참조": {"종류": "단산", "번": 10},
}
base = {"갈래": "기초", "키": "GC001140", "입력값": {}, "배": 1, "몫": None}
book = [("personal", "1/2", {ledger.key(sk): {"갈래": "짝", "짝": [base]}})]
out = row.judge(sk, {}, {}, book, folder="x")
assert out["판정"] == "2차 필요" and out["후보"][0]["키"] == "GC001140"
def test_환산_옮김():
for have, want, spec in (
("㎡", "㎡", ""),
("cm", "m", ""),
("㎡", "㎥", "T=20cm"),
("개", "㎥", ""),
):
assert hm.factor(have, want, spec) == row.factor(have, want, spec)
assert row.factor("cm", "m")["배"] == 0.01
q = row.quantity({"수량": 2.5, "단위": "㎡", "규격": "T=20cm"}, "㎥", 0.5)
assert q["배"] == 0.2 and q["우리"] == 0.25
# ── 유가전력 MO ─────────────────────────────────────────────────────────
def _oil_price() -> float:
return float(m01.loaded()[1].get(OIL)["값"])
def test_유가_후보():
keys = [x["키"] for x in elem.oils("경유")]
assert keys[0] == OIL and all(k.startswith("MO") for k in keys)
assert elem.lost({"갈래": "유가", "이름": "경유"})[0]["키"] == OIL
OIL_DOC = {
"열": "rv",
"담은로직": [
{"로직": "GC000421", "수량": {"손": 2}},
{
"로직": "",
"고정": {
"이름": "경유",
"단위": "L",
"단가": {"재료비": 1376},
"이음": {"유가": OIL},
}, # fmt: skip
"수량": {"손": 3},
},
{
"로직": "",
"고정": {
"이름": "잡재료",
"단위": "%",
"단가": {},
"비율": {"기준": "재료", "비목": "재료비"},
}, # fmt: skip
"수량": {"손": 10},
},
],
"자재": [{"코드": OIL, "수량": {"손": 2}, "별도계상": "직접계산"}], # 기본 = 별도계상(합 밖)
}
def test_고정_줄_유가_이음_셈():
doc = labor.clean(OIL_DOC)
fx = doc["담은로직"][1]["고정"]
assert fx["이음"] == {"노임": None, "자재": None, "기계": None, "유가": OIL}
assert fixed.group_of(fx) == "재료"
assert doc["자재"][0]["코드"] == OIL # 자재 목록 MO
out = labor.calc(doc, {})
line = out["줄"][1]
assert line["기준"]["재료비"] == pytest.approx(_oil_price()) and line["이은"] == ["재료비"]
assert out["자재"][0]["기준단가"] == pytest.approx(_oil_price())
with pytest.raises(labor.store.StoreError): # 머리 틀림 = 400
labor.clean({**OIL_DOC, "담은로직": [{**OIL_DOC["담은로직"][1], "고정": {
**OIL_DOC["담은로직"][1]["고정"], "이음": {"유가": "MT000008"}}}]}) # fmt: skip
old = labor.clean({"담은로직": [{"로직": "", "고정": {"이름": "a", "단가": {}}}]})
assert old["담은로직"][0]["고정"]["이음"] == {"노임": None, "자재": None, "기계": None}
def _ts(answer: dict, doc: dict, values: dict) -> dict:
proc = subprocess.run(
["node", os.path.join(HERE, "helper_m02_labor_calc.cjs")],
input=json.dumps({"답": answer, "문서": doc, "값": values}, ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
check=True,
)
return json.loads(proc.stdout)
def _same(a, b) -> None:
if isinstance(a, dict):
assert set(a) == set(b)
for k in a:
_same(a[k], b[k])
elif isinstance(a, list):
assert len(a) == len(b)
for x, y in zip(a, b):
_same(x, y)
elif isinstance(a, (int, float)) and not isinstance(a, bool):
assert b == pytest.approx(a, rel=1e-9)
else:
assert a == b
def test_유가_율_고정_줄_TS_거울():
doc = labor.clean(OIL_DOC)
answer = labor.calc(doc, {})
py = labor.settle(answer, doc, {})
_same(py, _ts(answer, doc, {}))
rate = py["줄"][2]
oil = py["줄"][1]["반영"]["재료비"]
mats = py["자재"][0]["반영"] or 0
assert rate["기준"]["재료비"] == pytest.approx(oil + mats) # 묶음 재료 = 유가 줄 + 자재
-210
View File
@@ -1,210 +0,0 @@
# -*- coding: utf-8 -*-
"""642-5(PLAN 785-5) 만들기 · 길 지킴 시험(설계 642-A 5-3 넷째 줄) — 임시 storage(`client` 마당).
- 판정 길(목록 · 호표 · 블록) · 검산 길(입력값 = 엔진이 쓴 칸 모두 · 지역 · 자재지역 · 조종원직종 비움)
- 옮기기 → SC 줄(고른 것마다 · 같은 별칭 · 본뜬 {호표, 차례, 행} · 수량 = 엑셀 × 몫) · 같은 기초 둘째 = 사본 ·
별도계상 자재 · 입력 필요 = 그 줄만 막힘(고정 줄로 남음) · SK 더한줄(바탕 없음) · 장부 · 다시 옮김 = SC 안 늚
- 금액 · 품 칸 막음 · M01 원본 그대로
"""
import json
from M02_MasterTemplete import M02_Hopyo_Bind as hb
from M02_MasterTemplete import M02_Hopyo_Build as hbuild
from M02_MasterTemplete import M02_Hopyo_Judge as judge
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Template_Layers as layers
# 올리기 길 시험의 `client` 마당을 같이 씀
# ruff: noqa: F811
from resources.tester.test_m02_hopyo_excel import _post, client, master_hash # noqa: F401
API = "/api/m02/layers/personal"
KEY = "HP-000001"
def _mine():
return layers.personal_dir(7, 42)
def _doc(kind, key):
return json.loads((_mine() / kind / f"{key}.json").read_bytes())
def _at(unit, no):
"""호표 줄 판에서 단산 N 을 부르는 줄."""
return next(r for r in unit["줄"] if r.get("번") == no and "단가산출" in r["갈래"])
def _base(row, sec=0):
"""그 줄 풀이 절의 고른 기초 · 입력값."""
part = row["풀이"]["절"][sec]
cand = next(c for c in part["후보"] if c["키"] == part["고름"])
return {"갈래": "기초", "키": cand["키"], "입력값": cand["바인딩"]["입력값"]}
def _specs(code):
m = hb.m01.cm.master()
return {s["이름"] for s in hb.specs_of(m.logic(code))} - set(hb.SKIP)
def test_판정_검산_옮기기(client):
before = master_hash()
assert _post(client).status_code == 200
got = client.get(f"{API}/hopyo-judge/{KEY}").json()
assert got["셈중"] is True and got["블록"][9]["판정"] is None # 뒤 셈 전
assert judge.judged(_mine(), KEY) is None
judge.solve_all(_mine(), KEY) # 올림 뒤 뒤 셈(시험은 바로)
got = client.get(f"{API}/hopyo-judge/{KEY}").json()
assert got["셈중"] is False and len(got["호표"]) == 68 and len(got["블록"]) == 95
assert got["블록"][9]["번"] == 10 and got["블록"][9]["판정"] == "기초로 풂"
assert judge.judged(_mine(), KEY)[10] == {"판정": "기초로 풂", "고를수": 0}
assert got["호표"][0]["판정"] == {"제안 있음": 2, "2차 필요": 4}
two = client.get(f"{API}/hopyo-judge/{KEY}/2").json()
ten = _at(two, 10) # 예시 1 야면석소운반 단산10
assert ten["이름"] and "단가" in ten and ten["풀이"]["판정"] == "기초로 풂"
ex1 = _base(ten)
assert ex1["키"] == "GC001140" and ex1["입력값"]["운반무게"] == 575
blk = client.get(f"{API}/hopyo-judge/{KEY}/block/10").json()
assert blk["부름"] and "몫" in blk["절"][0] and "금액" in blk
check = client.post(
f"{API}/hopyo/{KEY}/check", json={"호표": 2, "차례": ten["차례"], "고른": [ex1]}
).json()
one = check["고른"][0]
assert one["판정"] == "품 같음" and one["입력필요"] == []
assert set(one["입력값"]) == _specs("GC001140")
assert not set(one["입력값"]) & set(hb.SKIP) # 지역 · 자재지역 · 조종원직종 비움
assert int(one["금액"]["계"]) == 94227 # 엑셀 94,227 (원 미만 버림)
assert check["수량"]["우리"] == ten["수량"]["엑셀"]
first = client.get(f"{API}/hopyo-judge/{KEY}/1").json()
rebar, form = _at(first, 47), _at(first, 24) # 예시 2 철근 · 예시 3 거푸집
a, b = _base(rebar, 0), _base(rebar, 1)
b["입력값"] = {**b["입력값"], "구분": "토목"} # 엇갈림 = 사용자 고름
a["몫"] = 0.5
mat = first["줄"][0] # 레미콘 별도계상 — 786-1 후보 = 고정 줄 하나(품목은 프로젝트에서)
assert mat["판정"] == "제안 있음" and len(mat["후보"]) == 1
cement = mat["후보"][0]
assert (cement["갈래"], cement["키"], cement["별도계상"]) == ("고정", None, True)
assert cement["품목"] == "프로젝트에서"
stuck = {"갈래": "기초", "키": "GC000211", "입력값": {"유형": "보통"}}
ex3 = _base(form) # 예시 3 GC000211 보통 · 4회 · 높이감률 · 구멍땜 = 입력 필요
url = f"{API}/hopyo/{KEY}/check"
need = client.post(url, json={"호표": 1, "차례": form["차례"], "고른": [ex3]}).json()
assert (
need["고른"][0]["입력필요"] == ["높이감률", "구멍땜"]
and need["고른"][0]["금액"]["계"] is None
)
ex3["입력값"] |= {"높이감률": 0, "구멍땜": 0}
got3 = client.post(url, json={"호표": 1, "차례": form["차례"], "고른": [ex3]}).json()["고른"][0]
assert (
got3["판정"] == "품 같음" and abs(got3["금액"]["노무비"] - 38291) <= 1
) # 엑셀 노무 38,291
other = next(r for r in two["줄"] if r["차례"] > ten["차례"]) # 같은 기초 둘째 줄
twice = {**ex1, "입력값": {**ex1["입력값"], "운반거리": 12}}
body = {
"층": "personal",
"호표": {
"1": {
str(rebar["차례"]): [a, b],
str(form["차례"]): [stuck],
"0": [cement],
},
"2": {str(ten["차례"]): [ex1], str(other["차례"]): [twice]},
},
"블록": {"10": [{**ex1, "몫": 1}]},
}
res = client.post(f"{API}/hopyo/{KEY}/move", json=body)
assert res.status_code == 200, res.text
out = res.json()
assert [x["번"] for x in out["만듦"]] == [1, 2] and out["장부"]["씀"] >= 4
assert out["막힘"] == [
{
"호표": 1,
"차례": form["차례"],
"까닭": out["막힘"][0]["까닭"],
"칸": out["막힘"][0]["칸"],
}
]
assert out["막힘"][0]["까닭"].startswith("입력 필요") and out["막힘"][0]["칸"]
sc2 = _doc("precombo", out["만듦"][1]["키"])
lines = sc2["담은로직"]
mine = next(x for x in lines if x.get("로직") == "GC001140")
assert mine["별칭"] == ten["이름"][: len(mine["별칭"])]
assert mine["본뜬"] == {"호표": 2, "차례": ten["차례"], "행": ten["행"]}
assert mine["수량"]["손"] == ten["수량"]["엑셀"]
assert set(mine["입력값"]) == _specs("GC001140")
wrap = next(x for x in lines if x.get("별칭") == other["이름"][: len(x.get("별칭") or "")])
inner = wrap["참조"]["몸"]["담은로직"][
0
] # 같은 기초 둘째 = 한 줄 SC 사본(본뜬 = 사본 출처 740-1)
assert inner["로직"] == "GC001140" and inner["입력값"]["운반거리"] == 12
assert len(lines) == len(two["줄"]) # 확인 안 한 줄 = 고정(빠짐 0)
sc1 = _doc("precombo", out["만듦"][0]["키"])
pair = [x for x in sc1["담은로직"] if (x.get("본뜬") or {}).get("차례") == rebar["차례"]]
assert [x["로직"] for x in pair] == ["GC000206", "GC000207"]
assert pair[0]["별칭"] == pair[1]["별칭"] # 한 엑셀 줄 = 우리 두 줄 · 같은 별칭
excel = rebar["수량"]["엑셀"]
assert pair[0]["수량"]["손"] == excel * 0.5 and pair[1]["수량"]["손"] == excel
assert pair[1]["입력값"]["구분"] == "토목"
held = next(
x
for x in sc1["담은로직"]
if (x.get("고정") or {}).get("출처", {}).get("줄") == form["차례"]
)
assert "본뜬" not in held # 막힌 줄 = 고정 줄(엑셀 숫자)
held_apart = next(
x for x in sc1["담은로직"] if (x.get("본뜬") or {}).get("차례") == mat["차례"]
)
fx = held_apart["고정"] # 별도계상 줄 = 합 밖 고정 줄(단가 0 · 자재 목록에 안 넣음)
assert fx["이름"] == mat["이름"] and fx["규격"] == "별도계상" and not any(fx["단가"].values())
assert held_apart["수량"]["손"] == mat["수량"]["엑셀"]
assert held_apart["메모"].startswith("별도계상 · 품목은 프로젝트에서")
without = {**sc1, "담은로직": [x for x in sc1["담은로직"] if x is not held_apart]}
got_sum, base_sum = labor.server(sc1)["계산값"], labor.server(without)["계산값"]
assert got_sum["반영합"] == base_sum["반영합"] # 서버 정본 셈 — 별도계상 줄은 합에 안 듦
sk = _doc("sangeun", out["단가산출"][0]["키"])
assert sk["바탕"] is None and [x["로직"] for x in sk["더한줄"]] == ["GC001140"]
assert sk["더한줄"][0]["입력값"]["운반무게"] == 575
book = ledger.read(_mine())
assert any(
p.get("갈래") == "기초" and p.get("키") == "GC000207"
for row in book.values()
for p in row.get("짝") or []
)
scs = sorted(p.stem for p in (_mine() / "precombo").glob("SC-*.json"))
stop = client.post(f"{API}/hopyo/{KEY}/move", json=body) # 이미 옮긴 것 = 덮음 없이 409
assert stop.status_code == 409 and [x["번"] for x in stop.json()["detail"]["호표"]] == [1, 2]
assert sorted(p.stem for p in (_mine() / "precombo").glob("SC-*.json")) == scs
again = client.post(f"{API}/hopyo/{KEY}/move", json={**body, "덮음": True})
assert again.status_code == 200, again.text
again = again.json()
assert [x["키"] for x in again["만듦"]] == [x["키"] for x in out["만듦"]]
assert sorted(p.stem for p in (_mine() / "precombo").glob("SC-*.json")) == scs
assert hbuild.confirmed(_mine())[hbuild.mark(KEY, 2)] == out["만듦"][1]["키"]
assert client.delete(f"{API}/hopyo/{KEY}").status_code == 200 # [끝내기] = 저장본도 지움
assert not list((_mine() / "실무자료" / "_derived").glob(f"*{KEY}*"))
assert master_hash() == before
def test_막는_길(client):
assert _post(client).status_code == 200
pick = {"갈래": "기초", "키": "GC001140", "입력값": {}, "금액": 1}
bad = client.post(
f"{API}/hopyo/{KEY}/move", json={"층": "personal", "호표": {"2": {"0": [pick]}}}
)
assert bad.status_code == 422 # 금액 · 품 칸 = 서버가 셈
none = client.post(f"{API}/hopyo/{KEY}/move", json={"층": "personal", "호표": {"99": {}}})
assert none.status_code == 404
wrong = {"갈래": "인력", "키": "LB999999"}
res = client.post(
f"{API}/hopyo/{KEY}/move", json={"층": "personal", "호표": {"2": {"0": [wrong]}}}
)
assert res.status_code == 400
-351
View File
@@ -1,351 +0,0 @@
# -*- 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 호표
+12 -9
View File
@@ -1,5 +1,5 @@
"""779-2 불러오기 엑셀 하이라이트 재료 — 목록 `줄상태` 수 = 줄 수 · 상태 = 1차 줄 판정(785-7 · `Judge.view`) ·
`hopyo-marks` 길 읽기만(층 폴더 파일 변화 0) · 블록 판정 = 블록 풀이 저장본(셈 전 None) · SK = 만든 것."""
"""779-2 불러오기 엑셀 하이라이트 재료 — 목록 `줄상태` 수 = 줄 수 · 상태 = 줄 표지(809-1 · `View.listing`) ·
`hopyo-marks` 길 읽기만(층 폴더 파일 변화 0) · 블록 판정 = 불리는 블록 「가져온 식」 · 안 불림 None · SK = 만든 것."""
import hashlib
@@ -24,22 +24,24 @@ def _ready(client):
return layers.personal_dir(7, 42)
def test_줄상태_수_줄_수_1차_판정_같음(client):
def test_줄상태_수_줄_수_표지_같음(client):
_ready(client)
base = URL.format("personal")
whole = client.get(f"{base}/{KEY}").json()
assert len(whole["호표"]) == 68 and whole["셈"]["끝"] is False # 뒤 셈 안 돌림(시험)
assert len(whole["호표"]) == 68 and whole["셈"]["끝"] is True # 뒤 셈 없음(809-1)
for h in whole["호표"][:4]:
one = client.get(f"{base}-judge/{KEY}/{h['번']}").json()["줄"]
one = client.get(f"{base}-view/{KEY}/{h['번']}").json()["줄"]
assert len(h["줄상태"]) == len(one) == h["맞춤"][1], h["번"]
assert [x[1] for x in h["줄상태"]] == [r["판정"] for r in one]
assert [x[2] for x in h["줄상태"]] == [r["판정"] == "제안 있음" for r in one]
assert [x[1] for x in h["줄상태"]] == [r["표지"] for r in one]
assert [x[2] for x in h["줄상태"]] == [
r["갈래"] == "로직" or r["표지"] == "연결됨" for r in one
]
assert h["맞춤"][0] == sum(x[2] for x in h["줄상태"]) and h["만든"] is None
assert [x[0] for x in h["줄상태"]] == list(range(len(h["줄상태"])))
assert whole["맞춤"][1] == 415
def test_marks_읽기만_셈_전_판정_없음(client):
def test_marks_읽기만_가져온_식_판정(client):
folder = _ready(client)
base = URL.format("personal")
before = _tree(folder)
@@ -48,7 +50,8 @@ def test_marks_읽기만_셈_전_판정_없음(client):
rows = got.json()["블록"]
assert _tree(folder) == before # 읽기만 — 파일 변화 0
assert rows and [r["번"] for r in rows] == sorted(r["번"] for r in rows)
assert all(r["판정"] is None and r["고를수"] is None for r in rows) # 블록 풀이 셈 전
assert all(r["판정"] == ("가져온 식" if r["부름"] else None) for r in rows) # 경쟁사 로직 인정
assert all("고를수" not in r and r["이름"] for r in rows)
assert all(r["SK"] is None and r["갈래"] is None and r["못옮김"] == [] for r in rows)
# 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}
+7 -16
View File
@@ -43,21 +43,12 @@ def test_결과_층_고름_둘_이상일_때만():
assert 'let pick: ManageLayer = "personal";' in side
def test_줄_판_자리_좁은_폭():
# 셈 중(블록 풀이) = 3 초마다 다시 읽음 · 줄 판 = 시트 아래 넓은 자리 · 좁은 폭 = 줄 판 높이 바닥
assert "POLL_MS" in _src("SheetSk") and "POLL_MS" in _src("Sheet")
sheet = _src("Sheet")
assert 'attrs: { "data-hopyo-lines": "" }' in sheet and "openLines," in sheet
assert "openLines: (make: (host: HTMLElement)" in _src("SheetSk") # 785-6 줄 판 자리 한 손잡이
css = (M02 / "M02_MasterTemplete_Hopyo.css").read_text(encoding="utf-8")
narrow = css.split("/* 좁은 폭(785-7 · 390)")[1].split("/*")[0]
assert ".m02-hopyo-sheet__lines:not([hidden])" in narrow and "min-height: 16rem;" in narrow
def test_끝내기_머리_막대_끝냄_뒤_빈_본_칸():
imp = _src("Import")
end = imp.split("async function end()")[1]
assert "unmoved()" in end.split("showConfirmDialog(")[0] # 확인하고 안 옮긴 수 = 물음 글만(막지 않음)
assert (
"skCount()" in end.split("showConfirmDialog(")[0]
) # 안 만든 단가산출 = 물음 글만(막지 않음)
assert "deleteHopyo(scope, file.키)" in end and "a.onEnded();" in end
side = _src("Side")
assert "onEnded: () => {" in side and "void refresh();" in side
@@ -77,8 +68,8 @@ def test_부품_상자만_700줄_늘리지_않음():
assert not re.search(r"#[0-9a-fA-F]{3,6}\b|rgb\(", css) and ".m02-hopyo-import" in css
def test_해시_캐시_열쇠_그대로():
picks = (M02 / "logic" / "M02_MasterTemplete_Hopyo_Picks.ts").read_text(encoding="utf-8")
assert "m02-hopyo-judge:" in picks # 판정 고름 캐시 열쇠 하나(옛 확인 · 단가산출 캐시 걷음)
assert "draftKey(layer, null, file.키)" in _src("Import")
def test_해시_캐시_열쇠_없음():
# 809-1 — 판정 고름 캐시(`Hopyo_Picks`) 걷음 · 옮기기 = [내 일위대가로]
assert not (M02 / "logic" / "M02_MasterTemplete_Hopyo_Picks.ts").exists()
assert "draftKey" not in _src("Import") and "dirty: () => false" in _src("Import")
assert "location.hash" not in _src("Import") + _src("Side") # 새 해시 열쇠 없음
-123
View File
@@ -1,123 +0,0 @@
# -*- coding: utf-8 -*-
"""786-2 불러오기 [수정] 모달 지킴 — 「확인 — 제N호표」 옛 꼴 되살림 + 새 기능 「새」 표지
(요구 644 · 645).
- 우리 금액 길(`/hopyo/{키}/price` · 저장 없음) — 옮기기와 같은 길로 한 줄 문서 →
`Labor_Doc.calc`: 예시 1 야면석소운반(단산10 · GC001140 · 입력 필요 0 · 엑셀 1 단위 94,227) ·
예시 3 합판거푸집(GC000211 · 높이감률 · 구멍땜 입력 필요 → 0 넣으면 노무 38,291) ·
쓰기 0(SC · SK 수 그대로)
- 화면 소스 — [수정] = 모달(`openHopyoCheck` · 부품 상자 모달) · 시트 아래 호표 줄 판 몸 없음 ·
「새」 배지 · 구조물 조합 공용 부품(`Labor_Rows` · `Labor_MatMarks` · `Labor_Input` ·
`Labor_Pick`)은 import 만 · 날 button · input · select 0 · 700 줄
임시 storage(`client` 마당) · M01 원본 그대로.
"""
import re
from pathlib import Path
from M02_MasterTemplete import M02_Hopyo_Judge as judge
from M02_MasterTemplete import M02_Template_Layers as layers
# ruff: noqa: F811
from resources.tester.test_m02_hopyo_excel import _post, client, master_hash # noqa: F401
ROOT = Path(__file__).resolve().parents[2]
M02 = ROOT / "M02_MasterTemplete"
API = "/api/m02/layers/personal"
KEY = "HP-000001"
MINE = ("Check", "CheckRows", "ToItem", "Rows")
SHARED = ("Labor_Rows", "Labor_MatMarks", "Labor_Input", "Labor_Pick", "Labor_Detail", "Structure")
def _src(name: str) -> str:
return (M02 / f"M02_MasterTemplete_{name}.ts").read_text(encoding="utf-8")
def _at(unit, no):
return next(r for r in unit["줄"] if r.get("번") == no and "단가산출" in r["갈래"])
def _base(row):
part = row["풀이"]["절"][0]
cand = next(c for c in part["후보"] if c["키"] == part["고름"])
return {"갈래": "기초", "키": cand["키"], "입력값": cand["바인딩"]["입력값"]}
def _count(kind):
folder = layers.personal_dir(7, 42) / kind
return len(list(folder.glob("*.json"))) if folder.exists() else 0
def test_우리금액_옮기기와_같은_길_저장없음(client):
before = master_hash()
assert _post(client).status_code == 200
judge.solve_all(layers.personal_dir(7, 42), KEY)
made = (_count("precombo"), _count("sangeun"))
url = f"{API}/hopyo/{KEY}/price"
two = client.get(f"{API}/hopyo-judge/{KEY}/2").json()
ten = _at(two, 10) # 예시 1 야면석소운반
got = client.post(
url, json={"층": "personal", "호표": 2, "줄": {str(ten["차례"]): [_base(ten)]}}
)
assert got.status_code == 200, got.text
line = got.json()["줄"][str(ten["차례"])]
assert line["까닭"] == [] and line["칸"] == []
assert int(line["금액"]["계"] / ten["수량"]["엑셀"]) == 94227 == ten["단가"]["계"]
one = client.get(f"{API}/hopyo-judge/{KEY}/1").json()
form = _at(one, 24) # 예시 3 합판거푸집
ex3 = _base(form)
need = client.post(url, json={"층": "personal", "호표": 1, "줄": {str(form["차례"]): [ex3]}})
stuck = need.json()["줄"][str(form["차례"])]
assert stuck["금액"] is None and stuck["칸"] == ["높이감률", "구멍땜"]
ex3["입력값"] = {**ex3["입력값"], "높이감률": 0, "구멍땜": 0}
ok = client.post(url, json={"층": "personal", "호표": 1, "줄": {str(form["차례"]): [ex3]}})
money = ok.json()["줄"][str(form["차례"])]["금액"]
assert abs(money["노무비"] / form["수량"]["엑셀"] - 38291) <= 2 # 엑셀 노무 38,291(줄 0.1 원 끝수)
bad = client.post(
url, json={"층": "personal", "호표": 2, "줄": {"0": [{**_base(ten), "금액": 1}]}}
)
assert bad.status_code == 422 # 금액 칸 = 서버가 셈
assert (_count("precombo"), _count("sangeun")) == made # 쓰기 0
assert master_hash() == before
def test_수정은_모달_줄판몸_없음():
sheet = _src("Hopyo_Sheet")
edit = sheet.split("const editButton")[1].split("function paintList")[0]
assert "openHopyoCheck({" in edit and "openLines" not in edit
assert "createJudgeRows" not in sheet + _src("Hopyo_Rows")
check = _src("Hopyo_Check")
assert 'from "@ui/ui_template_modal"' in check and "openModal({" in check
assert 'L("M02_CheckTitle")' in check and "confirmSuggested(" in check # [호표 제안 모두 확인]
assert "runMove(" in check and "[String(no)]: mine" in check # [사전조합 생성] = 이 호표만
assert "h.만든" in check.split("const item = ")[1].split(";")[0] # 옮겨진 뒤만
locale = (ROOT / "ui_template/ui_template_locale_m2_judge.ts").read_text(encoding="utf-8")
assert 'M02_CheckTitle: ["확인 — 제{n}호표"' in locale and 'M02_CheckNew: ["새"' in locale
def test_새_배지_옛_표_꼴():
rows = _src("Hopyo_CheckRows")
assert "createBadge(" in rows and "newBadge()" in rows and "dataset.checkNew" in rows
assert "newBadge()" in _src("Hopyo_Check")
for part in ("moneyCell(", "PRICE_SLOTS", "markCells(", "markHeads()", '"logic"', '"mat"'):
assert part in rows, part
assert "M02_CheckApart" in _src("Hopyo_Rows") # 별도계상 줄 = 고름 칩 없이 글
def test_공용파일_import_만_부품상자만_700줄():
texts = {n: _src(f"Hopyo_{n}") for n in MINE}
joined = "\n".join(texts.values())
for name in ("Labor_Rows", "Labor_MatMarks", "Labor_Input", "Labor_Pick"):
assert f'from "./M02_MasterTemplete_{name}"' in joined, name
for name in SHARED: # 공용 쪽은 새 모달을 모름(거꾸로 이음 없음)
assert "Hopyo_Check" not in _src(name) and "Hopyo_ToItem" not in _src(name), name
for name, text in texts.items():
assert len(text.splitlines()) <= 700, name
assert not re.search(r"""\bel\(\s*["'](button|input|select)["']""", text), name
assert not re.search(r"""createElement\(\s*["'](button|input|select)["']""", text), name
assert "<button" not in text and "<input" not in text, name
assert not re.search(r"#[0-9a-fA-F]{3,6}\b|rgba?\(", text), name
assert len(_src("Hopyo_Sheet").splitlines()) <= 402 # 늘리지 않음
@@ -5,7 +5,7 @@
EST 줄 자리) · 단가 = 엑셀 목록표 값 → 셈 = 엑셀 금액 · 줄마다 검산
- 덮개 · 상수 덮개 · 식 칸 · 변수 줄이 가져온 줄에도 먹음
- 셈 답 `원래` = 엑셀 금액 · `가져온` 원본 글
- [우리 단가로] · [엑셀 단가로] · 이음 고름(덮개 열쇠 옮김) · [우리 기초로 바꾸기]
- [우리 단가로] · [엑셀 단가로] · 이음 고름(덮개 열쇠 옮김) · [우리 기초로 바꾸기] 없음(809-1)
- 만들기(임시 층) · 같은 내용 409 · 옛 SK 지문 그대로 · 펼침 막음 · B09 굳힘 · 단가 전파
"""
@@ -218,20 +218,9 @@ def test_이음_고름_덮개_열쇠_옮김():
imp.link(doc, "labor:없는줄#0", CARP)
def test_우리_기초로_바꾸기():
doc = _doc(덮개={"labor:형틀목공#0": {"qty": 0.2}})
got = imp.to_base(doc, FORM, F211)
new = got["문서"]
assert new["바탕"] == {"로직": FORM} and not imp.is_imported(new) and new["덮개"] == {}
assert float(got["후"]["계"]) == 51419 and float(got["전"]["계"]) > 45652
assert got["차이"]["계"] == got["후"]["계"] - got["전"]["계"]
meta = new[imp.KEY]
assert meta["원본"]["파일"] == "봉화.xlsx" and meta["바꿈"]["로직"]["이름"].startswith("합판")
assert _total(new) == 51419 and same.finger("sangeun", new) == same.finger(
"sangeun", {k: v for k, v in new.items() if k != imp.KEY}
)
with pytest.raises(sd.DocError):
imp.to_base(doc, "GC999999", {})
def test_우리_기초로_바꾸기_없음():
"""809-1 · 요구 703 — 경쟁사 로직 인정 · 가져온 식 → 우리 기초 바꾸기 길 없음."""
assert not hasattr(imp, "to_base")
# ── 꼴 검사 ───────────────────────────────────────────────────────────
@@ -341,7 +330,6 @@ def test_원본_보존_사본():
keep = copy.deepcopy(doc)
imp.to_price(doc, "우리")
imp.link(doc, "labor:형틀목공#0", CARP)
imp.to_base(doc, FORM, F211)
assert doc == keep # 받은 문서 안 고침(화면 캐시 = 답 문서로 바꿈)
assert refs.kind_of("SK-000001") == "sangeun" and MINE["층"] == "personal"
assert flat.flat_doc("sangeun", doc) == doc # 참조 없음 = 그대로
-259
View File
@@ -1,259 +0,0 @@
"""793-3a 지킴 — 근거 제안 엔진(`M02_San_Basis*`) · 실무 여덟 파일 산근 459 블록.
판정 수 ≥ 분석 648-D2 합친 길(기초 짚음 260 · 옵션 전부 52 · 품 검산 맞음 141 · 없음 94) ·
손 확인 견본(합판거푸집 넷 · 철근 Type-Ⅱ 가공 + 조립 · 레미콘 소량감률 50 · 터파기 #51 ·
야면석소운반 GC001140 석재류 · 불량 · 큰돌쌓기 ÷10 근접) · 지어 넣은 값 0 · 보정 자료 = 여덟 파일 ·
저장본 길(임시 폴더). 엑셀 = `helper_m02_hopyo_golden.FILES`(읽기만 · 없으면 skip) ·
STmate 숨은 G 식 = 올림 길과 같은 `M02_San_Basis_Store.hidden`.
794-2c 넓힘 — 없음 85 → 58 · 확정 단 125 → 164 · 손 확인 견본 열(기계 작업량 길 · 섞기 오인 넷 ·
옛 번호 대조 · 같은 말 묶음) · 기계 상수 읽기 · 옛 번호 대조표.
"""
from __future__ import annotations
import json
import time
from collections import Counter
from functools import lru_cache
import helper_m02_hopyo_golden as golden
import pytest
from M02_MasterTemplete import M02_Hopyo_Judge as judge
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_San_Basis as sb
from M02_MasterTemplete import M02_San_Basis_Const as bc
from M02_MasterTemplete import M02_San_Basis_Index as bi
from M02_MasterTemplete import M02_San_Basis_Map as bm
from M02_MasterTemplete import M02_San_Basis_Store as store
from M02_MasterTemplete import M02_San_Book as san_book
WAYS = {
"B1",
"B2",
"B3",
"B4",
"B5",
"B6",
"V",
"검산",
"검산(같은 품 여럿)",
"근접",
"근접(같은 품 여럿)",
}
@lru_cache(maxsize=1)
def books() -> list[tuple]:
"""[(파일, HP 꼴 책 + 숨은식)] — 없는 파일이면 skip."""
out = []
for tag, path in golden.FILES.items():
book = golden.read(tag) if path.exists() else None
if book is None:
pytest.skip(f"실무 엑셀 없음 {tag}")
sheets = san_book.sheets(path.name, path.read_bytes()) if book["꼴"] == "STmate" else {}
out.append((tag, {**book, "숨은식": store.hidden(book, sheets)}))
return out
@lru_cache(maxsize=1)
def results() -> tuple[dict, float]:
sb.ctx()
t0 = time.time()
got = {}
for tag, book in books():
for blk in book["단산"]:
got[f"{tag}#{blk['번']}"] = store.one(book, int(blk["번"]))
return got, time.time() - t0
def test_counts_at_least_d2():
got, sec = results()
kind = Counter(r["꼴"] for r in got.values())
tier = Counter(r["단"] for r in got.values())
same = sum(
1
for r in got.values()
if r["꼴"].startswith("확정")
and "다름" not in (st := [sb.checked(s) for s in sb.chosen_units(r)])
and "같음" in st
)
print(f"\n블록 {len(got)} · 꼴 {dict(kind)} · 단 {dict(tier)} · 품 같음 {same} · {sec:.1f} 초")
assert len(got) == 459
assert sum(v for k, v in kind.items() if k.startswith("확정")) >= 260
assert kind["확정·전부"] >= 52
assert same >= 141
assert tier["없음"] <= 94 and kind["없음"] <= 94
# 794-2c — 같은 말 묶음 · 옛 번호 대조표 · 기계 작업량 길(전 85 · 125 · 260)
assert tier["없음"] <= 58 and tier["확정"] >= 164
assert sum(v for k, v in kind.items() if k.startswith("확정")) >= 309
def _first(bid: str) -> dict:
return results()[0][bid]["근거후보"]
def test_plywood_four_same():
for bid in ("북부청임도#24", "북부청사방#66", "거창#41", "동부청영월#1841"):
(c,) = _first(bid)
assert (c["기초"], c["단"], c["품검산"]) == ("GC000211", "확정", "같음"), bid
assert {c["옵션"][k] for k in ("사용횟수", "유형")} == {"4회", "보통"}, bid
assert {"높이감률", "구멍땜"} <= set(c["입력필요"]), bid # 글에 없는 감률 = 입력 필요
def test_rebar_type2_cut_and_join():
got = {c["기초"]: c for c in _first("북부청임도#72")}
assert set(got) == {"GC000206", "GC000207"}
assert got["GC000206"]["옵션"]["Type"] == got["GC000207"]["옵션"]["Type"] == "Type-Ⅱ"
assert all(c["단"] == "확정" for c in got.values())
def test_remicon_small_50():
(c,) = _first("북부청임도#21")
assert c["기초"] == "GC000198" and c["단"] == "확정"
assert c["옵션"]["소량감률"] == 50 and c["옵션길"]["소량감률"] == "B5"
def test_excavation_51():
(c,) = _first("북부청임도#51")
assert (c["기초"], c["단"], c["옵션"]["Type"], c["옵션"]["깊이"]) == (
"GC000089",
"확정",
"TypeⅢ",
"5m 이하",
)
def test_rubble_carry_gc001140():
r = results()[0]["동부청영월#1466"]
c = r["근거후보"][0]
assert r["단"] == "사람" and c["기초"] == "GC001140"
assert c["옵션"] == {"재료갈래": "석재류", "운반로": "불량"}
assert set(c["옵션길"].values()) == {"V"}
def test_big_stone_near_unit_basis():
r = results()[0]["북부청임도#32"]
(c,) = r["근거후보"]
assert (r["단"], c["기초"], c["단"], c["품검산"]) == ("근접", "GF000226", "근접", "다름")
assert {t for t, _ in c["까닭"]} == {"단위 기준 차이"} and "÷10" in c["까닭"][0][1]
def test_no_made_up_values():
"""옵션마다 출처 꼬리표 · 입력 필요 · 0 간주 칸은 옵션에 없음 ·
없음 단은 근거후보 없이 보강 까닭만."""
for bid, r in results()[0].items():
for c in r["근거후보"]:
assert set(c["옵션"]) <= set(c.get("옵션길") or {}), bid
assert set((c.get("옵션길") or {}).values()) <= WAYS, bid
assert not set(c["옵션"]) & set(c["입력필요"]), bid
assert all(v is not None for v in c["옵션"].values()), bid
assert c["단"] in ("확정", "근접", "사람")
if r["단"] == "없음":
assert r["근거후보"] == [] and r["보강"]["까닭"], bid
def test_corpus_is_eight_files():
"""보정 자료 = 여덟 파일 상수(일반 뺌) — 뽑기 규칙을 바꾸면 다시 지어 넣음."""
preps = [
bc.prep(blk, book["꼴"], (book["숨은식"] or {}).get(str(blk["번"])))
for _, book in books()
for blk in book["단산"]
]
shipped = json.loads(bi.CORPUS.read_text(encoding="utf-8"))
assert sorted(map(json.dumps, bc.corpus(preps))) == sorted(map(json.dumps, shipped))
def test_store_round_trip(tmp_path):
"""임시 폴더 HP → 뒤 셈 저장본 · 목록 · 블록 하나 · 끝내기."""
_, book = books()[0]
small = {**book, "단산": [b for b in book["단산"] if b["번"] in (21, 51)]}
row = hopyo_store.save(tmp_path, "시험.xls", small, {"id": 0, "이름": "시험"})
key = row["키"]
assert store.solved(tmp_path, key) is None
got = store.solve_all(tmp_path, key)
assert set(got) == {"21", "51"} and store.solved(tmp_path, key) == json.loads(json.dumps(got))
assert store.listing(tmp_path, key)["셈중"] is False
assert store.block(tmp_path, key, 51)["근거후보"][0]["기초"] == "GC000089"
with pytest.raises(Exception):
store.block(tmp_path, key, 999)
store.forget(tmp_path, key)
assert store.solved(tmp_path, key) is None
judge.forget(tmp_path, key)
def test_hidden_g_lines():
book = {"꼴": "STmate", "단산": [{"번": "3", "행": [2, 4]}]}
rows = [[None] * 7, [None] * 6 + ["_x0007_L=20"], [None] * 6 + ["_x0007_"], [None] * 6 + ["x"]]
assert store.hidden(book, {store.SHEET: rows}) == {"3": ["L=20"]}
assert store.hidden({"꼴": "엑셀꼴", "단산": []}, {}) == {}
# ── 794-2c 손 확인 견본 ─────────────────────────────────────────────
HAND = [
("북부청임도#56", "GC001148", "확정"), # 잔토정리 Q 식 · 제목에 품셈 이름 없음 → 기계 작업량 길
("울진대흥#2264", "GC001148", "확정"), # 섞기 오인(GF000132) → 옛 11-3 · 굴착기
("울진대흥#2269", "GC001148", "확정"), # 섞기 오인 → 되메우기 굴착기 Q 식
("울진대흥#2290", "GC001148", "근접"), # 섞기 오인 → 적상 굴착기(운반 · 정지 절 미확정)
("동부청영월#111", "GC001189", "근접"), # 섞기 오인 → 옛 8-2-35 = 현행 8-2-32 임목파쇄기
("북부청임도#41", "GC000227", "확정"), # 휠라 = 채움재 · 옛 6-3-10 같은 절
("울진소광#1329", "GC000143", "확정"), # 평떼 = 잔디 · 옛 4-1-1 같은 절
("울진소광#794", "GF000298", "근접"), # 사방(옛) 4-16-2 → 판 대응표 산림 5-16-1
("동부청영월#33", "GC000129", "근접"), # 옛 3-7 → 현행 3-9-1 벌목(표 값 같음)
("울진대흥#2277", "GC001148", "확정"), # 옛 11-3 → 8-2-3 · 번호 + 검산
]
@pytest.mark.parametrize("bid,key,tier", HAND)
def test_794_2c_hand(bid, key, tier):
r = results()[0][bid]
keys = [c["기초"] for c in r["근거후보"]]
assert key in keys and r["단"] == tier, (bid, r["단"], keys)
assert "GF000132" not in keys, bid # 섞기(굴착기 0.7) 값 우연 짝 없음
def test_794_2c_no_invented_divisor():
"""「/ Q / 3」(1/3 적용) 블록은 검산 못 함 = 사람 · 1/3 을 지어 넣지 않음."""
r = results()[0]["봉화#24"]
assert r["단"] == "사람" and {c["기초"] for c in r["근거후보"]} == {"GC001147", "GC001148"}
assert all(c["옵션"] == {} for c in r["근거후보"])
def test_machine_consts_reader():
got = bm.consts(
[
"q = 0.7 , k = 0.7 , f =1/1.175= 0.85",
"E = 0.5 , Cm = 18 sec(90)",
"Q = 3600*q*k*E*f/Cm = 41.65 m3/hr",
]
)
assert got == [
{"기호": "q", "값": 0.7},
{"기호": "K", "값": 0.7},
{"기호": "f", "값": 0.85},
{"기호": "E", "값": 0.5},
{"기호": "Cm", "값": 18.0, "덧": "90˚"},
]
got = bm.consts(
[
"q (버킷용량(m3)) = 0.7 , f (체적환산계수) = 1/1.25 , K (버킷계수) = 0.9",
"Cm (1회 사이클 시간(초)) = 20 sec (135˚) , E = ( 0.55 + 0.45 ) / 2. - 0.05 = 0.45",
]
)
assert [(x["기호"], x["값"]) for x in got] == [
("q", 0.7),
("f", 0.8),
("K", 0.9),
("Cm", 20.0),
("E", 0.45),
]
assert bm.has_q(["3,600. × q × K × f × E", "Cm 20."]) and not bm.has_q(["Q = 6 ㎥/hr"])
def test_old_number_map():
assert {o["키"] for o in bm.old_rows("건설품셈(옛)", "11-3")} >= {"GC001147", "GC001148"}
assert "GC000129" in {o["키"] for o in bm.old_rows("건설품셈(옛)", "3-7")}
assert "GC001189" in {o["키"] for o in bm.old_rows("건설품셈(옛)", "8-2-35")}
assert bm.sabang_map()["4-16-2"] == "5-16-1"
assert "3-7" not in bm.sabang_map() # 임도(2017) 줄은 사방에 안 씀
assert bm.old_rows("건설품셈(옛)", "8-2") == [] # 대조 없는 번호 = 없음(윗마디 안 받음)
+20 -83
View File
@@ -5,10 +5,10 @@
블록 → 793-2a 내보내기 → 793-2 가져온 식 SK(만들 층 · 엑셀 단가 그대로 · 검산 = 엑셀) ·
같은 내용 = 그 SK 다시 씀 · 산근 없는 옛 HP = 409 · HP 지우면 산근도 지움 ·
STmate 블록 끝전 = 엑셀 계와 맞는 쪽
- 화면 소스 — [수정] 모달 단산 줄 · 오른쪽 블록 판 [가져온 식으로 열기] · 시트 모달
- 화면 소스 — 단가산출 탭 목록 [가져온 식으로 열기](`Hopyo_SheetSk`) · 시트 모달
(`Sangeun_Detail`) · 머리 판(검산 · [우리 단가로] / [엑셀 단가로] · [원본 글 보기] ·
[이음 고르기] · 근거 세 단 · [우리 기초로 바꾸기] · 입력 필요 붉은 칸) ·
[이 줄에 잇기] = 줄 고른 것 SK · 결과 칩 · 「새」 배지 · 부품 상자만 · 700 줄
[이음 고르기] · 소요량 이음) · 부품 상자만 · 700 줄
- 809-1 걷음(요구 703) — [우리 기초로 바꾸기] · 근거 세 단 · 입력 필요 · [이 줄에 잇기] · 옮기기 판정 길 없음
임시 storage(`client` 마당) · M01 원본 그대로.
"""
@@ -30,7 +30,7 @@ ROOT = Path(__file__).resolve().parents[2]
M02 = ROOT / "M02_MasterTemplete"
API = "/api/m02/layers/personal"
KEY = "HP-000001"
MINE = ("Hopyo_Imported", "Hopyo_ImportedPanel", "Hopyo_Check", "Hopyo_CheckRows", "Hopyo_Block")
MINE = ("Hopyo_Imported", "Hopyo_ImportedPanel", "Hopyo_SheetSk", "Hopyo_View")
def _src(name: str) -> str:
@@ -66,10 +66,10 @@ def test_단산_블록_가져온_식_만들기_다시씀_지움(client):
with store.at(folder):
doc = store.read("sangeun", got["이름"])["문서"]
assert doc["바탕"] is None and doc["가져온"]["원본"]["엑셀금액"]["계"] == 30369
ten = _open(client, 10).json() # 야면석소운반 — 793-3a 근거 제안 = SK 가져온.근거후보
ten = _open(client, 10).json() # 야면석소운반 — 근거 기초 제안 없음(809-1 · 요구 703)
with store.at(folder):
cand = store.read("sangeun", ten["이름"])["문서"]["가져온"]["근거후보"]
assert cand and cand[0]["기초"] == "GC001140" and cand[0]["단"] in ("확정", "근접", "사람")
meta = store.read("sangeun", ten["이름"])["문서"]["가져온"]
assert meta["로직"] and "근거후보" not in meta
assert _open(client, 99999).status_code == 404
fh._path(folder, KEY).unlink() # 산근 없는 옛 HP = 다시 올림
old = _open(client)
@@ -99,45 +99,30 @@ def test_STmate_끝전_엑셀_계와_맞는_쪽(tag, code, rule):
# ── 화면 소스 ─────────────────────────────────────────────────────────
def test_단산_줄_길_잇기():
check, rows, block = _src("Hopyo_Check"), _src("Hopyo_CheckRows"), _src("Hopyo_Block")
assert "openImported({" in check and "onLink: (p) => kit.edit(row, [p])" in check
assert "imported }" in check.split("const ctx = ")[1].split(";")[0]
assert 'L("M02_CheckImported")' in rows and "row.번 !== undefined && ctx.imported" in rows
assert 'import("./M02_MasterTemplete_Hopyo_Imported")' in block # 돌림 import 피함
assert 'L("M02_CheckImported")' in block and "dataset.checkNew" in block
sk, view = _src("Hopyo_SheetSk"), _src("Hopyo_View")
assert "openImported({" in sk and "dataset.skOpen" in sk # 단가산출 탭 목록 = 가져온 식 모달
assert (
'"/sangeun/from-hopyo"' in view and "openSkEdit(" in view
) # [수정] 단산 줄 = 수식 탭 모달
imp = _src("Hopyo_Imported")
assert '"/sangeun/from-hopyo"' in imp and "/sangeun/imported/${how}" in imp
assert "mountSangeunDetail(" in imp and "saveTemplate(" in imp and "openModal({" in imp
assert "skPick(sk" in imp and "o.onLink" in imp # [이 줄에 잇기] = 줄 고른 것 SK
for how in ('run("price"', 'run("link"', 'run("to-base"'):
for how in ('run("price"', 'run("link"'):
assert how in imp, how
for gone in ("to-base", "toBase", "onLink", "skPick", "checkJudge", "readJudgeBlock"):
assert gone not in imp, gone # 809-1 · 요구 703
# 옛 M02 수식 나무 화면(「엑셀 원래」 위 줄) 걷음 — 수식 탭 = 공용 수식 문서 화면(802-4b-2)
assert not (M02 / "M02_MasterTemplete_Sangeun_Equation.ts").exists()
locale = (ROOT / "ui_template/ui_template_locale_m2_judge.ts").read_text(encoding="utf-8")
assert 'M02_CheckImported: ["가져온 식으로 열기"' in locale
def test_머리_판_결과_칩():
def test_머리_판_기초_없음():
panel = _src("Hopyo_ImportedPanel")
for word in (
"ToOurs",
"ToExcel",
"Source",
"Link",
"ToBase",
"PickBase",
"Need",
"Same",
"Diff",
):
for word in ("ToOurs", "ToExcel", "Source", "Link", "Same", "Diff"):
assert f'tx("{word}")' in panel, word
assert "newBadge()" in panel and "data-basis-tiers" in panel and "TIERS.map(" in panel
assert 'f.setError(tx("Need"))' in panel # 입력 필요 = 붉은 칸
for gone in ("ToBase", "PickBase", "data-basis-tiers", "우리 기초로 바꾸기", "근거 제안"):
assert gone not in panel, gone
model = (M02 / "logic/M02_Hopyo_Imported_Model.ts").read_text(encoding="utf-8")
assert '["확정", "근접", "사람"]' in model and "필요: need" in model
assert "입력 필요 ${need.length}" in model and '"가져온 식 있음"' in model
rows = _src("Hopyo_CheckRows")
assert "rowChips(ctx.kit.picksOf(row))" in rows and "dataset.judgeResult" in rows
assert "BasisCand" not in model and "skPick" not in model and "근거후보" not in model
assert not re.search(r"from \"@ui|document\.|fetch\(|sessionStorage\.", model) # 순수
@@ -167,51 +152,3 @@ def test_가져온_식_시트_M01바뀜_없음(client):
sheet = ans["시트"]
assert sheet["자리"] and not any(x["M01바뀜"] for x in sheet["자리"].values())
assert sheet["짝잃음"] == []
def test_우리_기초로_바꾸기_환산_입력_필요(tmp_path):
"""북부청 단산10 야면석소운반(㎡) → GC001140(원/자재 단위) — 환산 없으면 후 None ·
필요 「환산」 · 블록 풀이 바인딩 옵션 + 계수 1 = 엑셀 94,227(품 같음)."""
from M02_MasterTemplete import M02_Hopyo_Store as hs
from M02_MasterTemplete import M02_Sangeun_Imported as imp
path = golden.FILES["북부청임도"]
if not path.exists():
pytest.skip("실무 엑셀 없음")
hs._dir(tmp_path).mkdir(parents=True)
(hs._dir(tmp_path) / f"{KEY}.json").write_text(f'{{"키": "{KEY}", "판": {hs.BOOK}}}', "utf-8")
fh.keep(tmp_path, KEY, path.name, path.read_bytes())
out = fh.emit(tmp_path, KEY, 10)
doc = {"키": "SK-000000", "이름": "야면석소운반", "단위": "㎡", "바탕": None, "입력값": {}}
doc[imp.KEY] = {"로직": out["로직"], "단가기준": "엑셀", "원본": out["원본"]}
given = {"운반거리": 6, "경사각도": 35, "운반회수": 42, "운반무게": 575, "회당운반량": 25}
given |= {"재료종별": "암석", "재료형상": "해당 없음", "재료갈래": "토사류", "운반로": "양호"}
given |= {"실작업시간": 0}
bare = imp.to_base(doc, "GC001140", given)
assert bare["후"] is None and bare["필요"] == ["환산"] and round(bare["전"]["계"]) == 94227
got = imp.to_base(doc, "GC001140", given, {"계수": 1})
assert (
"필요" not in got and round(got["후"]["계"]) == 94227 and got["문서"]["환산"] == {"계수": 1}
)
assert got["문서"]["바탕"] == {"로직": "GC001140"} and got["문서"][imp.KEY]["바꿈"]["전"]
def test_이_줄에_잇기_옮기기_SK_참조(client):
"""[이 줄에 잇기] 고른 것 {갈래 단가산출, 키 SK}(층 · 주인 없음) → 옮기기 = 그 SK 참조 줄."""
from M02_MasterTemplete import M02_Hopyo_Judge as judge
client.app.include_router(sangeun_router.router)
assert _post(client).status_code == 200
folder = layers.personal_dir(7, 42)
judge.solve_all(folder, KEY)
sk = _open(client, 10).json()["이름"]
pick = {"갈래": "단가산출", "키": sk, "이름": "야면석소운반", "단위": "㎡"} # 화면 skPick 꼴
pick |= {"입력값": {}, "출처": {}, "몫": None, "필요": [], "표지": ["가져온 식 있음"]}
body = {"층": "personal", "호표": {"2": {"3": [pick]}}, "블록": {}}
got = client.post(f"{API}/hopyo/{KEY}/move", json=body)
assert got.status_code == 200, got.text
sc = got.json()["만듦"][0]["키"]
with store.at(folder):
lines = store.read("precombo", sc)["문서"]["담은로직"]
refs = [x["참조"]["키"] for x in lines if x.get("참조")]
assert got.json()["막힘"] == [] and refs == [sk], got.json()["막힘"]
+2 -44
View File
@@ -4,8 +4,8 @@
채집 = 실무 봉화#3 식(q = 4/16 · Q 12.38 · 소운반 Q 9.44) · 견본 자동값으로 멈추지 않음.
② GF000284 — 고르기 첫째(굴착기 0.7㎥ · 15ton) = 넓히기 전 식(원문 수 0.7 · 15 · 20 · 0.85 · 22초) 그대로 ·
다른 고름 = 건설 공통 8-2-3 싸이클 · 8-2-5 로더 cm(거창#27 1.8 × 8 + 6 + 14 = 34.4) · 덤프 T.
③ 근거 제안(793-3a) — 794-2a 「없음」 블록 견본이 새 줄을 기초로 잡음(철근 · 깬잡석 · 파쇄석).
마스터 = 지금 M01(읽기만) · 엑셀 = `helper_m02_hopyo_golden.FILES`(없으면 ③ skip).
③ 근거 제안 시험은 809-1 에서 지움(근거 → 기초 제안 길 걷음 · 요구 703).
마스터 = 지금 M01(읽기만).
"""
from __future__ import annotations
@@ -15,7 +15,6 @@ import itertools
from decimal import Decimal as D
from functools import lru_cache
import helper_m02_hopyo_golden as golden
import pytest
from M01_MasterData import M01_MasterData_Store as m01 # scripts 길을 둠
@@ -183,44 +182,3 @@ def test_덤프운반_넓힌_고름():
for dump, ton in (("0602-0080", 8), ("0602-0240", 24)):
mid = run(DUMP, {**g, "덤프": dump})["중간"]
assert mid["덤프T"] == ton and mid["q"] == ton / mid["감마"] * mid["L"]
# ── ③ 근거 제안 ───────────────────────────────────────────────────────
SAMPLES = { # 블록: (기초, 단)
"북부청임도#36": (REBAR, {"확정", "근접"}), # 철근운반 [산림사업표준품셈 10-2]
"봉화#117": (
CEMENT,
{"확정", "근접", "사람"},
), # 시멘트운반 — 제목 단독(톤급 「10.5ton」 못 읽음 = 794-2c)
"봉화#3": (GATHER, {"확정", "근접"}), # 깬잡석채집
"울진소광#1448": (GATHER, {"확정", "근접"}), # 파쇄석 채집
}
@lru_cache(maxsize=8)
def _book(tag: str):
from M02_MasterTemplete import M02_San_Basis_Store as store
from M02_MasterTemplete import M02_San_Book as san_book
path = golden.FILES[tag]
if not path.exists():
return None
book = golden.read(tag)
if book is None:
return None
sheets = san_book.sheets(path.name, path.read_bytes()) if book["꼴"] == "STmate" else {}
return {**book, "숨은식": store.hidden(book, sheets)}
@pytest.mark.parametrize("block", sorted(SAMPLES))
def test_근거제안_없음에서_새_기초(block):
from M02_MasterTemplete import M02_San_Basis_Store as store
tag, no = block.split("#")
book = _book(tag)
if book is None:
pytest.skip("실무 엑셀 없음")
got = store.one(book, int(no))
key, tiers = SAMPLES[block]
assert got["단"] in tiers, got["단"]
assert key in [c["기초"] for c in got["근거후보"]]
+1 -104
View File
@@ -2,21 +2,15 @@
① 로직 줄 칸 「개정」 = 빌더(`build_로직_개정.py`)가 원문 절 머리에서 뽑은 값과 같음 · 채움 수.
② 셈 값 그대로 — 791-1 지문 fixture 가 지금 마스터 지문(건너뜀 없이 1,515 전후 같음을 돎).
③ 근거 제안 까닭 판 번호 — 봉화#98 철근 현장조립 3.5 ↔ 3.4 = 「판 차이('24 보완 · 파일 2024)」 ·
거창#28 · 봉화#8 뒤채움 같은 배 ×1.98 = 「판 차이 강함」(엑셀 = `helper_m02_hopyo_golden.FILES`).
③ 근거 제안 까닭 판 번호 시험은 809-1 에서 지움(근거 → 기초 제안 길 걷음 · 요구 703).
④ 잔토처리(인력) GC001194 = ㎥당 보통인부 0.2 인(건설 공통 3-2-1 비고).
"""
import importlib
import json
import helper_m02_hopyo_golden as golden
import pytest
from M01_MasterData import M01_MasterData_Store as m01 # scripts 길을 둠
from M02_MasterTemplete import M02_San_Basis_Check as chk
from M02_MasterTemplete import M02_San_Basis_Store as store
from M02_MasterTemplete import M02_San_Book as san_book
rev = importlib.import_module("build_로직_개정")
mf = importlib.import_module("master_formula")
@@ -53,100 +47,3 @@ def test_잔토처리_단독():
raw = mf.run(whole, "GC001194", {})
(line,) = raw["줄"]
assert (line["이름"], float(line["수량"])) == ("보통인부", 0.2)
def test_연도_읽기():
assert [chk.file_year(p.name) for p in golden.FILES.values()] == [
2026,
2026,
2025,
2024,
2024,
2024,
2025,
2025,
]
assert chk.text_year({"글": ["2017년 품셈 6-3-1"]}) == 2017
assert chk.text_year({"글": ["「24년 건설표준품셈 6-2-3」"]}) == 2024
assert chk.text_year({"글": ["건설표준품셈 6-2(철근) 적용"]}) is None
assert chk.edition_tag("GC000207", [2008, 2014, 2022, 2024], 2024, "파일") == (
"'24 보완 · 파일 2024"
)
assert chk.edition_tag("GC000211", [2017, 2018], 2017, "글") == "'18 보완 · 글 2017"
assert chk.edition_tag("GF000123", [2026], 2026, "글") == ""
def test_판_차이_강함_틀():
a = {
"1": {
"근거후보": [
{
"기초": "GC1",
"판": "'25 보완 · 파일 2024",
"까닭": [["그 밖", "보통인부 ×1.98"], ["단위 기준 차이", "x ×10"]],
}
]
}
}
b = {
"9": {
"근거후보": [
{"기초": "GC1", "판": "'25 보완 · 파일 2025", "까닭": [["그 밖", "보통인부 ×1.99"]]}
]
}
}
got = chk.strong(a, [b])["1"]["근거후보"][0]["까닭"]
assert got == [
["판 차이 강함('25 보완 · 파일 2024)", "보통인부 ×1.98"],
["단위 기준 차이", "x ×10"],
]
assert chk.strong(a, [])["1"]["근거후보"][0]["까닭"][0][0] == "그 밖"
assert a["1"]["근거후보"][0]["까닭"][0][0] == "그 밖" # 저장본 안 고침
def _one(tag: str, no: int) -> dict:
path = golden.FILES[tag]
book = golden.read(tag) if path.exists() else None
if book is None:
pytest.skip(f"실무 엑셀 없음 {tag}")
sheets = san_book.sheets(path.name, path.read_bytes()) if book["꼴"] == "STmate" else {}
book = {**book, "숨은식": store.hidden(book, sheets), "파일": path.name}
return store.one(book, no)
def test_봉화98_판_번호():
got = {c["기초"]: c for c in _one("봉화", 98)["근거후보"]}
c = got["GC000207"]
assert c["단"] == "근접" and c["판"] == "'24 보완 · 파일 2024"
assert ["판 차이('24 보완 · 파일 2024)", "철근공 ×0.9714"] in c["까닭"]
def test_뒤채움_두_파일_판_차이_강함():
a, b = _one("봉화", 8), _one("거창", 28)
marks = [
lab for c in chk.strong({"8": a}, [{"28": b}])["8"]["근거후보"] for lab, _ in c["까닭"]
]
assert "판 차이 강함('25 보완 · 파일 2024)" in marks
def test_listing_다른_HP_대조(monkeypatch):
mine = {
"1": {
"근거후보": [
{"기초": "GC1", "판": "'25 보완 · 파일 2024", "까닭": [["그 밖", "보통인부 ×1.98"]]}
]
}
}
other = {
"9": {
"근거후보": [
{"기초": "GC1", "판": "'25 보완 · 파일 2025", "까닭": [["그 밖", "보통인부 ×1.98"]]}
]
}
}
got = {"HP-000001": mine, "HP-000002": other, "HP-000003": None}
monkeypatch.setattr(store, "solved", lambda folder, key: got[key])
monkeypatch.setattr(store.hopyo_store, "rows", lambda folder: [{"키": k} for k in got])
out = store.listing("x", "HP-000001")
assert out["블록"]["1"]["근거후보"][0]["까닭"][0][0] == "판 차이 강함('25 보완 · 파일 2024)"
assert store.block("x", "HP-000001", 1)["근거후보"][0]["까닭"][0][0].startswith("판 차이 강함")
@@ -153,8 +153,8 @@ def test_source_find_on_m01_and_m02_panels() -> None:
assert "createSourceFind" in m02 and "createSourceFind" in m01
assert "originOf" in m02
sheet = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Hopyo_Mine.ts").read_text("utf-8")
block = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Hopyo_Block.ts").read_text("utf-8")
block = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts").read_text("utf-8")
assert "/hopyo-build-all" in sheet and "번: h.번" in sheet
assert 'target: { layer: "personal", owner: null }' in block and "openImported" in block
assert '{ layer: "personal", owner: null }' in block and "openImported" in block
detail = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts").read_text("utf-8")
assert "doc.가져온);" in detail # 원문 패널에 가져온 머리를 넘김
+138
View File
@@ -0,0 +1,138 @@
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""809-1 엑셀 불러오기 일위대가 = 경쟁사 로직 인정 · 요소만 연결(요구 702 · 703) — 임시 storage · M01 그대로.
① 기초 길 0 — 판정 · 근거 기초 제안 · [우리 기초로 바꾸기] · [옮기기] 서버 · 화면 모두 없음
② 줄 표지 = 로직(단가산출 · 일위대가) / 요소(연결됨 · 후보 · 못 이음) — 실무 여덟 1,350 줄 · 갈래 수 그대로
③ 로직 줄 = 가져온 식 SK(경쟁사 로직 그대로 · 엑셀 단가 = 엑셀)
④ [수정] 모달 = 일위대가 컨테이너 부품(`Labor_Table` · 읽기 · 덧칸 표지 · 엑셀 금액)
⑤ 금액 같음 — 보기 문서(엑셀 숫자 그대로 고정 줄) 1 단위 값 = 엑셀 호표 단가(실무 북부청 호표 모두)
실무 엑셀 없으면 그 시험만 skip.
"""
from __future__ import annotations
import importlib.util
from collections import Counter
from pathlib import Path
import helper_m02_hopyo_golden as golden
import pytest
from M02_MasterTemplete import M02_Hopyo_Build as hb
from M02_MasterTemplete import M02_Hopyo_Excel as hx
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
from M02_MasterTemplete import M02_Hopyo_Store as hs
from M02_MasterTemplete import M02_Hopyo_View as hv
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh
from M02_MasterTemplete import M02_Sangeun_Imported as imp
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
ROOT = Path(__file__).resolve().parents[2]
M02 = ROOT / "M02_MasterTemplete"
MARKS = {"단가산출", "일위대가", "연결됨", "후보", "못 이음"}
def _ts(name: str) -> str:
return (M02 / f"M02_MasterTemplete_{name}.ts").read_text(encoding="utf-8")
# ── ① 기초 길 0 ──────────────────────────────────────────────────────────
def test_basis_paths_gone() -> None:
for mod in ("Hopyo_Judge", "Hopyo_Solve", "Hopyo_Bind", "San_Basis", "San_Basis_Store"):
assert importlib.util.find_spec(f"M02_MasterTemplete.M02_{mod}") is None, mod
for name in ("Hopyo_Check", "Hopyo_CheckRows", "Hopyo_Rows", "Hopyo_Block", "Hopyo_JudgeApi"):
assert not (M02 / f"M02_MasterTemplete_{name}.ts").exists(), name
assert not (M02 / "logic/M02_MasterTemplete_Hopyo_Picks.ts").exists()
assert not hasattr(imp, "to_base")
router = (M02 / "M02_Hopyo_Router.py").read_text(encoding="utf-8")
for path in ('"/layers/{layer}/hopyo-judge', '/check"', '/move"', '/price"', "hopyo-basis/"):
assert f"@router.get({path}" not in router and f"@router.post({path}" not in router
assert "/hopyo/{key}/move" not in router and "/hopyo/{key}/check" not in router
assert '"to-base"' not in (M02 / "M02_Sangeun_Router.py").read_text(encoding="utf-8")
from M02_MasterTemplete import M02_Hopyo_Make as hmake
assert not hasattr(hmake, "Move") and not hasattr(hmake.Lines, "picks")
panel, model = _ts("Hopyo_ImportedPanel"), (M02 / "logic/M02_Hopyo_Imported_Model.ts").read_text("utf-8") # fmt: skip
for gone in ("ToBase", "PickBase", "우리 기초로 바꾸기", "BasisCand"):
assert gone not in panel + model + _ts("Hopyo_Imported"), gone
# ── ② 줄 표지 ───────────────────────────────────────────────────────────
def test_marks_on_eight_files() -> None:
linker, total, n = hfix.Linker(True), Counter(), 0
for tag in golden.FILES:
book = golden.read(tag) if golden.FILES[tag].exists() else None
if book is None:
pytest.skip(f"실무 엑셀 없음 {tag}")
for h in book["호표"]:
got = hv.of(book, int(h["번"]), linker)
assert len(got["줄"]) == len(h.get("줄") or []) == len(got["문서"]["담은로직"])
for m in got["줄"]:
assert m["표지"] in MARKS and m["갈래"] == ("로직" if m["표지"] in ("단가산출", "일위대가") else "요소") # fmt: skip
if m["표지"] == "연결됨":
assert m["이음"] # 노임 · 기계 · 자재 · 비율 중 하나
if m["표지"] == "후보":
assert m["후보"] and all(c["키"] for c in m["후보"])
total[m["표지"]] += 1
n += 1
assert n == 1350 # 호표 줄 수 전후 같음
assert (total["단가산출"], total["일위대가"]) == (
735,
90,
) # 로직 줄 = 옛 갈래 단가산출 · 일위대가 그대로
assert total["연결됨"] + total["후보"] + total["못 이음"] == 525
def _hp(world, tag: str = "북부청임도") -> tuple[Path, str]:
src = golden.FILES.get(tag)
if src is None or not src.exists():
pytest.skip(f"실무 엑셀 없음 {tag}")
folder, data = world["personal"], src.read_bytes()
with store.at(folder):
parsed = hx.read_book(src.name, data)
row = hs.save(folder, src.name, parsed, {"이름": "sub1임시"}, None, hs.fingerprint(data))
fh.keep(folder, row["키"], src.name, data)
return folder, row["키"]
# ── ③ 로직 줄 = 가져온 식 SK ─────────────────────────────────────────────
def test_logic_line_is_imported_sk(world) -> None:
folder, hp = _hp(world)
with store.at(folder):
got = hv.view(folder, hp, 3) # 제3호표 — 단산 셋
logic = [m for m in got["줄"] if m["표지"] == "단가산출"]
assert len(logic) == 3 and all(m["번"] for m in logic)
made = fh.make(folder, hp, logic[0]["번"])
doc = store.read("sangeun", made["이름"])["문서"]
assert imp.is_imported(doc) and doc[imp.KEY]["단가기준"] == "엑셀" # 경쟁사 로직 그대로
assert doc.get("바탕") is None and "근거후보" not in doc[imp.KEY]
assert not list((folder / "precombo").glob("*.json")) # 보기는 일위대가를 안 씀
# ── ④ 모달 = Labor_Table ────────────────────────────────────────────────
def test_modal_is_labor_table() -> None:
view, sheet, table = _ts("Hopyo_View"), _ts("Hopyo_Sheet"), _ts("Labor_Table")
assert "mountLaborTable(host, r.문서" in view and "readOnly: true" in view
assert 'mode: "precombo"' in view and "openSkEdit(" in view and "mineButton(" in view
assert "/hopyo-view/" in view and "x_mark" in view and "x_excel" in view
assert "openHopyoView({" in sheet and "openHopyoCheck" not in sheet
assert "opt.view?.onRow?.(n)" in table and "viewRows(g)" in table # 덧칸 · 읽기만
for name in ("Hopyo_View", "Hopyo_Sheet", "Hopyo_SheetSk", "Labor_Table"):
assert len(_ts(name).splitlines()) <= 700, name
# ── ⑤ 금액 같음 ─────────────────────────────────────────────────────────
def test_view_money_equals_excel(world) -> None:
folder, hp = _hp(world)
with store.at(folder):
book = hs.read(folder, hp)
linker, bad = hfix.Linker(True), []
for h in book["호표"]:
doc = hv.of(book, int(h["번"]), linker)["문서"]
key = refs.create("precombo", {**doc, "구분": "실무자료"})["이름"]
got = hb.compare({"층": refs.here()[0], "주인": refs.here()[1], "키": key}, h)
if not got["같음"]:
bad.append((h["번"], got["엑셀"], got["우리"]))
assert not bad, bad[:5]
+1 -2
View File
@@ -122,8 +122,7 @@ def test_xlsx_길():
# ── 저장 · 길 ──────────────────────────────────────────────────────────
def calc_cache(tmp_path, monkeypatch) -> None:
"""블록 풀이 뒤 셈 안 돌림(785-5 · 옛 맞춤셈 캐시는 785-7 에서 지움)."""
monkeypatch.setattr(hopyo_router.hopyo_judge, "_start", lambda *_: None)
"""뒤 셈 없음(블록 풀이 판정은 809-1 에서 지움) — 부르는 쪽 꼴 그대로."""
@pytest.fixture()
+1 -1
View File
@@ -91,7 +91,7 @@ def test_올리기_격자_200_404_지우기(client):
assert not (folder / hs.DIRNAME / hs.GRID / "HP-000002.json").exists()
assert client.get(GRID.format("personal", "HP-000002")).status_code == 404
assert client.get(GRID.format("personal", "../x")).status_code == 404
gone = client.get(URL.format("personal") + "-judge/HP-000001/3") # 718-1 지운 뒤 = 없음
gone = client.get(URL.format("personal") + "-view/HP-000001/3") # 718-1 지운 뒤 = 없음
assert gone.status_code == 404 and "없는 실무자료" in gone.json()["detail"]
assert master_hash() == before
+2 -2
View File
@@ -180,8 +180,8 @@ def test_여섯_파일_올라감(client):
assert sum(1 for b in doc["단산"] if not b["글"]) <= 3 # 봉화 무대처리 · 흙 쌓 기 = 값 0
assert any(b["자원"] for b in doc["단산"])
first = doc["호표"][0]["번"]
view = client.get(f"{base.URL.format('personal')}-judge/HP-{at:06d}/{first}")
assert view.status_code == 200, view.text # 줄 판(785-5)이 STmate 줄을 받음
view = client.get(f"{base.URL.format('personal')}-view/HP-{at:06d}/{first}")
assert view.status_code == 200, view.text # [수정] 보기(809-1)가 STmate 줄을 받음
assert {tag: hashlib.sha256(_path(tag).read_bytes()).hexdigest() for tag in FILES} == sources
assert master_hash() == before