Files
Aislo/resources/tester/test_b01_settings_cards.py
T
eomsangdonandClaude Opus 5.5 c6df183983 feat(718-3): M01 별칭 장부 화면 — 왼쪽 칸 · 모인 후보 표(이름 · 짝 · 회사 수 · 사람 수 · 충돌) · 승인 · 반려 · 마스터 장부 보기 · 지움 · 대시보드 줄
- 새 M01_MasterData_UI_Alias — 모인 후보 / 마스터 장부 탭 · 승인 · 반려 · 지움 = 확인 창 뒤 서버(718-2 길) · 마스터 장부 = M02 별칭 장부 부품 그대로(마스터 층) + 마스터 공사 내역 맞춤
- M01 왼쪽 판 = 구조물 옵션 정의 아래 「별칭 장부」 칸(본 칸 = 옵션 정의와 같은 자리) · 주소 ?open=별칭(M01 페이지 한 줄)
- 대시보드 설계 기본값 마스터 데이터 묶음에 「별칭 장부」 줄(m_settings_nav 한 줄 · 화면 글 키 하나)
- 시험 test_b01_settings_cards · test_197_1 = 줄 하나 늘어난 자리 · 별칭 칸 대조로 고침(낡은 줄 번호)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 15:09:14 +09:00

272 lines
14 KiB
Python

"""B01 대시보드 설계 기본값 카드 · 회사 고르기 시험(27-3 · 29-1 · 546) — 화면 조작은 ORCA.
· 역할별 카드(제목 · 설명 · 이동 해시)를 Node 로 바로 돌림 — 가짜 DOM
· 회사 고르기 — 시스템 관리자만 · 위 머리 회사 이름 · 해시 company · 서버 인자 한 곳
"""
from __future__ import annotations
import json
import re
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
PAGE = ROOT / "B01_Dashboard" / "B01_Dashboard_UI_Legacy.ts" # 고른 회사 자리(546)
SHELL = ROOT / "B01_Dashboard" / "B01_Dashboard_UI_Shell.ts"
NAV = ROOT / "A00_Common" / "m_settings_nav.ts" # 카드 규칙 한 벌(334)
def _text(path: Path) -> str:
return path.read_text(encoding="utf-8")
def _fn(src: str, name: str) -> str:
found = re.search(rf"^(?:export )?function {name}\([\s\S]*?^}}", src, re.M)
assert found, f"{name} 를 못 찾음"
return found.group(0).removeprefix("export ")
def _cards(user: dict, company_id: int | None = None) -> list:
# 카드 = settingLinks 한 벌 · 누름 = 설계 기본값 페이지(SysDefaults)와 같은 갈래(계산 자리만 창)
script = (
"interface DashboardUser { role: string; is_master: boolean }\n"
"const ROUTES = { M01_MASTER_DATA: 'm01-master-data',"
" M02_MASTER_TEMPLATE: 'm02-master-template', B01_ACCOUNT: 'b01-account' };\n"
"const L = (k: string) => k;\n"
"let went = '';\n"
"const navigateTo = (r: string, q?: Record<string, string>) =>"
" { went = r + (q ? '?' + new URLSearchParams(q) : ''); };\n"
"const openCalcSitesDialog = async () => { went = 'calc-sites-dialog'; };\n"
+ _fn(_text(NAV), "settingLinks")
+ "\nconst user = JSON.parse(process.argv[1]);"
+ "\nconst links = settingLinks(user, JSON.parse(process.argv[2]));"
+ "\nconsole.log(JSON.stringify(links.map((l: any) => {"
+ " if (l.key === 'calcSites') void openCalcSitesDialog();"
+ " else navigateTo(l.route, l.query);"
+ " return [l.title, l.desc, decodeURIComponent(went)]; })));"
)
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", script, json.dumps(user), json.dumps(company_id)],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def _pages(user: dict, company_id: int | None = None) -> list:
# ☰ 메뉴 = pageLinks(settingLinks 의 구조물 조합 줄을 바탕으로 페이지 둘만)
script = (
"const ROUTES = { M01_MASTER_DATA: 'm01-master-data',"
" M02_MASTER_TEMPLATE: 'm02-master-template', B01_ACCOUNT: 'b01-account' };\n"
"const L = (k: string) => k;\n"
"let went = '';\n"
"const navigateTo = (r: string, q?: Record<string, string>) =>"
" { went = r + (q ? '?' + new URLSearchParams(q) : ''); };\n"
+ _fn(_text(NAV), "settingLinks")
+ "\n"
+ _fn(_text(NAV), "pageLinks")
+ "\nconst user = JSON.parse(process.argv[1]);"
+ "\nconst links = pageLinks(user, JSON.parse(process.argv[2]));"
+ "\nconsole.log(JSON.stringify(links.map((l: any) => {"
+ " navigateTo(l.route, l.query); return [l.key, l.title, decodeURIComponent(went)]; })));"
)
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", script, json.dumps(user), json.dumps(company_id)],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
M01 = "m01-master-data"
M02 = "m02-master-template"
READ_SRC = "B01_Dashboard_PermSourceRead"
WRITE_M = "B01_Dashboard_PermMasterWrite"
# 계산 자리(504-2) — 시스템 관리자만 · 이동 대신 창
CALC_SITES = ["B01_CalcSites_Title", "B01_CalcSites_Desc", "calc-sites-dialog"]
READ_CO = (
"B01_Dashboard_PermMasterRead · B01_Dashboard_PermCompanyRead · B01_Dashboard_PermPersonalWrite"
)
WRITE_CO = READ_CO.replace("CompanyRead", "CompanyWrite")
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_시스템_관리자_줄_칸별_여덟_고른_회사_해시():
# 716-2 · 718-3 — 대시보드 설계 기본값 = 칸별 줄: M01 4(마스터 요소 · 단가산출 로직 · 구조물 옵션 정의 ·
# 별칭 장부) +
# M02 3(일위대가 · 구조물 조합 · 도면 양식) + 계산 자리
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3) == [
["M01_Title", READ_SRC, M01],
["B01_Dashboard_CardLogic", READ_SRC, f"{M01}?open=로직"],
["B01_Dashboard_CardStructOptionDef", WRITE_M, f"{M01}?open=옵션"],
["B01_Dashboard_CardAliasLedger", WRITE_M, f"{M01}?open=별칭"],
["B01_Dashboard_CardPrecombo", WRITE_M, f"{M02}?open=일위대가조합&root=master&company=3"],
["B01_Dashboard_CardStructCombo", WRITE_M, f"{M02}?open=구조물조합&root=master&company=3"],
["B01_Dashboard_CardDrawing", WRITE_M, f"{M02}?open=도면&root=master&company=3"],
CALC_SITES,
]
# 회사가 없으면 해시에 안 실음
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[5][2] == (
f"{M02}?open=구조물조합&root=master"
)
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_메뉴_페이지_둘만_역할별():
# 716-2 — ☰ = M01 · M02 두 페이지(시스템 관리자) / M02 한 페이지(그 밖) · 열쇠 m01 · m02
admin = _pages({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": 1}, 3)
assert admin == [
["m01", "B01_Dashboard_MasterElements", M01],
["m02", "M02_Title", f"{M02}?open=구조물조합&root=personal&company=3"],
]
assert _pages({"role": "ADMIN", "is_master": False, "company_id": None}) == [
["m02", "M02_Title", f"{M02}?open=구조물조합&root=company"],
]
assert _pages({"role": "USER", "is_master": False, "company_id": 1}) == [
["m02", "M02_Title", f"{M02}?open=구조물조합&root=personal"],
]
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_회사_관리자_개인_카드_하나():
company = [
["B01_Dashboard_CardStructCombo", WRITE_CO, f"{M02}?open=구조물조합&root=company"],
]
assert _cards({"role": "ADMIN", "is_master": False}, 3) == company # 회사 인자 안 실음
assert _cards({"role": "USER", "is_master": True}) == company # 회사 마스터 = 회사 수정
assert _cards({"role": "USER", "is_master": False}) == [
["B01_Dashboard_CardStructCombo", READ_CO, f"{M02}?open=구조물조합&root=personal"],
]
def test_회사_고르기_시스템_관리자만():
src = _text(PAGE)
assert "StructureManage" not in src and "buildStructurePanel" not in src
config = _text(ROOT / "config" / "config_frontend.ts")
assert re.search(r"PROTECTED_ROUTES[\s\S]*?ROUTES\.M02_MASTER_TEMPLATE,[\s\S]*?\];", config)
shell = _text(SHELL)
pick = _fn(shell, "buildCompany")
assert 'if (ctx.role !== "SYSTEM_ADMIN") {' in pick # 그 밖 역할 = 이름만
assert "openCompanyPicker(companies, ctx.companyId" in pick # 조회 후 고르기(29-5)
assert "sessionStorage.setItem(ADMIN_COMPANY_KEY, String(company.id))" in pick
assert "pickAdminCompany(me, companies)" in shell
assert 'export const ADMIN_COMPANY_KEY = "aislo.adminCompany";' in src # M02 와 같은 키
people = _text(ROOT / "B01_Dashboard" / "B01_Dashboard_UI_View_People.ts")
for call in ("fetchAllUsers(ctx.companyId)", "fetchCompanyJoinRequests(system ? ctx.companyId",
"openAddMemberModal(undefined, companyId)"): # fmt: skip
assert call in people, call
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_고른_회사_저장값_검사():
src = _text(PAGE)
script = (
"interface DashboardUser { company_id?: number | null }\n"
"interface CompanyInfo { id: number }\n"
"const ADMIN_COMPANY_KEY = 'aislo.adminCompany';\n"
"const store: Record<string, string> = {};\n"
"const sessionStorage = { getItem: (k: string) => store[k] ?? null };\n"
+ _fn(src, "pickAdminCompany")
+ "\nconst cs = [{ id: 1 }, { id: 3 }];"
+ "\nconst out = [pickAdminCompany({ company_id: 1 }, cs)];"
+ "\nstore[ADMIN_COMPANY_KEY] = '3'; out.push(pickAdminCompany({ company_id: 1 }, cs));"
+ "\nstore[ADMIN_COMPANY_KEY] = '9'; out.push(pickAdminCompany({ company_id: 1 }, cs));"
+ "\nout.push(pickAdminCompany({ company_id: null }, cs));"
+ "\nconsole.log(JSON.stringify(out));"
)
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", script],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
# 기본 자기 회사 · 고른 회사 · 없는 회사면 자기 회사 · 자기 회사 없으면 첫 회사
assert json.loads(result.stdout) == [1, 3, 1, 1]
def test_서버_회사_인자_한_곳():
scope = _text(ROOT / "B01_Dashboard" / "B01_Dashboard_Scope.py")
assert 'if session["role"] == "SYSTEM_ADMIN" and company_id is not None:' in scope
router = _text(ROOT / "B01_Dashboard" / "B01_Dashboard_Router.py")
for name in (
"admin_add_member",
"admin_invite_member",
"admin_join_requests",
"admin_projects",
):
body = router.split(f"async def {name}(")[1].split("@router.")[0]
assert "company_id: int | None = Query(None, gt=0)" in body, name
assert "_scope_company(session, company_id)" in body, name
users = router.split("async def system_users(")[1].split("@router.")[0]
assert "require_system_admin" in users and "list_all_users(company_id)" in users
assert "router.include_router(assets_router)" in router
assert len(router.splitlines()) <= 700
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_회사_찾기_거르기():
"""이름 · 사업자번호(줄표 빼고도) · 띄어쓰기 단어 모두 맞음(29-5)."""
src = _text(ROOT / "B01_Dashboard" / "B01_Dashboard_UI_CompanyPick.ts")
found = re.search(r"^export function filterCompanies[\s\S]*?^}", src, re.M)
assert found, "filterCompanies 를 못 찾음"
script = (
"interface CompanyInfo { id: number; name: string;"
" business_registration_number?: string | null }\n"
+ found.group(0).replace("export ", "")
+ "\nconst cs = [{ id: 1, name: '캐미팩토리',"
+ " business_registration_number: '123-45-67890' },"
+ " { id: 3, name: '브레인검증회사', business_registration_number: null },"
+ " { id: 4, name: '캐미 건설', business_registration_number: '999-00-11111' }];"
+ "\nconst ids = (t: string) => filterCompanies(cs, t).map((c) => c.id);"
+ "\nconsole.log(JSON.stringify([ids(''), ids('캐미'), ids('캐미 건설'), ids('1234567890'),"
+ " ids('123-45'), ids('브레인 없음'), ids(' ')]));"
)
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", script],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
assert json.loads(result.stdout) == [[1, 3, 4], [1, 4], [4], [1], [1], [], [1, 3, 4]]
assert "const LIMIT = 50;" in src and "found.slice(0, LIMIT)" in src
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_구조물_조합_기본_층_개인():
"""123-1 — 회사 있는 사람은 역할 무관 구조물 조합 = 개인 · 회사 없으면 지금 규칙 · 다른 곳 그대로 ·
134-4 — 층 없는 주소도 개인(회사 없으면 마스터) · 사전조합 기본 층도 같은 규칙."""
combo = f"{M02}?open=구조물조합&root=personal"
rows = _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": 1}, 3)
assert rows[4:7] == [
["B01_Dashboard_CardPrecombo", WRITE_M, f"{M02}?open=일위대가조합&root=personal&company=3"],
["B01_Dashboard_CardStructCombo", WRITE_M, f"{combo}&company=3"],
["B01_Dashboard_CardDrawing", WRITE_M, f"{M02}?open=도면&root=master&company=3"],
]
assert _cards({"role": "ADMIN", "is_master": False, "company_id": 1}) == [
["B01_Dashboard_CardStructCombo", WRITE_CO, combo],
]
assert _cards({"role": "USER", "is_master": True, "company_id": 1})[0][2] == combo
assert _cards({"role": "USER", "is_master": False, "company_id": 1})[0][2] == combo
# 개인 층 없음(회사 없음) = 지금 규칙
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": None})[5][2] == (
f"{M02}?open=구조물조합&root=master"
)
assert _cards({"role": "ADMIN", "is_master": False, "company_id": None})[0][2] == (
f"{M02}?open=구조물조합&root=company"
)
# 다른 곳 기본 그대로 — 고르기 탭(일위대가 · 복사 생성 · 복사해 가져오기)
layer = _text(ROOT / "M02_MasterTemplete" / "M02_MasterTemplete_Layer.ts")
assert 'return { tabs, first: isSystem || !own ? "master" : "personal" };' in layer
# 134-4 — 층 없는 주소 = 기본 층(개인 · 회사 없으면 마스터) · 사전조합 층도 같은 기본
assert 'ownOwner("personal", user) ? "personal" : "master";' in layer
assert ': user === undefined\n ? "master"\n : defaultRoot(user);' in layer
page = _text(ROOT / "M02_MasterTemplete" / "M02_MasterTemplete_UI_Page.ts")
assert "const entry = entryFromHash(user);" in page
assert "let pre = preScope(entry.pre ?? defaultRoot(user));" in page