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
20 changes: 20 additions & 0 deletions scripts/strip-cursor-attribution 2.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Cursor 에이전트가 붙이는 Git 메시지 trailer 제거.
* GitHub가 Co-authored-by를 파싱해 Cursor 아바타를 붙이는 것을 막기 위함.
*/
import { readFileSync, writeFileSync } from "node:fs";

const path = process.argv[2];
if (!path) process.exit(0);

let text = readFileSync(path, "utf8");
const next = text
.split("\n")
.filter(
(line) =>
!/^Co-authored-by:\s*Cursor\b/i.test(line) &&
!/^Made-with:\s*Cursor\b/i.test(line),
)
.join("\n");

if (next !== text) writeFileSync(path, next);
9 changes: 9 additions & 0 deletions src/assets/icon/ai/warning 2.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/chevron/chevron-down 2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icon/common/camera 2.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/common/check-white 2.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/common/lock 2.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/common/userProfileCircle 2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icon/timeline/chevron-left 2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icon/timeline/chevron-right 2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/assets/icon/timeline/filter 2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/assets/icon/timeline/kebab 2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/assets/icon/timeline/sort 2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/assets/logo/service-logo/logo 2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/assets/logo/social-logo/plain/meta 2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/mockup/iOS app dock 2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/mockup/iPad Air mockup 2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/mockup/laptop_mockup 2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions src/components/auth/common/AuthFormShell 2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { type ReactNode } from "react";
import { twMerge } from "tailwind-merge";

const shellBase = "mx-auto w-full max-w-130 px-6";

type TAuthFormShellVariant = "page" | "social" | "step";

const variantClasses: Record<TAuthFormShellVariant, string> = {
page: "py-12",
social: "flex flex-col items-center",
step: "pb-12",
};

interface IAuthFormShellProps {
children: ReactNode;
variant?: TAuthFormShellVariant;
className?: string;
}

export default function AuthFormShell({
children,
variant = "step",
className,
}: IAuthFormShellProps) {
return (
<div className={twMerge(shellBase, variantClasses[variant], className)}>
{children}
</div>
);
}
52 changes: 52 additions & 0 deletions src/components/common/ComingSoonPlaceholder 2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { ReactNode } from "react";
import { memo } from "react";
import { twMerge } from "tailwind-merge";

import WarningIcon from "@/assets/icon/ai/warning.svg?react";

export interface IComingSoonPlaceholderProps {
title: ReactNode;
description: ReactNode;
footer: ReactNode;
className?: string;
}

const ComingSoonPlaceholder = memo(function ComingSoonPlaceholder({
title,
description,
footer,
className,
}: IComingSoonPlaceholderProps) {
return (
<div
className={twMerge(
"flex h-full min-h-[70vh] w-full flex-1 flex-col items-center justify-center p-4",
className,
)}
>
<div
className="coming-soon-stagger flex w-full max-w-[420px] flex-col items-center rounded-[32px] bg-white px-8 py-12 text-center shadow-card transition-smooth hover:shadow-card-hover tablet:px-6"
role="status"
aria-live="polite"
>
<div className="mb-6 flex items-center justify-center">
<WarningIcon className="h-30 w-auto shrink-0" aria-hidden />
</div>

<h1 className="mb-4 text-balance font-heading1 font-bold leading-snug tracking-tight text-text-main">
{title}
</h1>

<p className="break-keep font-body1 leading-relaxed text-text-auth-sub">
{description}
</p>

<div className="mt-8 flex w-full items-center justify-center rounded-component-md bg-chart-inactive px-6 py-4">
<p className="font-body1 font-medium text-text-main">{footer}</p>
</div>
</div>
</div>
);
});

export default ComingSoonPlaceholder;
2 changes: 1 addition & 1 deletion src/components/dashboard/charts/BudgetGaugeChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ const BudgetGaugeChart = memo(function BudgetGaugeChart({
</span>
</div>

<div className="mt-auto flex items-center gap-3 rounded-2xl bg-chart-inactive px-5 py-4">
<div className="flex items-center gap-3 rounded-2xl bg-chart-inactive px-5 py-4">
<WarnCircleIcon
className="block size-5 shrink-0 text-text-sub"
aria-hidden="true"
Expand Down
163 changes: 163 additions & 0 deletions src/components/dashboard/platform/AllPlatformTrafficChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { memo, useMemo } from "react";
import ReactApexChart from "react-apexcharts";
import type { ApexOptions } from "apexcharts";

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

import { platformTrafficMock } from "@/pages/dashboard/platform/platformDashboard.mock";

interface IAllPlatformTrafficChartProps {
isLoading?: boolean;
}

const PLATFORM_COLORS = {
GOOGLE: "#f9ab00",
NAVER: "#03c75a",
META: "#1877f2",
};

const AllPlatformTrafficChart = memo(function AllPlatformTrafficChart({
isLoading,
}: IAllPlatformTrafficChartProps) {
// 3개 플랫폼의 데이터를 모두 변환하여 series 구성
const seriesData = useMemo(() => {
const platforms = ["GOOGLE", "NAVER", "META"] as const;

return platforms.map((platform) => {
const data = platformTrafficMock[platform];
return {
name:
platform === "GOOGLE"
? "Google"
: platform === "NAVER"
? "NAVER"
: "Meta",
color: PLATFORM_COLORS[platform],
data: data.timeSeriesData.map((d) => {
const year = parseInt(d.minute.slice(0, 4), 10);
const month = parseInt(d.minute.slice(4, 6), 10) - 1;
const day = parseInt(d.minute.slice(6, 8), 10);
const hour = parseInt(d.minute.slice(8, 10), 10);
const min = parseInt(d.minute.slice(10, 12), 10);
return {
x: new Date(year, month, day, hour, min).getTime(),
y: d.count,
};
}),
};
});
}, []);

// 모든 플랫폼 데이터 중 최대값을 찾아 Y축 범위 계산
const yMax = useMemo(() => {
const allCounts = seriesData.flatMap((s) => s.data.map((d) => d.y));
const maxCount = allCounts.length > 0 ? Math.max(...allCounts) : 0;
if (maxCount <= 0) return 1000;

const magnitude = Math.pow(10, Math.floor(Math.log10(maxCount)));
const unit = magnitude >= 100 ? magnitude : 100;
return Math.ceil((maxCount * 1.2) / unit) * unit;
}, [seriesData]);

// X축 범위 (첫 번째 데이터 기준)
const { xMin, xMax } = useMemo(() => {
const firstSeries = seriesData[0].data;
if (firstSeries.length === 0) return { xMin: undefined, xMax: undefined };
return {
xMin: firstSeries[0].x,
xMax: firstSeries[firstSeries.length - 1].x,
};
}, [seriesData]);

const chartOptions: ApexOptions = {
chart: {
type: "area",
toolbar: { show: false },
zoom: { enabled: false },
fontFamily: "Pretendard",
animations: { enabled: true, speed: 800 },
},
dataLabels: { enabled: false },
stroke: {
curve: "smooth",
width: 2,
},
fill: {
type: "gradient",
gradient: {
shadeIntensity: 1,
opacityFrom: 0.2,
opacityTo: 0.05,
stops: [20, 100],
},
},
markers: { size: 0, hover: { size: 5 } },
xaxis: {
type: "numeric",
min: xMin,
max: xMax,
tickAmount: 4,
labels: {
formatter: (val: string | number) => {
const ts = Number(val);
const d = new Date(ts);
const h = String(d.getHours()).padStart(2, "0");
const m = String(d.getMinutes()).padStart(2, "0");
return `${h}:${m}`;
},
style: { colors: "#8b8b8f", fontSize: "12px" },
},
axisBorder: { show: false },
axisTicks: { show: false },
tooltip: { enabled: false },
},
yaxis: {
min: 0,
max: yMax,
tickAmount: 5,
labels: {
style: { colors: "#8b8b8f", fontSize: "12px" },
formatter: (val) => {
const rounded = Math.round(val);
if (rounded <= 0) return "";
if (rounded < 1000) return rounded.toLocaleString();
return `${Math.round(rounded / 1000)}K`;
},
},
},
grid: {
borderColor: "#f2f4f6",
xaxis: { lines: { show: false } },
yaxis: { lines: { show: true } },
padding: {
left: 16,
right: 24,
},
},
tooltip: {
shared: true, // 여러 플랫폼 동시 비교 가능
intersect: false,
x: { show: false },
y: { formatter: (val) => `${val.toLocaleString()} 클릭` },
theme: "light",
},
legend: { show: false },
};

if (isLoading) {
return <Skeleton className="w-full h-full rounded-xl" />;
}

return (
<div className="w-full h-full min-h-75">
<ReactApexChart
options={chartOptions}
series={seriesData}
type="area"
height="100%"
/>
</div>
);
});

export default AllPlatformTrafficChart;
31 changes: 18 additions & 13 deletions src/components/dashboard/platform/AllPlatformView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import Card from "@/components/common/card/Card";
import ChartLegend from "@/components/common/chart/ChartLegend";
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 {
AdStatusChartSkeleton,
BadgeSkeleton,
PerformanceEfficiencyChartSkeleton,
PlatformDetailCardSkeleton,
TopPerformanceListSkeleton,
TrafficChartSkeleton,
} from "@/components/dashboard/platform/skeleton/PlatformSkeleton";
import TopPerformanceList from "@/components/dashboard/platform/TopPerformanceList";

Expand Down Expand Up @@ -102,18 +102,23 @@ export default function AllPlatformView({ isLoading }: IAllPlatformViewProps) {
</Card>
</div>

{/* 실시간 트래픽 변화 */}
<Card title="실시간 트래픽 변화" className="min-h-125 flex flex-col">
{isLoading ? (
<TrafficChartSkeleton />
) : (
<div className="flex flex-1 flex-col items-center justify-center gap-3">
<Badge variant="running">Coming Soon</Badge>
<p className="text-text-sub font-caption text-center">
실시간 클릭 트래픽 모니터링 기능을 준비하고 있습니다.
</p>
</div>
)}
{/* 플랫폼별 실시간 클릭수 비교 */}
<Card
title="플랫폼별 실시간 클릭수 비교"
className="h-120 flex flex-col"
description={
<ChartLegend
items={[
{ label: "Google", color: "#f9ab00" },
{ label: "NAVER", color: "#03c75a" },
{ label: "Meta", color: "#1877f2" },
]}
/>
}
>
<div className="flex-1 min-h-0">
<AllPlatformTrafficChart isLoading={isLoading} />
</div>
</Card>

{/* 개별 플랫폼 상세 */}
Expand Down
Loading
Loading