Files
Aislo/resources/tester/test_547_2_home.py
T
eomsangdonandClaude Opus 5.5 54e08d0d65 feat(D 예정노선): 설계 내용 = 프로젝트 폴더 한 파일 D03_PlanAnalysis/plan_network.json — 저장 모듈 · 서버 길 옮김 · 업체 연도 = 파일 조각 (PLAN 590-1 · 요구 452 · 453)
- 저장 모듈 D03_PlanAnalysis_Store — 읽기 · 원자 쓰기 · 판 · 옛 DB 칸 읽을 때 옮김(지점 셋 꼴 · 연도 행 접기 · 옛 연도 노선) · plan_state(조각 · 연도 지문 · 확정 = 지문 같을 때만)
- D03 [저장] · [초기화] · D02 지점 = 파일만 + 연도 상태 줄(지문 · 확정) · 프로젝트 잠금 · 파일 쓰기 실패 = DB 되돌림 · 빈 연도 줄 지움
- D04 요약 · 업체 지정 · 확정 메일 · 내보내기 · 대시보드 [연결] · 받은 예정노선 · 산림청 홈 · 개요 연결 = 파일에서 · 업체 줄 연도 칸 안 읽음(⑤)
- 산림청 홈 「분석 전」 = 노선 안 그은 예정노선 프로젝트 수 · plan-source 지운 원본 거름
- 옛 연도 단위 지정 · 옛 연도 행 노선 길 걷음(DB 실측 0) · 연결 때 설계속도 씨앗 걷음(591-1 이음)
- SQL 023 = 업체 줄 year NULL 허용(옛 칸은 590-3 에서 지움)
- 시험: 새 지킴 590_1(옮김 · 원자 쓰기 · 연도 읽기 · 잠금) · 낡은 시험 고침 — 529_3 · 530_2 · 531_3 · 531_d04 · 547_2 · 563_2(DB 조각 · 연도 행 흉내 → 파일 흉내 · 옛 연도 지정 시험 · 설계속도 씨앗 시험 지움 = 그 길 걷음)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:52:51 +09:00

190 lines
7.2 KiB
Python

"""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}