diff --git a/src/assets/icon/dashboard/trend-down.svg b/src/assets/icon/dashboard/trend-down.svg new file mode 100644 index 00000000..3acc9e78 --- /dev/null +++ b/src/assets/icon/dashboard/trend-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icon/dashboard/trend-up.svg b/src/assets/icon/dashboard/trend-up.svg new file mode 100644 index 00000000..09b4860a --- /dev/null +++ b/src/assets/icon/dashboard/trend-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/common/card/Card.stories.tsx b/src/components/common/card/Card.stories.tsx new file mode 100644 index 00000000..1ebdec85 --- /dev/null +++ b/src/components/common/card/Card.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import Card from "./Card"; +import Button from "../button/Button"; + +const meta: Meta = { + title: "Common/Card", + component: Card, + args: { + title: "카드 제목", + className: "w-80", + }, + parameters: { layout: "centered" }, +}; + +export default meta; +type TCardStory = StoryObj; + +export const Default: TCardStory = { + args: { + title: undefined, + children: ( +

카드 내용이 들어갑니다.

+ ), + }, +}; + +export const WithTitle: TCardStory = { + args: { + children: ( +

카드 내용이 들어갑니다.

+ ), + }, +}; + +export const WithDescription: TCardStory = { + args: { + description: "제목에 대한 설명이 들어갑니다.", + children: ( +

카드 내용이 들어갑니다.

+ ), + }, +}; + +export const WithRightElement: TCardStory = { + args: { + RightElement: , + children: ( +

카드 내용이 들어갑니다.

+ ), + }, +}; + +export const WithDescriptionAndRightElement: TCardStory = { + args: { + description: "데이터 기준 | 2026.01.06 09:13", + RightElement: , + children: ( +

카드 내용이 들어갑니다.

+ ), + }, +}; diff --git a/src/components/common/card/Card.tsx b/src/components/common/card/Card.tsx index e69de29b..0e3fffb5 100644 --- a/src/components/common/card/Card.tsx +++ b/src/components/common/card/Card.tsx @@ -0,0 +1,42 @@ +import type { HTMLAttributes, ReactNode } from "react"; +import { twMerge } from "tailwind-merge"; + +export interface ICardProps extends HTMLAttributes { + title?: string; + description?: string; + RightElement?: ReactNode; +} + +export default function Card({ + title, + description, + RightElement, + children, + className, + ...rest +}: ICardProps) { + const hasHeader = title || description || RightElement; + + return ( +
+ {hasHeader && ( +
+
+ {title &&

{title}

} + {description && ( +

{description}

+ )} +
+ {RightElement} +
+ )} + {children} +
+ ); +} diff --git a/src/components/common/card/StatCard.stories.tsx b/src/components/common/card/StatCard.stories.tsx new file mode 100644 index 00000000..451a87ee --- /dev/null +++ b/src/components/common/card/StatCard.stories.tsx @@ -0,0 +1,67 @@ +import { Fragment } from "react"; +import type { Meta, StoryObj } from "@storybook/react"; + +import StatCard from "./StatCard"; + +const meta: Meta = { + title: "Common/StatCard", + component: StatCard, + args: { + title: "클릭수(CTR)", + value: "1,280", + }, + parameters: { layout: "centered" }, +}; + +export default meta; +type TStatCardStory = StoryObj; + +export const Default: TStatCardStory = {}; + +export const WithTrendUp: TStatCardStory = { + args: { + trend: { direction: "up", value: "13.77%" }, + }, +}; + +export const WithTrendDown: TStatCardStory = { + args: { + title: "노출수", + value: "3,175,630", + trend: { direction: "down", value: "13.77%" }, + }, +}; + +export const KpiGroup: TStatCardStory = { + render: () => ( +
+ {[ + { + title: "클릭수(CTR)", + value: "1,280", + trend: { direction: "up" as const, value: "13.77%" }, + }, + { + title: "노출수", + value: "3,175,630", + trend: { direction: "down" as const, value: "13.77%" }, + }, + { + title: "전환율(CVR)", + value: "27.09%", + trend: { direction: "up" as const, value: "13.77%" }, + }, + { + title: "광고비 대비 매출", + value: "37.58%", + trend: { direction: "down" as const, value: "13.77%" }, + }, + ].map((kpi, index) => ( + + {index > 0 &&
} + + + ))} +
+ ), +}; diff --git a/src/components/common/card/StatCard.tsx b/src/components/common/card/StatCard.tsx new file mode 100644 index 00000000..d851d43d --- /dev/null +++ b/src/components/common/card/StatCard.tsx @@ -0,0 +1,58 @@ +import type { HTMLAttributes } from "react"; +import { twMerge } from "tailwind-merge"; + +import TrendDownIcon from "@/assets/icon/dashboard/trend-down.svg?react"; +import TrendUpIcon from "@/assets/icon/dashboard/trend-up.svg?react"; + +export interface ITrend { + direction: "up" | "down"; + value: string; +} + +export interface IStatCardProps extends HTMLAttributes { + title: string; + value: string | number; + trend?: ITrend; +} + +const trendClasses: Record = { + up: "bg-status-red/10 text-status-red", + down: "bg-status-blue/10 text-status-blue", +}; + +export default function StatCard({ + title, + value, + trend, + className, + ...rest +}: IStatCardProps) { + return ( +
+

{title}

+

{value}

+ {trend && ( + + {trend.direction === "up" ? ( + + ) : ( + + )} + {trend.value} + + )} +
+ ); +} diff --git a/src/index.css b/src/index.css index e91ffb3b..71d8cfb9 100644 --- a/src/index.css +++ b/src/index.css @@ -333,3 +333,4 @@ button { animation: shimmer 1.5s infinite linear; } } + diff --git a/src/pages/dashboard/overview/OverviewDashboard.tsx b/src/pages/dashboard/overview/OverviewDashboard.tsx index 925ba2be..0bfec86f 100644 --- a/src/pages/dashboard/overview/OverviewDashboard.tsx +++ b/src/pages/dashboard/overview/OverviewDashboard.tsx @@ -1,3 +1,53 @@ +import { Fragment } from "react"; + +import Button from "@/components/common/button/Button"; +import Card from "@/components/common/card/Card"; +import StatCard from "@/components/common/card/StatCard"; + +import { overviewMockData } from "./overview.mock"; + export default function OverviewDashboard() { - return
OverviewDashboard
; + return ( +
+
+
+

통합 대시보드

+

+ 데이터 기준 | 2026.01.06 09:13 +

+
+ {/* TODO: AI 요약 버튼 */} +
+ +
+ {overviewMockData.kpis.map((kpi, index) => ( + + {index > 0 && ( +
+ )} +
+ + {index % 2 === 0 && ( +
+ )} + {index < 2 && ( +
+ )} +
+ + ))} +
+ +
+ + +
+ + 플랫폼 대시보드 보기} + /> +
+ ); } diff --git a/src/pages/dashboard/overview/overview.mock.ts b/src/pages/dashboard/overview/overview.mock.ts new file mode 100644 index 00000000..de2d40f5 --- /dev/null +++ b/src/pages/dashboard/overview/overview.mock.ts @@ -0,0 +1,26 @@ +import type { IOverviewData } from "@/types/dashboard/overview"; + +export const overviewMockData: IOverviewData = { + kpis: [ + { + title: "클릭수(CTR)", + value: "1,280", + trend: { direction: "up", value: "13.77%" }, + }, + { + title: "노출수", + value: "3,175,630", + trend: { direction: "down", value: "13.77%" }, + }, + { + title: "전환율(CVR)", + value: "27.09%", + trend: { direction: "up", value: "13.77%" }, + }, + { + title: "광고비 대비 매출", + value: "37.58%", + trend: { direction: "down", value: "13.77%" }, + }, + ], +}; diff --git a/src/types/dashboard/overview.ts b/src/types/dashboard/overview.ts new file mode 100644 index 00000000..90334ff0 --- /dev/null +++ b/src/types/dashboard/overview.ts @@ -0,0 +1,14 @@ +interface ITrend { + direction: "up" | "down"; + value: string; +} + +export interface IKpiMetric { + title: string; + value: string | number; + trend?: ITrend; +} + +export interface IOverviewData { + kpis: IKpiMetric[]; +}