"""라이브러리 — STmate 출력 엑셀에서 고정형 항목 뽑아 넣기 창구(PLAN 4장 · 2026-09-14 브레인 판정). 두 걸음: [읽기]는 호표 수·구성 줄 수·못 읽은 사유만 돌려주고 아무것도 안 씀 → [넣기]는 같은 파일을 서버가 **다시 읽어** 고른 호표를 개인 단에 씀(브라우저가 보낸 줄을 받아 적지 않음 · 개인 단만 · 판정 Ⓗ). """ from __future__ import annotations import io import json import sys from pathlib import Path import openpyxl import pytest from fastapi import FastAPI from fastapi.testclient import TestClient ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) import B08_Quantity.B08_Quantity_Engine_StructureLibrary as library_module # noqa: E402 import B08_Quantity.B08_Quantity_Router_StmateLibrary as router_module # noqa: E402 from common_util.common_util_auth import verify_session # noqa: E402 PROJECT_ID = "33333333-3333-3333-3333-333333333333" BASE = f"/api/projects/{PROJECT_ID}/quantity/structure-sheets/library/stmate" def _xlsx() -> bytes: book = openpyxl.Workbook() ws = book.active ws.title = "일위대가표" for row in [ ["일 위 대 가 표"], ["공사명 : 시험 공사"], ["명 칭", "규 격", "수 량", "단위"], [None], [" 제 1 호표"], ["돌기슭막이(메쌓기)", "H=2.0", None, "m"], ["메쌓기", "L3=55cm이하", 2.09, "m2"], ["고임돌채집", "기계", 0.31, "m3"], ["합 계"], [" 제 2 호표"], ["규준틀설치", "종단", None, "개소"], ["각재", "외송", "약간", "M3"], ["합 계"], ]: ws.append(row) buffer = io.BytesIO() book.save(buffer) return buffer.getvalue() @pytest.fixture() def client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient: monkeypatch.setattr(library_module, "STORAGE_BASE_DIR", str(tmp_path / "storage")) app = FastAPI() app.include_router(router_module.router) app.dependency_overrides[verify_session] = lambda: {"company_id": 7, "user_id": 42} return TestClient(app) def _files() -> dict: return {"file": ("견본.xlsx", _xlsx(), "application/octet-stream")} def test_읽기는_수와_사유만_주고_안_쓴다(client: TestClient, tmp_path: Path) -> None: body = client.post(f"{BASE}/read", files=_files()).json() assert body["counts"] == {"hopyo": 1, "rows": 2} assert body["hopyo"][0]["name"] == "돌기슭막이(메쌓기)" and body["hopyo"][0]["rows"] == 2 assert any("C12" in p for p in body["problems"]) # 수량이 수가 아닌 2호표는 사유로 assert not (tmp_path / "storage").exists() def test_넣기는_서버가_다시_읽어_개인_단에_고정형으로(client: TestClient, tmp_path: Path) -> None: response = client.post( f"{BASE}/save", files=_files(), data={"hopyo_no": "1", "type_id": "masonry_dry"} ) assert response.status_code == 200, response.text code = response.json()["code"] saved = json.loads( (tmp_path / "storage" / "7" / "42" / "library" / f"{code}.json").read_text(encoding="utf-8") ) assert saved["library_tier"] == "personal" and saved["origin"]["project"] == "시험 공사" assert [r["amount"] for r in saved["rows"]] == ["2.09", "0.31"] dirs = library_module.tier_dirs(7, 42) assert [i["kind"] for i in library_module.list_items(dirs, "masonry_dry")] == ["fixed"] def test_회사_없으면_403_없는_호표는_404_모르는_종류는_400(client: TestClient) -> None: bad = client.post( f"{BASE}/save", files=_files(), data={"hopyo_no": "2", "type_id": "masonry_dry"} ) assert bad.status_code == 404 # 못 읽은 호표는 넣을 수 없음 unknown = client.post(f"{BASE}/save", files=_files(), data={"hopyo_no": "1", "type_id": "없음"}) assert unknown.status_code == 400 client.app.dependency_overrides[verify_session] = lambda: {"company_id": None, "user_id": 1} denied = client.post( f"{BASE}/save", files=_files(), data={"hopyo_no": "1", "type_id": "masonry_dry"} ) assert denied.status_code == 403 def test_앱에_창구가_걸린다() -> None: main = (ROOT / "main.py").read_text(encoding="utf-8") assert "B08_Quantity_Router_StmateLibrary" in main and "b08_stmate_library_router" in main