+
) : (
diff --git a/src/components/landing/CTASection 2.tsx b/src/components/landing/CTASection 2.tsx
new file mode 100644
index 00000000..079c5327
--- /dev/null
+++ b/src/components/landing/CTASection 2.tsx
@@ -0,0 +1,3 @@
+export default function CTASection() {
+ return <>>;
+}
diff --git a/src/components/landing/FeatureSection 2.tsx b/src/components/landing/FeatureSection 2.tsx
new file mode 100644
index 00000000..fb9ede34
--- /dev/null
+++ b/src/components/landing/FeatureSection 2.tsx
@@ -0,0 +1,3 @@
+export default function FeatureSection() {
+ return <>>;
+}
diff --git a/src/components/landing/GuideOverviewChart 2.tsx b/src/components/landing/GuideOverviewChart 2.tsx
new file mode 100644
index 00000000..be1feac3
--- /dev/null
+++ b/src/components/landing/GuideOverviewChart 2.tsx
@@ -0,0 +1,144 @@
+import { lazy, Suspense } from "react";
+import type { ApexOptions } from "apexcharts";
+
+import Card from "@/components/common/card/Card";
+import ChartLegend from "@/components/common/chart/ChartLegend";
+
+const ReactApexChart = lazy(() => import("react-apexcharts"));
+
+const splitIndex = 12;
+const clicks = [
+ 29600, 31100, 31600, 32100, 33600, 37100, 44600, 46100, 46600, 45600, 49600,
+ 52600, 53600, 51600, 47600, 45600, 43600, 42600, 45100, 40600, 37600, 35600,
+ 41600, 47600, 50600,
+];
+const peak = Math.max(...clicks);
+const targetPeak = 48500;
+const normalizedClicks = clicks.map((value) =>
+ Math.round((value / peak) * targetPeak),
+);
+
+const actualSeries = normalizedClicks.map((y, i) =>
+ i <= splitIndex ? { x: i, y } : { x: i, y: null },
+);
+const projectedSeries = normalizedClicks.map((y, i) =>
+ i >= splitIndex ? { x: i, y } : { x: i, y: null },
+);
+
+const options: ApexOptions = {
+ chart: {
+ type: "line",
+ toolbar: { show: false },
+ zoom: { enabled: false },
+ animations: { enabled: false },
+ fontFamily: "Pretendard",
+ },
+ dataLabels: { enabled: false },
+ stroke: {
+ curve: "smooth",
+ width: [3.5, 3.2],
+ dashArray: [0, 6],
+ lineCap: "round",
+ },
+ colors: ["var(--color-logo-2)", "var(--color-brand-500)"],
+ markers: { size: [0, 0] },
+ tooltip: { enabled: false },
+ xaxis: {
+ type: "numeric",
+ min: 0,
+ max: 24,
+ tickAmount: 24,
+ labels: { show: false },
+ axisBorder: { show: false },
+ axisTicks: { show: false },
+ },
+ yaxis: {
+ min: 0,
+ max: 50000,
+ tickAmount: 6,
+ labels: {
+ formatter: (val: number) =>
+ val === 0 ? "" : `${(val / 1000).toFixed(0)}K`,
+ style: { colors: "var(--color-text-sub)", fontSize: "10px" },
+ offsetX: -2,
+ },
+ },
+ grid: {
+ borderColor: "var(--color-bg-disabled)",
+ strokeDashArray: 5,
+ xaxis: { lines: { show: false } },
+ yaxis: { lines: { show: true } },
+ padding: { left: 12, right: 8 },
+ },
+ legend: { show: false },
+ annotations: {
+ xaxis: [
+ {
+ x: splitIndex,
+ borderColor: "var(--color-brand-500)",
+ strokeDashArray: 0,
+ },
+ ],
+ points: [
+ {
+ x: splitIndex,
+ y: normalizedClicks[splitIndex],
+ marker: {
+ size: 5,
+ fillColor: "var(--color-logo-2)",
+ strokeColor: "var(--color-logo-2)",
+ },
+ },
+ ],
+ },
+};
+
+const series = [
+ { name: "클릭수", data: actualSeries },
+ { name: "예측 클릭수", data: projectedSeries },
+];
+
+export default function GuideOverviewChart() {
+ return (
+
+
+ }
+ >
+
+ }
+ >
+
+
+
+ 광고 클릭수 추이
+
+
+ 오후 12시 기준 클릭수 48,500
+
+
+ 전시간 대비 +1.9%
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/landing/GuidePlatform 2.tsx b/src/components/landing/GuidePlatform 2.tsx
new file mode 100644
index 00000000..ec056fd3
--- /dev/null
+++ b/src/components/landing/GuidePlatform 2.tsx
@@ -0,0 +1,123 @@
+import type { ReactNode } from "react";
+import { useState } from "react";
+
+import ChevronDown from "@/assets/icon/chevron/chevron-down.svg?react";
+import GoogleAdsPlain from "@/assets/logo/social-logo/plain/google_ads.png";
+import MetaPlain from "@/assets/logo/social-logo/plain/meta.svg?react";
+import GoogleWordmark from "@/assets/logo/social-logo/wordmark/google-wordmark.svg?react";
+import KakaoWordmark from "@/assets/logo/social-logo/wordmark/kakao-wordmark.svg?react";
+import NaverWordmarkPng from "@/assets/logo/social-logo/wordmark/naver-wordmark.png";
+
+type TPlatform = {
+ id: string;
+ label: string;
+ content: ReactNode;
+ offsetClass?: string;
+};
+
+const PLATFORMS: TPlatform[] = [
+ {
+ id: "naver",
+ label: "Naver",
+ content: (
+
+ ),
+ },
+ {
+ id: "kakao",
+ label: "Kakao",
+ content:
,
+ },
+ {
+ id: "google",
+ label: "Google",
+ content:
,
+ },
+ {
+ id: "meta",
+ label: "Meta",
+ content:
,
+ offsetClass: "-ml-2",
+ },
+ {
+ id: "googleads",
+ label: "Google Ads",
+ content: (
+
+ ),
+ offsetClass: "-ml-2",
+ },
+];
+
+export default function GuidePlatform() {
+ const [isMenuOpen, setIsMenuOpen] = useState(true);
+ const [selectedIds, setSelectedIds] = useState
([
+ "googleads",
+ "meta",
+ "naver",
+ ]);
+
+ function togglePlatform(id: string) {
+ setSelectedIds((prev) =>
+ prev.includes(id) ? prev.filter((value) => value !== id) : [...prev, id],
+ );
+ }
+
+ return (
+
+
+
setIsMenuOpen((prev) => !prev)}
+ aria-expanded={isMenuOpen}
+ className="h-11 w-full rounded-xl px-4 border border-chart-inactive/70 bg-white text-[13px] font-semibold text-text-main flex items-center justify-between hover:bg-brand-300/40 transition-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-logo-2/30"
+ >
+ 플랫폼 선택
+
+
+
+
+
+ {isMenuOpen && (
+
+
+ {PLATFORMS.map((platform) => {
+ const isSelected = selectedIds.includes(platform.id);
+ return (
+
togglePlatform(platform.id)}
+ className={`w-full h-14 px-4 flex items-center justify-start border-b last:border-b-0 transition-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-logo-2/25 focus-visible:ring-inset ${
+ isSelected
+ ? "bg-brand-300/22 border-chart-inactive/40"
+ : "bg-white hover:bg-brand-300/25 border-chart-inactive/40"
+ }`}
+ >
+
+ {platform.content}
+
+ {platform.label}
+
+ );
+ })}
+
+
+ )}
+
+
+ );
+}
diff --git a/src/components/landing/GuideTimeline 2.tsx b/src/components/landing/GuideTimeline 2.tsx
new file mode 100644
index 00000000..5ebf4d06
--- /dev/null
+++ b/src/components/landing/GuideTimeline 2.tsx
@@ -0,0 +1,175 @@
+import ChevronLeftIcon from "@/assets/icon/timeline/chevron-left.svg?react";
+import ChevronRightIcon from "@/assets/icon/timeline/chevron-right.svg?react";
+import FilterIcon from "@/assets/icon/timeline/filter.svg?react";
+import KebabIcon from "@/assets/icon/timeline/kebab.svg?react";
+import SortIcon from "@/assets/icon/timeline/sort.svg?react";
+
+const columns = [
+ { day: "M", date: 30, isWeekend: false },
+ { day: "T", date: 31, isWeekend: false },
+ { day: "W", date: 1, isWeekend: false },
+ { day: "T", date: 2, isWeekend: false, isToday: true },
+ { day: "F", date: 3, isWeekend: false },
+ { day: "S", date: 4, isWeekend: true },
+ { day: "S", date: 5, isWeekend: true },
+ { day: "M", date: 6, isWeekend: false },
+ { day: "T", date: 7, isWeekend: false },
+ { day: "W", date: 8, isWeekend: false },
+ { day: "T", date: 9, isWeekend: false },
+ { day: "F", date: 10, isWeekend: false },
+ { day: "S", date: 11, isWeekend: true },
+ { day: "S", date: 12, isWeekend: true },
+ { day: "M", date: 13, isWeekend: false },
+ { day: "T", date: 14, isWeekend: false },
+ { day: "W", date: 15, isWeekend: false },
+];
+
+const cards = [
+ {
+ id: 1,
+ title: "봄 프로모션 캠페인",
+ subtitle: "Google Ads · 전환",
+ colStart: 3.2,
+ colEnd: 9,
+ row: 1,
+ colorClass: "bg-status-blue",
+ },
+ {
+ id: 2,
+ title: "리타겟팅 캠페인",
+ subtitle: "Meta · 트래픽",
+ colStart: 8.9,
+ colEnd: 15.3,
+ row: 2,
+ colorClass: "bg-logo-2",
+ },
+ {
+ id: 3,
+ title: "브랜드 검색 캠페인",
+ subtitle: "Naver · 검색",
+ colStart: 2.4,
+ colEnd: 8,
+ row: 3,
+ colorClass: "bg-status-green",
+ },
+];
+
+export default function GuideTimeline() {
+ const colWidth = 55;
+ const rowHeight = 92;
+ const rowOffset = 24;
+ const totalWidth = columns.length * colWidth;
+
+ return (
+
+ {/* Top Navigation */}
+
+
+
+ Day
+
+
+ Week
+
+
+ Month
+
+
+
+
+
+
+
+ 27 Dec - 4 Jan
+
+
+
+
+
+
+
+
+ Sort
+
+
+
+ Filter
+
+
+
+
+ {/* Chart Area */}
+
+
+ {/* Header (Dates) */}
+
+ {columns.map((c, i) => (
+
+
+ {c.day} {c.date}
+
+
+ ))}
+
+
+ {/* Timeline Body Grid */}
+
+ {/* Columns Background */}
+ {columns.map((_, i) => (
+
+ ))}
+
+ {/* Today Line removed for cleaner mock */}
+
+ {/* Cards */}
+ {cards.map((card) => {
+ const x = (card.colStart - 1) * colWidth;
+ const width = (card.colEnd - card.colStart) * colWidth;
+ const y = rowOffset + (card.row - 1) * rowHeight;
+
+ return (
+
+ {/* Left Indicator */}
+
+
+ {/* Text */}
+
+
+ {card.title}
+
+
+ {card.subtitle}
+
+
+
+ {/* Menu */}
+
+
+
+
+
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/src/components/landing/HeroSection 2.tsx b/src/components/landing/HeroSection 2.tsx
new file mode 100644
index 00000000..5dd32060
--- /dev/null
+++ b/src/components/landing/HeroSection 2.tsx
@@ -0,0 +1,330 @@
+import { useEffect, useRef, useState } from "react";
+import { motion, useAnimation } from "framer-motion";
+
+import { LANDING_META } from "@/constants/landing";
+
+import { staggerContainer, staggerItem } from "@/hooks/useScrollAnimation";
+
+// ─── 숫자 카운터 훅 ──────────────────────────────────────────────────────────
+function useCounter(target: number, duration = 1600, delay = 0) {
+ const [count, setCount] = useState(0);
+ const rafRef = useRef(0);
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ const startTime = performance.now();
+ const tick = (now: number) => {
+ const progress = Math.min((now - startTime) / duration, 1);
+ const eased = 1 - Math.pow(1 - progress, 4); // easeOutQuart
+ setCount(Math.floor(eased * target));
+ if (progress < 1) rafRef.current = requestAnimationFrame(tick);
+ };
+ rafRef.current = requestAnimationFrame(tick);
+ }, delay);
+
+ return () => {
+ clearTimeout(timer);
+ cancelAnimationFrame(rafRef.current);
+ };
+ }, [target, duration, delay]);
+
+ return count;
+}
+
+// ─── 채널 카드 ───────────────────────────────────────────────────────────────
+interface IChannelCardProps {
+ channel: string;
+ metric: string;
+ target: number;
+ color: string;
+ dot: string;
+ delay: number;
+}
+
+function ChannelCard({
+ channel,
+ metric,
+ target,
+ color,
+ dot,
+ delay,
+}: IChannelCardProps) {
+ // 목업 진입(0.3s delay + 0.8s duration) 이후 시작
+ const count = useCounter(target, 1600, 1100 + delay);
+
+ return (
+
+
+
+
+ {channel}
+
+
+
{metric}
+
+ {count.toLocaleString()}
+
+
+
+
+
+ );
+}
+
+// ─── 대시보드 목업 ────────────────────────────────────────────────────────────
+const CHANNEL_CARDS: IChannelCardProps[] = [
+ {
+ channel: "Google Ads",
+ metric: "오늘 클릭수",
+ target: 12847,
+ color: "bg-blue-600/30 border-blue-500/30",
+ dot: "bg-blue-400",
+ delay: 0,
+ },
+ {
+ channel: "Meta",
+ metric: "노출 횟수",
+ target: 98532,
+ color: "bg-purple-600/30 border-purple-500/30",
+ dot: "bg-purple-400",
+ delay: 150,
+ },
+ {
+ channel: "Naver",
+ metric: "전환 수",
+ target: 3241,
+ color: "bg-emerald-600/30 border-emerald-500/30",
+ dot: "bg-emerald-400",
+ delay: 300,
+ },
+];
+
+function DashboardMockup() {
+ return (
+
+ {/* 브라우저 크롬 바 */}
+
+
+
+
+
+ app.whereyouad.com/dashboard
+
+
+
+
+ {/* 상단 레이블 */}
+
+
통합 성과 개요
+
+
+ LIVE
+
+
+
+ {/* 채널 카드 */}
+
+ {CHANNEL_CARDS.map((card) => (
+
+ ))}
+
+
+ {/* 미니 SVG 차트 */}
+
+
클릭 추이 (최근 24h)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// ─── HeroSection ─────────────────────────────────────────────────────────────
+export default function HeroSection() {
+ const textControls = useAnimation();
+ const mockupControls = useAnimation();
+
+ useEffect(() => {
+ const run = async () => {
+ await textControls.start("visible");
+ mockupControls.start("visible");
+ };
+ run();
+ }, [textControls, mockupControls]);
+
+ return (
+
+ {/* 배경 도트 패턴 */}
+
+ {/* 배경 글로우 블롭 */}
+
+
+ {/* ── 텍스트 블록 ── */}
+
+ {/* 뱃지 */}
+
+
+
+ 실시간 광고 모니터링 플랫폼
+
+
+
+ {/* 헤드라인 */}
+
+ {LANDING_META.tagline}
+
+
+ {/* 서브 카피 */}
+
+ {LANDING_META.subCopy.split("\n").map((line, i) => (
+
+ {line}
+ {i < LANDING_META.subCopy.split("\n").length - 1 && }
+
+ ))}
+
+
+ {/* CTA 버튼 */}
+
+
+ 무료로 시작하기
+
+
+
+ 데모 보기
+
+
+
+
+ {/* ── 대시보드 목업 ── */}
+
+ {/* float 루프 */}
+
+
+
+
+ {/* 하단 페이드 아웃 */}
+
+
+
+ {/* ── 스크롤 인디케이터 ── */}
+
+
+ Scroll
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/landing/HowItWorksSection 2.tsx b/src/components/landing/HowItWorksSection 2.tsx
new file mode 100644
index 00000000..174b37bb
--- /dev/null
+++ b/src/components/landing/HowItWorksSection 2.tsx
@@ -0,0 +1,282 @@
+import { useEffect, useRef, useState } from "react";
+import { useInView } from "react-intersection-observer";
+import type { Variants } from "framer-motion";
+import { motion, useAnimation } from "framer-motion";
+
+import { HOW_IT_WORKS_STEPS } from "@/constants/landing";
+
+import { staggerContainer } from "@/hooks/useScrollAnimation";
+
+// ─── 스텝별 아이콘 ────────────────────────────────────────────────────────────
+const STEP_ICONS = [
+ // 광고 채널 연결 — 플러그/링크
+ ({ active }: { active: boolean }) => (
+
+
+
+
+ ),
+ // 트래킹 URL 발급 — 코드/슬래시
+ ({ active }: { active: boolean }) => (
+
+
+
+
+ ),
+ // 클릭 이벤트 수집 — 번개
+ ({ active }: { active: boolean }) => (
+
+
+
+ ),
+ // 통합 모니터링 — 바 차트
+ ({ active }: { active: boolean }) => (
+
+
+
+
+
+
+ ),
+] as const;
+
+// ─── 점선 연결선 ──────────────────────────────────────────────────────────────
+function ConnectorLine({ animate: shouldAnimate }: { animate: boolean }) {
+ return (
+
+
+
+ {/* 화살표 머리 */}
+
+
+
+ );
+}
+
+// ─── 스텝 카드 ────────────────────────────────────────────────────────────────
+interface IStepCardProps {
+ step: (typeof HOW_IT_WORKS_STEPS)[number];
+ index: number;
+ active: boolean;
+}
+
+const stepVariant: Variants = {
+ hidden: { opacity: 0, x: -24 },
+ visible: (i: number) => ({
+ opacity: 1,
+ x: 0,
+ transition: { duration: 0.5, ease: "easeOut" as const, delay: i * 0.2 },
+ }),
+};
+
+function StepCard({ step, index, active }: IStepCardProps) {
+ const Icon = STEP_ICONS[index];
+
+ return (
+
+ {/* 아이콘 + 번호 원 */}
+
+
+
+
+ {/* 번호 뱃지 */}
+
+ {String(index + 1).padStart(2, "0")}
+
+
+
+ {/* 텍스트 */}
+
+
+ {step.title}
+
+
+ {step.description}
+
+
+
+ );
+}
+
+// ─── HowItWorksSection ────────────────────────────────────────────────────────
+export default function HowItWorksSection() {
+ const controls = useAnimation();
+ const [sectionRef, inView] = useInView({ threshold: 0.2, triggerOnce: true });
+ const [activeStep, setActiveStep] = useState(0);
+ const intervalRef = useRef>(null);
+
+ useEffect(() => {
+ if (inView) {
+ controls.start("visible");
+ // 뷰포트 진입 후 스텝 순차 하이라이트
+ intervalRef.current = setInterval(() => {
+ setActiveStep((prev) => (prev + 1) % HOW_IT_WORKS_STEPS.length);
+ }, 2200);
+ }
+ return () => {
+ if (intervalRef.current) clearInterval(intervalRef.current);
+ };
+ }, [inView, controls]);
+
+ return (
+
+ {/* 배경 */}
+
+
+
+ {/* 섹션 헤더 */}
+
+
+ How it works
+
+
+ 3분이면 시작할 수 있어요
+
+
+ 복잡한 설정 없이, 채널 연결만 하면 데이터가 바로 흐릅니다.
+
+
+
+ {/* 스텝 + 연결선 */}
+
+ {HOW_IT_WORKS_STEPS.map((step, i) => (
+
+
setActiveStep(i)}
+ style={{ cursor: "pointer" }}
+ >
+
+
+ {i < HOW_IT_WORKS_STEPS.length - 1 && (
+
i} />
+ )}
+
+ ))}
+
+
+ {/* 진행 바 */}
+
+ {HOW_IT_WORKS_STEPS.map((_, i) => (
+ setActiveStep(i)}
+ className="h-1 overflow-hidden rounded-full bg-white/10 transition-all duration-300"
+ style={{ width: activeStep === i ? 32 : 8 }}
+ >
+ {activeStep === i && (
+
+ )}
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/landing/LandingFAQ 2.tsx b/src/components/landing/LandingFAQ 2.tsx
new file mode 100644
index 00000000..517d339a
--- /dev/null
+++ b/src/components/landing/LandingFAQ 2.tsx
@@ -0,0 +1,61 @@
+import LandingSectionHeader from "@/components/landing/LandingSectionHeader";
+
+import ChevronDown from "@/assets/icon/chevron/chevron-down.svg?react";
+import ChevronUp from "@/assets/icon/chevron/chevron-up.svg?react";
+
+type TFAQItem = { q: string; a: string };
+
+const FAQ_ITEMS: TFAQItem[] = [
+ {
+ q: "무료로 시작할 수 있나요?",
+ a: "네. 스타터 플랜으로 주요 기능을 바로 체험할 수 있습니다.",
+ },
+ {
+ q: "어떤 광고 채널을 연동할 수 있나요?",
+ a: "Google, Meta, 카카오, 네이버 등 주요 채널을 지원합니다. (추가 채널은 순차 확대 예정입니다.)",
+ },
+ {
+ q: "요금은 언제부터 결제되나요?",
+ a: "프로 플랜은 무료 체험 이후에 결제가 시작됩니다. 자세한 내용은 요금제에서 확인하세요.",
+ },
+ {
+ q: "엔터프라이즈는 어떤 기능이 포함되나요?",
+ a: "조직/권한 관리, 보안 옵션, 전용 지원 등 규모에 맞춘 기능을 제공합니다.",
+ },
+ {
+ q: "데모를 받을 수 있나요?",
+ a: "네. 엔터프라이즈 문의를 통해 데모 일정을 조율할 수 있습니다.",
+ },
+];
+
+export default function LandingFAQ() {
+ return (
+
+
+
+
+
+
+
+ {FAQ_ITEMS.map(({ q, a }) => (
+
+
+ {q}
+
+
+
+
+
+
+ {a}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/landing/LandingFeatures 2.tsx b/src/components/landing/LandingFeatures 2.tsx
new file mode 100644
index 00000000..b4dcfee9
--- /dev/null
+++ b/src/components/landing/LandingFeatures 2.tsx
@@ -0,0 +1,256 @@
+import type { ReactNode } from "react";
+import { useEffect, useRef, useState } from "react";
+import { motion, useInView } from "framer-motion";
+
+import LandingSectionHeader from "@/components/landing/LandingSectionHeader";
+
+import SparkleIcon from "@/assets/icon/ai/sparkle.svg?react";
+import ChevronRight from "@/assets/icon/chevron/chevron-right.svg?react";
+import ChevronUp from "@/assets/icon/chevron/chevron-up.svg?react";
+import UserIcon from "@/assets/icon/common/user.svg?react";
+import GoogleAdsLogo from "@/assets/logo/social-logo/circle/googleAds-circle.svg?react";
+import KakaoLogo from "@/assets/logo/social-logo/circle/kakao-circle.svg?react";
+import NaverLogo from "@/assets/logo/social-logo/circle/naver-circle.svg?react";
+
+type TFeatureCardProps = {
+ delay: number;
+ title: string;
+ description: string;
+ Graphic: () => ReactNode;
+};
+
+function IntegrationGraphic() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Google Ads
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function WorkflowGraphic() {
+ return (
+
+
+
+
+ AI로 요약하기
+
+
+
+
+ 다운로드
+
+
+ );
+}
+
+function WorkspaceGraphic() {
+ const fullText = "WhereYouAd@email.com";
+ const [typedText, setTypedText] = useState("");
+ const [showCursor, setShowCursor] = useState(false);
+ const containerRef = useRef(null);
+ const isInView = useInView(containerRef, { once: true, amount: 0.45 });
+
+ useEffect(() => {
+ if (!isInView) return;
+
+ setShowCursor(true);
+ let index = 0;
+ const timer = setInterval(() => {
+ index += 1;
+ setTypedText(fullText.slice(0, index));
+ if (index >= fullText.length) {
+ clearInterval(timer);
+ setShowCursor(false);
+ }
+ }, 40);
+
+ return () => clearInterval(timer);
+ }, [isInView]);
+
+ return (
+
+
+
+
+
+
+
+ You
+
+ Team JSON
+
+
+
+ 새로운 멤버를 초대하세요!
+
+
+
+
+
+
+ {typedText}
+
+ {showCursor && (
+
|
+ )}
+
+
+
+
+
+ );
+}
+
+export default function LandingFeatures() {
+ const featureCards: Omit[] = [
+ {
+ title: "광고 매체 연동",
+ description:
+ "클릭 한 번으로 모든 광고 매체와 마테크 서비스의 데이터를 간편하게 연동하세요.",
+ Graphic: IntegrationGraphic,
+ },
+ {
+ title: "AI 기반 워크플로우 제안",
+ description:
+ "데이터를 바탕으로 최적의 다음 액션과 워크플로우를 자동으로 제안받으세요.",
+ Graphic: WorkflowGraphic,
+ },
+ {
+ title: "조직을 이어주는 워크스페이스",
+ description:
+ "에이전시 및 팀원들과 캠페인 맥락을 잃지 않고 한 곳에서 효율적으로 소통하세요.",
+ Graphic: WorkspaceGraphic,
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+ {featureCards.map(({ title, description, Graphic }, idx) => (
+
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/landing/LandingFooter 2.tsx b/src/components/landing/LandingFooter 2.tsx
new file mode 100644
index 00000000..8de23428
--- /dev/null
+++ b/src/components/landing/LandingFooter 2.tsx
@@ -0,0 +1,44 @@
+export default function LandingFooter() {
+ return (
+
+
+
+
+
+ © 2026 WhereYouAd. All rights reserved.
+
+
+
+
+ );
+}
diff --git a/src/components/landing/LandingGuide 2.tsx b/src/components/landing/LandingGuide 2.tsx
new file mode 100644
index 00000000..47ee5a9b
--- /dev/null
+++ b/src/components/landing/LandingGuide 2.tsx
@@ -0,0 +1,218 @@
+import { motion } from "framer-motion";
+
+import GuideOverviewChart from "@/components/landing/GuideOverviewChart";
+import GuidePlatform from "@/components/landing/GuidePlatform";
+import GuideTimeline from "@/components/landing/GuideTimeline";
+import LandingSectionHeader from "@/components/landing/LandingSectionHeader";
+
+import TimelineDashboard from "@/assets/mockup/optimized/timeline_dashboard.jpg";
+
+type TGuideStep = { step: number; title: string; text: string };
+
+type TGuidePage = {
+ number: string;
+ label: string;
+ title: string;
+ description: string;
+ steps: TGuideStep[];
+ image?: string;
+ alt?: string;
+ reverse: boolean;
+ useOverview?: boolean;
+ useTimeline?: boolean;
+ usePlatform?: boolean;
+};
+
+const pages: TGuidePage[] = [
+ {
+ number: "01",
+ label: "통합 대시보드",
+ title: "광고 현황을 한눈에 파악하세요",
+ description: `주요 KPI부터 채널별 성과, 실시간 알림까지
+모든 정보를 하나의 화면에서 확인할 수 있습니다.`,
+ steps: [
+ {
+ step: 1,
+ title: "핵심 지표를 즉시 확인",
+ text: "상단 요약 카드에서 노출수·클릭수·전환율 등 핵심 KPI를 빠르게 확인합니다.",
+ },
+ {
+ step: 2,
+ title: "채널별 효율 비교",
+ text: "채널별 성과 차트로 매체 간 효율을 비교하고 예산 재배분을 결정합니다.",
+ },
+ ],
+ useOverview: true,
+ reverse: false,
+ },
+ {
+ number: "02",
+ label: "매체 통합 관리",
+ title: "플랫폼별 캠페인을 한 곳에서 관리하세요",
+ description: `다수의 광고 매체를 별도 로그인 없이 통합 관리하고
+캠페인 설정부터 소재 심사까지 원스톱으로 처리합니다.`,
+ steps: [
+ {
+ step: 1,
+ title: "플랫폼 선택 후 현황 조회",
+ text: "매체사 목록에서 플랫폼을 선택해 캠페인 전체 현황을 확인합니다.",
+ },
+ {
+ step: 2,
+ title: "인라인으로 빠르게 편집",
+ text: "예산·기간·타겟 설정을 인라인 편집으로 빠르게 수정하고 저장합니다.",
+ },
+ ],
+ usePlatform: true,
+ reverse: true,
+ },
+ {
+ number: "03",
+ label: "일정 관리 타임라인",
+ title: "타임라인으로 캠페인 일정을 계획하세요",
+ description: `간트 차트 방식으로 전체 캠페인 기간을 시각화하고
+기간별 성과를 비교합니다.`,
+ steps: [
+ {
+ step: 1,
+ title: "캠페인을 한눈에 파악",
+ text: "타임라인 뷰에서 캠페인 일정을 주·월 단위로 한눈에 확인합니다.",
+ },
+ {
+ step: 2,
+ title: "기간별 성과 세부 확인",
+ text: "타임라인 바를 클릭하면 해당 기간의 클릭수, 전환 등 세부 성과 지표를 확인할 수 있습니다.",
+ },
+ ],
+ image: TimelineDashboard,
+ alt: "Timeline 대시보드 화면",
+ useTimeline: true,
+ reverse: false,
+ },
+];
+
+export default function LandingGuide() {
+ return (
+
+
+
+
+
+
+
+
+ {pages.map((page) => (
+
+
+ {page.useOverview ? (
+
+
+
+ ) : page.useTimeline ? (
+
+
+
+ ) : page.usePlatform ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {page.number}
+
+
+ {page.label}
+
+
+
+
+ {page.title}
+
+
+
+ {page.description}
+
+
+
+ {page.steps.map((item) => (
+
+
+
+ {item.title}
+
+
+
+ {item.text}
+
+
+ ))}
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/landing/LandingHeader 2.tsx b/src/components/landing/LandingHeader 2.tsx
new file mode 100644
index 00000000..5a1d5008
--- /dev/null
+++ b/src/components/landing/LandingHeader 2.tsx
@@ -0,0 +1,88 @@
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+
+import logoSvg from "@/assets/logo/service-logo/logo.svg";
+
+const navItems = [
+ { label: "기능", targetId: "features" },
+ { label: "이용방법", targetId: "guide" },
+ { label: "요금제", targetId: "pricing" },
+];
+
+function scrollToSection(id: string) {
+ document.getElementById(id)?.scrollIntoView({ behavior: "smooth" });
+}
+
+export default function LandingHeader() {
+ const [isScrolled, setIsScrolled] = useState(false);
+
+ useEffect(() => {
+ let rafId: number | null = null;
+
+ function update() {
+ rafId = null;
+ const next = window.scrollY > 8;
+ setIsScrolled((prev) => (prev === next ? prev : next));
+ }
+
+ function onScroll() {
+ if (rafId != null) return;
+ rafId = window.requestAnimationFrame(update);
+ }
+
+ update();
+ window.addEventListener("scroll", onScroll, { passive: true });
+ return () => {
+ if (rafId != null) window.cancelAnimationFrame(rafId);
+ window.removeEventListener("scroll", onScroll);
+ };
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+ {navItems.map(({ label, targetId }) => (
+ scrollToSection(targetId)}
+ className="text-[15px] font-medium text-text-sub hover:text-text-main transition-colors rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-logo-2/35 focus-visible:ring-offset-2 focus-visible:ring-offset-white"
+ >
+ {label}
+
+ ))}
+
+
+
+
+ 로그인
+
+
+ 회원가입
+
+
+
+ );
+}
diff --git a/src/components/landing/LandingHero 2.tsx b/src/components/landing/LandingHero 2.tsx
new file mode 100644
index 00000000..ebfc59e2
--- /dev/null
+++ b/src/components/landing/LandingHero 2.tsx
@@ -0,0 +1,60 @@
+import { motion } from "framer-motion";
+
+import ChevronDown from "@/assets/icon/chevron/chevron-down.svg?react";
+import MockupTestImage from "@/assets/mockup/optimized/mockup_test.jpg";
+
+export default function LandingHero() {
+ return (
+
+
+
+
+
+
+ WhereYouAd
+
+
+ 광고 통합 대시보드
+
+
+
+ 광고 성과를 실시간으로{"\n"}한 화면에서 관리하세요
+
+
+ Google·Meta 파트너 서비스로 광고 데이터를 한 곳에서 관리하세요.
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/landing/LandingMultiDevice 2.tsx b/src/components/landing/LandingMultiDevice 2.tsx
new file mode 100644
index 00000000..e2bd4f43
--- /dev/null
+++ b/src/components/landing/LandingMultiDevice 2.tsx
@@ -0,0 +1,62 @@
+import IosAppDockImage from "@/assets/mockup/iOS app dock.png";
+import IpadMockupImage from "@/assets/mockup/iPad Air mockup.png";
+import LaptopMockupImage from "@/assets/mockup/laptop_mockup.png";
+
+export default function LandingMultiDevice() {
+ return (
+
+
+
+
Multi-device support
+
+ 모바일·태블릿·데스크탑에서 모두 사용 가능
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/landing/LandingNav 2.tsx b/src/components/landing/LandingNav 2.tsx
new file mode 100644
index 00000000..af967e1d
--- /dev/null
+++ b/src/components/landing/LandingNav 2.tsx
@@ -0,0 +1,69 @@
+import { useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import { motion, useReducedMotion, useScroll } from "framer-motion";
+
+export default function LandingNav() {
+ const [scrolled, setScrolled] = useState(false);
+ const { scrollY } = useScroll();
+ const prefersReduced = useReducedMotion();
+
+ useEffect(() => {
+ return scrollY.on("change", (y) => setScrolled(y > 50));
+ }, [scrollY]);
+
+ return (
+
+
+ {/* 로고 */}
+
+ Where you ad
+
+
+ {/* 네비게이션 링크 */}
+
+
+ 기능
+
+
+ 작동 방식
+
+
+
+ {/* 우측 CTA */}
+
+
+ 로그인
+
+
+ 무료 시작
+
+
+
+
+ );
+}
diff --git a/src/components/landing/LandingPricing 2.tsx b/src/components/landing/LandingPricing 2.tsx
new file mode 100644
index 00000000..9d62d2c2
--- /dev/null
+++ b/src/components/landing/LandingPricing 2.tsx
@@ -0,0 +1,233 @@
+import { useNavigate } from "react-router-dom";
+import { motion } from "framer-motion";
+
+import LandingSectionHeader from "@/components/landing/LandingSectionHeader";
+
+type TFeature = { text: string; enabled: boolean };
+
+type TPlan = {
+ name: string;
+ target: string;
+ price: string;
+ priceUnit?: string;
+ priceSubText?: string;
+ buttonText: string;
+ featured: boolean;
+ assurance: string;
+ features: TFeature[];
+};
+
+const plans: TPlan[] = [
+ {
+ name: "프리",
+ target: "서비스를 처음 체험하는 누구나",
+ price: "무료",
+ buttonText: "무료로 시작하기",
+ featured: false,
+ assurance: "카드 불필요",
+ features: [
+ { text: "광고 매체 연동 최대 3개", enabled: true },
+ { text: "멤버 1명", enabled: true },
+ { text: "단일 워크스페이스", enabled: true },
+ { text: "AI 성과 리포트", enabled: false },
+ { text: "분석보고서 이메일 전송", enabled: false },
+ ],
+ },
+ {
+ name: "스타터",
+ target: "1인 창업자",
+ price: "₩30,000",
+ priceUnit: "/월",
+ priceSubText: "(부가세 별도)",
+ buttonText: "시작하기",
+ featured: false,
+ assurance: "카드 필요",
+ features: [
+ { text: "광고 매체 연동 최대 10개", enabled: true },
+ { text: "멤버 1명", enabled: true },
+ { text: "워크스페이스 최대 5개", enabled: true },
+ { text: "AI 요약 기능", enabled: true },
+ { text: "분석보고서 이메일 전송", enabled: false },
+ ],
+ },
+ {
+ name: "팀",
+ target: "소규모 팀 (2~10인)",
+ price: "₩150,000",
+ priceUnit: "/월",
+ priceSubText: "(부가세 별도)",
+ buttonText: "14일 무료 체험하기",
+ featured: true,
+ assurance: "무료 체험 가능 · 카드 필요",
+ features: [
+ { text: "광고 매체 연동 최대 30개", enabled: true },
+ { text: "멤버 최대 10명", enabled: true },
+ { text: "워크스페이스 최대 10개", enabled: true },
+ { text: "AI 요약 기능", enabled: true },
+ { text: "분석보고서 이메일 전송", enabled: true },
+ ],
+ },
+ {
+ name: "프로",
+ target: "성장 기업 및 에이전시",
+ price: "₩500,000",
+ priceUnit: "/월",
+ priceSubText: "(부가세 별도)",
+ buttonText: "영업팀에 문의",
+ featured: false,
+ assurance: "맞춤 견적 · 데모 제공",
+ features: [
+ { text: "광고 매체 연동 무제한", enabled: true },
+ { text: "멤버 무제한", enabled: true },
+ { text: "워크스페이스 무제한", enabled: true },
+ { text: "광고 성과 변화 알림 및 실시간 대응", enabled: true },
+ { text: "전담 어카운트 매니저(AM)", enabled: true },
+ ],
+ },
+];
+
+function CheckIcon({ enabled }: { enabled: boolean }) {
+ return (
+
+
+
+ );
+}
+
+export default function LandingPricing() {
+ const navigate = useNavigate();
+
+ function handleCta(planName: string) {
+ if (planName === "프로") {
+ const subject = encodeURIComponent("WhereYouAd 요금제 문의");
+ const body = encodeURIComponent("문의하실 내용을 입력해 주세요.");
+ window.location.href = `mailto:whereyouadofficial@gmail.com?subject=${subject}&body=${body}`;
+ return;
+ }
+
+ navigate("/signup", { replace: false });
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {plans.map((plan, i) => (
+
+ {plan.featured && (
+
+ 가장 인기
+
+ )}
+
+
+ {plan.name}
+
+
+ {plan.target}
+
+
+
+
+
+ {plan.price}
+
+ {plan.priceUnit && (
+
+ {plan.priceUnit}
+
+ )}
+
+ {plan.priceUnit && plan.priceSubText && (
+
+ {plan.priceSubText}
+
+ )}
+
+
+ handleCta(plan.name)}
+ className={`w-full h-12 rounded-component-md font-semibold transition-colors mb-4 ${
+ plan.featured
+ ? "bg-logo-2 text-white hover:bg-logo-2-dark shadow-[0_10px_24px_rgba(96,136,254,0.28)]"
+ : "bg-white border border-chart-inactive/70 text-text-main hover:bg-brand-300"
+ } focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-logo-2/35 focus-visible:ring-offset-2 focus-visible:ring-offset-white`}
+ >
+ {plan.buttonText}
+
+
+ {plan.assurance}
+
+
+
+
+ {plan.features.map((feature) => (
+
+
+
+ {feature.text}
+
+
+ ))}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/landing/LandingSectionHeader 2.tsx b/src/components/landing/LandingSectionHeader 2.tsx
new file mode 100644
index 00000000..1c3354fa
--- /dev/null
+++ b/src/components/landing/LandingSectionHeader 2.tsx
@@ -0,0 +1,28 @@
+type TAlign = "left" | "center";
+
+export default function LandingSectionHeader({
+ title,
+ subtitle,
+ align = "center",
+ className = "",
+}: {
+ title: string;
+ subtitle?: string;
+ align?: TAlign;
+ className?: string;
+}) {
+ const alignClass = align === "left" ? "text-left" : "text-center";
+
+ return (
+
+
+ {title}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ );
+}
diff --git a/src/components/landing/ProblemSection 2.tsx b/src/components/landing/ProblemSection 2.tsx
new file mode 100644
index 00000000..17eae897
--- /dev/null
+++ b/src/components/landing/ProblemSection 2.tsx
@@ -0,0 +1,185 @@
+import { motion } from "framer-motion";
+
+import { PROBLEMS } from "@/constants/landing";
+
+import { useScrollAnimation } from "@/hooks/useScrollAnimation";
+
+// ─── 문제별 아이콘 ────────────────────────────────────────────────────────────
+const PROBLEM_ICONS = [
+ // 채널마다 따로 접속 — 분리된 창 아이콘
+ ({ className }: { className?: string }) => (
+
+
+
+
+
+
+ ),
+ // 이슈를 너무 늦게 발견 — 알람 + 느낌표 아이콘
+ ({ className }: { className?: string }) => (
+
+
+
+
+
+
+ ),
+ // 데이터가 파편화 — 흩어진 점들 아이콘
+ ({ className }: { className?: string }) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+] as const;
+
+const CARD_ACCENT = [
+ {
+ border: "border-blue-500/20 hover:border-blue-500/60",
+ icon: "text-blue-400 bg-blue-500/10",
+ glow: "group-hover:shadow-blue-500/10",
+ badge: "text-blue-400 bg-blue-500/10",
+ },
+ {
+ border: "border-orange-500/20 hover:border-orange-500/60",
+ icon: "text-orange-400 bg-orange-500/10",
+ glow: "group-hover:shadow-orange-500/10",
+ badge: "text-orange-400 bg-orange-500/10",
+ },
+ {
+ border: "border-rose-500/20 hover:border-rose-500/60",
+ icon: "text-rose-400 bg-rose-500/10",
+ glow: "group-hover:shadow-rose-500/10",
+ badge: "text-rose-400 bg-rose-500/10",
+ },
+] as const;
+
+// ─── ProblemSection ───────────────────────────────────────────────────────────
+export default function ProblemSection() {
+ const { ref, controls, staggerContainer, staggerItem } = useScrollAnimation();
+
+ return (
+
+ {/* 배경 그라디언트 */}
+
+
+
+ {/* 섹션 헤더 */}
+
+
+ Pain Points
+
+
+ 광고팀이 매일 겪는 문제들
+
+
+ 채널이 늘어날수록 관리 비용은 기하급수적으로 증가합니다.
+
+
+
+ {/* 문제 카드 그리드 */}
+
+ {PROBLEMS.map((problem, i) => {
+ const Icon = PROBLEM_ICONS[i];
+ const accent = CARD_ACCENT[i];
+
+ return (
+
+ {/* 호버 시 배경 글로우 */}
+
+
+ {/* 아이콘 */}
+
+
+
+
+ {/* 텍스트 */}
+
+ {problem.title}
+
+
+ {problem.description}
+
+
+ {/* 우하단 번호 */}
+
+ 0{i + 1}
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/src/components/landing/SocialProofSection 2.tsx b/src/components/landing/SocialProofSection 2.tsx
new file mode 100644
index 00000000..fae0ab36
--- /dev/null
+++ b/src/components/landing/SocialProofSection 2.tsx
@@ -0,0 +1,3 @@
+export default function SocialProofSection() {
+ return <>>;
+}
diff --git a/src/components/setting/PasswordSection 2.tsx b/src/components/setting/PasswordSection 2.tsx
new file mode 100644
index 00000000..9e55a403
--- /dev/null
+++ b/src/components/setting/PasswordSection 2.tsx
@@ -0,0 +1,110 @@
+import { useState } from "react";
+
+import Input from "../common/input/Input";
+
+import EyeIcon from "@/assets/icon/common/eye.svg?react";
+import EyeOffIcon from "@/assets/icon/common/eye-off.svg?react";
+import LockIcon from "@/assets/icon/common/lock.svg?react";
+
+type TPasswordSectionProps = {
+ currentPassword: string;
+ setCurrentPassword: (v: string) => void;
+ newPassword: string;
+ setNewPassword: (v: string) => void;
+ confirmNewPassword: string;
+ setConfirmNewPassword: (v: string) => void;
+ errors: {
+ currentPassword: string;
+ newPassword: string;
+ confirmNewPassword: string;
+ };
+};
+
+export default function PasswordSection({
+ currentPassword,
+ setCurrentPassword,
+ newPassword,
+ setNewPassword,
+ confirmNewPassword,
+ setConfirmNewPassword,
+ errors,
+}: TPasswordSectionProps) {
+ const [showCurrent, setShowCurrent] = useState(false);
+ const [showNew, setShowNew] = useState(false);
+ const [showConfirm, setShowConfirm] = useState(false);
+
+ return (
+
+ );
+}
diff --git a/src/components/setting/ProfileSection 2.tsx b/src/components/setting/ProfileSection 2.tsx
new file mode 100644
index 00000000..99118312
--- /dev/null
+++ b/src/components/setting/ProfileSection 2.tsx
@@ -0,0 +1,153 @@
+import type React from "react";
+
+import Button from "../common/button/Button";
+import Input from "../common/input/Input";
+
+import CameraIcon from "@/assets/icon/common/camera.svg?react";
+import CheckIcon from "@/assets/icon/common/check.svg?react";
+import UserProfileCircleIcon from "@/assets/icon/common/userProfileCircle.svg?react";
+
+type TProfileSectionProps = {
+ name: string;
+ setName: (v: string) => void;
+ organizations: { name: string; position: string }[];
+ email: string;
+ phoneNumber: string;
+ fileRef: React.RefObject;
+ preview: string | null;
+ openFilePicker: () => void;
+ onPickFile: (e: React.ChangeEvent) => void;
+ resetImage: () => void;
+};
+export default function ProfileSection({
+ name,
+ setName,
+ organizations,
+ email,
+ phoneNumber,
+ fileRef,
+ preview,
+ onPickFile,
+ openFilePicker,
+ resetImage,
+}: TProfileSectionProps) {
+ return (
+
+
+
+
+
+ 프로필 이미지
+
+
+
+ {preview ? (
+
+ ) : (
+
+ )}
+
+
+
+ 변경
+
+
+ 초기화
+
+
+
+
+
+ setName(e.target.value)}
+ />
+
+
+
소속 조직
+ {organizations.length === 0 ? (
+
+ 소속된 조직이 없습니다.
+
+ ) : (
+ <>
+
+ {organizations.map((org, idx) => (
+
+
+
+ ))}
+
+
+ 조직 정보는 별도 조직페이지에서 수정할 수 있습니다.
+
+
+ >
+ )}
+
+
+
}
+ readOnly
+ />
+
+ 이메일은 변경할 수 없습니다.
+
+
+
+
}
+ readOnly
+ />
+
+ 전화번호는 변경할 수 없습니다.
+
+
+
+
+
+ );
+}
diff --git a/src/constants/landing 2.ts b/src/constants/landing 2.ts
new file mode 100644
index 00000000..0a04f89d
--- /dev/null
+++ b/src/constants/landing 2.ts
@@ -0,0 +1,88 @@
+export const LANDING_META = {
+ serviceName: "Where you ad",
+ tagline: "흩어진 광고 채널, 하나의 화면으로",
+ subCopy:
+ "인스타그램, 구글, 네이버, 카카오 — 모든 광고 성과를\n실시간으로 한 대시보드에서 모니터링하세요",
+} as const;
+
+export const PROBLEMS = [
+ {
+ id: 1,
+ title: "채널마다 따로 접속",
+ description: "각 플랫폼 대시보드를 오가며 시간 낭비",
+ },
+ {
+ id: 2,
+ title: "이슈를 너무 늦게 발견",
+ description: "클릭 급증, 예산 초과를 뒤늦게 파악",
+ },
+ {
+ id: 3,
+ title: "데이터가 파편화",
+ description: "집계 단위가 달라 채널 간 비교가 불가",
+ },
+] as const;
+
+export const FEATURES = [
+ {
+ id: 1,
+ title: "실시간 통합 대시보드",
+ description: "1분/5분/1시간 단위 차트로 전 채널 성과를 한눈에",
+ },
+ {
+ id: 2,
+ title: "트래킹 링크 생성",
+ description: "클릭 이벤트 자동 수집으로 정확한 전환 추적",
+ },
+ {
+ id: 3,
+ title: "가상 시뮬레이터",
+ description: "실제 과금 없이 광고 전략을 미리 테스트",
+ },
+ {
+ id: 4,
+ title: "AI 인사이트",
+ description: "LLM 기반 예산 추천 & 자연어 요약으로 빠른 의사결정",
+ },
+] as const;
+
+export const HOW_IT_WORKS_STEPS = [
+ {
+ step: 1,
+ title: "광고 채널 연결",
+ description: "Google / Meta / Naver / Kakao 계정을 한 번에 연동",
+ },
+ {
+ step: 2,
+ title: "트래킹 URL 발급 & 광고에 등록",
+ description: "자동 생성된 트래킹 링크를 광고 소재에 삽입",
+ },
+ {
+ step: 3,
+ title: "실시간 클릭 이벤트 자동 수집",
+ description: "방문자 행동 데이터가 즉시 수집·처리",
+ },
+ {
+ step: 4,
+ title: "대시보드에서 통합 성과 모니터링",
+ description: "모든 채널 지표를 단일 화면에서 비교·분석",
+ },
+] as const;
+
+export const SOCIAL_PROOF_STATS = [
+ {
+ id: 1,
+ label: "연동 광고 채널",
+ value: "4+",
+ },
+ {
+ id: 2,
+ label: "실시간 처리",
+ value: "1,000건/초",
+ },
+ {
+ id: 3,
+ label: "대시보드 로딩",
+ value: "< 1초",
+ },
+] as const;
diff --git a/src/hooks/common/useComingSoon 2.ts b/src/hooks/common/useComingSoon 2.ts
new file mode 100644
index 00000000..ee7b5485
--- /dev/null
+++ b/src/hooks/common/useComingSoon 2.ts
@@ -0,0 +1,16 @@
+import { useCallback } from "react";
+import { toast } from "sonner";
+
+const DEFAULT_MESSAGE =
+ "해당 기능은 아직 준비 중이에요. 곧 이용하실 수 있어요!";
+
+export function useComingSoon() {
+ const showComingSoon = useCallback((message: string = DEFAULT_MESSAGE) => {
+ toast.info(message, {
+ id: "coming-soon",
+ duration: 4000,
+ });
+ }, []);
+
+ return { showComingSoon };
+}
diff --git a/src/hooks/common/useImageUploader 2.ts b/src/hooks/common/useImageUploader 2.ts
new file mode 100644
index 00000000..951d1903
--- /dev/null
+++ b/src/hooks/common/useImageUploader 2.ts
@@ -0,0 +1,34 @@
+import type { ChangeEvent } from "react";
+import { useEffect, useRef, useState } from "react";
+
+export function useImageUploader() {
+ const fileRef = useRef(null);
+ const [file, setFile] = useState(null);
+ const [preview, setPreview] = useState(null);
+
+ const openFilePicker = () => {
+ fileRef.current?.click();
+ };
+ const onPickFile = (e: ChangeEvent) => {
+ const f = e.target.files?.[0];
+ if (!f) return;
+
+ setFile(f);
+
+ const url = URL.createObjectURL(f);
+ setPreview(url);
+ };
+ const resetImage = () => {
+ if (fileRef.current) {
+ fileRef.current.value = "";
+ }
+ setFile(null);
+ setPreview(null);
+ };
+ useEffect(() => {
+ return () => {
+ if (preview) URL.revokeObjectURL(preview);
+ };
+ });
+ return { fileRef, file, preview, openFilePicker, onPickFile, resetImage };
+}
diff --git a/src/hooks/dashboard/useOverviewCampaignList 2.ts b/src/hooks/dashboard/useOverviewCampaignList 2.ts
new file mode 100644
index 00000000..57f5fef7
--- /dev/null
+++ b/src/hooks/dashboard/useOverviewCampaignList 2.ts
@@ -0,0 +1,17 @@
+import type { ICampaign } from "@/types/ads/campaign";
+
+import { useCoreQuery } from "@/hooks/customQuery";
+
+import { getCampaignList } from "@/api/ads/ads";
+import useWorkspaceStore from "@/store/useWorkspaceStore";
+
+/** 광고 목록과 동일 쿼리 키로 캐시 공유 */
+export function useOverviewCampaignList() {
+ const orgId = useWorkspaceStore((s) => s.selectedOrgId);
+
+ return useCoreQuery(
+ ["campaigns", orgId],
+ () => getCampaignList(orgId!),
+ { enabled: !!orgId },
+ );
+}
diff --git a/src/hooks/useScrollAnimation 2.ts b/src/hooks/useScrollAnimation 2.ts
new file mode 100644
index 00000000..a857c618
--- /dev/null
+++ b/src/hooks/useScrollAnimation 2.ts
@@ -0,0 +1,79 @@
+import { useEffect } from "react";
+import { useInView } from "react-intersection-observer";
+import type { Variants } from "framer-motion";
+import { useAnimation, useReducedMotion } from "framer-motion";
+
+export const fadeUp: Variants = {
+ hidden: { opacity: 0, y: 40 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.6, ease: "easeOut" },
+ },
+};
+
+export const fadeIn: Variants = {
+ hidden: { opacity: 0 },
+ visible: {
+ opacity: 1,
+ transition: { duration: 0.6, ease: "easeOut" },
+ },
+};
+
+export const staggerContainer: Variants = {
+ hidden: {},
+ visible: {
+ transition: {
+ staggerChildren: 0.15,
+ delayChildren: 0.1,
+ },
+ },
+};
+
+export const staggerItem: Variants = {
+ hidden: { opacity: 0, y: 30 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: { duration: 0.5, ease: "easeOut" },
+ },
+};
+
+// reduced-motion: 위치 이동 없이 opacity만 페이드
+const reducedFadeUp: Variants = {
+ hidden: { opacity: 0 },
+ visible: { opacity: 1, transition: { duration: 0.3 } },
+};
+
+const reducedStaggerItem: Variants = {
+ hidden: { opacity: 0 },
+ visible: { opacity: 1, transition: { duration: 0.3 } },
+};
+
+const reducedStaggerContainer: Variants = {
+ hidden: {},
+ visible: { transition: { staggerChildren: 0.05 } },
+};
+
+export function useScrollAnimation(threshold = 0.15) {
+ const controls = useAnimation();
+ const prefersReduced = useReducedMotion();
+ const [ref, inView] = useInView({ threshold, triggerOnce: true });
+
+ useEffect(() => {
+ if (inView) {
+ controls.start("visible");
+ }
+ }, [controls, inView]);
+
+ return {
+ ref,
+ controls,
+ fadeUp: prefersReduced ? reducedFadeUp : fadeUp,
+ fadeIn,
+ staggerContainer: prefersReduced
+ ? reducedStaggerContainer
+ : staggerContainer,
+ staggerItem: prefersReduced ? reducedStaggerItem : staggerItem,
+ };
+}
diff --git a/src/pages/LandingPage 2.tsx b/src/pages/LandingPage 2.tsx
new file mode 100644
index 00000000..84919308
--- /dev/null
+++ b/src/pages/LandingPage 2.tsx
@@ -0,0 +1,66 @@
+import { lazy, Suspense } from "react";
+
+import HeroSection from "@/components/landing/HeroSection";
+import LandingFooter from "@/components/landing/LandingFooter";
+import LandingNav from "@/components/landing/LandingNav";
+
+// 폴드 아래 섹션은 lazy load
+const ProblemSection = lazy(
+ () => import("@/components/landing/ProblemSection"),
+);
+const FeatureSection = lazy(
+ () => import("@/components/landing/FeatureSection"),
+);
+const HowItWorksSection = lazy(
+ () => import("@/components/landing/HowItWorksSection"),
+);
+const SocialProofSection = lazy(
+ () => import("@/components/landing/SocialProofSection"),
+);
+const CTASection = lazy(() => import("@/components/landing/CTASection"));
+
+function SectionFallback() {
+ return (
+
+ );
+}
+
+export default function LandingPage() {
+ return (
+
+
+
+ {/* Hero — 즉시 렌더 */}
+
+
+ {/* 폴드 아래 — lazy */}
+
}>
+
+
+
+
}>
+
+
+
+
}>
+
+
+
+
}>
+
+
+
+
}>
+
+
+
+
+
+ );
+}
diff --git a/src/pages/dashboard/overview/OverviewAiDrawer 2.tsx b/src/pages/dashboard/overview/OverviewAiDrawer 2.tsx
new file mode 100644
index 00000000..c0ac4ea6
--- /dev/null
+++ b/src/pages/dashboard/overview/OverviewAiDrawer 2.tsx
@@ -0,0 +1,60 @@
+import { lazy, Suspense } from "react";
+
+import Drawer from "@/components/common/drawer/Drawer";
+
+import DownloadIcon from "@/assets/icon/common/download.svg?react";
+import LinkIcon from "@/assets/icon/common/link.svg?react";
+
+const OverviewAiReportPanel = lazy(() => import("./OverviewAiReportPanel"));
+
+export function OverviewAiDrawer({
+ isOpen,
+ onClose,
+ onShareLink,
+ onDownloadPdf,
+}: {
+ isOpen: boolean;
+ onClose: () => void;
+ onShareLink: () => void;
+ onDownloadPdf: () => void;
+}) {
+ return (
+
+ ),
+ onClick: onShareLink,
+ },
+ {
+ label: "PDF로 저장하기",
+ icon: (
+
+ ),
+ onClick: onDownloadPdf,
+ },
+ ]}
+ >
+ {isOpen && (
+
+
+
+ )}
+
+ );
+}
diff --git a/src/pages/dashboard/overview/OverviewBudgetSection 2.tsx b/src/pages/dashboard/overview/OverviewBudgetSection 2.tsx
new file mode 100644
index 00000000..75d4c0ed
--- /dev/null
+++ b/src/pages/dashboard/overview/OverviewBudgetSection 2.tsx
@@ -0,0 +1,118 @@
+import type { IApiErrorResponse } from "@/types/common/common";
+
+import type { useOverviewBudget } from "@/hooks/dashboard/useOverviewBudget";
+
+import Badge from "@/components/common/badge/Badge";
+import Card from "@/components/common/card/Card";
+import ChartLegend, {
+ type IChartLegendItem,
+} from "@/components/common/chart/ChartLegend";
+import { Skeleton } from "@/components/common/skeleton/Skeleton";
+import type { getBudgetStatus } from "@/components/dashboard/charts/BudgetGaugeChart";
+import BudgetGaugeChart, {
+ statusBadgeVariant,
+} from "@/components/dashboard/charts/BudgetGaugeChart";
+
+import OverviewCampaignSnapshotCard from "@/pages/dashboard/overview/OverviewCampaignSnapshotCard";
+
+const budgetStatusLegendItems: IChartLegendItem[] = [
+ { label: "안정", colorClass: "bg-status-green" },
+ { label: "주의", colorClass: "bg-status-yellow" },
+ { label: "위험", colorClass: "bg-status-red" },
+];
+
+function BudgetGaugeSkeleton() {
+ return (
+
+ );
+}
+
+export function OverviewBudgetSection({
+ budget,
+ isBudgetLoading,
+ isBudgetError,
+ budgetError,
+ budgetStatus,
+}: {
+ budget: ReturnType["data"];
+ isBudgetLoading: boolean;
+ isBudgetError: boolean;
+ budgetError: IApiErrorResponse | null;
+ budgetStatus: ReturnType | null;
+}) {
+ return (
+
+
+
+ }
+ RightElement={
+ budgetStatus ? (
+
+ {budgetStatus}
+
+ ) : undefined
+ }
+ >
+
+ {isBudgetError ? (
+
+ {budgetError?.message ?? (
+ <>
+ 예산 데이터를 불러오지 못했습니다.
+
+ 잠시 후 다시 시도해 주세요.
+ >
+ )}
+
+ ) : isBudgetLoading ? (
+
+ ) : budget ? (
+
+ ) : null}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/dashboard/overview/OverviewCampaignSnapshotCard 2.tsx b/src/pages/dashboard/overview/OverviewCampaignSnapshotCard 2.tsx
new file mode 100644
index 00000000..693502ff
--- /dev/null
+++ b/src/pages/dashboard/overview/OverviewCampaignSnapshotCard 2.tsx
@@ -0,0 +1,149 @@
+import { memo, useCallback, useMemo } from "react";
+import { useNavigate } from "react-router-dom";
+import { twMerge } from "tailwind-merge";
+
+import type { ICampaign } from "@/types/ads/campaign";
+
+import { useOverviewCampaignList } from "@/hooks/dashboard/useOverviewCampaignList";
+
+import Button from "@/components/common/button/Button";
+import Card from "@/components/common/card/Card";
+import { Skeleton } from "@/components/common/skeleton/Skeleton";
+
+import ChevronDoubleRightIcon from "@/assets/icon/chevron/chervon-double-right.svg?react";
+import useWorkspaceStore from "@/store/useWorkspaceStore";
+
+function visibleCampaigns(list: ICampaign[]) {
+ return list.filter((c) => c.status !== "OVER");
+}
+
+const platformShort: Record = {
+ naver: "Naver",
+ google: "Google",
+ kakao: "Kakao",
+};
+
+const SnapshotRow = memo(function SnapshotRow({
+ campaign,
+ onOpen,
+}: {
+ campaign: ICampaign;
+ onOpen: (projectId: number) => void;
+}) {
+ return (
+ onOpen(campaign.projectId)}
+ className="flex w-full min-w-0 items-center gap-2 px-3 py-3 text-left transition-colors hover:bg-bg-surface/80"
+ >
+
+
+ {campaign.name}
+
+
+ {campaign.providers.length > 0
+ ? campaign.providers.map((p) => platformShort[p] ?? p).join(" · ")
+ : "플랫폼 미지정"}
+
+
+
+ {campaign.budgetUsageRate.toFixed(0)}%
+
+
+ );
+});
+
+type TOverviewCampaignSnapshotCardProps = {
+ className?: string;
+};
+
+export default memo(function OverviewCampaignSnapshotCard({
+ className,
+}: TOverviewCampaignSnapshotCardProps) {
+ const navigate = useNavigate();
+ const orgId = useWorkspaceStore((s) => s.selectedOrgId);
+ const {
+ data: campaigns = [],
+ isPending,
+ isError,
+ error,
+ } = useOverviewCampaignList();
+
+ const { totalVisible, topByBudgetUsage } = useMemo(() => {
+ const visible = visibleCampaigns(campaigns);
+ const top = [...visible]
+ .sort((a, b) => b.budgetUsageRate - a.budgetUsageRate)
+ .slice(0, 2);
+ return {
+ totalVisible: visible.length,
+ topByBudgetUsage: top,
+ };
+ }, [campaigns]);
+
+ const openCampaign = useCallback(
+ (projectId: number) => {
+ if (orgId == null) return;
+ navigate(`/ads/${orgId}/${projectId}`);
+ },
+ [navigate, orgId],
+ );
+
+ if (!orgId) return null;
+
+ return (
+
+ 예산 소진 상위
+
+ )
+ }
+ RightElement={
+ navigate("/ads")}
+ className="group flex h-8 shrink-0 items-center gap-1 rounded-full border-none bg-bg-surface/60 px-3 hover:bg-bg-surface"
+ >
+
+ 목록
+
+
+
+ }
+ >
+
+ {isError ? (
+
+ {error?.message ??
+ "캠페인 목록을 불러오지 못했습니다. 잠시 후 다시 시도해 주세요."}
+
+ ) : isPending ? (
+
+
+
+
+ ) : totalVisible === 0 ? (
+
+ 표시할 캠페인이 없습니다. 광고 관리에서 캠페인을 등록해 보세요.
+
+ ) : (
+
+
+ {topByBudgetUsage.map((c) => (
+
+ ))}
+
+
+ )}
+
+
+ );
+});
diff --git a/src/pages/dashboard/overview/OverviewKpiSection 2.tsx b/src/pages/dashboard/overview/OverviewKpiSection 2.tsx
new file mode 100644
index 00000000..b4648d1b
--- /dev/null
+++ b/src/pages/dashboard/overview/OverviewKpiSection 2.tsx
@@ -0,0 +1,85 @@
+import { Suspense } from "react";
+
+import type { IApiErrorResponse } from "@/types/common/common";
+
+import type { useOverviewMetrics } from "@/hooks/dashboard/useOverviewMetrics";
+
+import Card from "@/components/common/card/Card";
+import StatCard from "@/components/common/card/StatCard";
+import ChartLegend from "@/components/common/chart/ChartLegend";
+import { Skeleton } from "@/components/common/skeleton/Skeleton";
+import TrafficChart, {
+ TrafficChartDownload,
+} from "@/components/dashboard/charts/TrafficChart";
+
+function KpiSkeletonCard() {
+ return (
+
+
+
+
+
+ );
+}
+
+export function OverviewKpiSection({
+ kpis,
+ isKpisLoading,
+ isKpisError,
+ kpisError,
+}: {
+ kpis: ReturnType["data"];
+ isKpisLoading: boolean;
+ isKpisError: boolean;
+ kpisError: IApiErrorResponse | null;
+}) {
+ const kpiList = kpis ?? [];
+
+ return (
+
+ {isKpisError ? (
+
+ {kpisError?.message ??
+ "지표 데이터를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요."}
+
+ ) : (
+
+ {isKpisLoading
+ ? [0, 1, 2, 3].map((i) => )
+ : kpiList.map((kpi) => (
+
+ ))}
+
+ )}
+
+
+ }
+ RightElement={
}
+ >
+
+
+ }
+ >
+
+
+
+
+
+ );
+}
diff --git a/src/pages/dashboard/overview/OverviewPlatformSection 2.tsx b/src/pages/dashboard/overview/OverviewPlatformSection 2.tsx
new file mode 100644
index 00000000..8ebfcc06
--- /dev/null
+++ b/src/pages/dashboard/overview/OverviewPlatformSection 2.tsx
@@ -0,0 +1,103 @@
+import type { IApiErrorResponse } from "@/types/common/common";
+
+import type { useOverviewRoasRankings } from "@/hooks/dashboard/useOverviewRoasRankings";
+
+import Button from "@/components/common/button/Button";
+import Card from "@/components/common/card/Card";
+import {
+ Skeleton,
+ SkeletonCircle,
+} from "@/components/common/skeleton/Skeleton";
+import PlatformRoasTable, {
+ PLATFORM_ROAS_TABLE_COL,
+} from "@/components/dashboard/platform/PlatformRoasTable";
+
+import ChevronDoubleRightIcon from "@/assets/icon/chevron/chervon-double-right.svg?react";
+import WarnCircleIcon from "@/assets/icon/common/warn-circle.svg?react";
+
+function PlatformRankingSkeleton() {
+ return (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ );
+}
+
+export function OverviewPlatformSection({
+ rankings,
+ isRankingsLoading,
+ isRankingsError,
+ rankingsError,
+ onNavigate,
+}: {
+ rankings: ReturnType["data"];
+ isRankingsLoading: boolean;
+ isRankingsError: boolean;
+ rankingsError: IApiErrorResponse | null;
+ onNavigate: () => void;
+}) {
+ return (
+
+
+ 플랫폼 대시보드 살펴보기
+
+
+
+ }
+ description={
+
+
+ ROAS 산출: 매출 ÷ 광고비 × 100
+
+ }
+ >
+ {isRankingsError ? (
+
+ {rankingsError?.message ??
+ "플랫폼 데이터를 불러오지 못했습니다. 잠시 후 다시 시도해 주세요."}
+
+ ) : isRankingsLoading ? (
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/src/pages/dashboard/platform/PlatformDashboard.tsx b/src/pages/dashboard/platform/PlatformDashboard.tsx
index 39cc2016..0f9e4af0 100644
--- a/src/pages/dashboard/platform/PlatformDashboard.tsx
+++ b/src/pages/dashboard/platform/PlatformDashboard.tsx
@@ -1,13 +1,20 @@
import { type ReactNode, useEffect, useMemo, useState } from "react";
import { useOutletContext } from "react-router-dom";
+import { toast } from "sonner";
import { twMerge } from "tailwind-merge";
+import { printAsPdf } from "@/utils/download";
+
import Button from "@/components/common/button/Button";
import { DropdownMenu } from "@/components/common/dropdownmenu/DropdownMenu";
import AllPlatformView from "@/components/dashboard/platform/AllPlatformView";
import SinglePlatformView from "@/components/dashboard/platform/SinglePlatformView";
+import { OverviewAiDrawer } from "../overview/OverviewAiDrawer";
+
+import SparkleCircleIcon from "@/assets/icon/ai/sparkle-circle.svg?react";
import ChevronDownIcon from "@/assets/icon/chevron/chevron-up.svg?react";
+import AiButtonSvg from "@/assets/logo/service-logo/ai-요약버튼.svg?react";
type TDashboardHeaderContext = {
setHeaderRight?: (node: ReactNode | null) => void;
@@ -16,6 +23,7 @@ type TDashboardHeaderContext = {
export default function PlatformDashboard() {
const [selectedPlatform, setSelectedPlatform] = useState("전체");
const [isLoading, setIsLoading] = useState(true);
+ const [isAiPanelOpen, setIsAiPanelOpen] = useState(false);
const { setHeaderRight } = useOutletContext();
const isAllView = selectedPlatform === "전체";
@@ -83,6 +91,23 @@ export default function PlatformDashboard() {
}
items={platformItems}
/>
+
+ setIsAiPanelOpen(true)}
+ className="group relative ml-4 -mr-2 inline-flex h-8 cursor-pointer items-center justify-center overflow-hidden rounded-2xl px-1 outline-none focus-visible:ring-2 focus-visible:ring-logo-2/35 focus-visible:ring-offset-2 focus-visible:ring-offset-white"
+ aria-label="AI 요약하기"
+ >
+
+
+
,
);
@@ -96,6 +121,19 @@ export default function PlatformDashboard() {
) : (