"""547-2 대시보드 홈 — 개수별 꼴 고르기(TS logic) · 역할 가르기 · 역할별 홈 API 칸.""" import asyncio import json import re import subprocess from pathlib import Path import pytest import B01_Dashboard.B01_Dashboard_Router_Home as home ROOT = Path(__file__).resolve().parents[2] TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" LOGIC = ROOT / "B01_Dashboard" / "logic" / "B01_Dashboard_Home_Layout.ts" _RUNNER = """ import { writeFileSync } from "node:fs"; import * as m from "./B01_Dashboard_Home_Layout.js"; const counts = [0, 1, 2, 3, 4, 8, 9, 40]; const out = { auto: counts.map((n) => m.homeLayout(n)), small: counts.map((n) => m.homeLayout(n, "small")), large: counts.map((n) => m.homeLayout(n, "large")), buckets: m.stageBuckets([ { stage_index: 0, stage_total: 7 }, { stage_index: 3, stage_total: 7 }, { stage_index: 6, stage_total: 7 }, { stage_index: 2, stage_total: 3 }, ]), stale: [m.isStale({ stage_index: 1, stage_total: 7, stale_days: 13 }), m.isStale({ stage_index: 1, stage_total: 7, stale_days: 14 })], count: m.countBy(["나", "가", "나"], (x) => x), rest: m.topWithRest([5, 4, 3, 2, 1, 1].map((c, i) => ({ label: "c" + i, count: c })), 5), donut: m.donutArcs([1, 1, 2]), }; writeFileSync(process.argv[2], JSON.stringify(out)); """ @pytest.fixture(scope="module") def ts(tmp_path_factory): if not TSC.is_file(): pytest.skip("프론트엔드 의존성 미설치") tmp = tmp_path_factory.mktemp("home") out = tmp / "js" args = ["--outDir", str(out), "--module", "esnext", "--target", "es2022", "--ignoreConfig"] subprocess.run( # noqa: S603 — 고정 실행 파일 ["node", str(TSC), str(LOGIC), *args], cwd=str(ROOT), check=True, capture_output=True ) for emitted in out.glob("*.js"): text = emitted.read_text(encoding="utf-8") emitted.write_text(re.sub(r'(from\s+"\./[^"]+)"', r'\1.js"', text), encoding="utf-8") (out / "runner.mjs").write_text(_RUNNER, encoding="utf-8") dst = tmp / "out.json" subprocess.run(["node", str(out / "runner.mjs"), str(dst)], check=True, capture_output=True) # noqa: S603, S607 return json.loads(dst.read_text(encoding="utf-8")) def test_layout_by_count(ts): # 개수 0 1 2 3 4 8 9 40 assert ts["auto"] == ["empty", "one", "few", "few", "list", "list", "many", "many"] assert ts["small"] == ["empty", "list", "list", "list", "list", "list", "many", "many"] assert ts["large"] == ["empty", "one", "few", "few", "few", "few", "few", "few"] def test_counts_and_charts(ts): assert ts["buckets"] == {"start": 1, "working": 1, "last": 2} assert ts["stale"] == [False, True] assert ts["count"] == [{"label": "나", "count": 2}, {"label": "가", "count": 1}] assert ts["rest"][-1] == {"label": "", "count": 2, "rest": 2} and len(ts["rest"]) == 5 assert [a["dash"] for a in ts["donut"]] == ["25 75", "25 75", "50 50"] assert [a["offset"] for a in ts["donut"]] == [25, 0, -25] def test_home_role_order_matches_shell(): s = {"email": "a@forest.go.kr"} assert home.home_role({**s, "role": "SYSTEM_ADMIN"}) == "SYSTEM_ADMIN" assert home.home_role({**s, "role": "ADMIN"}) == "ADMIN" assert home.home_role({**s, "role": "USER"}) == "FOREST" assert home.home_role({"email": "a@b.com", "role": "USER"}) == "USER" class _Cursor: async def __aenter__(self): return self async def __aexit__(self, *_): return False async def execute(self, sql, args=()): self.sql = sql async def fetchone(self): return {"n": 2, "name": "회사", "member_count": 3, "companies": 4, "users": 9} async def fetchall(self): if "temp_upload_batches" in self.sql or "plan_route_assignees" in self.sql: return [] return [{"id": 1, "name": "회사"}] class _Pool: def acquire(self): class _Conn: async def __aenter__(self): return self async def __aexit__(self, *_): return False def cursor(self, *_): return _Cursor() return _Conn() @pytest.mark.parametrize( ("session", "keys", "join"), [ ({"role": "SYSTEM_ADMIN", "user_id": 1, "company_id": 1}, {"system"}, True), ({"role": "ADMIN", "user_id": 1, "company_id": 1}, {"company"}, True), ({"role": "USER", "user_id": 1, "company_id": 4, "email": "x@a.go.kr"}, {"forest"}, False), ({"role": "USER", "user_id": 1, "company_id": 1, "email": "x@a.com"}, set(), False), ], ) def test_home_blocks_by_role(monkeypatch, session, keys, join): async def fake_ids(_session, _role): return ["a", "b"] async def fake_summaries(ids): return [ {"id": i, "done": i == "b", "thumb": [[0, 0], [1, 1]]} for i in ids ] # b = 완료 → 빠짐 async def fake_plans(_session): return {"routes": []} async def fake_resources(days): return {"current": {"cpu_usage_percent": 1, "memory_usage_percent": 2}} import D01_PlanRegister.D01_PlanRegister_Router as d01 monkeypatch.setattr(home, "home_project_ids", fake_ids) monkeypatch.setattr(home, "project_summaries", fake_summaries) monkeypatch.setattr(home, "get_db_pool", lambda: _Pool()) monkeypatch.setattr(home, "get_system_resources", fake_resources) monkeypatch.setattr(d01, "assigned_plan_routes", fake_plans) body = asyncio.run(home.dashboard_home(session=session)) assert {"system", "company", "forest"} & body.keys() == keys assert [p["id"] for p in body["projects"]] == ["a"] and body["project_total"] == 2 assert ("join_requests" in body["todos"]) is join if "system" in body: assert body["system"]["server"] == {"cpu": 1, "memory": 2, "disk": None} def test_forest_counts_from_plan_file(monkeypatch): """산림청 홈(PLAN 590-1) — 분석 전 = 노선 안 그은 프로젝트 수 · 분석됨 · 확정 = 파일 연도 · 지정 대기 = 확정 연도 중 그 연도 조각에 업체 없음.""" import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from helper_plan_file import year_line_plan from D03_PlanAnalysis import D03_PlanAnalysis_Network as network from D03_PlanAnalysis import D03_PlanAnalysis_Store as store drawn = year_line_plan({2026: [[0.0, 0.0], [100.0, 0.0]], 2027: [[100.0, 0.0], [100.0, 50.0]]}) plans = {"빈": store.empty_plan(), "그음": drawn, "그음2": drawn} prints = network.year_rows(store.plan_units(drawn)) async def load(pid): return Path("."), plans[pid] async def status(pid): return {2026: {"fingerprint": prints[2026]["fingerprint"], "confirmed_at": "x"}} monkeypatch.setattr(store, "load_plan", load) monkeypatch.setattr(store, "year_status", status) class Cursor(_Cursor): async def fetchall(self): return [ {"id": "빈", "piece_id": None}, {"id": "그음", "piece_id": None}, {"id": "그음2", "piece_id": "p26"}, # 확정 2026 에 업체 있음 ] out = asyncio.run(home._forest(Cursor(), 4)) assert out == {"plan_status": {"draft": 1, "analyzed": 2, "confirmed": 2}, "assign_waiting": 1}