feat(설정): 334-2 ☰ 시스템 설정 4 페이지 바로가기 — 🏠 대시보드 + 카드 · 지금 칸 강조 · M02 → M02 이동은 떠나기 물음 먼저
- 새 A00_Common/m_settings_nav: settingLinks = 대시보드 카드 역할 규칙 한 벌 · mountSettingsDrawer = ☰ 진행 단계 자리 - B01 시스템 설정 카드가 settingLinks 로만 그림(화면 그대로) - M01 · M02 page 한 줄씩(M02 = 진입 open · company 이어 실음) - test_b01_settings_cards 가 옮긴 함수 소스를 같이 실음(기대값 그대로) · test_225 뒷부분 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
/* =============================================================================
|
||||
* m_settings_nav.ts
|
||||
* 시스템 설정 바로가기(334 · 요구 225-2) — 대시보드 「시스템 설정」 카드 · M01 · M02 ☰ 가 같이 쓰는
|
||||
* 역할 규칙 한 벌(`settingLinks`) · ☰ 진행 단계 자리에 🏠 대시보드 + 카드(`mountSettingsDrawer`).
|
||||
* ========================================================================== */
|
||||
|
||||
import { ROUTES, type RoutePath } from "@config/config_frontend";
|
||||
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
||||
import { createStepBar } from "@ui/ui_template_workflow_layout";
|
||||
import { setDrawerSteps } from "./app_shell";
|
||||
import { currentRoute, navigateTo, takeLeaveGuard } from "./router";
|
||||
|
||||
interface SettingUser {
|
||||
role?: string;
|
||||
is_master?: boolean;
|
||||
company_id?: number | null;
|
||||
}
|
||||
|
||||
/** key = M01 은 「m01」 · M02 는 진입 인자 `open` */
|
||||
export interface SettingLink {
|
||||
key: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
route: RoutePath;
|
||||
query?: Record<string, string>;
|
||||
}
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
const ICONS: Record<string, string> = { m01: "🗄️", 구조물조합: "🧱", 옵션: "🎛️", 도면: "📑" };
|
||||
|
||||
/** 시스템 설정 — 역할로 카드 숨김 · M02 카드는 `?open=&root=` 로 진입(27-1d) */
|
||||
export function settingLinks(user: SettingUser, companyId: number | string | null): SettingLink[] {
|
||||
// 시스템 관리자는 고른 회사를 해시에 실음(29-1)
|
||||
const company: Record<string, string> =
|
||||
user.role === "SYSTEM_ADMIN" && companyId ? { company: String(companyId) } : {};
|
||||
const m02 = (title: string, open: string, root: string, desc: string): SettingLink => ({
|
||||
key: open,
|
||||
title,
|
||||
desc,
|
||||
route: ROUTES.M02_MASTER_TEMPLATE,
|
||||
query: { open, root, ...company },
|
||||
});
|
||||
// 구조물 조합 기본 층 = 개인(123-1 · 누구든) · 개인 층 없음(회사 없음) = 아래 역할 규칙
|
||||
const comboRoot = (fallback: string): string => (user.company_id ? "personal" : fallback);
|
||||
if (user.role === "SYSTEM_ADMIN") {
|
||||
const desc = L("B01_Dashboard_PermMasterWrite");
|
||||
return [
|
||||
{
|
||||
key: "m01",
|
||||
title: L("B01_Dashboard_MasterElements"),
|
||||
desc: L("B01_Dashboard_PermSourceRead"),
|
||||
route: ROUTES.M01_MASTER_DATA,
|
||||
},
|
||||
m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", comboRoot("master"), desc),
|
||||
m02(L("B01_Dashboard_CardStructOption"), "옵션", "master", desc),
|
||||
m02(L("B01_Dashboard_CardDrawing"), "도면", "master", desc),
|
||||
];
|
||||
}
|
||||
// 회사 수정 = 회사 관리자 · 회사 마스터(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(" · ");
|
||||
return [m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", comboRoot(root), desc)];
|
||||
}
|
||||
|
||||
/** ☰ 진행 단계 자리 — 🏠 대시보드 + 카드 · 지금 칸 강조 · 같은 길(M02 → M02)은 머리 가드가 안 물어 여기서 먼저 */
|
||||
export function mountSettingsDrawer(
|
||||
user: SettingUser,
|
||||
activeKey: string,
|
||||
companyId: number | string | null,
|
||||
): void {
|
||||
const links = settingLinks(user, companyId);
|
||||
const bar = createStepBar(
|
||||
links.map((link) => link.title),
|
||||
links.findIndex((link) => link.key === activeKey),
|
||||
{
|
||||
orientation: "vertical",
|
||||
icons: links.map((link) => ICONS[link.key] ?? ""),
|
||||
routes: links.map((link) => link.route),
|
||||
homeButton: true,
|
||||
onStepClick: (index) => void go(links[index]),
|
||||
},
|
||||
);
|
||||
setDrawerSteps(bar);
|
||||
}
|
||||
|
||||
async function go(link: SettingLink): Promise<void> {
|
||||
const guard = link.route === currentRoute() ? takeLeaveGuard() : null;
|
||||
if (guard && !(await guard())) return;
|
||||
navigateTo(link.route, link.query);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { section } from "@ui/ui_template_general_blocks";
|
||||
import { createGeneralLayout } from "@ui/ui_template_general_layout";
|
||||
import { createStepBar } from "@ui/ui_template_workflow_layout";
|
||||
import { setDrawerSteps } from "../A00_Common/app_shell";
|
||||
import { settingLinks } from "../A00_Common/m_settings_nav";
|
||||
import { currentRoute, navigateTo } from "../A00_Common/router";
|
||||
import {
|
||||
fetchAllCompanies,
|
||||
@@ -164,40 +165,15 @@ function settingCard(title: string, desc: string, go: () => void): HTMLElement {
|
||||
return card;
|
||||
}
|
||||
|
||||
/** 시스템 설정 — 역할로 카드 숨김 · M02 카드는 `?open=&root=` 로 진입(27-1d) */
|
||||
/** 시스템 설정 — 카드 규칙은 `settingLinks` 한 벌(☰ 바로가기와 같이 씀 · 334) */
|
||||
function buildSystemSettingsPanel(user: DashboardUser, companyId: number | null): HTMLElement {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "b01-dashboard__setting-grid";
|
||||
// 시스템 관리자는 고른 회사를 해시에 실음(29-1)
|
||||
const company: Record<string, string> =
|
||||
user.role === "SYSTEM_ADMIN" && companyId ? { company: String(companyId) } : {};
|
||||
const m02 = (title: string, open: string, root: string, desc: string): HTMLElement =>
|
||||
settingCard(title, desc, () =>
|
||||
navigateTo(ROUTES.M02_MASTER_TEMPLATE, { open, root, ...company }),
|
||||
);
|
||||
// 구조물 조합 기본 층 = 개인(123-1 · 누구든) · 개인 층 없음(회사 없음) = 아래 역할 규칙
|
||||
const comboRoot = (fallback: string): string => (user.company_id ? "personal" : fallback);
|
||||
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_CardStructCombo"), "구조물조합", comboRoot("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(m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", comboRoot(root), desc));
|
||||
wrap.append(
|
||||
...settingLinks(user, companyId).map((link) =>
|
||||
settingCard(link.title, link.desc, () => navigateTo(link.route, link.query)),
|
||||
),
|
||||
);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { createWorkflowOverlays } from "@ui/ui_template_overlay";
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import { mountSettingsDrawer } from "../A00_Common/m_settings_nav";
|
||||
import { navigateTo } from "../A00_Common/router";
|
||||
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||
import { saveFiles, type FileInfo } from "./M01_MasterData_Api_Fetch";
|
||||
@@ -54,6 +55,7 @@ export async function renderM01MasterData(root: HTMLElement): Promise<void> {
|
||||
}
|
||||
root.innerHTML = "";
|
||||
root.append(buildPage());
|
||||
mountSettingsDrawer(user, "m01", null); // ☰ 시스템 설정 바로가기(334)
|
||||
}
|
||||
|
||||
function buildPage(): HTMLElement {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ROUTES } from "@config/config_frontend";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { createWorkflowOverlays } from "@ui/ui_template_overlay";
|
||||
import { mountSettingsDrawer } from "../A00_Common/m_settings_nav";
|
||||
import { navigateTo, type MountedPage } from "../A00_Common/router";
|
||||
import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch";
|
||||
import { fetchOwners, setActiveLayer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
@@ -238,6 +239,7 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<Mounte
|
||||
// 진입 해시 `open` — 옵션 · 도면 컨테이너는 마스터에만 ·
|
||||
// 옵션 화면이 뒤에 열리면 편집 칸 차례(`seq`)가 늦게 온 집계표를 버림
|
||||
if (entry.open !== "구조물조합") side.reveal(entry.open);
|
||||
mountSettingsDrawer(user ?? {}, entry.open, entry.company); // ☰ 시스템 설정 바로가기(334)
|
||||
void tree.refresh();
|
||||
// 이 화면을 떠나면 층 길을 시스템 · 자기 층으로 되돌림(다른 화면의 항목 고르기가 남의 층을 안 부르게)
|
||||
return { covers: () => false, show: () => {}, dispose: () => setActiveLayer("system") };
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""PLAN 334 — ☰ 바로가기(요구 225-1 · 225-2).
|
||||
|
||||
① Node — 진행 단계 「시스템 관리자 전용」 = 전처리 길 기준(칸 번호 아님) · 대시보드 둘째 칸은 안 꺼짐
|
||||
settingLinks 역할 셋 · 설정 ☰ 지금 칸(진입 인자)
|
||||
② 실제 브라우저(sub2 화면 서버 5177 · 꺼지면 건너뜀) — 대시보드 ☰ 이름 = 구역 제목 차례(역할 셋) ·
|
||||
「보안」 누름 → 서랍 닫힘 · 보안 위 끝 ≈ ☰ 자리(52)
|
||||
「보안」 누름 → 서랍 닫힘 · 보안 위 끝 ≈ ☰ 자리(52) · M01 ☰ 다섯 · 강조 · 「도면 양식」 → open=도면 ·
|
||||
M02 → M02 이동은 떠나기 물음 먼저(가드 자리에 물음 창을 심어 확인 · 취소 = 머묾)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -19,6 +21,7 @@ import pytest
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BASE = os.environ.get("AISLO_UI_BASE", "http://localhost:5177") # sub2 화면 서버(OWNERS 자기 줄)
|
||||
LAYOUT = ROOT / "ui_template" / "ui_template_workflow_layout.ts"
|
||||
NAV = ROOT / "A00_Common" / "m_settings_nav.ts"
|
||||
STEPS = ["파일입력", "전처리", "종단설계"]
|
||||
WF_ROUTES = ["b03-file-input", "b04-preprocess", "b05-profile"]
|
||||
|
||||
@@ -81,6 +84,58 @@ def test_전처리_잠금은_길_기준():
|
||||
assert _step_bar("USER", titles) == [False, False, False]
|
||||
|
||||
|
||||
def _settings(user: dict, active: str, company) -> dict:
|
||||
"""settingLinks 목록 + ☰ 에 맡긴 단계 막대(이름 · 지금 칸 · 아이콘 · 🏠)"""
|
||||
src = NAV.read_text(encoding="utf-8")
|
||||
icons = re.search(r"^const ICONS.*$", src, re.M).group(0)
|
||||
script = (
|
||||
"const ROUTES = { M01_MASTER_DATA: 'm01-master-data',"
|
||||
" M02_MASTER_TEMPLATE: 'm02-master-template' };\n"
|
||||
"const L = (k: string) => k;\nlet bar: any = null;\n"
|
||||
"const createStepBar = (titles: string[], active: number, o: any) =>"
|
||||
" ({ titles, active, icons: o.icons, home: o.homeButton, routes: o.routes });\n"
|
||||
"const setDrawerSteps = (b: any) => { bar = b; };\nconst go = () => {};\n"
|
||||
+ icons
|
||||
+ "\n"
|
||||
+ _fn(src, "settingLinks")
|
||||
+ "\n"
|
||||
+ _fn(src, "mountSettingsDrawer")
|
||||
+ "\nconst [user, active, company] = JSON.parse(process.argv[1]);"
|
||||
"\nmountSettingsDrawer(user, active, company);"
|
||||
"\nconsole.log(JSON.stringify({ links: settingLinks(user, company).map((l) =>"
|
||||
" [l.key, l.route, l.query ?? null]), bar }));"
|
||||
)
|
||||
return _node(script, json.dumps([user, active, company]))
|
||||
|
||||
|
||||
M02 = "m02-master-template"
|
||||
|
||||
|
||||
def test_설정_바로가기_역할_셋_지금_칸():
|
||||
admin = _settings({"role": "SYSTEM_ADMIN", "company_id": 1}, "옵션", "3")
|
||||
assert admin["links"] == [
|
||||
["m01", "m01-master-data", None],
|
||||
["구조물조합", M02, {"open": "구조물조합", "root": "personal", "company": "3"}],
|
||||
["옵션", M02, {"open": "옵션", "root": "master", "company": "3"}],
|
||||
["도면", M02, {"open": "도면", "root": "master", "company": "3"}],
|
||||
]
|
||||
bar = admin["bar"]
|
||||
assert bar["active"] == 2 and bar["home"] is True
|
||||
assert bar["icons"] == ["🗄️", "🧱", "🎛️", "📑"]
|
||||
assert _settings({"role": "SYSTEM_ADMIN"}, "m01", None)["bar"]["active"] == 0
|
||||
# 일위대가조합 = 강조 없음
|
||||
assert _settings({"role": "SYSTEM_ADMIN"}, "일위대가조합", None)["bar"]["active"] == -1
|
||||
# 그 밖 역할 = 🏠 + 구조물 조합 하나(회사 인자 안 실음)
|
||||
company = _settings({"role": "ADMIN", "company_id": 1}, "구조물조합", "3")
|
||||
assert company["links"] == [["구조물조합", M02, {"open": "구조물조합", "root": "personal"}]]
|
||||
assert company["bar"]["active"] == 0
|
||||
assert _settings({"role": "USER", "is_master": True}, "구조물조합", None)["links"][0][2] == {
|
||||
"open": "구조물조합",
|
||||
"root": "company",
|
||||
}
|
||||
assert _settings({"role": "USER"}, "구조물조합", None)["links"][0][2]["root"] == "personal"
|
||||
|
||||
|
||||
# ── 실제 브라우저 ─────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -157,3 +212,61 @@ def test_대시보드_구역_바로가기(who, count):
|
||||
pass
|
||||
assert abs(page.evaluate(top) - 52) <= 2, page.evaluate(top)
|
||||
browser.close()
|
||||
|
||||
|
||||
ACTIVE = "document.querySelector('.app-drawer__steps .is-active .ui-workflow-layout__step-label')?.textContent"
|
||||
HASH = "decodeURIComponent(location.hash)"
|
||||
|
||||
|
||||
def _drawer_click(page, label: str):
|
||||
_open_drawer(page)
|
||||
page.click(f".app-drawer__steps .ui-workflow-layout__step:has-text('{label}')")
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _up(), reason="sub2 화면 서버(5177) 꺼짐")
|
||||
def test_설정_4_페이지_바로가기():
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(channel="msedge", headless=True)
|
||||
page = _page(browser, "시스템 관리자", "m01-master-data")
|
||||
page.wait_for_selector(".m01-master", timeout=60000)
|
||||
_open_drawer(page)
|
||||
assert page.evaluate(DRAWER_LABELS) == [
|
||||
"대시보드",
|
||||
"마스터 데이터",
|
||||
"구조물 조합",
|
||||
"구조물 옵션",
|
||||
"도면 양식",
|
||||
]
|
||||
assert page.evaluate(ACTIVE) == "마스터 데이터"
|
||||
page.click(".app-drawer__steps .ui-workflow-layout__step:has-text('도면 양식')")
|
||||
page.wait_for_function("decodeURIComponent(location.hash).includes('open=도면')")
|
||||
assert M02 in page.evaluate(HASH)
|
||||
page.wait_for_selector(".m02-master", timeout=60000)
|
||||
page.wait_for_load_state("networkidle")
|
||||
_open_drawer(page)
|
||||
assert page.evaluate(ACTIVE) == "도면 양식"
|
||||
page.click(".app-burger") # 닫음
|
||||
|
||||
# M02 → M02 — 저장 안 한 고침 대신 가드 자리에 앱의 물음 창을 심음
|
||||
page.evaluate(
|
||||
"""async () => {
|
||||
const url = (p) => performance.getEntriesByType('resource')
|
||||
.map((e) => e.name).find((n) => n.includes(p));
|
||||
const router = await import(url('/router.ts'));
|
||||
const ui = await import(url('/ui_template_elements.ts'));
|
||||
router.setLeaveGuard(() => ui.showConfirmDialog('저장 안 한 고침'));
|
||||
}"""
|
||||
)
|
||||
before = page.evaluate(HASH)
|
||||
_drawer_click(page, "구조물 옵션")
|
||||
page.wait_for_selector(".ui-confirm")
|
||||
page.click(".ui-confirm__actions button >> nth=0") # 취소 = 머묾
|
||||
page.wait_for_timeout(300)
|
||||
assert page.evaluate(HASH) == before
|
||||
_drawer_click(page, "구조물 옵션")
|
||||
page.wait_for_selector(".ui-confirm")
|
||||
page.click(".ui-confirm__actions button >> nth=1") # 버림 = 이동
|
||||
page.wait_for_function("decodeURIComponent(location.hash).includes('open=옵션')")
|
||||
browser.close()
|
||||
|
||||
@@ -17,6 +17,7 @@ import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
PAGE = ROOT / "B01_Dashboard" / "B01_Dashboard_UI_Page.ts"
|
||||
NAV = ROOT / "A00_Common" / "m_settings_nav.ts" # 카드 규칙 한 벌(334)
|
||||
|
||||
|
||||
def _text(path: Path) -> str:
|
||||
@@ -24,9 +25,9 @@ def _text(path: Path) -> str:
|
||||
|
||||
|
||||
def _fn(src: str, name: str) -> str:
|
||||
found = re.search(rf"^function {name}\([\s\S]*?^}}", src, re.M)
|
||||
found = re.search(rf"^(?:export )?function {name}\([\s\S]*?^}}", src, re.M)
|
||||
assert found, f"{name} 를 못 찾음"
|
||||
return found.group(0)
|
||||
return found.group(0).removeprefix("export ")
|
||||
|
||||
|
||||
def _cards(user: dict, company_id: int | None = None) -> list:
|
||||
@@ -46,6 +47,8 @@ def _cards(user: dict, company_id: int | None = None) -> list:
|
||||
"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]));"
|
||||
|
||||
Reference in New Issue
Block a user