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
28 changes: 28 additions & 0 deletions src/components/common/error/AreaErrorFallback.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";

import AreaErrorFallback from "./AreaErrorFallback";

const meta: Meta<typeof AreaErrorFallback> = {
title: "Common/Error/AreaErrorFallback",
component: AreaErrorFallback,
args: {
error: new Error("영역을 불러오지 못했습니다."),
resetErrorBoundary: fn(),
},
parameters: { layout: "centered" },
};

export default meta;
type TStory = StoryObj<typeof AreaErrorFallback>;

export const Default: TStory = {};

export const InCard: TStory = {
render: (args) => (
<div className="w-120 rounded-3xl bg-surface-100 p-7 shadow-Soft">
<p className="mb-4 font-heading4 text-text-title">캠페인 목록</p>
<AreaErrorFallback {...args} />
</div>
),
};
36 changes: 36 additions & 0 deletions src/components/common/error/AreaErrorFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { memo } from "react";

import Button from "@/components/common/button/Button";

import type { FallbackProps } from "./ErrorBoundary";

import WarnCircleIcon from "@/assets/icon/common/warn-circle.svg?react";

const AreaErrorFallback = memo(function AreaErrorFallback({
resetErrorBoundary,
}: FallbackProps) {
return (
<div
role="alert"
className="flex min-h-40 w-full flex-1 flex-col items-center justify-center gap-4 rounded-2xl bg-surface-200/40 px-6 py-8 text-center"
>
<WarnCircleIcon
className="h-7 w-7 shrink-0 text-info-red"
aria-hidden="true"
/>
<div className="flex flex-col gap-1">
<p className="font-body1 text-text-title">
이 영역을 불러오지 못했어요
</p>
<p className="font-body2 text-text-muted">
일시적인 오류입니다. 잠시 후 다시 시도해 주세요.
</p>
</div>
<Button variant="outline" size="small" onClick={resetErrorBoundary}>
다시 시도
</Button>
</div>
);
});

export default AreaErrorFallback;
39 changes: 33 additions & 6 deletions src/components/dashboard/platform/AllPlatformTrafficChart.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { memo, useMemo } from "react";
import { memo, useCallback, useMemo } from "react";
import ReactApexChart from "react-apexcharts";
import type { ApexOptions } from "apexcharts";

Expand All @@ -17,6 +17,7 @@ import { parseMinuteToTimestamp } from "@/utils/dashboard/parseMinuteToTimestamp

import { useClickStream } from "@/hooks/dashboard/useClickStream";

import Button from "@/components/common/button/Button";
import { Skeleton } from "@/components/common/skeleton/Skeleton";

const STREAM_MODE = "dummy" as const;
Expand All @@ -37,6 +38,12 @@ const AllPlatformTrafficChart = memo(function AllPlatformTrafficChart() {

const streams = [googleStream, naverStream, metaStream];

const reconnectFailedStreams = useCallback(() => {
if (googleStream.isError) googleStream.reconnect();
if (naverStream.isError) naverStream.reconnect();
if (metaStream.isError) metaStream.reconnect();
}, [googleStream, naverStream, metaStream]);

// 첫 응답도 에러도 없는 스트림이 있으면 로딩
const isLoading = streams.some((s) => s.data == null && !s.isError);

Expand Down Expand Up @@ -159,8 +166,18 @@ const AllPlatformTrafficChart = memo(function AllPlatformTrafficChart() {

if (isAllFailed) {
return (
<div className="flex h-75 items-center justify-center font-body2 text-text-muted">
실시간 데이터를 불러오지 못했습니다.
<div className="flex h-75 flex-col items-center justify-center gap-4 text-center">
<p className="font-body2 text-text-muted">
실시간 데이터를 불러오지 못했습니다.
</p>
<Button
variant="outline"
size="small"
type="button"
onClick={reconnectFailedStreams}
>
다시 시도
</Button>
</div>
);
}
Expand All @@ -180,9 +197,19 @@ const AllPlatformTrafficChart = memo(function AllPlatformTrafficChart() {
return (
<div className="flex h-full min-h-75 w-full flex-col">
{hasPartialError && (
<p className="mb-2 font-caption text-text-muted">
일부 플랫폼 실시간 데이터를 불러오지 못했습니다.
</p>
<div className="mb-2 flex flex-wrap items-center gap-2">
<p className="font-caption text-text-muted">
일부 플랫폼 실시간 데이터를 불러오지 못했습니다.
</p>
<Button
variant="outline"
size="small"
type="button"
onClick={reconnectFailedStreams}
>
다시 시도
</Button>
</div>
)}
<div className="min-h-0 flex-1">
<ReactApexChart
Expand Down
142 changes: 83 additions & 59 deletions src/components/dashboard/platform/AllPlatformView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { usePlatformRoasRankings } from "@/hooks/dashboard/usePlatformRoasRankin
import Badge from "@/components/common/badge/Badge";
import Card from "@/components/common/card/Card";
import ChartLegend from "@/components/common/chart/ChartLegend";
import ChartErrorFallback from "@/components/common/error/ChartErrorFallback";
import { ErrorBoundary } from "@/components/common/error/ErrorBoundary";
import AdStatusChart from "@/components/dashboard/charts/AdStatusChart";
import PerformanceEfficiencyChart from "@/components/dashboard/charts/PerformanceEfficiencyChart";
import AllPlatformTrafficChart from "@/components/dashboard/platform/AllPlatformTrafficChart";
Expand Down Expand Up @@ -63,19 +65,24 @@ export default function AllPlatformView() {
}
className="flex-1 min-h-67 flex flex-col"
>
{isRankingsLoading ? (
<TopPerformanceListSkeleton />
) : isRankingsError || !roasRankings ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
데이터를 불러오지 못했습니다.
</div>
) : roasRankings.length === 0 ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
표시할 순위 데이터가 없습니다.
</div>
) : (
<TopPerformanceList rankings={roasRankings} />
)}
<ErrorBoundary
FallbackComponent={ChartErrorFallback}
resetKeys={[roasRankings]}
>
{isRankingsLoading ? (
<TopPerformanceListSkeleton />
) : isRankingsError || !roasRankings ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
데이터를 불러오지 못했습니다.
</div>
) : roasRankings.length === 0 ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
표시할 순위 데이터가 없습니다.
</div>
) : (
<TopPerformanceList rankings={roasRankings} />
)}
</ErrorBoundary>
</Card>

{/* 광고 소재 현황 */}
Expand All @@ -98,19 +105,24 @@ export default function AllPlatformView() {
}
className="flex-1 min-h-67 flex flex-col"
>
{isAdStatusLoading ? (
<AdStatusChartSkeleton />
) : isAdStatusError || !adStatus ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
데이터를 불러오지 못했습니다.
</div>
) : adStatus.providerCount.length === 0 ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
표시할 광고 소재가 없습니다.
</div>
) : (
<AdStatusChart data={adStatus.providerCount} />
)}
<ErrorBoundary
FallbackComponent={ChartErrorFallback}
resetKeys={[adStatus]}
>
{isAdStatusLoading ? (
<AdStatusChartSkeleton />
) : isAdStatusError || !adStatus ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
데이터를 불러오지 못했습니다.
</div>
) : adStatus.providerCount.length === 0 ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
표시할 광고 소재가 없습니다.
</div>
) : (
<AdStatusChart data={adStatus.providerCount} />
)}
</ErrorBoundary>
</Card>

{/* 플랫폼별 성과 효율 비교 */}
Expand All @@ -127,19 +139,24 @@ export default function AllPlatformView() {
/>
}
>
{isPerformanceLoading ? (
<PerformanceEfficiencyChartSkeleton />
) : isPerformanceError || !platformPerformance ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
데이터를 불러오지 못했습니다.
</div>
) : platformPerformance.length === 0 ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
표시할 성과 데이터가 없습니다.
</div>
) : (
<PerformanceEfficiencyChart data={platformPerformance} />
)}
<ErrorBoundary
FallbackComponent={ChartErrorFallback}
resetKeys={[platformPerformance]}
>
{isPerformanceLoading ? (
<PerformanceEfficiencyChartSkeleton />
) : isPerformanceError || !platformPerformance ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
데이터를 불러오지 못했습니다.
</div>
) : platformPerformance.length === 0 ? (
<div className="flex flex-1 items-center justify-center font-body2 text-text-sub">
표시할 성과 데이터가 없습니다.
</div>
) : (
<PerformanceEfficiencyChart data={platformPerformance} />
)}
</ErrorBoundary>
</Card>
</div>

Expand All @@ -150,30 +167,37 @@ export default function AllPlatformView() {
description={<ChartLegend items={platformChartLegendItems} />}
>
<div className="flex-1 min-h-0">
<AllPlatformTrafficChart />
<ErrorBoundary FallbackComponent={ChartErrorFallback}>
<AllPlatformTrafficChart />
</ErrorBoundary>
</div>
Comment thread
YermIm marked this conversation as resolved.
Resolved
</Card>

{/* 개별 플랫폼 상세 */}
<div className="grid grid-cols-3 tablet:grid-cols-1 gap-6">
{isPerformanceLoading ? (
Array.from({ length: 3 }).map((_, i) => (
<PlatformDetailCardSkeleton key={i} />
))
) : isPerformanceError || !platformPerformance ? (
<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} />
))
)}
</div>
<ErrorBoundary
FallbackComponent={ChartErrorFallback}
resetKeys={[platformPerformance]}
>
<div className="grid grid-cols-3 tablet:grid-cols-1 gap-6">
{isPerformanceLoading ? (
Array.from({ length: 3 }).map((_, i) => (
<PlatformDetailCardSkeleton key={i} />
))
) : isPerformanceError || !platformPerformance ? (
<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} />
))
)}
</div>
</ErrorBoundary>
</div>
);
}
37 changes: 32 additions & 5 deletions src/components/dashboard/platform/PlatformTrafficChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "@/utils/dashboard/metricRegistry";
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";

Expand All @@ -23,6 +24,7 @@ interface IPlatformTrafficChartProps {
platform: TProviderType;
isError?: boolean;
suspectDetail: IClickStreamItem["suspectDetail"] | null;
onRetry?: () => void;
}

// 이상 징후 상세 버블
Expand Down Expand Up @@ -75,6 +77,7 @@ const PlatformTrafficChart = memo(function PlatformTrafficChart({
platform,
isError = false,
suspectDetail = null,
onRetry,
}: IPlatformTrafficChartProps) {
const containerRef = useRef<HTMLDivElement>(null);
const anomalyBubbleId = useId();
Expand Down Expand Up @@ -254,8 +257,20 @@ const PlatformTrafficChart = memo(function PlatformTrafficChart({

if (isError && !data) {
return (
<div className="flex h-75 items-center justify-center font-body2 text-text-muted">
실시간 데이터를 불러오지 못했습니다.
<div className="flex h-75 flex-col items-center justify-center gap-4 text-center">
<p className="font-body2 text-text-muted">
실시간 데이터를 불러오지 못했습니다.
</p>
{onRetry ? (
<Button
variant="outline"
size="small"
type="button"
onClick={onRetry}
>
다시 시도
</Button>
) : null}
</div>
);
}
Expand All @@ -275,9 +290,21 @@ const PlatformTrafficChart = memo(function PlatformTrafficChart({
return (
<div className="flex h-full min-h-75 w-full flex-col">
{isError && (
<p className="mb-2 font-caption text-text-muted">
연결이 원활하지 않아 마지막 데이터를 표시합니다.
</p>
<div className="mb-2 flex flex-wrap items-center gap-2">
<p className="font-caption text-text-muted">
연결이 원활하지 않아 마지막 데이터를 표시합니다.
</p>
{onRetry ? (
<Button
variant="outline"
size="small"
type="button"
onClick={onRetry}
>
다시 시도
</Button>
) : null}
</div>
)}
<div
ref={containerRef}
Expand Down
Loading
Loading