From 5a9fa8059082203c81512cccb340d19420670ac1 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Tue, 23 Jun 2026 02:21:57 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=EC=84=B1=EA=B3=BC=EC=9A=94?= =?UTF-8?q?=EC=95=BD=ED=8C=A8=EB=84=90=20=EA=B8=B0=EB=B3=B8=20=ED=8B=80=20?= =?UTF-8?q?UI=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timeline/TimelinePerformancePanel.tsx | 224 ++++++++++++++++++ src/types/timeline/summary.ts | 6 +- src/types/timeline/timeline.mock.ts | 1 + 3 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 src/components/timeline/TimelinePerformancePanel.tsx diff --git a/src/components/timeline/TimelinePerformancePanel.tsx b/src/components/timeline/TimelinePerformancePanel.tsx new file mode 100644 index 00000000..96d82b16 --- /dev/null +++ b/src/components/timeline/TimelinePerformancePanel.tsx @@ -0,0 +1,224 @@ +import { useEffect, useState } from "react"; +import { twMerge } from "tailwind-merge"; + +import { PLATFORM_MAP } from "@/types/dashboard/provider"; +import type { ITimelineSummaryPanelData } from "@/types/timeline/summary"; +import type { TTimelineViewUnit } from "@/types/timeline/ui"; +import { TIMELINE_PERFORMANCE_STATUS_STYLE } from "@/constants/timeline/statusStyle"; + +import TimelinePeriodSelector from "./TimelinePeriodSelector"; +import Badge from "../common/badge/Badge"; +import Button from "../common/button/Button"; +import Card from "../common/card/Card"; +import StatCard from "../common/card/StatCard"; +import ChartLegend from "../common/chart/ChartLegend"; +import Drawer from "../common/drawer/Drawer"; +import { DropdownMenu } from "../common/dropdownmenu/DropdownMenu"; +import ProgressBar from "../common/progressbar/ProgressBar"; +import { Skeleton } from "../common/skeleton/Skeleton"; + +import ChevronRightIcon from "@/assets/icon/chevron/chevron-right.svg?react"; +import MoreIcon from "@/assets/icon/common/more.svg?react"; +import TrashIcon from "@/assets/icon/common/trash.svg?react"; + +type TAiSummaryUiState = "idle" | "loading" | "done"; + +const AI_SUMMARY_LOADING_MS = 1500; + +interface ITimelinePerformancePanelProps { + isOpen: boolean; + onClose: () => void; + data: ITimelineSummaryPanelData; + onEdit?: () => void; + onDelete?: () => void; + className?: string; +} + +function formatMetricValue(value: number, unit?: string) { + const formatted = Number.isInteger(value) + ? value.toLocaleString() + : value.toFixed(2); + return unit ? `${formatted}${unit}` : formatted; +} + +function formatChangeRate(changeRate: number) { + return `${Math.abs(changeRate * 100).toFixed(1)}%`; +} + +export default function TimelinePerformancePanel({ + isOpen, + onClose, + data, + onEdit, + onDelete, + className, +}: ITimelinePerformancePanelProps) { + const [aiState, setaiState] = useState("idle"); + const [viewUnit, setViewUnit] = useState("WEEK"); + + const statusStyle = TIMELINE_PERFORMANCE_STATUS_STYLE[data.performanceStatus]; + + //패널 열릴때, 서버에 요약있으면 done, 없으면 idle + useEffect(() => { + if (!isOpen) return; + setaiState(data.aiSummary.trim() ? "done" : "idle"); + }, [isOpen, data.aiSummary]); + + const handleGenerateSummary = () => { + setaiState("loading"); + window.setTimeout(() => { + setaiState("done"); + }, AI_SUMMARY_LOADING_MS); + }; + + const menuItems = [ + { + label: "삭제하기", + icon: , + onClick: () => onDelete?.(), + }, + { + label: "수정하기", + onClick: () => onEdit?.(), + }, + ]; + return ( +
+ +
+
+
+ + } + aria-label="더보기" + items={menuItems} + /> +
+

{data.timelineName}

+
+
기간
+
{data.periodLabel}
+
성과 상태
+
+ + + +
+
성과 지표
+
+ {data.metrics.map((metric) => ( + + {metric.label} + + ))} +
+
+
+ + {/* AI 요약 파트 */} +
+ {aiState === "idle" && ( + + )} + + {aiState === "loading" && ( +
+ + + +
+ )} + + {aiState === "done" && data.aiSummary &&

{data.aiSummary}

} +
+ + {/* KPI 카드 */} +
+ {data.metrics.map((metric) => ( + = 0 ? "up" : "down", + value: formatChangeRate(metric.changeRate), + } + : undefined + } + className="p-5" + /> + ))} +
+ + {/* 차트 */} + + } + RightElement={ + {}} + onNextPeriod={() => {}} + /> + } + > +
+ 차트 영역 (ApexChart 연동예정) +
+
+ + {/* 플랫폼 기여 파트 */} +
+

플랫폼 기여 정보

+ {data.platformShare.map(({ provider, contributionRate }) => ( +
+ {PLATFORM_MAP[provider]} + +
+ ))} +
+
+
+
+ ); +} diff --git a/src/types/timeline/summary.ts b/src/types/timeline/summary.ts index 3684a106..ef6d8c80 100644 --- a/src/types/timeline/summary.ts +++ b/src/types/timeline/summary.ts @@ -1,5 +1,8 @@ import type { TProviderType } from "@/types/dashboard/provider"; -import type { TTimelineMetric } from "@/types/timeline/api"; +import type { + TTimelineMetric, + TTimelinePerformanceStatus, +} from "@/types/timeline/api"; /*패널 상단 KPI 카드 한개*/ export interface ITimelineSummaryMetric { @@ -20,6 +23,7 @@ export interface ITimelineSummaryPlatformShare { export interface ITimelineSummaryPanelData { timelineName: string; periodLabel: string; + performanceStatus: TTimelinePerformanceStatus; aiSummary: string; metrics: ITimelineSummaryMetric[]; platformShare: ITimelineSummaryPlatformShare[]; diff --git a/src/types/timeline/timeline.mock.ts b/src/types/timeline/timeline.mock.ts index 15c7b4e8..c16bd9c5 100644 --- a/src/types/timeline/timeline.mock.ts +++ b/src/types/timeline/timeline.mock.ts @@ -118,6 +118,7 @@ export const TIMELINE_GRID_MOCK: ITimelineGridData = { export const TIMELINE_SUMMARY_PANEL_MOCK: ITimelineSummaryPanelData = { timelineName: TIMELINE_DETAIL_MOCK.name, periodLabel: "2026.06.01 ~ 2026.06.30", + performanceStatus: "ABOVE_AVERAGE", aiSummary: TIMELINE_DETAIL_MOCK.summary, metrics: [ { metric: "CLICK", label: "클릭", value: 3730, changeRate: 0.08 }, From 1440bfe164045feb6fd0c903765be05e34c2c750 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Wed, 24 Jun 2026 02:54:03 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=ED=83=80=EC=9E=84=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EC=84=B1=EA=B3=BC=20=EC=9A=94=EC=95=BD=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20UI=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TimelinePerformancePanel.stories.tsx | 84 ++++++ .../timeline/TimelinePerformancePanel.tsx | 268 +++++++++--------- src/types/timeline/timeline.mock.ts | 5 + 3 files changed, 226 insertions(+), 131 deletions(-) create mode 100644 src/components/timeline/TimelinePerformancePanel.stories.tsx diff --git a/src/components/timeline/TimelinePerformancePanel.stories.tsx b/src/components/timeline/TimelinePerformancePanel.stories.tsx new file mode 100644 index 00000000..6bb0cd32 --- /dev/null +++ b/src/components/timeline/TimelinePerformancePanel.stories.tsx @@ -0,0 +1,84 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; + +import { + TIMELINE_SUMMARY_PANEL_MOCK, + TIMELINE_SUMMARY_PANEL_NO_AI_MOCK, +} from "@/types/timeline/timeline.mock"; + +import TimelinePerformancePanel from "./TimelinePerformancePanel"; + +const meta: Meta = { + title: "Timeline/PerformancePanel", + component: TimelinePerformancePanel, + parameters: { layout: "fullscreen" }, + args: { + onClose: fn(), + onEdit: fn(), + onDelete: fn(), + }, +}; + +export default meta; +type TStory = StoryObj; + +function ClosedPreivew() { + const [open, setOpen] = useState(false); + + return ( + <> +
+ +
+ setOpen(false)} + data={TIMELINE_SUMMARY_PANEL_NO_AI_MOCK} + onEdit={fn()} + onDelete={fn()} + /> + + ); +} + +export const Closed: TStory = { + render: () => , +}; + +export const Open: TStory = { + args: { + isOpen: true, + data: TIMELINE_SUMMARY_PANEL_NO_AI_MOCK, + }, +}; + +export const OpenWithSummary: TStory = { + args: { + isOpen: true, + data: TIMELINE_SUMMARY_PANEL_MOCK, + }, +}; + +function InteractivePreview() { + const [open, setOpen] = useState(true); + return ( + setOpen(false)} + data={TIMELINE_SUMMARY_PANEL_MOCK} + onEdit={fn()} + onDelete={fn()} + /> + ); +} + +export const Interactive: TStory = { + render: () => , +}; diff --git a/src/components/timeline/TimelinePerformancePanel.tsx b/src/components/timeline/TimelinePerformancePanel.tsx index 96d82b16..dce29022 100644 --- a/src/components/timeline/TimelinePerformancePanel.tsx +++ b/src/components/timeline/TimelinePerformancePanel.tsx @@ -83,142 +83,148 @@ export default function TimelinePerformancePanel({ }, ]; return ( -
- -
-
-
- - } - aria-label="더보기" - items={menuItems} - /> + +
+
+
+ + } + aria-label="더보기" + items={menuItems} + className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg transition-colors hover:bg-surface-200" + /> +
+

{data.timelineName}

+
+
기간
+
{data.periodLabel}
+
성과 상태
+
+ + + +
+
성과 지표
+
+ {data.metrics.map((metric) => ( + + {metric.label} + + ))} +
+
+
+ + {/* AI 요약 파트 */} +
+ {aiState === "idle" && ( + + )} + + {aiState === "loading" && ( +
+ + +
-

{data.timelineName}

-
-
기간
-
{data.periodLabel}
-
성과 상태
-
- - - -
-
성과 지표
-
- {data.metrics.map((metric) => ( - - {metric.label} - - ))} -
-
-
- - {/* AI 요약 파트 */} -
- {aiState === "idle" && ( - - )} - - {aiState === "loading" && ( -
- - - -
- )} - - {aiState === "done" && data.aiSummary &&

{data.aiSummary}

} -
- - {/* KPI 카드 */} -
- {data.metrics.map((metric) => ( - = 0 ? "up" : "down", - value: formatChangeRate(metric.changeRate), - } - : undefined - } - className="p-5" - /> - ))} -
- - {/* 차트 */} - - } - RightElement={ - {}} - onNextPeriod={() => {}} - /> - } - > -
- 차트 영역 (ApexChart 연동예정) -
-
- - {/* 플랫폼 기여 파트 */} -
-

플랫폼 기여 정보

+ )} + + {aiState === "done" && data.aiSummary && ( +

+ {data.aiSummary} +

+ )} +
+ + {/* KPI 카드 */} +
+ {data.metrics.map((metric) => ( + = 0 ? "up" : "down", + value: formatChangeRate(metric.changeRate), + } + : undefined + } + className="p-5" + /> + ))} +
+ + {/* 차트 */} + + } + RightElement={ + {}} + onNextPeriod={() => {}} + /> + } + > +
+ + 차트 영역 (ApexChart 연동예정) + +
+
+ + {/* 플랫폼 기여 파트 */} +
+

플랫폼 기여 정보

+
{data.platformShare.map(({ provider, contributionRate }) => (
- {PLATFORM_MAP[provider]} + + {PLATFORM_MAP[provider]} +
))} -
-
-
-
+ + + + ); } diff --git a/src/types/timeline/timeline.mock.ts b/src/types/timeline/timeline.mock.ts index c16bd9c5..ab86c2bb 100644 --- a/src/types/timeline/timeline.mock.ts +++ b/src/types/timeline/timeline.mock.ts @@ -138,3 +138,8 @@ export const TIMELINE_SUMMARY_PANEL_MOCK: ITimelineSummaryPanelData = { { provider: "NAVER", contributionRate: 0.17 }, ], }; + +export const TIMELINE_SUMMARY_PANEL_NO_AI_MOCK: ITimelineSummaryPanelData = { + ...TIMELINE_SUMMARY_PANEL_MOCK, + aiSummary: "", +}; From b20e7523d12983be5217e1fa1c6890423d4fed68 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Wed, 24 Jun 2026 04:50:40 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=EC=84=B1=EA=B3=BC=EC=9A=94?= =?UTF-8?q?=EC=95=BD=20=ED=8C=A8=EB=84=90=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/dropdownmenu/DropdownMenu.tsx | 13 +- .../timeline/TimelinePerformancePanel.tsx | 314 ++++++++++++------ src/styles/utilities.css | 6 + src/types/timeline/timeline.mock.ts | 4 +- 4 files changed, 235 insertions(+), 102 deletions(-) diff --git a/src/components/common/dropdownmenu/DropdownMenu.tsx b/src/components/common/dropdownmenu/DropdownMenu.tsx index 6ed9aa8f..dcaffd7b 100644 --- a/src/components/common/dropdownmenu/DropdownMenu.tsx +++ b/src/components/common/dropdownmenu/DropdownMenu.tsx @@ -7,6 +7,8 @@ export type TMenuItem = { icon?: React.ReactNode; onClick: () => void; active?: boolean; + danger?: boolean; + labelClassName?: string; }; const easeOut = [0, 0, 0.2, 1] as const; @@ -114,9 +116,11 @@ export function DropdownMenu({ }} className={twMerge( "group flex w-full items-center justify-between rounded-2xl px-5 py-4 text-left font-body2 transition-ui-fast", - it.active - ? "bg-info-blue/10 text-info-blue" - : "text-text-body hover:bg-primary-100/50 hover:text-info-blue", + it.danger + ? "text-info-red hover:bg-info-red/10 hover:text-info-red" + : it.active + ? "bg-info-blue/10 text-info-blue" + : "text-text-body hover:bg-primary-100/50 hover:text-info-blue", )} >
@@ -137,6 +141,9 @@ export function DropdownMenu({ className={twMerge( "min-w-0 truncate font-body2 text-left", it.active && "font-label", + it.danger && + "text-info-red group-hover:text-info-red", + it.labelClassName, )} > {it.label} diff --git a/src/components/timeline/TimelinePerformancePanel.tsx b/src/components/timeline/TimelinePerformancePanel.tsx index dce29022..f5ec0164 100644 --- a/src/components/timeline/TimelinePerformancePanel.tsx +++ b/src/components/timeline/TimelinePerformancePanel.tsx @@ -1,30 +1,64 @@ +import type { FC, SVGProps } from "react"; import { useEffect, useState } from "react"; import { twMerge } from "tailwind-merge"; -import { PLATFORM_MAP } from "@/types/dashboard/provider"; +import type { TProviderType } from "@/types/dashboard/provider"; import type { ITimelineSummaryPanelData } from "@/types/timeline/summary"; import type { TTimelineViewUnit } from "@/types/timeline/ui"; import { TIMELINE_PERFORMANCE_STATUS_STYLE } from "@/constants/timeline/statusStyle"; -import TimelinePeriodSelector from "./TimelinePeriodSelector"; -import Badge from "../common/badge/Badge"; -import Button from "../common/button/Button"; -import Card from "../common/card/Card"; -import StatCard from "../common/card/StatCard"; -import ChartLegend from "../common/chart/ChartLegend"; -import Drawer from "../common/drawer/Drawer"; -import { DropdownMenu } from "../common/dropdownmenu/DropdownMenu"; -import ProgressBar from "../common/progressbar/ProgressBar"; -import { Skeleton } from "../common/skeleton/Skeleton"; +import Badge from "@/components/common/badge/Badge"; +import Button from "@/components/common/button/Button"; +import ChartLegend from "@/components/common/chart/ChartLegend"; +import Drawer from "@/components/common/drawer/Drawer"; +import { DropdownMenu } from "@/components/common/dropdownmenu/DropdownMenu"; +import { Skeleton } from "@/components/common/skeleton/Skeleton"; +import TimelinePeriodSelector from "@/components/timeline/TimelinePeriodSelector"; import ChevronRightIcon from "@/assets/icon/chevron/chevron-right.svg?react"; import MoreIcon from "@/assets/icon/common/more.svg?react"; import TrashIcon from "@/assets/icon/common/trash.svg?react"; +import GoogleWordmark from "@/assets/logo/social-logo/wordmark/google-wordmark.svg?react"; +import MetaWordmark from "@/assets/logo/social-logo/wordmark/meta-wordmark.svg?react"; +import NaverWordmark from "@/assets/logo/social-logo/wordmark/naver-wordmark.svg?react"; type TAiSummaryUiState = "idle" | "loading" | "done"; const AI_SUMMARY_LOADING_MS = 1500; +const CHART_PERIOD_LABELS = ["오늘", "1월 21일 → 25일", "1월 14일 → 20일"]; + +const SECTION_SHELL_CLASS = + "rounded-3xl border border-surface-300/50 bg-surface-100 shadow-Soft-xs"; + +const SECTION_INNER_CLASS = "flex flex-col gap-5 px-6 py-6"; + +const SOFT_CARD_CLASS = + "rounded-2xl border border-surface-300/40 bg-surface-100 shadow-Soft-xs"; + +const PLATFORM_WORDMARKS: Record< + TProviderType, + { Logo: FC>; className: string; label: string } +> = { + GOOGLE: { + Logo: GoogleWordmark, + className: "h-5 w-auto", + label: "Google", + }, + NAVER: { + Logo: NaverWordmark, + className: "h-4 w-auto", + label: "NAVER", + }, + META: { + Logo: MetaWordmark, + className: "h-3.5 w-auto", + label: "Meta", + }, +}; + +const SECTION_TITLE_CLASS = "font-heading4 text-text-title"; + interface ITimelinePerformancePanelProps { isOpen: boolean; onClose: () => void; @@ -45,6 +79,40 @@ function formatChangeRate(changeRate: number) { return `${Math.abs(changeRate * 100).toFixed(1)}%`; } +function PlatformContributionRow({ + provider, + value, +}: { + provider: TProviderType; + value: number; +}) { + const { + Logo, + className: logoClassName, + label, + } = PLATFORM_WORDMARKS[provider]; + const progress = Math.min(Math.max(value, 0), 100); + + return ( +
+
+ +
+
+
+
+
+ + {progress}% + +
+
+ ); +} + export default function TimelinePerformancePanel({ isOpen, onClose, @@ -53,28 +121,44 @@ export default function TimelinePerformancePanel({ onDelete, className, }: ITimelinePerformancePanelProps) { - const [aiState, setaiState] = useState("idle"); + const [aiState, setAiState] = useState("idle"); const [viewUnit, setViewUnit] = useState("WEEK"); + const [chartPeriodIndex, setChartPeriodIndex] = useState(0); const statusStyle = TIMELINE_PERFORMANCE_STATUS_STYLE[data.performanceStatus]; + const chartPeriodLabel = + CHART_PERIOD_LABELS[chartPeriodIndex] ?? CHART_PERIOD_LABELS[0]; - //패널 열릴때, 서버에 요약있으면 done, 없으면 idle useEffect(() => { if (!isOpen) return; - setaiState(data.aiSummary.trim() ? "done" : "idle"); + setAiState(data.aiSummary.trim() ? "done" : "idle"); }, [isOpen, data.aiSummary]); const handleGenerateSummary = () => { - setaiState("loading"); + setAiState("loading"); window.setTimeout(() => { - setaiState("done"); + setAiState("done"); }, AI_SUMMARY_LOADING_MS); }; + const handlePrevChartPeriod = () => { + setChartPeriodIndex((prev) => + prev === 0 ? CHART_PERIOD_LABELS.length - 1 : prev - 1, + ); + }; + + const handleNextChartPeriod = () => { + setChartPeriodIndex((prev) => + prev === CHART_PERIOD_LABELS.length - 1 ? 0 : prev + 1, + ); + }; + const menuItems = [ { label: "삭제하기", - icon: , + icon: , + danger: true, + labelClassName: "text-info-red", onClick: () => onDelete?.(), }, { @@ -82,10 +166,12 @@ export default function TimelinePerformancePanel({ onClick: () => onEdit?.(), }, ]; + return ( -
-
+
+ {/* 헤더 */} +
+

{data.timelineName}

-
-
기간
-
{data.periodLabel}
-
성과 상태
-
- + +
+
+ 기간 + + {data.periodLabel} + +
+ +
+ 성과 상태 + + {statusStyle.label} -
-
성과 지표
-
- {data.metrics.map((metric) => ( - - {metric.label} - - ))} -
-
+
+ +
+ 성과 지표 +
+ {data.metrics.map((metric) => ( + + {metric.label} + + ))} +
+
+
- {/* AI 요약 파트 */} -
+ {/* AI 요약 */} +
{aiState === "idle" && ( )} {aiState === "loading" && ( -
+
@@ -154,74 +257,91 @@ export default function TimelinePerformancePanel({ )} {aiState === "done" && data.aiSummary && ( -

+

{data.aiSummary}

)}
- {/* KPI 카드 */} -
+ {/* KPI — 선택한 지표 수만큼 가로 균등 분할 */} +
{data.metrics.map((metric) => ( - = 0 ? "up" : "down", - value: formatChangeRate(metric.changeRate), - } - : undefined - } - className="p-5" - /> + className={twMerge( + SOFT_CARD_CLASS, + "flex min-w-0 flex-1 flex-col gap-1 px-4 py-3.5", + )} + > + + {metric.label} + + + {formatMetricValue(metric.value, metric.unit)} + + {metric.changeRate !== undefined && ( + = 0 ? "text-info-red" : "text-info-blue", + )} + > + {metric.changeRate >= 0 ? "▲" : "▼"}{" "} + {formatChangeRate(metric.changeRate)} + + )} +
))} - {/* 차트 */} - - } - RightElement={ - {}} - onNextPeriod={() => {}} - /> - } - > -
- - 차트 영역 (ApexChart 연동예정) - -
-
- - {/* 플랫폼 기여 파트 */} -
-

플랫폼 기여 정보

-
- {data.platformShare.map(({ provider, contributionRate }) => ( -
- - {PLATFORM_MAP[provider]} - - + {/* 차트 placeholder */} +
+
+
+
+

일별 변화 추이

+
- ))} + +
+
+ + 차트 영역 (ApexChart 연동예정) + +
+
+
+ + {/* 플랫폼 기여 */} +
+
+

플랫폼 기여 정보

+
+ {data.platformShare.map(({ provider, contributionRate }) => ( + + ))} +
diff --git a/src/styles/utilities.css b/src/styles/utilities.css index c14eabe1..c7e58ac8 100644 --- a/src/styles/utilities.css +++ b/src/styles/utilities.css @@ -68,6 +68,12 @@ 0 4px 20px color-mix(in srgb, var(--color-text-400) 6%, transparent); } + .shadow-Soft-xs { + box-shadow: + 0 1px 2px color-mix(in srgb, var(--color-text-400) 3%, transparent), + 0 2px 8px color-mix(in srgb, var(--color-text-400) 4%, transparent); + } + /* UI 트랜지션 */ .transition-ui-smooth { transition-property: transform, opacity, background-color, border-color, color, diff --git a/src/types/timeline/timeline.mock.ts b/src/types/timeline/timeline.mock.ts index ab86c2bb..30dcc0c4 100644 --- a/src/types/timeline/timeline.mock.ts +++ b/src/types/timeline/timeline.mock.ts @@ -33,7 +33,7 @@ export const TIMELINE_DETAIL_MOCK: ITimelineDetail = { performanceStatus: "ON_TRACK", metrics: ["CLICK", "CONVERSION", "ROAS"], summary: - "Google Ads 전환이 전주 대비 12% 상승했습니다. Meta는 노출은 늘었으나 ROAS가 소폭 하락했습니다.", + "해당 기간 동안 클릭수는 전반적으로 증가하며 평균 이상의 성과를 보였습니다. 특히 1월 23일에 가장 높은 클릭수를 기록했으며, 이후에도 높은 수준을 유지했습니다. 다만 전환수는 후반부로 갈수록 소폭 감소하는 흐름을 보였습니다.", dailyTrend: [ { date: "2026-06-18", @@ -128,7 +128,7 @@ export const TIMELINE_SUMMARY_PANEL_MOCK: ITimelineSummaryPanelData = { metric: "ROAS", label: "ROAS", value: 3.17, - unit: "x", + unit: "배", changeRate: -0.03, }, ], From 7711a21bc240a82e06e9ae0cd12e535db20d7614 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Wed, 24 Jun 2026 05:11:13 +0900 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20=EC=A0=84=EC=B2=B4=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=9D=BC=EB=95=8C=20=EC=B5=9C=EB=8C=80=20=EB=84=88?= =?UTF-8?q?=EB=B9=84=20=EB=8D=94=20=EA=B8=B8=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/timeline/TimelinePerformancePanel.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/timeline/TimelinePerformancePanel.tsx b/src/components/timeline/TimelinePerformancePanel.tsx index f5ec0164..0352227c 100644 --- a/src/components/timeline/TimelinePerformancePanel.tsx +++ b/src/components/timeline/TimelinePerformancePanel.tsx @@ -168,7 +168,12 @@ export default function TimelinePerformancePanel({ ]; return ( - +
{/* 헤더 */}
From 11a6aa6962f323da400d60e9bf425d23c08832e9 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Wed, 24 Jun 2026 05:27:04 +0900 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=9E=98=EB=B9=97?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/dropdownmenu/DropdownMenu.tsx | 8 ++++--- src/components/timeline/TimelineBar.tsx | 8 ++++++- .../TimelinePerformancePanel.stories.tsx | 2 +- .../timeline/TimelinePerformancePanel.tsx | 21 +++++++++++++++---- 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/components/common/dropdownmenu/DropdownMenu.tsx b/src/components/common/dropdownmenu/DropdownMenu.tsx index dcaffd7b..265de431 100644 --- a/src/components/common/dropdownmenu/DropdownMenu.tsx +++ b/src/components/common/dropdownmenu/DropdownMenu.tsx @@ -128,9 +128,11 @@ export function DropdownMenu({
From bdbceeaa17f7c084c3c70c673376bdde5c4c4583 Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Wed, 24 Jun 2026 15:54:37 +0900 Subject: [PATCH 6/7] =?UTF-8?q?style:=20shadow-Soft-xs=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=ED=9B=84=20section=20=EC=99=B8=EA=B3=BD=20border=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/timeline/TimelinePerformancePanel.tsx | 4 ++-- src/styles/utilities.css | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/timeline/TimelinePerformancePanel.tsx b/src/components/timeline/TimelinePerformancePanel.tsx index e1001d22..6dce9c2f 100644 --- a/src/components/timeline/TimelinePerformancePanel.tsx +++ b/src/components/timeline/TimelinePerformancePanel.tsx @@ -29,12 +29,12 @@ const AI_SUMMARY_LOADING_MS = 1500; const CHART_PERIOD_LABELS = ["오늘", "1월 21일 → 25일", "1월 14일 → 20일"]; const SECTION_SHELL_CLASS = - "rounded-3xl border border-surface-300/50 bg-surface-100 shadow-Soft-xs"; + "rounded-3xl border border-surface-300/70 bg-surface-100"; const SECTION_INNER_CLASS = "flex flex-col gap-5 px-6 py-6"; const SOFT_CARD_CLASS = - "rounded-2xl border border-surface-300/40 bg-surface-100 shadow-Soft-xs"; + "rounded-2xl border border-surface-300/70 bg-surface-100"; const PLATFORM_WORDMARKS: Record< TProviderType, diff --git a/src/styles/utilities.css b/src/styles/utilities.css index c7e58ac8..c14eabe1 100644 --- a/src/styles/utilities.css +++ b/src/styles/utilities.css @@ -68,12 +68,6 @@ 0 4px 20px color-mix(in srgb, var(--color-text-400) 6%, transparent); } - .shadow-Soft-xs { - box-shadow: - 0 1px 2px color-mix(in srgb, var(--color-text-400) 3%, transparent), - 0 2px 8px color-mix(in srgb, var(--color-text-400) 4%, transparent); - } - /* UI 트랜지션 */ .transition-ui-smooth { transition-property: transform, opacity, background-color, border-color, color, From 7803b95f7df205091bcb1dfb2a0c390d39868f7d Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Wed, 24 Jun 2026 16:01:23 +0900 Subject: [PATCH 7/7] =?UTF-8?q?style:=20KPI=EC=A7=80=ED=91=9C=EB=A7=8C=20b?= =?UTF-8?q?order=EB=8C=80=EC=8B=A0=20shadow-Soft=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD,=20=EB=82=B4=EB=B6=80=20=ED=85=8D=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=99=BC=EC=AA=BD=20=EB=A7=88=EC=A7=84=201=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../timeline/TimelinePerformancePanel.tsx | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/components/timeline/TimelinePerformancePanel.tsx b/src/components/timeline/TimelinePerformancePanel.tsx index 6dce9c2f..b7e856a8 100644 --- a/src/components/timeline/TimelinePerformancePanel.tsx +++ b/src/components/timeline/TimelinePerformancePanel.tsx @@ -33,8 +33,7 @@ const SECTION_SHELL_CLASS = const SECTION_INNER_CLASS = "flex flex-col gap-5 px-6 py-6"; -const SOFT_CARD_CLASS = - "rounded-2xl border border-surface-300/70 bg-surface-100"; +const SOFT_CARD_CLASS = "rounded-2xl bg-surface-100 shadow-Soft"; const PLATFORM_WORDMARKS: Record< TProviderType, @@ -296,23 +295,27 @@ export default function TimelinePerformancePanel({ "flex min-w-0 flex-1 flex-col gap-1 px-4 py-3.5", )} > - - {metric.label} - - - {formatMetricValue(metric.value, metric.unit)} - - {metric.changeRate !== undefined && ( - = 0 ? "text-info-red" : "text-info-blue", - )} - > - {metric.changeRate >= 0 ? "▲" : "▼"}{" "} - {formatChangeRate(metric.changeRate)} + + + {metric.label} - )} + + {formatMetricValue(metric.value, metric.unit)} + + {metric.changeRate !== undefined && ( + = 0 + ? "text-info-red" + : "text-info-blue", + )} + > + {metric.changeRate >= 0 ? "▲" : "▼"}{" "} + {formatChangeRate(metric.changeRate)} + + )} +
))}