feat(B01): 대시보드 시스템 설정 컨테이너 재구성(27-3)
- 「구조물 관리」 컨테이너 삭제 · 「시스템 설정」을 모든 역할 맨 위 한 줄 · 통째로 접힘(attachCollapsible) - 안은 작은 카드(제목 + 한 줄 설명) 공용 양식 하나 · 역할로 숨김 - 시스템 관리자: 마스터 데이터 · 일위대가 조합 · 구조물 조합 · 구조물 옵션 · 도면 양식 - 회사 관리자 · 개인: 일위대가 조합 · 구조물 조합(마스터 읽기 · 회사 수정|읽기 · 개인 수정) - 카드 누르면 #/m02-master-template?open=…&root=… · 마스터 데이터는 M01 - 시험 test_b01_settings_cards 새로 · test_m02_layer_screen 옛 대시보드 카드 시험 뺌 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
||||
import {
|
||||
createButton,
|
||||
createTag,
|
||||
@@ -130,39 +131,63 @@ async function loadRoleData(state: DashboardState): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function buildSystemSettingsPanel(): HTMLElement {
|
||||
/** 설정 카드 공용 양식 — 제목 + 한 줄 설명 · 누르면 이동 */
|
||||
function settingCard(title: string, desc: string, go: () => void): HTMLElement {
|
||||
const card = document.createElement("button");
|
||||
card.type = "button";
|
||||
card.className = "b01-dashboard__setting-card";
|
||||
const head = document.createElement("strong");
|
||||
head.className = "b01-dashboard__setting-title";
|
||||
head.textContent = title;
|
||||
const sub = document.createElement("span");
|
||||
sub.className = "b01-dashboard__setting-desc";
|
||||
sub.textContent = desc;
|
||||
card.append(head, sub);
|
||||
card.addEventListener("click", go);
|
||||
return card;
|
||||
}
|
||||
|
||||
/** 시스템 설정 — 역할로 카드 숨김 · M02 카드는 `?open=&root=` 로 진입(27-1d) */
|
||||
function buildSystemSettingsPanel(user: DashboardUser): HTMLElement {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b01-dashboard__actions";
|
||||
wrap.className = "b01-dashboard__setting-grid";
|
||||
const m02 = (title: string, open: string, root: string, desc: string): HTMLElement =>
|
||||
settingCard(title, desc, () => navigateTo(ROUTES.M02_MASTER_TEMPLATE, { open, root }));
|
||||
if (user.role === "SYSTEM_ADMIN") {
|
||||
const desc = L("B01_Dashboard_PermMasterWrite");
|
||||
wrap.append(
|
||||
settingCard(L("B01_Dashboard_MasterElements"), L("B01_Dashboard_PermSourceRead"), () =>
|
||||
navigateTo(ROUTES.M01_MASTER_DATA),
|
||||
),
|
||||
m02(L("B01_Dashboard_CardUnitCombo"), "일위대가", "master", desc),
|
||||
m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", "master", desc),
|
||||
m02(L("B01_Dashboard_CardStructOption"), "옵션", "master", desc),
|
||||
m02(L("B01_Dashboard_CardDrawing"), "도면", "master", desc),
|
||||
);
|
||||
return wrap;
|
||||
}
|
||||
// 회사 수정 = 회사 관리자 · 회사 마스터(M02 accessFor 와 같은 규칙)
|
||||
const companyAdmin = user.role === "ADMIN" || user.is_master;
|
||||
const root = companyAdmin ? "company" : "personal";
|
||||
const desc = [
|
||||
L("B01_Dashboard_PermMasterRead"),
|
||||
L(companyAdmin ? "B01_Dashboard_PermCompanyWrite" : "B01_Dashboard_PermCompanyRead"),
|
||||
L("B01_Dashboard_PermPersonalWrite"),
|
||||
].join(" · ");
|
||||
wrap.append(
|
||||
createButton({
|
||||
label: L("B01_Dashboard_MasterElements"),
|
||||
variant: "ghost",
|
||||
onClick: () => navigateTo(ROUTES.M01_MASTER_DATA),
|
||||
}),
|
||||
createButton({
|
||||
label: L("B01_Dashboard_MasterTemplate"),
|
||||
variant: "ghost",
|
||||
onClick: () => navigateTo(ROUTES.M02_MASTER_TEMPLATE),
|
||||
}),
|
||||
m02(L("B01_Dashboard_CardUnitCombo"), "일위대가", root, desc),
|
||||
m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", root, desc),
|
||||
);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
/** 구조물 관리 카드 — 개인용은 모두 · 회사용은 회사 관리자와 시스템 관리자(21-0) */
|
||||
function buildStructurePanel(state: DashboardState): HTMLElement {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b01-dashboard__actions";
|
||||
const go = (layer: "company" | "personal", label: string): HTMLElement =>
|
||||
createButton({
|
||||
label,
|
||||
variant: "ghost",
|
||||
onClick: () => navigateTo(ROUTES.M02_MASTER_TEMPLATE, { layer }),
|
||||
});
|
||||
if (state.user.role === "ADMIN" || state.user.role === "SYSTEM_ADMIN") {
|
||||
wrap.append(go("company", L("B01_Dashboard_StructureCompany")));
|
||||
}
|
||||
wrap.append(go("personal", L("B01_Dashboard_StructurePersonal")));
|
||||
return wrap;
|
||||
/** 시스템 설정 컨테이너 — 모든 역할 · 한 줄 통째 · 제목 눌러 통째로 접힘 */
|
||||
function buildSystemSettingsSection(user: DashboardUser): HTMLElement {
|
||||
const card = section(L("B01_Dashboard_SystemSettings"), buildSystemSettingsPanel(user), true);
|
||||
card.querySelector(".ui-card__body")?.classList.add("ui-collapsible");
|
||||
card.querySelector(".b01-dashboard__section-header")?.classList.add("ui-collapsible__title");
|
||||
attachCollapsible(card);
|
||||
return card;
|
||||
}
|
||||
|
||||
function buildPage(state: DashboardState): HTMLElement {
|
||||
@@ -171,22 +196,16 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "b01-dashboard__grid";
|
||||
// 시스템 설정은 모든 역할 맨 위 한 줄(27-3a)
|
||||
grid.append(buildSystemSettingsSection(state.user));
|
||||
|
||||
if (state.user.role === "SYSTEM_ADMIN") {
|
||||
// 리소스 현황 오른쪽에 시스템 설정 — 앞으로 다른 설정 단추도 이 칸에 듦 (2026-09-15 브레인 Z01).
|
||||
const topRow = document.createElement("div");
|
||||
topRow.className = "b01-dashboard__top-row";
|
||||
topRow.append(
|
||||
section(L("B01_Dashboard_Resources"), buildResourcePanel(state.resources)),
|
||||
section(L("B01_Dashboard_SystemSettings"), buildSystemSettingsPanel()),
|
||||
);
|
||||
grid.append(
|
||||
topRow,
|
||||
section(L("B01_Dashboard_Resources"), buildResourcePanel(state.resources), true),
|
||||
section(L("B01_Dashboard_Projects"), projectTable(state.allProjects, state.user), true, [
|
||||
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
|
||||
]),
|
||||
buildTempUploadSection(),
|
||||
section(L("B01_Dashboard_StructureManage"), buildStructurePanel(state)),
|
||||
section(L("B01_Dashboard_Users"), userTable(state.allUsers, state.user), true, [
|
||||
createButton({ label: "+", onClick: () => openAddMemberModal() }),
|
||||
]),
|
||||
@@ -201,7 +220,6 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
|
||||
]),
|
||||
buildTempUploadSection(),
|
||||
section(L("B01_Dashboard_StructureManage"), buildStructurePanel(state)),
|
||||
section(L("B01_Dashboard_Members"), memberTable(state.members, state.user), true, [
|
||||
createButton({ label: "+", onClick: () => openAddMemberModal() }),
|
||||
]),
|
||||
@@ -216,7 +234,6 @@ function buildPage(state: DashboardState): HTMLElement {
|
||||
// 임시 보관함 — 프로젝트를 만들기 전에 자료를 올려 두는 곳이라 프로젝트 목록
|
||||
// 바로 아래에 둔다(2026-08-08 사용자 지시).
|
||||
buildTempUploadSection(),
|
||||
section(L("B01_Dashboard_StructureManage"), buildStructurePanel(state)),
|
||||
section(L("B01_Dashboard_Company"), buildCompanyPanel(state), true),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,12 +31,41 @@
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* 리소스 현황 | 시스템 설정 — 설정 칸은 단추만 들어 좁게. */
|
||||
.b01-dashboard__top-row {
|
||||
grid-column: 1 / -1;
|
||||
/* 시스템 설정 — 작은 카드(제목 + 한 줄 설명) 한 양식 */
|
||||
.b01-dashboard__setting-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 3fr) minmax(0, 1fr);
|
||||
gap: var(--spacing-24);
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: var(--spacing-8);
|
||||
}
|
||||
|
||||
.b01-dashboard__setting-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-8) var(--spacing-16);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-cards);
|
||||
background: var(--color-surface);
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b01-dashboard__setting-card:hover,
|
||||
.b01-dashboard__setting-card:focus-visible {
|
||||
border-color: var(--color-royal-amethyst);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.b01-dashboard__setting-title {
|
||||
font-size: var(--text-body);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.b01-dashboard__setting-desc {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.b01-dashboard__actions {
|
||||
@@ -288,8 +317,7 @@
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.b01-dashboard__header,
|
||||
.b01-dashboard__grid,
|
||||
.b01-dashboard__top-row {
|
||||
.b01-dashboard__grid {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"""B01 대시보드 「시스템 설정」 컨테이너 시험(27-3) — 화면 조작은 ORCA.
|
||||
|
||||
· 역할별 카드(제목 · 설명 · 이동 해시)를 Node 로 바로 돌림 — 가짜 DOM
|
||||
· 모든 역할 맨 위 한 줄 · 통째로 접힘 · 「구조물 관리」 컨테이너 없음
|
||||
"""
|
||||
|
||||
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_Page.ts"
|
||||
|
||||
|
||||
def _text(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _fn(src: str, name: str) -> str:
|
||||
found = re.search(rf"^function {name}\([\s\S]*?^}}", src, re.M)
|
||||
assert found, f"{name} 를 못 찾음"
|
||||
return found.group(0)
|
||||
|
||||
|
||||
def _cards(user: dict) -> list:
|
||||
src = _text(PAGE)
|
||||
script = (
|
||||
"interface DashboardUser { role: string; is_master: boolean }\n"
|
||||
"const ROUTES = { M01_MASTER_DATA: 'm01-master-data', M02_MASTER_TEMPLATE: 'm02-master-template' };\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 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(src, "buildSystemSettingsPanel")
|
||||
+ "\nconst user = JSON.parse(process.argv[1]);"
|
||||
+ "\nconsole.log(JSON.stringify(buildSystemSettingsPanel(user).children.map((c: any) => {"
|
||||
+ " c.on(); return [c.children[0].textContent, c.children[1].textContent,"
|
||||
+ " decodeURIComponent(went)]; })));"
|
||||
)
|
||||
result = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", script, json.dumps(user)],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
M02 = "m02-master-template"
|
||||
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_시스템_관리자_카드_다섯():
|
||||
m = "B01_Dashboard_PermMasterWrite"
|
||||
assert _cards({"role": "SYSTEM_ADMIN", "is_master": False}) == [
|
||||
["B01_Dashboard_MasterElements", "B01_Dashboard_PermSourceRead", "m01-master-data"],
|
||||
["B01_Dashboard_CardUnitCombo", m, f"{M02}?open=일위대가&root=master"],
|
||||
["B01_Dashboard_CardStructCombo", m, f"{M02}?open=구조물조합&root=master"],
|
||||
["B01_Dashboard_CardStructOption", m, f"{M02}?open=옵션&root=master"],
|
||||
["B01_Dashboard_CardDrawing", m, f"{M02}?open=도면&root=master"],
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
def test_회사_관리자_개인_카드_둘():
|
||||
company = [
|
||||
["B01_Dashboard_CardUnitCombo", WRITE_CO, f"{M02}?open=일위대가&root=company"],
|
||||
["B01_Dashboard_CardStructCombo", WRITE_CO, f"{M02}?open=구조물조합&root=company"],
|
||||
]
|
||||
assert _cards({"role": "ADMIN", "is_master": False}) == company
|
||||
assert _cards({"role": "USER", "is_master": True}) == company # 회사 마스터 = 회사 수정
|
||||
assert _cards({"role": "USER", "is_master": False}) == [
|
||||
["B01_Dashboard_CardUnitCombo", READ_CO, f"{M02}?open=일위대가&root=personal"],
|
||||
["B01_Dashboard_CardStructCombo", READ_CO, f"{M02}?open=구조물조합&root=personal"],
|
||||
]
|
||||
|
||||
|
||||
def test_모든_역할_맨_위_통째로_접힘():
|
||||
src = _text(PAGE)
|
||||
build = _fn(src, "buildPage")
|
||||
# 역할 갈래 앞에서 한 번 — 모든 역할 · 프로젝트 위
|
||||
assert build.index("grid.append(buildSystemSettingsSection(state.user))") < build.index(
|
||||
'if (state.user.role === "SYSTEM_ADMIN")'
|
||||
)
|
||||
assert "StructureManage" not in src and "buildStructurePanel" not in src
|
||||
assert "top-row" not in src
|
||||
sect = _fn(src, "buildSystemSettingsSection")
|
||||
assert "buildSystemSettingsPanel(user), true)" in sect # 한 줄 통째
|
||||
assert '"ui-collapsible"' in sect and '"ui-collapsible__title"' in sect
|
||||
assert "attachCollapsible(card)" in sect
|
||||
config = _text(ROOT / "config" / "config_frontend.ts")
|
||||
assert re.search(r"PROTECTED_ROUTES[\s\S]*?ROUTES\.M02_MASTER_TEMPLATE,[\s\S]*?\];", config)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
· 권한 규칙(`accessFor`)을 Node 로 바로 돌림 — 시스템 SYSTEM_ADMIN ·
|
||||
회사 ADMIN/is_master · 개인 본인 · 회사 없음
|
||||
· 주소 인자를 라우트에서 떼는지 · 대시보드 카드 역할별 단추 · 층 길 접두어
|
||||
· 주소 인자를 라우트에서 떼는지 · 층 길 접두어
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -71,20 +71,6 @@ def test_주소_인자를_라우트에서_뗌():
|
||||
assert 'value === "company" || value === "personal" ? value : "system"' in layer
|
||||
|
||||
|
||||
def test_대시보드_카드_역할별_단추():
|
||||
page = _text(ROOT / "B01_Dashboard" / "B01_Dashboard_UI_Page.ts")
|
||||
assert (
|
||||
page.count('section(L("B01_Dashboard_StructureManage"), buildStructurePanel(state))') == 3
|
||||
)
|
||||
body = page.split("function buildStructurePanel")[1].split("function buildPage")[0]
|
||||
# 개인용은 모두 · 회사용은 ADMIN · SYSTEM_ADMIN 만
|
||||
assert 'state.user.role === "ADMIN" || state.user.role === "SYSTEM_ADMIN"' in body
|
||||
assert body.index('go("company"') < body.index('go("personal"')
|
||||
assert "ROUTES.M02_MASTER_TEMPLATE, { layer }" in body
|
||||
config = _text(ROOT / "config" / "config_frontend.ts")
|
||||
assert re.search(r"PROTECTED_ROUTES[\s\S]*?ROUTES\.M02_MASTER_TEMPLATE,[\s\S]*?\];", config)
|
||||
|
||||
|
||||
def test_층_길_접두어와_고정_풀림():
|
||||
api = _text(M02 / "M02_MasterTemplete_Api_Fetch.ts")
|
||||
assert 'activeLayer === "system" ? "" : `/layers/${activeLayer}`' in api
|
||||
|
||||
@@ -139,6 +139,12 @@ export const ui_locales_b3 = {
|
||||
/* --- B01 시스템 설정 --- */
|
||||
B01_Dashboard_SystemSettings: ["시스템 설정", "System Settings"],
|
||||
B01_Dashboard_MasterData: ["마스터 데이터", "Master Data"],
|
||||
B01_Dashboard_PermSourceRead: ["원문 원천 · 읽기", "Source data · read"],
|
||||
B01_Dashboard_PermMasterRead: ["마스터 읽기", "Master read"],
|
||||
B01_Dashboard_PermMasterWrite: ["마스터 수정", "Master edit"],
|
||||
B01_Dashboard_PermCompanyRead: ["회사 읽기", "Company read"],
|
||||
B01_Dashboard_PermCompanyWrite: ["회사 수정", "Company edit"],
|
||||
B01_Dashboard_PermPersonalWrite: ["개인 수정", "Personal edit"],
|
||||
|
||||
/* --- B08·B09 빈 화면 (PLAN 7-3) --- */
|
||||
B08_Quantity_Rebuilding: [
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
* ========================================================================== */
|
||||
|
||||
export const ui_locales_m2 = {
|
||||
B01_Dashboard_MasterTemplate: ["마스터 템플릿", "Master Templates"],
|
||||
B01_Dashboard_CardUnitCombo: ["일위대가 조합", "Unit cost combos"],
|
||||
B01_Dashboard_CardStructCombo: ["구조물 조합", "Structure combos"],
|
||||
B01_Dashboard_CardStructOption: ["구조물 옵션", "Structure options"],
|
||||
B01_Dashboard_CardDrawing: ["도면 양식", "Drawing templates"],
|
||||
M02_Title: ["마스터 템플릿", "Master Templates"],
|
||||
M02_KindTable: ["구조물 조합", "Structure combos"],
|
||||
M02_KindDrawing: ["도면 양식", "Drawing templates"],
|
||||
@@ -57,9 +60,6 @@ export const ui_locales_m2 = {
|
||||
M02_ActionFailed: ["안 됨 — {value}", "Failed — {value}"],
|
||||
M02_StructureNone: ["없음", "None"],
|
||||
M02_Back: ["돌아가기", "Back"],
|
||||
B01_Dashboard_StructureManage: ["구조물 관리", "Structure management"],
|
||||
B01_Dashboard_StructureCompany: ["회사용", "Company"],
|
||||
B01_Dashboard_StructurePersonal: ["개인용", "Personal"],
|
||||
M02_TitleCompany: ["구조물 관리(회사)", "Structure management (company)"],
|
||||
M02_TitlePersonal: ["구조물 관리(개인)", "Structure management (personal)"],
|
||||
M02_ReadOnlyCompany: [
|
||||
|
||||
Reference in New Issue
Block a user