import { Fragment, useState, type CSSProperties, type MouseEvent, type ReactNode } from "react" import { DashboardScreen } from "./dashboard-composition" import type { Meta, StoryObj } from "@storybook/react-vite" import { ChartColumnIcon, ChevronRightIcon, LayoutGridIcon, MoreHorizontalIcon, SettingsIcon, type LucideIcon } from "lucide-react" import { Avatar, AvatarFallback, AvatarImage, Badge, Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle, DashboardContent, DashboardLayout, DashboardHeader, DashboardSidebar, Collapsible, CollapsibleContent, CollapsibleTrigger, DashboardMain, DataTable, DataTableColumnHeader, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, FilterBar, FilterBarActions, FilterBarSearch, PageHeader, PageHeaderActions, PageHeaderContent, PageHeaderDescription, PageHeaderTitle, Progress, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Skeleton, dataTableSelectColumn, useDataTable, type DataTableColumnDef, } from "@hm-design/ui" import { STORY_KICKER, StoryPage, StoryPageHeader, StorySection } from "@/components/story-page" import { formatDate } from "./sample-users" import { countDashboardWorkFilter, dashboardWorks, dashboardWorkFilters, dashboardWorkStatus, dashboardWorkStyle, matchesDashboardWorkStatus, type DashboardWork, type DashboardWorkFilter } from "./dashboard-work-status" import "./dashboard-layout.stories.css" // Dashboard Mode: internal navigation, Page Header, KPI cards, Filter Bar and Data Table. // Menu and profile data are local to this Story. See docs/patterns/dashboard-layout.md. const photo = "data:image/svg+xml;utf8," + encodeURIComponent('') const profileData = { name: "김한맥", description: "프로젝트 관리자", photo, fallback: "김", } const profileActions: { label: string; destructive?: boolean }[] = [ { label: "프로필 설정" }, { label: "로그아웃", destructive: true }, ] function ProfileAvatar() { return ( {profileData.fallback} ) } function ProfileMenu({ trigger, side = "bottom", align = "start", modal, matchTrigger = false }: { trigger: ReactNode; side?: "top" | "right" | "bottom" | "left"; align?: "start" | "center" | "end"; modal?: boolean; matchTrigger?: boolean }) { return ( {trigger} {profileData.name} {profileActions.map((action) => ( {action.destructive ? : null} {action.label} ))} ) } type Mode = "compact" | "sidebar" | "header-sidebar" type StatusFilter = DashboardWorkFilter const statusFilters = dashboardWorkFilters.map((id) => ({ id, ...dashboardWorkStatus[id] })) // Variant C (ui_viz patterns-variant-c--all) composition: a 48px Compact Header with the title on the left and a floating 40px status // filter bar centred on the header, followed immediately by the content surface. No GNB, LNB or profile. The structure mirrors the // reference (header > brand + status bar [context | 전체 | group]); colours, type and controls are HM. function CompactHeader({ data, status, onStatusChange }: { data: DashboardWork[]; status: StatusFilter; onStatusChange: (status: StatusFilter) => void }) { const count = (id: StatusFilter) => countDashboardWorkFilter(data, id) const filterButton = (filter: (typeof statusFilters)[number]) => ( ) const [all, ...rest] = statusFilters const workStates = rest.filter((filter) => filter.id !== "issue") const issue = rest.find((filter) => filter.id === "issue")! return (
프로젝트 대시보드
업무
) } // Dashboard internal menu; kept at every viewport width. type LocalNode = { id: string; label: string; icon?: LucideIcon; children?: LocalNode[] } const localNav = (thirdLevel?: boolean): LocalNode[] => [ { id: "overview", label: "개요", icon: LayoutGridIcon }, { id: "projects", label: "업무 관리", children: [ thirdLevel ? { id: "projects/tasks", label: "업무 현황", children: [ { id: "projects/tasks/all", label: "전체 업무" }, { id: "projects/tasks/mine", label: "담당 업무" }, { id: "projects/tasks/delayed", label: "지연 업무" }, ], } : { id: "projects/tasks", label: "업무 현황" }, { id: "projects/schedule", label: "일정" }, ], }, { id: "reports", label: "리포트", icon: ChartColumnIcon }, { id: "settings", label: "설정", icon: SettingsIcon }, ] const statusBadge = (status: Exclude) => ( {dashboardWorkStatus[status].label} ) const columns: DataTableColumnDef[] = [ dataTableSelectColumn(), { accessorKey: "name", header: ({ column }) => }, { accessorKey: "department", header: "부서" }, { accessorKey: "status", header: "상태", cell: ({ row }) => {statusBadge(row.original.status)}{row.original.issue ? statusBadge("issue") : null}, filterFn: (row, _columnId, value: StatusFilter) => matchesDashboardWorkStatus(row.original, value), enableSorting: false }, { accessorKey: "createdAt", header: ({ column }) => , cell: ({ row }) => formatDate(row.original.createdAt), }, ] function TaskList({ data, loading, compact, selectedStatus = "all", onReset }: { data: DashboardWork[]; loading?: boolean; compact?: boolean; selectedStatus?: StatusFilter; onReset?: () => void }) { const table = useDataTable({ columns, data, getRowId: (row) => row.id }) const [search, setSearch] = useState("") const [status, setStatus] = useState("all") const total = table.getFilteredRowModel().rows.length return ( 업무 목록 {dashboardWorkStatus[compact ? selectedStatus : status as StatusFilter].label} 업무 {total}건
{ setSearch(value) table.setGlobalFilter(value || undefined) }} placeholder="업무명·부서 검색" /> {compact ? null : }
) } function Kpis({ data, loading }: { data: DashboardWork[]; loading?: boolean }) { const kpis = statusFilters.map(({ id, label }) => { const value = countDashboardWorkFilter(data, id) const progress = data.length ? Math.round(value / data.length * 100) : 0 return { id, label, value, progress } }) return (
{kpis.map((kpi) => ( {kpi.label} {loading ? null : ( {kpi.id === "all" ? 전체 : statusBadge(kpi.id)} )} {loading ? (
) : (

{kpi.value}

)}
))}
) } function Screen({ mode = "compact", data = dashboardWorks, loading, longTitle, wide, thirdLevel, }: { mode?: Mode data?: DashboardWork[] loading?: boolean longTitle?: boolean wide?: boolean thirdLevel?: boolean }) { const [current, setCurrent] = useState(thirdLevel ? "projects/tasks/all" : "projects/tasks") const [compactStatus, setCompactStatus] = useState("all") const compact = mode === "compact" const resultData = compact ? data.filter((row) => matchesDashboardWorkStatus(row, compactStatus)) : data const [openGroups, setOpenGroups] = useState>({}) const go = (id: string) => (event: MouseEvent) => { event.preventDefault() setCurrent(id) } const isCurrent = (id: string) => current === id || current.startsWith(`${id}/`) const nav = localNav(thirdLevel) const dashboardMenu = (nodes: LocalNode[]): ReactNode => nodes.map((node) => node.children ? ( setOpenGroups((state) => ({ ...state, [node.id]: value }))} > {dashboardMenu(node.children)} ) : ( ) ) const title = longTitle ? "전사 프로젝트 포트폴리오의 진행 현황과 담당 부서별 업무 상세 현황" : "업무 현황" const sidebar = ( 프로젝트 대시보드 {profileData.name} } /> ) // Variant C content: heading, then the result card (Page Header's own 20px bottom spacing; no breadcrumb, actions or KPI cards in the reference). const compactMain = ( 업무 현황을 한눈에 확인하세요 setCompactStatus("all")} /> ) const main = compact ? compactMain : ( 홈 프로젝트 업무 현황 {title} 프로젝트별 업무 진행 상황을 확인하고 조회합니다. 일괄 수정 보관 {wide ? ( 넓은 내용 페이지를 밀지 않고 카드 안에서 가로 스크롤합니다.
) : null} ) return ( : mode === "header-sidebar" ? 업무 현황 : undefined} sidebar={compact ? undefined : sidebar} > {main} ) } const frame = (height?: number): CSSProperties | undefined => (height ? ({ "--hm-dashboard-height": `${height}px` } as CSSProperties) : undefined) // Shared by Storybook and the standalone Vite export. Keeping the representative // story composition here prevents the export from becoming a second hand-written UI. export function DashboardAll() { return ( ) } // Menu label is Dashboard; the fixed id keeps the existing story IDs (patterns-layout-dashboard-layout--*) used by links and QA inventory. const meta = { id: "patterns-layout-dashboard-layout", title: "Patterns/Layout/Dashboard", excludeStories: ["DashboardAll"], parameters: { layout: "fullscreen", controls: { disable: true } } } satisfies Meta export default meta type Story = StoryObj // Representative Story reproduces the ui_viz Variant C layout (48px Compact Header, centred status bar, content) with HM parts. // Operating DASHBOARD Mode has no Sidebar; retained Sidebar examples are compatibility verification only. // The page heading and Dashboard's native minimum-width scroll frame share the common Inner. export const All: Story = { name: "Dashboard", render: () => , } // Keep existing Sidebar coverage and Story IDs; these are not operating references. function SidebarVerification({ children }: { children: ReactNode }) { return <>

VERIFICATION ONLY — 운영 사용 금지

{children} } export const WithSidebar: Story = { tags: ["verification"], name: "VERIFICATION ONLY — 운영 사용 금지 · Dashboard Header + Sidebar", render: () => } export const ThirdLevel: Story = { tags: ["verification"], name: "VERIFICATION ONLY — 운영 사용 금지 · Third Level", render: () => } export const SidebarOnly: Story = { tags: ["verification"], name: "VERIFICATION ONLY — 운영 사용 금지 · Dashboard Sidebar", render: () => } export const LongContent: Story = { tags: ["verification"], name: "VERIFICATION ONLY — 운영 사용 금지 · Long Content", render: () => } export const Loading: Story = { tags: ["verification"], render: () => } export const Empty: Story = { tags: ["verification"], render: () => } export const Framed: Story = { tags: ["verification"], name: "VERIFICATION ONLY — 운영 사용 금지 · Framed", render: () =>
}