Files
Aislo/B01_Dashboard/B01_Dashboard_UI_Page.ts
T
eomsangdonandClaude Sonnet 5.5 d69686a0a8 feat(B01): 대시보드 좌측 패널 — 권한별 항목 목록 · 고른 항목 컨테이너만 우측에 · 고른 항목 해시 유지 (99-2)
- 공용 좌측 패널(createWorkflowOverlays · ui-sidebar-list) · 항목 = 권한별 기존 컨테이너 그대로(내 정보 = 기본정보 + 보안)
- 고른 항목 해시 ?tab= 에 남김(hashchange 없이) · 저장 뒤 다시 그려도 유지 · 좁은 화면은 고르면 접음
- 시험 test_b01_side_panel 신규 · test_b01_settings_cards 고침

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
2026-10-08 15:03:22 +09:00

399 lines
15 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 { createWorkflowOverlays } from "@ui/ui_template_overlay";
import { 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 {
buildSidePanel,
pickActiveKey,
rememberActiveKey,
type PanelItem,
} from "./B01_Dashboard_UI_SidePanel";
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;
}
/** 시스템 설정 — 역할로 카드 숨김 · M02 카드는 `?open=&root=` 로 진입(27-1d) */
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 }),
);
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"), "구조물조합", "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"), "구조물조합", root, desc));
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;
}
/** 좌측 패널 항목 — 권한별 · 항목 하나 = 지금 컨테이너 그대로(누를 때 만듦) */
function panelItems(state: DashboardState): PanelItem[] {
const one = (key: string, label: string, make: () => HTMLElement): PanelItem => ({
key,
label,
build: () => [make()],
});
const projectsAdd = [
createButton({ label: "+", onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER) }),
];
const myInfo: PanelItem = {
key: "profile",
label: L("B01_Panel_MyInfo"),
build: () => [
section(L("B01_Dashboard_Profile"), buildProfileForm(state.user)),
section(L("B01_Account_Section_Security"), buildSecurityForm()),
],
};
const settings = one("settings", L("B01_Dashboard_SystemSettings"), () =>
buildSystemSettingsSection(state),
);
const temp = one("temp", L("B01_Temp_Section"), () => buildTempUploadSection());
if (state.user.role === "SYSTEM_ADMIN") {
// 회사 알약이 붙은 컨테이너는 고른 회사 것
const picked = state.allCompanies.filter((c) => c.id === state.adminCompanyId);
return [
one("resources", L("B01_Dashboard_Resources"), () =>
section(L("B01_Dashboard_Resources"), buildResourcePanel(state.resources), true),
),
settings,
one("projects", L("B01_Dashboard_Projects"), () =>
withCompanyPill(
section(
L("B01_Dashboard_Projects"),
projectTable(state.allProjects, state.user),
true,
projectsAdd,
),
state,
),
),
temp,
one("users", L("B01_Dashboard_Users"), () =>
withCompanyPill(
section(L("B01_Dashboard_Users"), userTable(state.allUsers, state.user), true, [
createButton({
label: "+",
onClick: () => openAddMemberModal(undefined, state.adminCompanyId),
}),
]),
state,
),
),
one("requests", L("B01_Dashboard_JoinRequests"), () =>
withCompanyPill(
section(
L("B01_Dashboard_JoinRequests"),
joinRequestTable(state.allJoinRequests, true),
true,
),
state,
),
),
one("companies", L("B01_Dashboard_Companies"), () =>
withCompanyPill(
section(L("B01_Dashboard_Companies"), companyTable(picked, state.user), true, [
createButton({ label: "+", onClick: () => openCreateCompanyModal() }),
]),
state,
),
),
myInfo,
one("logs", L("B01_Dashboard_AuditLogs"), () =>
section(L("B01_Dashboard_AuditLogs"), auditLogTable(state.auditLogs), true),
),
];
}
const company = one("company", L("B01_Dashboard_Company"), () =>
section(L("B01_Dashboard_Company"), buildCompanyPanel(state), true),
);
if (state.user.role === "ADMIN") {
return [
settings,
one("projects", L("B01_Dashboard_Projects"), () =>
section(
L("B01_Dashboard_Projects"),
projectTable(state.companyProjects, state.user),
true,
projectsAdd,
),
),
temp,
one("members", L("B01_Dashboard_Members"), () =>
section(L("B01_Dashboard_Members"), memberTable(state.members, state.user), true, [
createButton({ label: "+", onClick: () => openAddMemberModal() }),
]),
),
one("requests", L("B01_Dashboard_JoinRequests"), () =>
section(L("B01_Dashboard_JoinRequests"), joinRequestTable(state.joinRequests, false), true),
),
company,
myInfo,
];
}
return [
settings,
one("projects", L("B01_Dashboard_Projects"), () =>
section(
L("B01_Dashboard_Projects"),
projectTable(state.userProjects, state.user),
true,
projectsAdd,
),
),
temp,
company,
myInfo,
];
}
function buildPage(state: DashboardState): HTMLElement {
const items = panelItems(state);
const detail = document.createElement("div");
detail.className = "b01-dashboard__grid";
// 제목·여백은 공용 템플릿을 따른다 (2026-09-06 사용자 지시) — 본문은 고른 항목 하나.
const page = document.createElement("div");
page.append(detail, 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);
// B 그룹 공용 좌측 패널(M01 · B03 과 같은 오버레이) — 열리면 본문을 밀어냄
const panel = buildSidePanel(items, pickActiveKey(items), (key) => show(key, true));
const overlays = createWorkflowOverlays({
title: L("B01_Panel_Title"),
optionsContent: panel.root,
showProjectName: false,
onOptionsOpenChange: (isOpen) => layout.root.classList.toggle("is-options-open", isOpen),
});
layout.root.append(overlays.root);
function show(key: string, picked: boolean): void {
detail.replaceChildren(...(items.find((item) => item.key === key) ?? items[0]).build());
panel.setActive(key);
rememberActiveKey(key);
// 좁은 화면(패널이 본문을 덮음)에서는 고르면 접음
if (picked && window.matchMedia("(max-width: 860px)").matches) overlays.setTitleOpen(false);
}
show(pickActiveKey(items), false);
return layout.root;
}