From 4ff989c1259d5882638d9a87ecab6a1736baa42f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 00:25:51 +0900 Subject: [PATCH] =?UTF-8?q?feat(=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80?= =?UTF-8?q?=EC=8B=A4=EB=AC=B4):=20338-4=20=EC=9D=B4=EC=9D=8C=EB=A7=A4=20?= =?UTF-8?q?=E2=80=94=20=ED=99=95=EC=9D=B8=20=ED=99=94=EB=A9=B4=20=E2=86=94?= =?UTF-8?q?=20=EC=8B=A4=EB=AC=BC=20=EB=A7=9E=EC=B6=A4=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=20=C2=B7=20guess=20=EA=B8=B8=20=EB=AA=A9=EB=A1=9D=20=C2=B7=20?= =?UTF-8?q?=EB=81=9D=EA=B9=8C=EC=A7=80=20=EC=8B=9C=ED=97=98=20=C2=B7=20?= =?UTF-8?q?=EC=9D=B4=EC=9D=8C=EB=A7=A4=20=EA=B8=B0=EC=A4=80=20=EC=83=88=20?= =?UTF-8?q?=EB=9C=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 길 — guess 몸 대상 list | dict · to = 만들 층 - 줄 맞춤 — 섞인 줄 1위 대상에 몫 · 배(고임돌 GF000211 × 0.1) · 장비 품 비교 열쇠 「+」(화면과 같음) · guess 가 사용자 배 그대로 - 화면 — 품 비교 장비 표시 열쇠를 갈래 묶음 이름으로 - 시험 — client 고정물 맞춤셈 캐시 = 임시 폴더 · 뒤 셈 안 돌림(tmp/m02_hopyo 안 남음 · 결과 흔들림 없음) · 끝까지(북부청 올림 → 셈 → 보기 → 화면 몸 → 만들기 → 장부 → 다시 올림 별칭 · 몸 없음 400 · hopyo-memory 404 · 영월 STmate) · 실물 대역 화면 시험 · 이음매 268 줄 맞음 122 · 다른 점 86 · 후보 60 · 후보 1순위 근거 맞음 123 → 9 - 응답 대역 JSON 실물로 다시 뽑음 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD --- M02_MasterTemplete/M02_Hopyo_Line.py | 10 +- M02_MasterTemplete/M02_Labor_Router.py | 13 +- .../M02_MasterTemplete_Hopyo_Check.ts | 2 +- .../fixtures/m02_hopyo_view_sample.json | 126 ++++++++++++-- resources/tester/helper_m02_hopyo_check.cjs | 31 ++++ resources/tester/test_m02_hopyo_excel.py | 17 ++ resources/tester/test_m02_hopyo_flow.py | 158 ++++++++++++++++++ resources/tester/test_m02_hopyo_match.py | 15 -- resources/tester/test_m02_hopyo_screen.py | 26 +++ resources/tester/test_m02_hopyo_seam.py | 15 +- 10 files changed, 370 insertions(+), 43 deletions(-) create mode 100644 resources/tester/test_m02_hopyo_flow.py diff --git a/M02_MasterTemplete/M02_Hopyo_Line.py b/M02_MasterTemplete/M02_Hopyo_Line.py index 2d77dd5e5..3e533a65b 100644 --- a/M02_MasterTemplete/M02_Hopyo_Line.py +++ b/M02_MasterTemplete/M02_Hopyo_Line.py @@ -102,7 +102,8 @@ def _qty(entry: dict, found: dict | None) -> dict: for m in ex["장비"]: mine = [x["시간"] for x in us["장비"] if set(x["갈래"]) & set(m["갈래"])] hit = any(option.near(m["시간"], q, fit.TOL[0], option.ru(m["시간"])) for q in mine) - same["·".join(m["갈래"])] = hit or same.get("·".join(m["갈래"]), False) + name = "+".join(m["갈래"]) # 화면 품 비교 열쇠와 같음(갈래 + 로 묶음) + same[name] = hit or same.get(name, False) return {"엑셀": ex, "우리": us, "같음": same} @@ -123,7 +124,10 @@ def _numeric(line: dict, ctx: Ctx, entry: dict) -> dict: for c in combo ] else: - targets = [_target(top, line, ctx, found, use)] if top else [] + part = next((c for c in entry.get("조합") or [] if c["로직"] == top), {}) + share = part.get("몫") if part.get("몫") not in (None, 1, 1.0) else None # 섞인 줄 인력 몫 + extra = {"몫": share, "어디": part["어디"]} if share else {} + targets = [_target(top, line, ctx, found, use, **extra)] if top else [] tags = list(judged.get("꼬리표") or []) tags += [f"조합 — {' + '.join(c['로직'] for c in combo)}"] if combo else [] for c in entry.get("조합") or []: @@ -304,6 +308,8 @@ def guess( raise store.StoreError(400, "돈 로직 · 우리 자재가 아님") given = t.get("입력값") one = _target(t["로직"], line, ctx, None, False, 몫=t.get("몫")) + if isinstance(t.get("배"), (int, float)) and t["배"] > 0: + one["배"] = t["배"] # 사용자가 정한 배는 그대로 if isinstance(given, dict): one["입력값"] = {**one["입력값"], **given} one["출처"].update({k: "고름" for k in given}) diff --git a/M02_MasterTemplete/M02_Labor_Router.py b/M02_MasterTemplete/M02_Labor_Router.py index f528dedf1..7480fb174 100644 --- a/M02_MasterTemplete/M02_Labor_Router.py +++ b/M02_MasterTemplete/M02_Labor_Router.py @@ -295,10 +295,10 @@ def hopyo_one( class GuessBody(BaseModel): - """`차례` = 호표 안 줄(0부터) · `대상` = {로직} | {자재}.""" + """`차례` = 호표 안 줄(0부터) · `대상` = [{로직 | 자재, 입력값?, 몫?}](낱개도 받음).""" 차례: int - 대상: dict[str, Any] + 대상: list[dict[str, Any]] | dict[str, Any] @router.post("/layers/{layer}/hopyo/{key}/{no}/guess") @@ -308,11 +308,16 @@ def hopyo_guess( no: int, body: GuessBody, owner: str | None = None, + to: Layer | None = None, + to_owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: - """고른 대상 미리 채움(저장 안 함) — 규격 → 입력값 · 환산.""" + """미리 셈(저장 안 함 · 설계 3-4) — 고른 대상 · 옵션으로 엔진 한 번 → `{대상, 품, 글}` · + `to` = 만들 층(없으면 파일 층).""" folder = _root(session, layer, owner) - return _call(folder, hopyo_match.guess, folder, key, no, body.차례, body.대상) + target = _root(session, to, to_owner) if to is not None else None + args = (folder, key, no, body.차례, body.대상) + return _call(folder, lambda: hopyo_match.guess(*args, to=target)) class _Strict(BaseModel): diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts index 7087db5b9..264872dbf 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts @@ -152,7 +152,7 @@ export function compareText(q: HopyoMatch["품"] | undefined): string { const name = (g: string | string[]): string => [g].flat().join("+"); for (const x of q.엑셀.장비 ?? []) { const o = (q.우리.장비 ?? []).find((y) => name(y.갈래) === name(x.갈래)); - out.push(`${name(x.갈래)} ${n4(x.시간)} → ${n4(o?.시간)} ${mark([x.갈래].flat()[0])}`); + out.push(`${name(x.갈래)} ${n4(x.시간)} → ${n4(o?.시간)} ${mark(name(x.갈래))}`); } return out.join(" · "); } diff --git a/resources/tester/fixtures/m02_hopyo_view_sample.json b/resources/tester/fixtures/m02_hopyo_view_sample.json index c7de6e3f7..3a6ba98ce 100644 --- a/resources/tester/fixtures/m02_hopyo_view_sample.json +++ b/resources/tester/fixtures/m02_hopyo_view_sample.json @@ -1,5 +1,5 @@ { - "_설명": "338 맞춤 응답 대역(설계 3-3 · 3-4) — 실물(북부청 임도 제1 · 4호표) 응답 · 가 창 1 단계 엔진", + "_설명": "338 맞춤 응답 대역(설계 3-3 · 3-4) — 실물(북부청 임도 제1 · 4호표) 응답", "길": { "파일": "GET /layers/{층}/hopyo/{키}", "호표": "GET /layers/{층}/hopyo/{키}/{번}?to=&to_owner=", @@ -8,7 +8,7 @@ "파일": { "키": "HP-000001", "파일": "2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls", - "올린때": "2026-10-15T23:51:45", + "올린때": "2026-10-16T00:18:16", "올린이": { "id": 0, "이름": "시험" @@ -75,7 +75,7 @@ "호표": { "키": "HP-000001", "파일": "2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls", - "올린때": "2026-10-15T23:51:45", + "올린때": "2026-10-16T00:18:16", "올린이": { "id": 0, "이름": "시험" @@ -250,7 +250,12 @@ "조항": [ "건설품셈 공통 6-1-1" ], - "까닭": "조항 같음" + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" } }, { @@ -262,6 +267,9 @@ "글": "후보", "근거": { "조항": [], + "직종": [], + "단위": true, + "점수": 0.0, "까닭": "" } }, @@ -276,7 +284,12 @@ "조항": [ "건설품셈 공통 6-1-2" ], - "까닭": "앞 마디 같음" + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" } }, { @@ -288,6 +301,9 @@ "글": "후보", "근거": { "조항": [], + "직종": [], + "단위": true, + "점수": 0.0, "까닭": "" } }, @@ -302,7 +318,12 @@ "조항": [ "건설품셈 공통 6-1-4" ], - "까닭": "앞 마디 같음" + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" } } ], @@ -439,7 +460,12 @@ "조항": [ "건설품셈 공통 6-3-1" ], - "까닭": "조항 같음" + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" } }, { @@ -451,6 +477,9 @@ "글": "후보", "근거": { "조항": [], + "직종": [], + "단위": true, + "점수": 0.0, "까닭": "" } }, @@ -465,7 +494,12 @@ "조항": [ "건설품셈 공통 6-3-3" ], - "까닭": "앞 마디 같음" + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" } }, { @@ -479,7 +513,12 @@ "조항": [ "건설품셈 공통 6-3-7" ], - "까닭": "앞 마디 같음" + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" } }, { @@ -493,7 +532,12 @@ "조항": [ "건설품셈 공통 6-3-4" ], - "까닭": "앞 마디 같음" + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" } } ], @@ -634,7 +678,12 @@ "조항": [ "건설품셈 공통 3-4-8" ], - "까닭": "조항 같음" + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" } }, { @@ -648,7 +697,12 @@ "조항": [ "건설품셈 공통 3-4-4" ], - "까닭": "앞 마디 같음" + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" } }, { @@ -662,7 +716,12 @@ "조항": [ "건설품셈 공통 3-4-6" ], - "까닭": "앞 마디 같음" + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" } }, { @@ -676,7 +735,12 @@ "조항": [ "건설품셈 공통 3-4-5" ], - "까닭": "앞 마디 같음" + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" } }, { @@ -690,7 +754,12 @@ "조항": [ "건설품셈 공통 3-4-7" ], - "까닭": "앞 마디 같음" + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" } } ], @@ -947,7 +1016,12 @@ "조항": [ "건설품셈 공통 6-2-3" ], - "까닭": "윗마디 같음" + "직종": [ + "철근공" + ], + "단위": true, + "점수": 1.65, + "까닭": "윗마디 같음 · 건설 공통 · 직종 철근공 · 단위 맞음" } }, { @@ -961,7 +1035,12 @@ "조항": [ "건설품셈 공통 6-2-2" ], - "까닭": "윗마디 같음" + "직종": [ + "철근공" + ], + "단위": true, + "점수": 1.65, + "까닭": "윗마디 같음 · 건설 공통 · 직종 철근공 · 단위 맞음" } }, { @@ -973,6 +1052,9 @@ "글": "후보", "근거": { "조항": [], + "직종": [], + "단위": true, + "점수": 0.0, "까닭": "" } }, @@ -985,6 +1067,9 @@ "글": "후보", "근거": { "조항": [], + "직종": [], + "단위": true, + "점수": 0.0, "까닭": "" } }, @@ -999,7 +1084,12 @@ "조항": [ "건설품셈 공통 6-2-4" ], - "까닭": "윗마디 같음" + "직종": [ + "철근공" + ], + "단위": true, + "점수": 1.65, + "까닭": "윗마디 같음 · 건설 공통 · 직종 철근공 · 단위 맞음" } } ], diff --git a/resources/tester/helper_m02_hopyo_check.cjs b/resources/tester/helper_m02_hopyo_check.cjs index 0af39f459..d91ae5ea6 100644 --- a/resources/tester/helper_m02_hopyo_check.cjs +++ b/resources/tester/helper_m02_hopyo_check.cjs @@ -54,6 +54,7 @@ global.sessionStorage = { removeItem: (k) => store.delete(k), }; +let out0 = null; (async () => { const input = JSON.parse(fs.readFileSync(0, "utf8")); const calls = []; @@ -73,6 +74,36 @@ global.sessionStorage = { const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts")); const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts")); const pick = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Pick.ts")); + if (input.real) { + // 실물 맞춤 응답(338-4) — 서버 줄을 그대로 먹여 체크 · 몸 · 빠질 줄 · 품 비교 · 글 + const lines = input.real; + const d = check.loadDraft("real"); + out0 = { + body: check.bodyOf(lines, d), + leftOut: check.leftOut(lines, d), + sure: check.sureLines(lines, d).map((l) => l.차례), + lines: lines.map((l) => { + const p = check.partsOf(l, d)[0]; + return { + 이름: l.이름, + 상태: l.맞춤.상태, + checkable: check.checkable(l, d), + checked: check.isChecked(l, d), + compare: check.compareText(l.맞춤.품), + qty: choose.qtyText(l.맞춤.품?.엑셀), + target: choose.targetText(p), + inputs: p ? choose.inputsText(p.입력값 ?? {}, p.못읽음 ?? [], p.출처 ?? {}) : "", + cands: (l.맞춤.후보 ?? []).map((c) => [ + choose.targetText(c), + c.판정 ?? "", + choose.basisTag(c.근거), + ]), + }; + }), + }; + process.stdout.write(JSON.stringify(out0)); + return; + } const { lines } = input; const scope = { layer: "personal", owner: null }; const key = check.draftKey(scope, "HP-000001", 3); diff --git a/resources/tester/test_m02_hopyo_excel.py b/resources/tester/test_m02_hopyo_excel.py index 7129e28de..3622b9d39 100644 --- a/resources/tester/test_m02_hopyo_excel.py +++ b/resources/tester/test_m02_hopyo_excel.py @@ -9,6 +9,7 @@ master_data 원본 그대로(sha256). 층은 임시 폴더. import hashlib import io +import shutil from pathlib import Path import pytest @@ -18,6 +19,8 @@ from fastapi.testclient import TestClient from common_util.common_util_auth import verify_session from config import config_system from M02_MasterTemplete import M02_Hopyo_Excel as hx +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Hopyo_Ours as ours from M02_MasterTemplete import M02_Hopyo_Store as hs from M02_MasterTemplete import M02_Labor_Router as labor_router from M02_MasterTemplete import M02_MasterTemplete_Store as store @@ -120,6 +123,19 @@ def test_xlsx_길(): # ── 저장 · 길 ────────────────────────────────────────────────────────── +def calc_cache(tmp_path, monkeypatch) -> Path: + """맞춤셈(338) — 캐시 = 임시 폴더(우리 지문 사본 · 시험 끝에 같이 지움) · 뒤 셈 안 돌림 + (`ensure` = 상태만) — 숫자 판정이 필요한 시험은 `fit.run_job` 을 바로 부름.""" + if not ours.ready(): + ours.build() + cache = tmp_path / "hopyo_cache" + cache.mkdir(exist_ok=True) + shutil.copy(ours.path_of(), cache / ours.path_of().name) + monkeypatch.setenv("AISLO_HOPYO_CACHE", str(cache)) + monkeypatch.setattr(fit, "ensure", fit.status) + return cache + + @pytest.fixture() def client(tmp_path, monkeypatch): system = tmp_path / "system" @@ -129,6 +145,7 @@ def client(tmp_path, monkeypatch): monkeypatch.setattr(layers, "SYSTEM_ROOT", system) (tmp_path / "storage").mkdir() monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(tmp_path / "storage")) + calc_cache(tmp_path, monkeypatch) app = FastAPI() app.include_router(labor_router.router) who = dict(USER) diff --git a/resources/tester/test_m02_hopyo_flow.py b/resources/tester/test_m02_hopyo_flow.py new file mode 100644 index 000000000..a65f991c9 --- /dev/null +++ b/resources/tester/test_m02_hopyo_flow.py @@ -0,0 +1,158 @@ +"""338-4 이음매 — 끝까지(시험 클라이언트 · 임시 층): 북부청 임도 올림 → 셈 끝 → 호표 보기(맞음 · +다른 점 · +후보) → 화면 도우미(`helper_m02_hopyo_check.cjs` · 실물 응답)가 만든 몸(체크한 줄만) → +[확인하고 사전조합 만들기] → 사전조합 줄(로직 여럿 같은 별칭 · 배 · 몫) · 별칭 장부 → 다시 올림 = +별칭 체크 · +몸 없는 만들기 400 · 옛 hopyo-memory 404 · STmate 영월 올림 → 상태 · master_data 원본 그대로. +""" + +import json +import os +import subprocess + +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester.helper_m02_hopyo_golden import FILES +from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401 + +# ruff: noqa: F811 +HERE = os.path.dirname(os.path.abspath(__file__)) +FOLDER = (7, 42) # USER 의 개인 층 + + +def _node(payload: dict) -> dict: + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_m02_hopyo_check.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) + + +def _up(client, key: str, name: str | None = None, data: bytes | None = None) -> None: + res = _post(client, data=data, **({"name": name} if name else {})) + assert res.status_code == 200, res.text + assert res.json()["키"] == key + fit.run_job(layers.personal_dir(*FOLDER), key) # 셈 끝(뒤 셈 대신 바로) + + +def _one(client, key: str, no: int) -> dict: + res = client.get(f"{URL.format('personal')}/{key}/{no}") + assert res.status_code == 200, res.text + return res.json()["호표"] + + +def _find(client, key: str, name: str) -> tuple[int, dict]: + whole = client.get(f"{URL.format('personal')}/{key}").json() + assert whole["셈"]["끝"] + for h in whole["호표"]: + one = _one(client, key, h["번"]) + if any(x["이름"] == name for x in one["줄"]): + return h["번"], one + raise KeyError(name) + + +def _build(client, key: str, no: int, lines: list, **extra) -> dict: + body = {"층": "personal", "키": key, "번": no, "줄": lines, **extra} + res = client.post("/api/m02/layers/personal/hopyo-build", json=body) + assert res.status_code == 200, res.text + return res.json() + + +def _doc(ref: dict) -> dict: + path = layers.personal_dir(*FOLDER) / "precombo" / f"{ref['키']}.json" + return json.loads(path.read_text(encoding="utf-8")) + + +def test_끝까지_확인한_줄만_별칭(client): + before = master_hash() + _up(client, "HP-000001") + no, hopyo = _find(client, "HP-000001", "레미콘타설(장비사용)") + by = {x["이름"]: x for x in hopyo["줄"]} + assert by["레미콘타설(장비사용)"]["맞춤"]["상태"] == "맞음" + screen = _node({"real": hopyo["줄"]}) + seen = {x["이름"]: x for x in screen["lines"]} + remicon = seen["레미콘타설(장비사용)"] + assert remicon["checked"] and remicon["checkable"] + assert remicon["compare"].startswith("콘크리트공 0.1091 → 0.1091 ✓") + assert "굴착기 0.29 → 0.2909 ✓" in remicon["compare"] + assert "소량감률=50(블록)" in remicon["inputs"] and remicon["target"].endswith("[㎥]") + states = {x["상태"] for x in screen["lines"]} + assert {"맞음", "다른 점", "후보"} <= states + body = screen["body"] # 화면이 만든 몸 = 서버 체크 제안 줄만 + suggested = [x["차례"] for x in hopyo["줄"] if x["맞춤"]["체크"]] + assert [x["차례"] for x in body] == suggested and screen["leftOut"]["확인 안 함"] > 0 + rebar = next(x for x in hopyo["줄"] if x["이름"] == "철근가공조립(현장)") + two = [{k: p[k] for k in ("로직", "입력값", "배")} for p in rebar["맞춤"]["대상"]] + assert {p["로직"] for p in two} == {"GC000206", "GC000207"} # 다른 점 → 사용자가 체크 + body = body + [{"차례": rebar["차례"], "대상": two}] + made = _build(client, "HP-000001", no, body) + assert made["다시씀"] is False and made["장부"]["씀"] == len(body) + assert all(m["까닭"] for m in made["빠진줄"]) # 확인 안 함 · % 줄 + doc = _doc(made["참조"]) + logics = [x for x in doc["담은로직"] if x.get("로직")] + alias = {x["로직"]: x["별칭"] for x in logics} + assert alias["GC000206"] == alias["GC000207"] == "철근가공조립(현장)" # 로직 여럿 · 같은 별칭 + assert doc["본뜬"]["확인"] and doc["본뜬"]["키"] == f"HP-000001 제{no}호표" + rows = client.get("/api/m02/layers/personal/hopyo-ledger").json()["줄"] + keys = {r["열쇠"]: r for r in rows} + assert {p["로직"] for p in keys[rebar["맞춤"]["열쇠"]]["짝"]} == {"GC000206", "GC000207"} + # 다시 올림 = 확인한 줄은 별칭 · 체크 · 만든 것이 보임 + _up(client, "HP-000002") + again = {x["이름"]: x["맞춤"] for x in _one(client, "HP-000002", no)["줄"]} + assert again["레미콘타설(장비사용)"]["상태"] == "별칭" and again["레미콘타설(장비사용)"]["체크"] + assert again["철근가공조립(현장)"]["상태"] == "별칭" + assert again["철근가공조립(현장)"]["대상"][0]["출처"] == { + k: "별칭" for k in again["철근가공조립(현장)"]["대상"][0]["입력값"] + } + first = _one(client, "HP-000001", no) + assert first["만든"] == {"키": made["참조"]["키"], "확인": True} + assert master_hash() == before + + +def test_섞인_줄_몫_배(client): + _up(client, "HP-000001") + no, hopyo = _find(client, "HP-000001", "고임돌 채집 및 운반") + line = next(x for x in hopyo["줄"] if x["이름"] == "고임돌 채집 및 운반") + part = line["맞춤"]["대상"][0] + assert (part["로직"], part["몫"], part["배"]) == ("GF000211", 0.1, 0.1) + screen = _node({"real": [line]}) + assert screen["lines"][0]["checkable"] and not screen["lines"][0]["checked"] + one = {k: part[k] for k in ("로직", "입력값", "배", "몫")} + made = _build(client, "HP-000001", no, [{"차례": line["차례"], "대상": [one]}]) + row = next(x for x in _doc(made["참조"])["담은로직"] if x.get("로직") == "GF000211") + assert row["수량"]["손"] == round(line["수량"] * 0.1, 6) + + +def test_막는_길(client): + _up(client, "HP-000001") + empty = client.post( + "/api/m02/layers/personal/hopyo-build", + json={"층": "personal", "키": "HP-000001", "번": 1, "줄": []}, + ) + assert empty.status_code == 400 and "확인한 줄이 없음" in empty.text + money = client.post( + "/api/m02/layers/personal/hopyo-build", + json={"층": "personal", "키": "HP-000001", "번": 1, "줄": [{"차례": 1, "금액": 1}]}, + ) + assert money.status_code in (400, 422) + gone = client.post("/api/m02/layers/personal/hopyo-memory", json={"줄": {}}) + assert gone.status_code in (404, 405) + url = f"{URL.format('personal')}/HP-000001/1/guess" + many = client.post(url, json={"차례": 1, "대상": [{"로직": "GC000198", "입력값": {}}]}) + assert many.status_code == 200 and many.json()["대상"][0]["로직"] == "GC000198" + + +def test_STmate_영월(client): + path = FILES["동부청영월"] + _up(client, "HP-000001", path.name, path.read_bytes()) + whole = client.get(f"{URL.format('personal')}/HP-000001").json() + assert whole["셈"]["끝"] and len(whole["호표"]) == 23 and whole["맞춤"][0] > 0 + states = set() + for h in whole["호표"]: + states |= {x["맞춤"]["상태"] for x in _one(client, "HP-000001", h["번"])["줄"]} + assert {"맞음", "후보"} <= states diff --git a/resources/tester/test_m02_hopyo_match.py b/resources/tester/test_m02_hopyo_match.py index 3a2e2d385..ec1231685 100644 --- a/resources/tester/test_m02_hopyo_match.py +++ b/resources/tester/test_m02_hopyo_match.py @@ -2,13 +2,10 @@ 맞춤 차례 · 기억 우선 · 길(보기 · 미리 채움 · 확정) · master_data 원본 그대로. 층은 임시 폴더. """ -import shutil - import pytest from M02_MasterTemplete import M02_Hopyo_Fit as fit from M02_MasterTemplete import M02_Hopyo_Match as hm -from M02_MasterTemplete import M02_Hopyo_Ours as ours from M02_MasterTemplete import M02_Template_Layers as layers # 올리기 길 시험의 `client` 마당을 같이 씀 @@ -91,18 +88,6 @@ def test_규격에서_입력값(): # ── 맞춤 응답(338 · 설계 3-3) ───────────────────────────────────────── -@pytest.fixture(autouse=True) -def calc(tmp_path, monkeypatch): - """맞춤셈 캐시 = 임시 폴더(우리 지문 사본) · 뒤 셈은 시험이 바로 돌림(`run_job`).""" - if not ours.ready(): - ours.build() - cache = tmp_path / "hopyo_cache" - cache.mkdir() - shutil.copy(ours.path_of(), cache / ours.path_of().name) - monkeypatch.setenv("AISLO_HOPYO_CACHE", str(cache)) - monkeypatch.setattr(fit, "ensure", fit.status) - - def _hopyo(client, no, layer="personal"): res = client.get(f"{URL.format(layer)}/HP-000001/{no}") assert res.status_code == 200, res.text diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 6492f4e37..547ba5573 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -342,3 +342,29 @@ def test_엑셀_보기_창_여는_자리와_늦게_불러옴(): r'^import \{[^}]*\} from "\.\./A00_Common/spreadsheet/spreadsheet"', sheet, re.M ) assert "readOnly: true" in sheet and "wide: true" in sheet + + +def test_실물_응답_대역_확인_창(): + """338-4 — 가 창 실물 응답(`fixtures/m02_hopyo_view_sample.json`)을 확인 창 · 고르기 창 도우미에 + 그대로 먹임 — 체크 = 서버 제안 · 몸 꼴 · 품 비교 · 옵션 출처 · 후보 판정 꼬리표.""" + sample = json.loads((Path(HERE) / "fixtures" / "m02_hopyo_view_sample.json").read_bytes()) + lines = sample["호표"]["호표"]["줄"] + got = _node("helper_m02_hopyo_check.cjs", {"real": lines}) + by = {x["이름"]: x for x in got["lines"]} + remicon = by["레미콘타설(장비사용)"] + assert remicon["상태"] == "맞음" and remicon["checked"] and remicon["checkable"] + assert ( + remicon["compare"] + == "콘크리트공 0.1091 → 0.1091 ✓ · 보통인부 0.0364 → 0.0364 ✓ · 굴착기 0.29 → 0.2909 ✓" + ) + assert ( + remicon["inputs"] + == "타설방법=장비사용타설(규격), 구조물=철근구조물(규격), 소량감률=50(블록)" + ) + assert remicon["qty"] == "콘크리트공 0.1091 · 보통인부 0.0364 · 굴착기 0.29hr" + assert remicon["cands"][0][1] == "맞음" and remicon["cands"][0][2].startswith("조항 건설품셈") + rebar = by["철근가공조립(현장)"] + assert rebar["상태"] == "다른 점" and not rebar["checked"] and rebar["checkable"] + assert [x["차례"] for x in got["body"]] == [x["차례"] for x in lines if x["맞춤"]["체크"]] + assert all({"로직", "입력값", "배"} <= set(t) for x in got["body"] for t in x["대상"]) + assert got["sure"] == [x["차례"] for x in lines if x["맞춤"]["상태"] == "맞음"] diff --git a/resources/tester/test_m02_hopyo_seam.py b/resources/tester/test_m02_hopyo_seam.py index 58b1aa8a6..45d5af159 100644 --- a/resources/tester/test_m02_hopyo_seam.py +++ b/resources/tester/test_m02_hopyo_seam.py @@ -2,14 +2,18 @@ 화면 도우미(`helper_m02_hopyo_seam.cjs` → Hopyo_Sheet · Hopyo_View)에 먹임. 행 22 → 제3호표 · 25 빈 줄 · 26 → 제4호표 · 영역 B20:O24 · 단산93 행 4229:4244 · -엑셀 근거 줄 · 근거 꼬리표 · 근거 맞음 · 북부청 「후보」 줄 1순위 근거 맞음 = 서버 기준 123 -(338 — 근거 맞음 모두 확정 단추 없앰 · 도우미는 고르기 창 `Hopyo_Choose`). +엑셀 근거 줄 · 근거 꼬리표 · 근거 맞음 · 도우미는 고르기 창 `Hopyo_Choose`. +338 — 같음 상태 없앰 · 숫자 판정(셈 끝)으로 북부청 단가산출 268 줄 = 맞음 122 · 다른 점 86 · 후보 60 +· +「후보」 줄 1순위 근거 맞음 123 → 9(근거 맞는 줄은 거의 맞음 · 다른 점으로 옮겨 감). """ import json import os import subprocess +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Template_Layers as layers from resources.tester import test_m02_hopyo_excel as base from resources.tester.test_m02_hopyo_excel import URL, _post @@ -32,6 +36,7 @@ def _node(payload: dict) -> dict: def test_실제_응답_화면_도우미(client): assert _post(client).status_code == 200 + fit.run_job(layers.personal_dir(7, 42), "HP-000001") # 셈 끝(338 숫자 판정) grid = client.get("/api/m02/layers/personal/hopyo-grid/HP-000001").json() lines = [] for no in (3, 12, 38, 58): @@ -42,7 +47,11 @@ def test_실제_응답_화면_도우미(client): res = client.get(f"{URL.format('personal')}/HP-000001/{one['번']}").json() every += [x for x in res["호표"]["줄"] if x["갈래"] == "단가산출"] got = _node({"grid": grid, "lines": lines, "all": every}) - assert got["count"]["top"] == 123 + states = { + k: sum(1 for x in every if x["맞춤"]["상태"] == k) for k in ("맞음", "다른 점", "후보") + } + assert states == {"맞음": 122, "다른 점": 86, "후보": 60} # 268 줄 · 셈 끝 + assert got["count"]["top"] == 9 # 「후보」 줄 1순위 근거 맞음(근거 맞는 줄은 거의 숫자로 풀림) assert got["at"] == [None, 3, 3, 3, None, 4] # 25행 = 호표 사이 빈 줄 assert got["span"] == "B20:O24" and got["rows"] == "4229:4244" by = {x["이름"]: x for x in got["lines"]}