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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import { useClickStream } from "@/hooks/dashboard/useClickStream";

import Button from "@/components/common/button/Button";
import { Skeleton } from "@/components/common/skeleton/Skeleton";
import {
PLATFORM_ALL_TRAFFIC_CHART_ID,
PLATFORM_ALL_TRAFFIC_CONTAINER_ID,
} from "@/components/dashboard/platform/platformTrafficChartDownload.config";

const STREAM_MODE = "dummy" as const;

Expand Down Expand Up @@ -93,6 +97,7 @@ const AllPlatformTrafficChart = memo(function AllPlatformTrafficChart() {

const chartOptions: ApexOptions = {
chart: {
id: PLATFORM_ALL_TRAFFIC_CHART_ID,
type: "area",
toolbar: { show: false },
zoom: { enabled: false },
Expand Down Expand Up @@ -211,7 +216,7 @@ const AllPlatformTrafficChart = memo(function AllPlatformTrafficChart() {
</Button>
</div>
)}
<div className="min-h-0 flex-1">
<div id={PLATFORM_ALL_TRAFFIC_CONTAINER_ID} className="min-h-0 flex-1">
<ReactApexChart
options={chartOptions}
series={seriesData}
Expand Down
69 changes: 59 additions & 10 deletions src/components/dashboard/platform/AllPlatformView.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { ReactNode } from "react";

import type { TProviderType } from "@/types/dashboard/provider";
import {
PLATFORM_CHART_COLORS,
PLATFORM_MAP,
Expand All @@ -19,6 +22,7 @@ import AdStatusChart from "@/components/dashboard/charts/AdStatusChart";
import PerformanceEfficiencyChart from "@/components/dashboard/charts/PerformanceEfficiencyChart";
import AllPlatformTrafficChart from "@/components/dashboard/platform/AllPlatformTrafficChart";
import PlatformDetailCard from "@/components/dashboard/platform/PlatformDetailCard";
import PlatformTrafficChartDownload from "@/components/dashboard/platform/PlatformTrafficChartDownload";
import {
AdStatusChartSkeleton,
BadgeSkeleton,
Expand All @@ -28,11 +32,37 @@ import {
} from "@/components/dashboard/platform/skeleton/PlatformSkeleton";
import TopPerformanceList from "@/components/dashboard/platform/TopPerformanceList";

import GoogleLogo from "@/assets/logo/social-logo/circle/google-circle.svg?react";
import MetaLogo from "@/assets/logo/social-logo/circle/meta-circle.svg?react";
import NaverLogo from "@/assets/logo/social-logo/circle/naver-circle.svg?react";

const platformChartLegendItems = PROVIDER_TYPES.map((provider) => ({
label: PLATFORM_MAP[provider],
color: PLATFORM_CHART_COLORS[provider],
}));

const PLATFORM_LOGOS: Record<TProviderType, ReactNode> = {
GOOGLE: <GoogleLogo className="w-10 h-8" />,
NAVER: <NaverLogo className="w-10 h-8" />,
META: <MetaLogo className="w-10 h-8" />,
};

function PlatformDetailErrorCard({ provider }: { provider: TProviderType }) {
return (
<Card className="min-w-0 flex-1 p-7">
<div className="mb-8 flex items-center gap-2">
<div className="shrink-0">{PLATFORM_LOGOS[provider]}</div>
<h3 className="truncate font-heading4 text-text-title">
{PLATFORM_MAP[provider]}
</h3>
</div>
<div className="flex min-h-40 items-center justify-center font-body2 text-text-sub">
데이터를 불러오지 못했습니다.
</div>
</Card>
);
}

export default function AllPlatformView() {
const {
data: roasRankings,
Expand All @@ -47,11 +77,15 @@ export default function AllPlatformView() {
} = usePlatformAdCount();

const {
data: platformPerformance,
data: performanceData,
isLoading: isPerformanceLoading,
isError: isPerformanceError,
} = usePlatformPerformance();

const platformPerformance = performanceData?.platforms;
const failedProviders = performanceData?.failedProviders ?? [];
const hasPartialFailure = failedProviders.length > 0;

return (
<div className="flex flex-col gap-8">
<div className="grid grid-cols-3 tablet:grid-cols-1 gap-6">
Expand Down Expand Up @@ -154,7 +188,14 @@ export default function AllPlatformView() {
표시할 성과 데이터가 없습니다.
</div>
) : (
<PerformanceEfficiencyChart data={platformPerformance} />
<div className="flex min-h-0 flex-1 flex-col">
{hasPartialFailure ? (
<p className="mb-2 font-caption text-text-muted">
일부 플랫폼 성과를 불러오지 못했습니다.
</p>
) : null}
<PerformanceEfficiencyChart data={platformPerformance} />
</div>
)}
</ErrorBoundary>
</Card>
Expand All @@ -165,6 +206,7 @@ export default function AllPlatformView() {
title="플랫폼별 실시간 클릭수 비교"
className="h-120 flex flex-col"
description={<ChartLegend items={platformChartLegendItems} />}
RightElement={<PlatformTrafficChartDownload />}
>
<div className="flex-1 min-h-0">
<ErrorBoundary FallbackComponent={ChartErrorFallback}>
Expand All @@ -176,7 +218,7 @@ export default function AllPlatformView() {
{/* 개별 플랫폼 상세 */}
<ErrorBoundary
FallbackComponent={ChartErrorFallback}
resetKeys={[platformPerformance]}
resetKeys={[platformPerformance, failedProviders]}
>
<div className="grid grid-cols-3 tablet:grid-cols-1 gap-6">
{isPerformanceLoading ? (
Expand All @@ -187,14 +229,21 @@ export default function AllPlatformView() {
<div className="col-span-3 tablet:col-span-1 flex items-center justify-center font-body2 text-text-sub py-16">
데이터를 불러오지 못했습니다.
</div>
) : platformPerformance.length === 0 ? (
<div className="col-span-3 tablet:col-span-1 flex items-center justify-center font-body2 text-text-sub py-16">
표시할 플랫폼 데이터가 없습니다.
</div>
) : (
platformPerformance.map((platform) => (
<PlatformDetailCard key={platform.provider} data={platform} />
))
PROVIDER_TYPES.map((provider) => {
const data = platformPerformance.find(
(platform) => platform.provider === provider,
);
if (data) {
return <PlatformDetailCard key={provider} data={data} />;
}
if (failedProviders.includes(provider)) {
return (
<PlatformDetailErrorCard key={provider} provider={provider} />
);
}
return null;
})
)}
</div>
</ErrorBoundary>
Expand Down
7 changes: 7 additions & 0 deletions src/components/dashboard/platform/PlatformTrafficChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ import { parseMinuteToTimestamp } from "@/utils/dashboard/parseMinuteToTimestamp
import Button from "@/components/common/button/Button";
import { Skeleton } from "@/components/common/skeleton/Skeleton";
import { useAnomalyMarkerPos } from "@/components/dashboard/charts/useAnomalyMarkerPos";
import {
PLATFORM_TRAFFIC_CHART_ID,
PLATFORM_TRAFFIC_CONTAINER_ID,
} from "@/components/dashboard/platform/platformTrafficChartDownload.config";

interface IPlatformTrafficChartProps {
data: IClickStreamItem | null;
Expand Down Expand Up @@ -175,6 +179,7 @@ const PlatformTrafficChart = memo(function PlatformTrafficChart({

const chartOptions: ApexOptions = {
chart: {
id: PLATFORM_TRAFFIC_CHART_ID,
type: "area",
toolbar: { show: false },
zoom: { enabled: false },
Expand Down Expand Up @@ -428,6 +433,7 @@ const PlatformTrafficChart = memo(function PlatformTrafficChart({
) : (
<div
ref={containerRef}
id={PLATFORM_TRAFFIC_CONTAINER_ID}
role="group"
aria-label={CHART_ARIA_LABEL}
data-hide-tooltip={showBubble || undefined}
Expand All @@ -446,6 +452,7 @@ const PlatformTrafficChart = memo(function PlatformTrafficChart({
) : (
<div
ref={containerRef}
id={PLATFORM_TRAFFIC_CONTAINER_ID}
role="group"
aria-label={CHART_ARIA_LABEL}
data-hide-tooltip={showBubble || undefined}
Expand Down
52 changes: 52 additions & 0 deletions src/components/dashboard/platform/PlatformTrafficChartDownload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { DropdownMenu } from "@/components/common/dropdownmenu/DropdownMenu";
import {
getPlatformAllTrafficFilename,
getPlatformTrafficFilename,
getTrafficChartDownloadItems,
PLATFORM_ALL_TRAFFIC_CHART_ID,
PLATFORM_ALL_TRAFFIC_CONTAINER_ID,
PLATFORM_TRAFFIC_CHART_ID,
PLATFORM_TRAFFIC_CONTAINER_ID,
} from "@/components/dashboard/platform/platformTrafficChartDownload.config";

import MoreIcon from "@/assets/icon/common/more.svg?react";

type TPlatformTrafficChartDownloadProps = {
/** 개별 보기: 플랫폼 코드 (GOOGLE 등). 없으면 전체보기 */
platform?: string;
};

export default function PlatformTrafficChartDownload({
platform,
}: TPlatformTrafficChartDownloadProps) {
const chartId = platform
? PLATFORM_TRAFFIC_CHART_ID
: PLATFORM_ALL_TRAFFIC_CHART_ID;
const containerId = platform
? PLATFORM_TRAFFIC_CONTAINER_ID
: PLATFORM_ALL_TRAFFIC_CONTAINER_ID;
const filename = platform
? getPlatformTrafficFilename(platform)
: getPlatformAllTrafficFilename();

return (
<DropdownMenu
aria-label="차트 다운로드"
trigger={
<button
type="button"
className="flex h-8 w-8 items-center justify-center rounded hover:bg-surface-200 transition-ui-fast"
aria-label="다운로드 메뉴 열기"
>
<MoreIcon
width={16}
height={16}
aria-hidden="true"
className="text-text-auth-sub"
/>
</button>
}
items={getTrafficChartDownloadItems(chartId, containerId, filename)}
/>
Comment thread
YermIm marked this conversation as resolved.
Resolved
);
}
87 changes: 55 additions & 32 deletions src/components/dashboard/platform/PlatformViewSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface IPlatformViewSwitcherProps {
onSelectAll: () => void;
layout?: "header" | "mobile";
className?: string;
isPlatformSelectDisabled?: boolean;
}

export default function PlatformViewSwitcher({
Expand All @@ -26,9 +27,19 @@ export default function PlatformViewSwitcher({
onSelectAll,
layout = "header",
className,
isPlatformSelectDisabled = false,
}: IPlatformViewSwitcherProps) {
const isMobileLayout = layout === "mobile";

const platformTriggerClassName = twMerge(
"flex h-12 items-center rounded-2xl",
isMobileLayout ? "w-full min-w-0" : "w-34 shrink-0",
isAllView &&
"border border-surface-400 bg-surface-100 text-text-muted hover:bg-surface-200",
isPlatformSelectDisabled &&
"cursor-not-allowed border border-surface-400 bg-surface-200 text-text-muted hover:bg-surface-200",
);

return (
<div
className={twMerge(
Expand All @@ -53,39 +64,51 @@ export default function PlatformViewSwitcher({
전체보기
</Button>
<div className={isMobileLayout ? "min-w-0" : undefined}>
<DropdownMenu
fullWidth={isMobileLayout}
menuClassName={twMerge("w-34", isMobileLayout && "w-full")}
trigger={
<Button
type="button"
size="small"
variant={!isAllView ? "primary" : "custom"}
className={twMerge(
"flex h-12 items-center rounded-2xl",
isMobileLayout ? "w-full min-w-0" : "w-34 shrink-0",
isAllView &&
"border border-surface-400 bg-surface-100 text-text-muted hover:bg-surface-200",
)}
>
<span
className={twMerge(
"truncate font-body1",
isAllView ? "text-text-muted" : "text-surface-100",
)}
{isPlatformSelectDisabled ? (
<Button
type="button"
size="small"
variant="custom"
disabled
aria-disabled="true"
className={platformTriggerClassName}
>
<span className="truncate font-body1 text-text-muted">
{selectedPlatformLabel}
</span>
<ChevronDownIcon className="ml-2 h-3 w-3 shrink-0 rotate-180 text-text-muted" />
</Button>
) : (
<DropdownMenu
fullWidth={isMobileLayout}
menuClassName={twMerge("w-34", isMobileLayout && "w-full")}
aria-label="플랫폼 선택"
trigger={
<Button
type="button"
size="small"
variant={!isAllView ? "primary" : "custom"}
className={platformTriggerClassName}
>
{selectedPlatformLabel}
</span>
<ChevronDownIcon
className={twMerge(
"ml-2 h-3 w-3 shrink-0 rotate-180 transition-transform",
isAllView ? "text-text-muted" : "text-surface-100",
)}
/>
</Button>
}
items={platformItems}
/>
<span
className={twMerge(
"truncate font-body1",
isAllView ? "text-text-muted" : "text-surface-100",
)}
>
{selectedPlatformLabel}
</span>
<ChevronDownIcon
className={twMerge(
"ml-2 h-3 w-3 shrink-0 rotate-180 transition-transform",
isAllView ? "text-text-muted" : "text-surface-100",
)}
/>
</Button>
}
items={platformItems}
/>
)}
</div>
</div>
);
Expand Down
2 changes: 2 additions & 0 deletions src/components/dashboard/platform/SinglePlatformView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import DashboardAiSummarySection from "@/components/dashboard/ai-report/componen
import BudgetGaugeChart from "@/components/dashboard/charts/BudgetGaugeChart";
import PlatformDetailTable from "@/components/dashboard/platform/PlatformDetailTable";
import PlatformTrafficChart from "@/components/dashboard/platform/PlatformTrafficChart";
import PlatformTrafficChartDownload from "@/components/dashboard/platform/PlatformTrafficChartDownload";
import {
PlatformDualBudgetGaugeSkeleton,
PlatformSingleBudgetGaugeSkeleton,
Expand Down Expand Up @@ -163,6 +164,7 @@ export default function SinglePlatformView({
]}
/>
}
RightElement={<PlatformTrafficChartDownload platform={platform} />}
>
<div
className={twMerge(
Expand Down
Loading
Loading