- 새 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
341 lines
13 KiB
TypeScript
341 lines
13 KiB
TypeScript
import { ROUTES } from "@config/config_frontend";
|
|
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
|
import {
|
|
createButton,
|
|
createTag,
|
|
hideLoadingOverlay,
|
|
showLoadingOverlay,
|
|
showToast,
|
|
} from "@ui/ui_template_elements";
|
|
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,
|
|
fetchAllUsers,
|
|
fetchAuditLogs,
|
|
fetchCompanyJoinRequests,
|
|
fetchCompanyMembers,
|
|
fetchCompanyProjects,
|
|
fetchDashboardMe,
|
|
fetchSystemResources,
|
|
fetchUserCompany,
|
|
fetchUserProjects,
|
|
type AuditLog,
|
|
type CompanyInfo,
|
|
type DashboardUser,
|
|
type JoinRequest,
|
|
type Member,
|
|
type ProjectItem,
|
|
type ResourceData,
|
|
} from "./B01_Dashboard_Api_Fetch";
|
|
import { auditLogTable, userTable } from "./B01_Dashboard_UI_Admin";
|
|
import { L, roleLabel } from "./B01_Dashboard_UI_Common";
|
|
import {
|
|
buildCompanyPanel,
|
|
companyTable,
|
|
joinRequestTable,
|
|
memberTable,
|
|
} from "./B01_Dashboard_UI_Company";
|
|
import { openAddMemberModal, openCreateCompanyModal } from "./B01_Dashboard_UI_Modals";
|
|
import { buildProfileForm, buildSecurityForm } from "./B01_Dashboard_UI_Profile";
|
|
import { openCompanyPicker } from "./B01_Dashboard_UI_CompanyPick";
|
|
import { projectTable } from "./B01_Dashboard_UI_Projects";
|
|
import { buildResourcePanel } from "./B01_Dashboard_UI_Resources";
|
|
import { buildTempUploadSection } from "./B01_Dashboard_UI_TempUpload";
|
|
import { buildKnowledgeChat } from "./B01_Dashboard_UI_Knowledge_Chat";
|
|
import "./B01_Dashboard_UI_Style.css";
|
|
|
|
export interface DashboardState {
|
|
user: DashboardUser;
|
|
userProjects: ProjectItem[];
|
|
companyProjects: ProjectItem[];
|
|
allProjects: ProjectItem[];
|
|
company: CompanyInfo | null;
|
|
members: Member[];
|
|
joinRequests: JoinRequest[];
|
|
allCompanies: CompanyInfo[];
|
|
allUsers: DashboardUser[];
|
|
allJoinRequests: JoinRequest[];
|
|
auditLogs: AuditLog[];
|
|
resources: ResourceData | null;
|
|
/** 시스템 관리자가 고른 회사(29-1) — 프로젝트 · 사용자 · 가입 요청 · 회사 관리가 이 회사 것 */
|
|
adminCompanyId: number | null;
|
|
}
|
|
|
|
/** 고른 회사 id 자리 — M02 도 같은 키를 읽음(이름 바꾸지 않음) */
|
|
const ADMIN_COMPANY_KEY = "aislo.adminCompany";
|
|
|
|
/** 고른 회사 — 저장값이 없거나 없는 회사면 자기 회사 */
|
|
function pickAdminCompany(user: DashboardUser, companies: CompanyInfo[]): number | null {
|
|
const saved = Number(sessionStorage.getItem(ADMIN_COMPANY_KEY));
|
|
if (companies.some((c) => c.id === saved)) return saved;
|
|
return user.company_id ?? companies[0]?.id ?? null;
|
|
}
|
|
|
|
export async function renderB01Dashboard(root: HTMLElement): Promise<void> {
|
|
showLoadingOverlay();
|
|
try {
|
|
const user = await fetchDashboardMe();
|
|
const state: DashboardState = {
|
|
user,
|
|
userProjects: [],
|
|
companyProjects: [],
|
|
allProjects: [],
|
|
company: null,
|
|
members: [],
|
|
joinRequests: [],
|
|
allCompanies: [],
|
|
allUsers: [],
|
|
allJoinRequests: [],
|
|
auditLogs: [],
|
|
resources: null,
|
|
adminCompanyId: null,
|
|
};
|
|
await loadRoleData(state);
|
|
root.innerHTML = "";
|
|
root.append(buildPage(state));
|
|
} catch (error) {
|
|
showToast(error instanceof Error ? error.message : L("B01_Dashboard_LoadFailed"), "error");
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
}
|
|
|
|
async function loadRoleData(state: DashboardState): Promise<void> {
|
|
if (state.user.role === "SYSTEM_ADMIN") {
|
|
// 회사 목록을 먼저 — 고른 회사를 가린 뒤 그 회사 것만 읽음(29-1)
|
|
state.allCompanies = await fetchAllCompanies().catch(() => []);
|
|
const companyId = pickAdminCompany(state.user, state.allCompanies);
|
|
state.adminCompanyId = companyId;
|
|
const results = await Promise.allSettled([
|
|
fetchAllUsers(companyId),
|
|
fetchCompanyJoinRequests(companyId),
|
|
fetchAuditLogs(),
|
|
fetchSystemResources(7),
|
|
fetchCompanyProjects(companyId),
|
|
]);
|
|
state.allUsers = results[0].status === "fulfilled" ? results[0].value : [];
|
|
state.allJoinRequests = results[1].status === "fulfilled" ? results[1].value : [];
|
|
state.auditLogs = results[2].status === "fulfilled" ? results[2].value : [];
|
|
state.resources = results[3].status === "fulfilled" ? results[3].value : null;
|
|
state.allProjects = results[4].status === "fulfilled" ? results[4].value : [];
|
|
|
|
results.forEach((res, idx) => {
|
|
if (res.status === "rejected") {
|
|
console.error(`System Admin API failed at index ${idx}:`, res.reason);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
const [projects, company] = await Promise.all([
|
|
fetchUserProjects().catch(() => []),
|
|
fetchUserCompany().catch(() => null),
|
|
]);
|
|
state.userProjects = projects;
|
|
state.company = company;
|
|
if (state.user.role === "ADMIN") {
|
|
const [members, requests, companyProjects] = await Promise.all([
|
|
fetchCompanyMembers().catch(() => []),
|
|
fetchCompanyJoinRequests().catch(() => []),
|
|
fetchCompanyProjects().catch(() => []),
|
|
]);
|
|
state.members = members;
|
|
state.joinRequests = requests;
|
|
state.companyProjects = companyProjects;
|
|
}
|
|
}
|
|
|
|
/** 설정 카드 공용 양식 — 제목 + 한 줄 설명 · 누르면 이동 */
|
|
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;
|
|
}
|
|
|
|
/** 시스템 설정 — 카드 규칙은 `settingLinks` 한 벌(☰ 바로가기와 같이 씀 · 334) */
|
|
function buildSystemSettingsPanel(user: DashboardUser, companyId: number | null): HTMLElement {
|
|
const wrap = document.createElement("div");
|
|
wrap.className = "b01-dashboard__setting-grid";
|
|
wrap.append(
|
|
...settingLinks(user, companyId).map((link) =>
|
|
settingCard(link.title, link.desc, () => navigateTo(link.route, link.query)),
|
|
),
|
|
);
|
|
return wrap;
|
|
}
|
|
|
|
/** 회사 알약(시스템 관리자만) — 컨테이너 제목 바로 옆 · 누르면 조회 후 고르기 · 고르면 저장 뒤 다시 읽음 */
|
|
function withCompanyPill(card: HTMLElement, state: DashboardState): HTMLElement {
|
|
const name = state.allCompanies.find((c) => c.id === state.adminCompanyId)?.name ?? "-";
|
|
const pill = document.createElement("button");
|
|
pill.type = "button";
|
|
pill.className = "ui-tag ui-tag--accent b01-dashboard__company-pill";
|
|
pill.title = L("B01_Dashboard_PickCompany");
|
|
pill.textContent = `${name} ▾`;
|
|
pill.addEventListener("click", (event) => {
|
|
// 제목 줄 누름 = 접힘 — 알약 누름은 올려 보내지 않음
|
|
event.stopPropagation();
|
|
openCompanyPicker(state.allCompanies, state.adminCompanyId, (company) => {
|
|
sessionStorage.setItem(ADMIN_COMPANY_KEY, String(company.id));
|
|
window.dispatchEvent(new HashChangeEvent("hashchange"));
|
|
});
|
|
});
|
|
card.querySelector(".b01-dashboard__section-header > .ui-card__title")?.after(pill);
|
|
return card;
|
|
}
|
|
|
|
/** 시스템 설정 컨테이너 — 모든 역할 · 한 줄 통째 · 제목 눌러 통째로 접힘 */
|
|
function buildSystemSettingsSection(state: DashboardState): HTMLElement {
|
|
const { user } = state;
|
|
const panel = buildSystemSettingsPanel(user, state.adminCompanyId);
|
|
const card = section(L("B01_Dashboard_SystemSettings"), panel, true);
|
|
if (user.role === "SYSTEM_ADMIN") withCompanyPill(card, state);
|
|
card.querySelector(".ui-card__body")?.classList.add("ui-collapsible");
|
|
card.querySelector(".b01-dashboard__section-header")?.classList.add("ui-collapsible__title");
|
|
attachCollapsible(card);
|
|
return card;
|
|
}
|
|
|
|
/** ☰ 구역 바로가기 아이콘(334) — 구역 제목 키로 짝(EN 글에서도 같음) */
|
|
const SECTION_ICONS: [Parameters<typeof L>[0], string][] = [
|
|
["B01_Dashboard_Resources", "📊"],
|
|
["B01_Dashboard_SystemSettings", "⚙️"],
|
|
["B01_Dashboard_Projects", "🗂️"],
|
|
["B01_Temp_Section", "📦"],
|
|
["B01_Dashboard_Users", "👥"],
|
|
["B01_Dashboard_Members", "👥"],
|
|
["B01_Dashboard_JoinRequests", "📨"],
|
|
["B01_Dashboard_Companies", "🏢"],
|
|
["B01_Dashboard_Company", "🏢"],
|
|
["B01_Dashboard_Profile", "👤"],
|
|
["B01_Account_Section_Security", "🔒"],
|
|
["B01_Dashboard_AuditLogs", "📜"],
|
|
];
|
|
|
|
/** ☰ 구역 바로가기(334) — 그린 구역 그대로 · 화면 차례 · 누름 = 서랍 닫힘 + 그 구역으로 굴림 */
|
|
function mountSectionShortcuts(grid: HTMLElement): void {
|
|
// 자료 기다리는 사이 다른 쪽으로 떠났으면 맡기지 않음
|
|
if (currentRoute() !== ROUTES.B01_ACCOUNT) return;
|
|
const sections = [...grid.children] as HTMLElement[];
|
|
const titles = sections.map((s) => s.querySelector(".ui-card__title")?.textContent ?? "");
|
|
const icons = titles.map((title) => SECTION_ICONS.find(([key]) => L(key) === title)?.[1] ?? "");
|
|
const bar = createStepBar(titles, -1, {
|
|
orientation: "vertical",
|
|
icons,
|
|
routes: titles,
|
|
onStepClick: (index) => sections[index].scrollIntoView({ behavior: "smooth", block: "start" }),
|
|
});
|
|
setDrawerSteps(bar);
|
|
}
|
|
|
|
function buildPage(state: DashboardState): HTMLElement {
|
|
// 제목·여백은 공용 템플릿을 따른다 (2026-09-06 사용자 지시) — B02 등 다른 화면과 같은 모양.
|
|
const page = document.createElement("div");
|
|
|
|
const grid = document.createElement("div");
|
|
grid.className = "b01-dashboard__grid";
|
|
|
|
if (state.user.role === "SYSTEM_ADMIN") {
|
|
// 시스템 설정은 리소스 현황 아래(29-1) · 회사 알약이 붙은 컨테이너는 고른 회사 것
|
|
const picked = state.allCompanies.filter((c) => c.id === state.adminCompanyId);
|
|
grid.append(
|
|
section(L("B01_Dashboard_Resources"), buildResourcePanel(state.resources), true),
|
|
buildSystemSettingsSection(state),
|
|
withCompanyPill(
|
|
section(L("B01_Dashboard_Projects"), projectTable(state.allProjects, state.user), true, [
|
|
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
|
|
]),
|
|
state,
|
|
),
|
|
buildTempUploadSection(),
|
|
withCompanyPill(
|
|
section(L("B01_Dashboard_Users"), userTable(state.allUsers, state.user), true, [
|
|
createButton({
|
|
label: "+",
|
|
onClick: () => openAddMemberModal(undefined, state.adminCompanyId),
|
|
}),
|
|
]),
|
|
state,
|
|
),
|
|
withCompanyPill(
|
|
section(
|
|
L("B01_Dashboard_JoinRequests"),
|
|
joinRequestTable(state.allJoinRequests, true),
|
|
true,
|
|
),
|
|
state,
|
|
),
|
|
withCompanyPill(
|
|
section(L("B01_Dashboard_Companies"), companyTable(picked, state.user), true, [
|
|
createButton({ label: "+", onClick: () => openCreateCompanyModal() }),
|
|
]),
|
|
state,
|
|
),
|
|
);
|
|
} else if (state.user.role === "ADMIN") {
|
|
// 시스템 설정은 맨 위 한 줄(27-3a)
|
|
grid.append(
|
|
buildSystemSettingsSection(state),
|
|
section(L("B01_Dashboard_Projects"), projectTable(state.companyProjects, state.user), true, [
|
|
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
|
|
]),
|
|
buildTempUploadSection(),
|
|
section(L("B01_Dashboard_Members"), memberTable(state.members, state.user), true, [
|
|
createButton({ label: "+", onClick: () => openAddMemberModal() }),
|
|
]),
|
|
section(L("B01_Dashboard_JoinRequests"), joinRequestTable(state.joinRequests, false), true),
|
|
section(L("B01_Dashboard_Company"), buildCompanyPanel(state), true),
|
|
);
|
|
} else {
|
|
grid.append(
|
|
buildSystemSettingsSection(state),
|
|
section(L("B01_Dashboard_Projects"), projectTable(state.userProjects, state.user), true, [
|
|
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
|
|
]),
|
|
// 임시 보관함 — 프로젝트를 만들기 전에 자료를 올려 두는 곳이라 프로젝트 목록
|
|
// 바로 아래에 둔다(2026-08-08 사용자 지시).
|
|
buildTempUploadSection(),
|
|
section(L("B01_Dashboard_Company"), buildCompanyPanel(state), true),
|
|
);
|
|
}
|
|
|
|
grid.append(
|
|
section(L("B01_Dashboard_Profile"), buildProfileForm(state.user)),
|
|
section(L("B01_Account_Section_Security"), buildSecurityForm()),
|
|
);
|
|
// 시스템 로그는 맨 아래 (2026-09-06 사용자 지시).
|
|
if (state.user.role === "SYSTEM_ADMIN") {
|
|
grid.append(section(L("B01_Dashboard_AuditLogs"), auditLogTable(state.auditLogs), true));
|
|
}
|
|
page.append(grid);
|
|
mountSectionShortcuts(grid);
|
|
page.append(buildKnowledgeChat());
|
|
const layout = createGeneralLayout({
|
|
pageClass: "b01-dashboard",
|
|
title: L("B01_Dashboard_Title"),
|
|
subtitle: L("B01_Dashboard_Subtitle"),
|
|
content: page,
|
|
});
|
|
// 역할 배지는 제목 줄 오른쪽에 둔다(자리는 CSS 격자가 잡는다).
|
|
const tag = createTag(
|
|
roleLabel(state.user.role),
|
|
state.user.role === "SYSTEM_ADMIN" ? "accent" : "neutral",
|
|
);
|
|
tag.classList.add("b01-dashboard__role");
|
|
layout.root.querySelector(".ui-general-layout__header")?.append(tag);
|
|
return layout.root;
|
|
}
|