From 38b5c2492eee9af99a1c5962bcb4ad74eaa34b18 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 8 Jul 2026 19:59:25 +0900 Subject: [PATCH] 260707_3 --- .../code_20260708_b01_dashboard.md | 0 ...ompletion_report_20260708_b01_dashboard.md | 0 .../plan_register_split.md | 0 .../plan_register_split_validation.md | 0 B01_Dashboard/B01_Dashboard_UI_Page.ts | 163 +++++++++++------- ui_template/ui_template_elements.ts | 4 +- 6 files changed, 100 insertions(+), 67 deletions(-) rename .agent/{ => complete_and_old}/code_20260708_b01_dashboard.md (100%) rename .agent/{ => complete_and_old}/completion_report_20260708_b01_dashboard.md (100%) rename .agent/{ => complete_and_old}/plan_register_split.md (100%) rename .agent/{ => complete_and_old}/plan_register_split_validation.md (100%) diff --git a/.agent/code_20260708_b01_dashboard.md b/.agent/complete_and_old/code_20260708_b01_dashboard.md similarity index 100% rename from .agent/code_20260708_b01_dashboard.md rename to .agent/complete_and_old/code_20260708_b01_dashboard.md diff --git a/.agent/completion_report_20260708_b01_dashboard.md b/.agent/complete_and_old/completion_report_20260708_b01_dashboard.md similarity index 100% rename from .agent/completion_report_20260708_b01_dashboard.md rename to .agent/complete_and_old/completion_report_20260708_b01_dashboard.md diff --git a/.agent/plan_register_split.md b/.agent/complete_and_old/plan_register_split.md similarity index 100% rename from .agent/plan_register_split.md rename to .agent/complete_and_old/plan_register_split.md diff --git a/.agent/plan_register_split_validation.md b/.agent/complete_and_old/plan_register_split_validation.md similarity index 100% rename from .agent/plan_register_split_validation.md rename to .agent/complete_and_old/plan_register_split_validation.md diff --git a/B01_Dashboard/B01_Dashboard_UI_Page.ts b/B01_Dashboard/B01_Dashboard_UI_Page.ts index c82dbac..322d080 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Page.ts +++ b/B01_Dashboard/B01_Dashboard_UI_Page.ts @@ -133,42 +133,49 @@ function buildPage(state: DashboardState): HTMLElement { grid.className = "b01-dashboard__grid"; if (state.user.role === "SYSTEM_ADMIN") { + // 순서: 리소스 현황(최상단), 프로젝트, 사용자 관리(사용자), 가입요청(1행 전체), 회사관리(회사목록), 시스템 로그(감사로그), 기본정보(프로필), 보안 grid.append( 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_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), ); - } else { + } else if (state.user.role === "ADMIN") { + // 순서: 프로젝트, 사용자 관리(멤버), 가입요청(1행 전체), 회사관리(회사패널), 기본정보(프로필), 보안 grid.append( - section( - L("B01_Dashboard_Projects"), - projectTable(state.user.role === "ADMIN" ? state.companyProjects : state.userProjects), - true, - [ - createButton({ - label: L("B01_Dashboard_NewProject"), - onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER), - }), - ], - ), - section(L("B01_Dashboard_Company"), buildCompanyPanel(state)), + section(L("B01_Dashboard_Projects"), projectTable(state.companyProjects), true, [ + createButton({ + label: L("B01_Dashboard_NewProject"), + onClick: () => navigateTo(ROUTES.B02_PROJ_REGISTER), + }), + ]), + section(L("B01_Dashboard_Members"), memberTable(state.members), true, [ + createButton({ + label: L("B01_Dashboard_AddMember"), + variant: "ghost", + onClick: () => openAddMemberModal(), + }), + ]), + 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( section(L("B01_Dashboard_Profile"), buildProfileForm(state.user)), section(L("B01_Account_Section_Security"), buildSecurityForm()), @@ -432,12 +439,25 @@ function formatChartTime(iso: string | null | undefined): string { } 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 ?? []; - + // 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(); - + // 12시간 단위(정오 또는 자정)로 올림 정렬하여 기준점 계산 const baseDate = new Date( now.getFullYear(), @@ -446,26 +466,26 @@ function buildResourceChart(resources: ResourceData | null): HTMLElement { now.getHours() >= 12 ? 24 : 12, 0, 0, - 0 + 0, ); - + for (let i = 0; i <= 42; i++) { // 7일 전부터 현재 정렬 시점까지 4시간 단위 버킷 생성 const time = new Date(baseDate.getTime() - (42 - i) * 4 * 60 * 60 * 1000); points.push({ timestamp: time, cpu: null, mem: null, disk: null }); } - + // DB에서 불러온 계측 이력 매핑 history.forEach((h) => { if (!h.timestamp) return; // DB에서 넘어오는 ISO 문자열을 UTC 기준으로 정확히 매핑하기 위해 Z를 보완하여 파싱 const tsStr = h.timestamp.endsWith("Z") ? h.timestamp : h.timestamp + "Z"; const hTime = new Date(tsStr); - + // 가장 가까운 4시간 단위 버킷 매핑 (±2시간 내) let closestIdx = -1; let minDiff = Infinity; - + points.forEach((p, idx) => { const diff = Math.abs(p.timestamp.getTime() - hTime.getTime()); if (diff < minDiff && diff <= 2 * 60 * 60 * 1000) { @@ -473,7 +493,7 @@ function buildResourceChart(resources: ResourceData | null): HTMLElement { closestIdx = idx; } }); - + if (closestIdx !== -1) { const p = points[closestIdx]; p.cpu = h.cpu_usage_percent; @@ -491,35 +511,48 @@ function buildResourceChart(resources: ResourceData | null): HTMLElement { return ""; }); - const chart = createLineChart({ - ariaLabel: L("B01_Dashboard_Resources_ChartAria"), - xLabels, - width: 800, - height: 140, - series: [ - { - 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), - }, - ], + let resizeTimeout: number | null = null; + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + const width = Math.floor(entry.contentRect.width); + if (width <= 0) continue; + + if (resizeTimeout) { + window.cancelAnimationFrame(resizeTimeout); + } + + resizeTimeout = window.requestAnimationFrame(() => { + chartWrap.innerHTML = ""; + const chart = createLineChart({ + ariaLabel: L("B01_Dashboard_Resources_ChartAria"), + xLabels, + width: width, + height: 140, + series: [ + { + 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"; - caption.textContent = L("B01_Dashboard_Resources_ChartCaption"); - const box = document.createElement("div"); - box.className = "b01-dashboard__chart"; - box.append(caption, chart); + + observer.observe(box); + return box; } diff --git a/ui_template/ui_template_elements.ts b/ui_template/ui_template_elements.ts index 912279d..923dbc6 100644 --- a/ui_template/ui_template_elements.ts +++ b/ui_template/ui_template_elements.ts @@ -699,11 +699,11 @@ const BASE_CSS = ` } .ui-chart__tick { fill: var(--color-text-muted); - font-size: 10px; + font-size: 12px; font-family: var(--font-body); } .ui-chart__tick--x { - font-size: 9px; + font-size: 11px; } .ui-chart__line { fill: none;