merge(B01): main_laptop_1(549-2 시스템 관리 · 내 정보) 합침 — 옛 구역 정리 · 시험 옮김

- B01_Dashboard_UI_Legacy.ts: 세부 페이지가 다 갈아 끼워 옛 구역 · mountLegacy 지움 · 고른 회사 자리 · 상태 타입만 남김
- B01_Dashboard_UI_Views.ts: 쓰는 곳 없는 legacy 도우미 지움
- B01_Dashboard_UI_Style_Pages.css: 배지 색(549-1) · 시스템 관리 꼴(549-2) 둘 다 살림
- test_b01_settings_cards · test_309_2: 지운 옛 카드 부품 대신 settingLinks · 설계 기본값 페이지 · 사람 페이지를 읽음(낡은 시험)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 18:33:32 +09:00
co-authored by Claude Opus 5.5
16 changed files with 1358 additions and 429 deletions
+1 -1
View File
@@ -264,7 +264,7 @@ def test_카드_시스템_관리자만_글_다_있음():
admin, rest = links.split('if (user.role === "SYSTEM_ADMIN") {')[1].split("// 회사 수정 =")
assert 'key: "calcSites"' in admin and "calcSites" not in rest
assert 'settingLinks(user, companyId).filter((link) => link.key !== "calcSites")' in nav
page = _text("B01_Dashboard/B01_Dashboard_UI_Legacy.ts")
page = _text("B01_Dashboard/B01_Dashboard_UI_View_SysDefaults.ts")
assert 'link.key === "calcSites" ? void openCalcSitesDialog()' in page
locale = _text("ui_template/ui_template_locale_m1.ts")
for group in calc_sites.GROUPS:
+133
View File
@@ -0,0 +1,133 @@
"""549-2 시스템 관리 · 내 정보 세부 페이지 시험 — 화면 조작은 ORCA.
· 거르기 · 게이지 색 · 로그 글 = 순수 함수를 Node 로 바로 돌림
· 여섯 열쇠가 새 페이지로 갈아 끼워짐 · 시스템 관리 페이지는 시스템 관리자만(등록부 + 서버 둘 다)
"""
from __future__ import annotations
import json
import re
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
B01 = ROOT / "B01_Dashboard"
def _text(path: Path) -> str:
return path.read_text(encoding="utf-8")
def _slice(name: str, start: str, end: str) -> str:
src = _text(B01 / name)
return src[src.index(start) : src.index(end)]
def _node(script: str) -> object:
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
return json.loads(result.stdout)
needs_node = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
@needs_node
def test_게이지_색_선():
src = _slice(
"B01_Dashboard_UI_View_SysOverview.ts", "export const GAUGE_WARN", "function statCard"
)
out = _node(
src + "console.log(JSON.stringify([null, 0, 69.9, 70, 89.9, 90, 100].map(gaugeTone)));"
)
assert out == ["ok", "ok", "ok", "warn", "warn", "bad", "bad"]
@needs_node
def test_사용자_거르기():
src = _slice("B01_Dashboard_UI_View_SysUsers.ts", "/** 역할 · 찾기", "const COLUMNS")
users = [
{"id": 1, "name": "홍길동", "email": "hong@a.kr", "company_name": "예시건설", "role": "ADMIN"},
{"id": 2, "name": "김영희", "email": "kim@b.kr", "company_name": None, "role": "USER"},
{"id": 3, "name": "관리", "email": "sys@c.kr", "department": "운영", "role": "SYSTEM_ADMIN"},
] # fmt: skip
out = _node(
"type DashboardUser = any;\n"
+ src
+ f"const u = {json.dumps(users, ensure_ascii=False)};"
+ "console.log(JSON.stringify(["
"filterUsers(u, '', '').map(x => x.id),"
"filterUsers(u, 'USER', '').map(x => x.id),"
"filterUsers(u, '', '예시').map(x => x.id),"
"filterUsers(u, '', 'KIM@B').map(x => x.id),"
"filterUsers(u, '', 'null').map(x => x.id),"
"filterUsers(u, 'ADMIN', '운영').map(x => x.id)]));"
)
assert out == [[1, 2, 3], [2], [1], [2], [], []]
@needs_node
def test_로그_거르기_글():
src = _slice("B01_Dashboard_UI_View_SysLogs.ts", "export const ACTION_KO", "function dateInput")
logs = [
{"action": "PROJECT_CREATE", "email": "a@x.kr", "resource_type": "project",
"resource_ref": "12345678-aaaa-bbbb-cccc-1234567890ab", "ip_address": "1.2.3.4",
"timestamp": "2026-10-20T09:00:00"},
{"action": "COMPANY_CREATE", "email": "b@x.kr", "resource_type": "COMPANY",
"resource_id": 7, "ip_address": None, "timestamp": "2026-10-25T10:00:00"},
{"action": "NEW_THING", "email": None, "timestamp": "2026-10-26T11:00:00"},
] # fmt: skip
out = _node(
"type AuditLog = any;\n"
+ src
+ f"const l = {json.dumps(logs, ensure_ascii=False)};"
+ "const f = (o: object) => filterLogs(l, { action: '', from: '', to: '', q: '', ...o }).length;"
+ "console.log(JSON.stringify(["
"actionLabel('PROJECT_CREATE'), actionLabel('NEW_THING'),"
"targetText(l[0]), targetText(l[1]), targetText(l[2]),"
"f({}), f({ action: 'COMPANY_CREATE' }),"
"f({ from: '2026-10-25' }), f({ to: '2026-10-20' }), f({ from: '2026-10-25', to: '2026-10-25' }),"
"f({ q: '프로젝트 생성' }), f({ q: '1.2.3' })]));"
)
assert out == [
"프로젝트 생성", "NEW_THING", "프로젝트 12345678…", "회사 7", "-",
3, 1, 2, 1, 1, 1, 1,
] # fmt: skip
def test_여섯_열쇠_새_페이지():
views = _text(B01 / "B01_Dashboard_UI_Views.ts")
want = {
"sys-overview": "mountSysOverview", "sys-companies": "mountSysCompanies",
"sys-users": "mountSysUsers", "sys-defaults": "mountSysDefaults",
"sys-logs": "mountSysLogs", "me": "mountMe",
} # fmt: skip
for key, mount in want.items():
line = next(x for x in views.splitlines() if f'key: "{key}"' in x)
assert f"mount: {mount} " in line + " ", key
if key != "me":
assert 'roles: ["SYSTEM_ADMIN"]' in line, key # 시스템 관리 = 시스템 관리자만
legacy = _text(B01 / "B01_Dashboard_UI_Legacy.ts")
for old in ("sysOverview", "sysCompanies", "sysUsers", "sysDefaults", "sysLogs", " me: (s)"):
assert old not in legacy, old # 옛 구역 지움
def test_서버도_시스템_관리자만():
router = _text(B01 / "B01_Dashboard_Router.py")
for route in ("/admin/companies", "/admin/users", "/admin/audit-logs", "/admin/resources"):
block = re.search(r'@router\.get\("%s"\)[\s\S]*?\n\n\n' % re.escape(route), router)
assert block and "Depends(require_system_admin)" in block.group(0), route
def test_로그_쪽_불러오기_있음():
api = _text(B01 / "B01_Dashboard_Api_Fetch.ts")
assert "export async function fetchAuditLogPage(" in api
assert "/dashboard/admin/audit-logs?page=${page}&limit=${limit}" in api
+11 -20
View File
@@ -15,7 +15,7 @@ from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
PAGE = ROOT / "B01_Dashboard" / "B01_Dashboard_UI_Legacy.ts" # 옛 구역 부품(546)
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)
@@ -31,7 +31,7 @@ def _fn(src: str, name: str) -> str:
def _cards(user: dict, company_id: int | None = None) -> list:
src = _text(PAGE)
# 카드 = settingLinks 한 벌 · 누름 = 설계 기본값 페이지(SysDefaults)와 같은 갈래(계산 자리만 창)
script = (
"interface DashboardUser { role: string; is_master: boolean }\n"
"const ROUTES = { M01_MASTER_DATA: 'm01-master-data',"
@@ -41,21 +41,13 @@ def _cards(user: dict, company_id: int | None = None) -> list:
"const navigateTo = (r: string, q?: Record<string, string>) =>"
" { went = r + (q ? '?' + new URLSearchParams(q) : ''); };\n"
"const openCalcSitesDialog = async () => { went = 'calc-sites-dialog'; };\n"
"const mk = (): any => ({ children: [] as any[], textContent: '', className: '',"
" on: null as any,"
" append(...c: any[]) { this.children.push(...c); },"
" addEventListener(_: string, f: any) { this.on = f; } });\n"
"const document = { createElement: () => mk() };\n"
+ _fn(src, "settingCard")
+ "\n"
+ _fn(_text(NAV), "settingLinks")
+ "\n"
+ _fn(src, "buildSystemSettingsPanel")
+ "\nconst user = JSON.parse(process.argv[1]);"
+ "\nconst panel = buildSystemSettingsPanel(user, JSON.parse(process.argv[2]));"
+ "\nconsole.log(JSON.stringify(panel.children.map((c: any) => {"
+ " c.on(); return [c.children[0].textContent, c.children[1].textContent,"
+ " decodeURIComponent(went)]; })));"
+ "\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",
@@ -115,11 +107,10 @@ def test_회사_고르기_시스템_관리자만():
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 와 같은 키
load = src.split("async function loadRoleData")[1].split("\n}\n")[0]
for call in ("fetchAllUsers(companyId)", "fetchCompanyJoinRequests(companyId)",
"fetchCompanyProjects(companyId)"): # fmt: skip
assert call in load, call
assert "openAddMemberModal(undefined, s.adminCompanyId)" in src
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 가 없음")