This commit is contained in:
2026-07-08 19:59:25 +09:00
parent 8ffa9b097a
commit 38b5c2492e
6 changed files with 100 additions and 67 deletions
+98 -65
View File
@@ -133,42 +133,49 @@ function buildPage(state: DashboardState): HTMLElement {
grid.className = "b01-dashboard__grid"; grid.className = "b01-dashboard__grid";
if (state.user.role === "SYSTEM_ADMIN") { if (state.user.role === "SYSTEM_ADMIN") {
// 순서: 리소스 현황(최상단), 프로젝트, 사용자 관리(사용자), 가입요청(1행 전체), 회사관리(회사목록), 시스템 로그(감사로그), 기본정보(프로필), 보안
grid.append( grid.append(
section(L("B01_Dashboard_Resources"), buildResourcePanel(state.resources), true), section(L("B01_Dashboard_Resources"), buildResourcePanel(state.resources), true),
section(L("B01_Dashboard_Companies"), companyTable(state.allCompanies), true),
section(L("B01_Dashboard_Users"), userTable(state.allUsers), true),
section(L("B01_Dashboard_Projects"), projectTable(state.allProjects), true), section(L("B01_Dashboard_Projects"), projectTable(state.allProjects), true),
section(L("B01_Dashboard_JoinRequests"), joinRequestTable(state.allJoinRequests, true)), section(L("B01_Dashboard_Users"), userTable(state.allUsers), true),
section(L("B01_Dashboard_JoinRequests"), joinRequestTable(state.allJoinRequests, true), true),
section(L("B01_Dashboard_Companies"), companyTable(state.allCompanies), true),
section(L("B01_Dashboard_AuditLogs"), auditLogTable(state.auditLogs), true), section(L("B01_Dashboard_AuditLogs"), auditLogTable(state.auditLogs), true),
); );
} else { } else if (state.user.role === "ADMIN") {
// 순서: 프로젝트, 사용자 관리(멤버), 가입요청(1행 전체), 회사관리(회사패널), 기본정보(프로필), 보안
grid.append( grid.append(
section( section(L("B01_Dashboard_Projects"), projectTable(state.companyProjects), true, [
L("B01_Dashboard_Projects"), createButton({
projectTable(state.user.role === "ADMIN" ? state.companyProjects : state.userProjects), label: L("B01_Dashboard_NewProject"),
true, onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER),
[ }),
createButton({ ]),
label: L("B01_Dashboard_NewProject"), section(L("B01_Dashboard_Members"), memberTable(state.members), true, [
onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER), createButton({
}), label: L("B01_Dashboard_AddMember"),
], variant: "ghost",
), onClick: () => openAddMemberModal(),
section(L("B01_Dashboard_Company"), buildCompanyPanel(state)), }),
]),
section(L("B01_Dashboard_JoinRequests"), joinRequestTable(state.joinRequests, false), true),
section(L("B01_Dashboard_Company"), buildCompanyPanel(state), true),
);
} else {
// 일반 사용자 (USER)
// 순서: 프로젝트, 회사관리(회사패널), 기본정보(프로필), 보안
grid.append(
section(L("B01_Dashboard_Projects"), projectTable(state.userProjects), true, [
createButton({
label: L("B01_Dashboard_NewProject"),
onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER),
}),
]),
section(L("B01_Dashboard_Company"), buildCompanyPanel(state), true),
); );
if (state.user.role === "ADMIN") {
grid.append(
section(L("B01_Dashboard_Members"), memberTable(state.members), false, [
createButton({
label: L("B01_Dashboard_AddMember"),
variant: "ghost",
onClick: () => openAddMemberModal(),
}),
]),
section(L("B01_Dashboard_JoinRequests"), joinRequestTable(state.joinRequests, false)),
);
}
} }
// 기본정보 (프로필) & 보안 순서
grid.append( grid.append(
section(L("B01_Dashboard_Profile"), buildProfileForm(state.user)), section(L("B01_Dashboard_Profile"), buildProfileForm(state.user)),
section(L("B01_Account_Section_Security"), buildSecurityForm()), section(L("B01_Account_Section_Security"), buildSecurityForm()),
@@ -432,12 +439,25 @@ function formatChartTime(iso: string | null | undefined): string {
} }
function buildResourceChart(resources: ResourceData | null): HTMLElement { function buildResourceChart(resources: ResourceData | null): HTMLElement {
const box = document.createElement("div");
box.className = "b01-dashboard__chart";
const caption = document.createElement("p");
caption.className = "b01-dashboard__chart-caption";
caption.textContent = L("B01_Dashboard_Resources_ChartCaption");
box.append(caption);
const chartWrap = document.createElement("div");
chartWrap.className = "b01-dashboard__chart-content";
box.append(chartWrap);
const history = resources?.history ?? []; const history = resources?.history ?? [];
// 7일간의 고정 타임라인 생성 (4시간 간격, 총 43개 포인트) // 7일간의 고정 타임라인 생성 (4시간 간격, 총 43개 포인트)
const points: { timestamp: Date; cpu: number | null; mem: number | null; disk: number | null }[] = []; const points: { timestamp: Date; cpu: number | null; mem: number | null; disk: number | null }[] =
[];
const now = new Date(); const now = new Date();
// 12시간 단위(정오 또는 자정)로 올림 정렬하여 기준점 계산 // 12시간 단위(정오 또는 자정)로 올림 정렬하여 기준점 계산
const baseDate = new Date( const baseDate = new Date(
now.getFullYear(), now.getFullYear(),
@@ -446,26 +466,26 @@ function buildResourceChart(resources: ResourceData | null): HTMLElement {
now.getHours() >= 12 ? 24 : 12, now.getHours() >= 12 ? 24 : 12,
0, 0,
0, 0,
0 0,
); );
for (let i = 0; i <= 42; i++) { for (let i = 0; i <= 42; i++) {
// 7일 전부터 현재 정렬 시점까지 4시간 단위 버킷 생성 // 7일 전부터 현재 정렬 시점까지 4시간 단위 버킷 생성
const time = new Date(baseDate.getTime() - (42 - i) * 4 * 60 * 60 * 1000); const time = new Date(baseDate.getTime() - (42 - i) * 4 * 60 * 60 * 1000);
points.push({ timestamp: time, cpu: null, mem: null, disk: null }); points.push({ timestamp: time, cpu: null, mem: null, disk: null });
} }
// DB에서 불러온 계측 이력 매핑 // DB에서 불러온 계측 이력 매핑
history.forEach((h) => { history.forEach((h) => {
if (!h.timestamp) return; if (!h.timestamp) return;
// DB에서 넘어오는 ISO 문자열을 UTC 기준으로 정확히 매핑하기 위해 Z를 보완하여 파싱 // DB에서 넘어오는 ISO 문자열을 UTC 기준으로 정확히 매핑하기 위해 Z를 보완하여 파싱
const tsStr = h.timestamp.endsWith("Z") ? h.timestamp : h.timestamp + "Z"; const tsStr = h.timestamp.endsWith("Z") ? h.timestamp : h.timestamp + "Z";
const hTime = new Date(tsStr); const hTime = new Date(tsStr);
// 가장 가까운 4시간 단위 버킷 매핑 (±2시간 내) // 가장 가까운 4시간 단위 버킷 매핑 (±2시간 내)
let closestIdx = -1; let closestIdx = -1;
let minDiff = Infinity; let minDiff = Infinity;
points.forEach((p, idx) => { points.forEach((p, idx) => {
const diff = Math.abs(p.timestamp.getTime() - hTime.getTime()); const diff = Math.abs(p.timestamp.getTime() - hTime.getTime());
if (diff < minDiff && diff <= 2 * 60 * 60 * 1000) { if (diff < minDiff && diff <= 2 * 60 * 60 * 1000) {
@@ -473,7 +493,7 @@ function buildResourceChart(resources: ResourceData | null): HTMLElement {
closestIdx = idx; closestIdx = idx;
} }
}); });
if (closestIdx !== -1) { if (closestIdx !== -1) {
const p = points[closestIdx]; const p = points[closestIdx];
p.cpu = h.cpu_usage_percent; p.cpu = h.cpu_usage_percent;
@@ -491,35 +511,48 @@ function buildResourceChart(resources: ResourceData | null): HTMLElement {
return ""; return "";
}); });
const chart = createLineChart({ let resizeTimeout: number | null = null;
ariaLabel: L("B01_Dashboard_Resources_ChartAria"), const observer = new ResizeObserver((entries) => {
xLabels, for (const entry of entries) {
width: 800, const width = Math.floor(entry.contentRect.width);
height: 140, if (width <= 0) continue;
series: [
{ if (resizeTimeout) {
name: L("B01_Dashboard_Metric_Cpu"), window.cancelAnimationFrame(resizeTimeout);
colorIndex: 0, }
values: points.map((p) => p.cpu),
}, resizeTimeout = window.requestAnimationFrame(() => {
{ chartWrap.innerHTML = "";
name: L("B01_Dashboard_Metric_Memory"), const chart = createLineChart({
colorIndex: 1, ariaLabel: L("B01_Dashboard_Resources_ChartAria"),
values: points.map((p) => p.mem), xLabels,
}, width: width,
{ height: 140,
name: L("B01_Dashboard_Metric_Disk"), series: [
colorIndex: 2, {
values: points.map((p) => p.disk), name: L("B01_Dashboard_Metric_Cpu"),
}, colorIndex: 0,
], values: points.map((p) => p.cpu),
},
{
name: L("B01_Dashboard_Metric_Memory"),
colorIndex: 1,
values: points.map((p) => p.mem),
},
{
name: L("B01_Dashboard_Metric_Disk"),
colorIndex: 2,
values: points.map((p) => p.disk),
},
],
});
chartWrap.append(chart);
});
}
}); });
const caption = document.createElement("p");
caption.className = "b01-dashboard__chart-caption"; observer.observe(box);
caption.textContent = L("B01_Dashboard_Resources_ChartCaption");
const box = document.createElement("div");
box.className = "b01-dashboard__chart";
box.append(caption, chart);
return box; return box;
} }
+2 -2
View File
@@ -699,11 +699,11 @@ const BASE_CSS = `
} }
.ui-chart__tick { .ui-chart__tick {
fill: var(--color-text-muted); fill: var(--color-text-muted);
font-size: 10px; font-size: 12px;
font-family: var(--font-body); font-family: var(--font-body);
} }
.ui-chart__tick--x { .ui-chart__tick--x {
font-size: 9px; font-size: 11px;
} }
.ui-chart__line { .ui-chart__line {
fill: none; fill: none;