Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/assets/icon/dashboard/trend-down.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icon/dashboard/trend-up.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
62 changes: 62 additions & 0 deletions src/components/common/card/Card.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { Meta, StoryObj } from "@storybook/react";

import Card from "./Card";
import Button from "../button/Button";

const meta: Meta<typeof Card> = {
title: "Common/Card",
component: Card,
args: {
title: "카드 제목",
className: "w-80",
},
parameters: { layout: "centered" },
};

export default meta;
type TCardStory = StoryObj<typeof Card>;

export const Default: TCardStory = {
args: {
title: undefined,
children: (
<p className="font-body2 text-text-sub">카드 내용이 들어갑니다.</p>
),
},
};

export const WithTitle: TCardStory = {
args: {
children: (
<p className="font-body2 text-text-sub">카드 내용이 들어갑니다.</p>
),
},
};

export const WithDescription: TCardStory = {
args: {
description: "제목에 대한 설명이 들어갑니다.",
children: (
<p className="font-body2 text-text-sub">카드 내용이 들어갑니다.</p>
),
},
};

export const WithRightElement: TCardStory = {
args: {
RightElement: <Button size="small">플랫폼 대시보드 보기</Button>,
children: (
<p className="font-body2 text-text-sub">카드 내용이 들어갑니다.</p>
),
},
};

export const WithDescriptionAndRightElement: TCardStory = {
args: {
description: "데이터 기준 | 2026.01.06 09:13",
RightElement: <Button size="small">보기</Button>,
children: (
<p className="font-body2 text-text-sub">카드 내용이 들어갑니다.</p>
),
},
};
42 changes: 42 additions & 0 deletions src/components/common/card/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { HTMLAttributes, ReactNode } from "react";
import { twMerge } from "tailwind-merge";

export interface ICardProps extends HTMLAttributes<HTMLDivElement> {
title?: string;
description?: string;
RightElement?: ReactNode;
}

export default function Card({
title,
description,
RightElement,
children,
className,
...rest
}: ICardProps) {
const hasHeader = title || description || RightElement;

return (
<div
className={twMerge(
"bg-white rounded-component-md border border-chart-inactive p-5",
className,
)}
{...rest}
>
{hasHeader && (
<div className="flex flex-wrap items-start justify-between gap-2 mb-4">
<div className="flex flex-col gap-0.5">
{title && <h3 className="font-body1 text-text-main">{title}</h3>}
{description && (
<p className="font-caption text-text-sub">{description}</p>
)}
</div>
{RightElement}
</div>
)}
{children}
</div>
);
}
67 changes: 67 additions & 0 deletions src/components/common/card/StatCard.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Fragment } from "react";
import type { Meta, StoryObj } from "@storybook/react";

import StatCard from "./StatCard";

const meta: Meta<typeof StatCard> = {
title: "Common/StatCard",
component: StatCard,
args: {
title: "클릭수(CTR)",
value: "1,280",
},
parameters: { layout: "centered" },
};

export default meta;
type TStatCardStory = StoryObj<typeof StatCard>;

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: () => (
<div className="flex bg-white rounded-component-md border border-chart-inactive w-200">
{[
{
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) => (
<Fragment key={kpi.title}>
{index > 0 && <div className="w-px bg-bg-disabled my-5" />}
<StatCard className="flex-1 border-0 rounded-none" {...kpi} />
</Fragment>
))}
</div>
),
};
58 changes: 58 additions & 0 deletions src/components/common/card/StatCard.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement> {
title: string;
value: string | number;
trend?: ITrend;
}

const trendClasses: Record<ITrend["direction"], string> = {
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 (
<div
className={twMerge(
"bg-white rounded-component-md border border-chart-inactive p-5 flex flex-col gap-2",
className,
)}
{...rest}
>
<p className="font-body2 text-text-main">{title}</p>
<p className="font-heading2 text-text-main">{value}</p>
{trend && (
<span
aria-label={`${trend.value} ${trend.direction === "up" ? "상승" : "하락"}`}
className={twMerge(
"inline-flex items-center gap-1 px-2 py-1 rounded-full font-caption w-fit",
trendClasses[trend.direction],
)}
>
{trend.direction === "up" ? (
<TrendUpIcon aria-hidden className="w-4 h-4" />
) : (
<TrendDownIcon aria-hidden className="w-4 h-4" />
)}
{trend.value}
</span>
)}
</div>
);
}
1 change: 1 addition & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,4 @@ button {
animation: shimmer 1.5s infinite linear;
}
}

52 changes: 51 additions & 1 deletion src/pages/dashboard/overview/OverviewDashboard.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>OverviewDashboard</div>;
return (
<div className="flex flex-col gap-5 p-6">
<div className="flex items-center justify-between">
<div>
<h1 className="font-heading3 text-text-main">통합 대시보드</h1>
<p className="font-caption text-text-sub mt-1">
데이터 기준 | 2026.01.06 09:13
</p>
</div>
{/* TODO: AI 요약 버튼 */}
</div>

<div className="grid grid-cols-2 bg-white rounded-component-md border border-chart-inactive lg:flex">
{overviewMockData.kpis.map((kpi, index) => (
<Fragment key={kpi.title}>
{index > 0 && (
<div className="hidden w-px bg-bg-surface lg:my-5 lg:block" />
)}
<div className="relative flex-1">
<StatCard className="border-0 rounded-none" {...kpi} />
{index % 2 === 0 && (
<div className="absolute right-0 top-4 bottom-4 w-px bg-bg-surface lg:hidden" />
)}
{index < 2 && (
<div className="absolute bottom-0 left-4 right-4 h-px bg-bg-surface lg:hidden" />
)}
Comment thread
Seojegyeong marked this conversation as resolved.
</div>
</Fragment>
))}
</div>

<div className="grid grid-cols-1 lg:grid-cols-[5fr_3fr] gap-4">
<Card title="실시간 트래픽 변화" className="min-h-100" />
<Card title="예산 소진 현황" className="min-h-100" />
</div>

<Card
title="플랫폼별 비교"
className="min-h-80"
RightElement={<Button size="small">플랫폼 대시보드 보기</Button>}
/>
</div>
);
}
26 changes: 26 additions & 0 deletions src/pages/dashboard/overview/overview.mock.ts
Original file line number Diff line number Diff line change
@@ -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%" },
},
],
};
14 changes: 14 additions & 0 deletions src/types/dashboard/overview.ts
Original file line number Diff line number Diff line change
@@ -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[];
}