feat: GA4 전환 이벤트 추적 + 전 페이지 스크롤 리빌 애니메이션
- lib/gtag.ts: GA4 이벤트 유틸리티 (trackCTAClick, trackToolDemo, trackDownload, trackOutboundClick) - ContactModal/ContactForm: 공용 trackEvent로 리팩토링 + generate_lead 이벤트 - 홈/tools/automation/prompt/website: CTA 클릭 이벤트 추적 추가 - 홈/freelance/ai-kit: IntersectionObserver 스크롤 리빌 애니메이션 신규 추가 - automation/prompt: GA4 trackCTAClick 적용 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect, Suspense } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { trackEvent } from '../../lib/gtag';
|
||||
|
||||
function ContactFormInner() {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -36,6 +37,10 @@ function ContactFormInner() {
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || '문의 전송에 실패했습니다.');
|
||||
setStatus('success');
|
||||
trackEvent('generate_lead', {
|
||||
event_category: 'contact',
|
||||
event_label: formData.service,
|
||||
});
|
||||
setFormData({ name: '', phone: '', email: '', service: '외주 개발 문의', message: '' });
|
||||
setTimeout(() => setStatus('idle'), 5000);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { trackEvent } from '../../lib/gtag';
|
||||
|
||||
interface ContactModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -63,17 +64,6 @@ export default function ContactModal({
|
||||
setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));
|
||||
};
|
||||
|
||||
// GA4 이벤트 헬퍼
|
||||
const trackEvent = (eventName: string, params?: Record<string, string>) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const w = window as any;
|
||||
if (typeof w.gtag === 'function') {
|
||||
w.gtag('event', eventName, params);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus('loading');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import ContactForm from '../components/ContactForm';
|
||||
|
||||
/* ─── Data ─── */
|
||||
@@ -233,12 +233,52 @@ const guarantees = [
|
||||
},
|
||||
];
|
||||
|
||||
/* ─── Scroll Reveal ─── */
|
||||
function useScrollReveal() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('is-visible');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
|
||||
);
|
||||
el.querySelectorAll('.reveal').forEach((child) => observer.observe(child));
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
return ref;
|
||||
}
|
||||
|
||||
/* ─── Main Page ─── */
|
||||
export default function FreelancePage() {
|
||||
const [_contactPreset] = useState('');
|
||||
const containerRef = useScrollReveal();
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-[#f0f5ff]">
|
||||
<div ref={containerRef} className="min-h-full bg-[#f0f5ff]">
|
||||
<style>{`
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(1.5rem);
|
||||
transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.reveal.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.reveal-d1 { transition-delay: 80ms; }
|
||||
.reveal-d2 { transition-delay: 160ms; }
|
||||
.reveal-d3 { transition-delay: 240ms; }
|
||||
.reveal-d4 { transition-delay: 320ms; }
|
||||
`}</style>
|
||||
|
||||
{/* ─── Hero ─── */}
|
||||
<div
|
||||
@@ -293,13 +333,13 @@ export default function FreelancePage() {
|
||||
{/* ─── 포트폴리오 ─── */}
|
||||
<div className="px-6 py-12 lg:px-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="reveal text-center mb-8">
|
||||
<p className="text-[#1a56db] text-xs font-bold uppercase tracking-widest mb-2">PORTFOLIO</p>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]">직접 개발한 프로젝트</h2>
|
||||
<p className="text-slate-500 text-sm mt-2">실제 운영 중인 서비스와 납품 완료 프로젝트입니다</p>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
<div className="reveal grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{portfolio.map((item) => (
|
||||
<div
|
||||
key={item.title}
|
||||
@@ -358,7 +398,7 @@ export default function FreelancePage() {
|
||||
</div>
|
||||
|
||||
{/* 추가 문구 */}
|
||||
<div className="mt-6 text-center">
|
||||
<div className="reveal mt-6 text-center">
|
||||
<p className="text-slate-400 text-sm">
|
||||
위 프로젝트 외에도 다양한 프로젝트 경험이 있습니다 ·{' '}
|
||||
<a href="mailto:bgg8988@gmail.com" className="text-[#1a56db] hover:underline font-medium">포트폴리오 전체 요청</a>
|
||||
@@ -370,13 +410,13 @@ export default function FreelancePage() {
|
||||
{/* ─── 고객 후기 ─── */}
|
||||
<div className="px-6 pb-12 lg:px-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="reveal text-center mb-8">
|
||||
<p className="text-[#1a56db] text-xs font-bold uppercase tracking-widest mb-2">REVIEWS</p>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]">실제 의뢰인 후기</h2>
|
||||
<p className="text-slate-500 text-sm mt-2" style={{ wordBreak: 'keep-all' }}>숫자보다 실제 말이 더 정직합니다</p>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 md:grid-cols-3 gap-5">
|
||||
<div className="reveal grid sm:grid-cols-2 md:grid-cols-3 gap-5">
|
||||
{testimonials.map((t) => (
|
||||
<div
|
||||
key={t.name}
|
||||
@@ -420,7 +460,7 @@ export default function FreelancePage() {
|
||||
* 의뢰인 동의 하에 게시된 후기입니다. 전체 대화 내역 공개 요청 시 제공 가능합니다.
|
||||
</p>
|
||||
|
||||
<div className="text-center py-6">
|
||||
<div className="reveal text-center py-6">
|
||||
<a href="#contact-form" className="inline-flex items-center gap-2 px-6 py-3 bg-[#1a56db] text-white font-semibold rounded-xl hover:bg-blue-700 transition shadow-sm">
|
||||
무료 상담 시작하기
|
||||
</a>
|
||||
@@ -432,14 +472,14 @@ export default function FreelancePage() {
|
||||
{/* ─── 진행 프로세스 ─── */}
|
||||
<div className="px-6 pb-12 lg:px-12">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="text-center mb-10">
|
||||
<div className="reveal text-center mb-10">
|
||||
<p className="text-[#1a56db] text-xs font-bold uppercase tracking-widest mb-2">PROCESS</p>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]">진행 프로세스</h2>
|
||||
<p className="text-slate-500 text-sm mt-2">투명하고 체계적인 6단계로 진행됩니다</p>
|
||||
</div>
|
||||
|
||||
{/* Vertical timeline */}
|
||||
<div className="relative">
|
||||
<div className="reveal relative">
|
||||
{/* connecting line */}
|
||||
<div className="absolute left-6 top-6 bottom-6 w-px bg-[#dbe8ff]" />
|
||||
|
||||
@@ -499,7 +539,7 @@ export default function FreelancePage() {
|
||||
<div className="max-w-5xl mx-auto grid md:grid-cols-2 gap-5">
|
||||
|
||||
{/* Tech Stack */}
|
||||
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-6">
|
||||
<div className="reveal reveal-d1 bg-white rounded-2xl border border-[#dbe8ff] p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="w-1 h-5 bg-[#1a56db] rounded-full" />
|
||||
<h3 className="font-bold text-[#04102b] text-sm">개발 가능 기술 스택</h3>
|
||||
@@ -527,7 +567,7 @@ export default function FreelancePage() {
|
||||
|
||||
{/* 신뢰 포인트 */}
|
||||
<div
|
||||
className="rounded-2xl border border-[#1a3a7a] p-6"
|
||||
className="reveal reveal-d2 rounded-2xl border border-[#1a3a7a] p-6"
|
||||
style={{
|
||||
background: '#04102b',
|
||||
backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.015) 0px, rgba(255,255,255,0.015) 1px, transparent 1px, transparent 30px)',
|
||||
@@ -601,13 +641,13 @@ export default function FreelancePage() {
|
||||
{/* ─── 문의 폼 ─── */}
|
||||
<div id="contact-form" className="px-6 pb-14 lg:px-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="reveal text-center mb-8">
|
||||
<p className="text-[#1a56db] text-xs font-bold uppercase tracking-widest mb-2">CONTACT</p>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]">프로젝트 문의</h2>
|
||||
<p className="text-slate-500 text-sm mt-2">개발사 연락 두절로 손해 본 경험 있으신가요? 여기선 계약서부터 시작합니다.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-5 gap-6">
|
||||
<div className="reveal grid md:grid-cols-5 gap-6">
|
||||
{/* 왼쪽: 간단 안내 */}
|
||||
<div className="md:col-span-2 space-y-4">
|
||||
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-5">
|
||||
|
||||
74
app/page.tsx
74
app/page.tsx
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
import ContactModal from './components/ContactModal';
|
||||
import { trackCTAClick } from '../lib/gtag';
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
쟁승메이드 홈페이지 — 리뉴얼 v2
|
||||
@@ -169,11 +170,50 @@ const SERVICE_LIST = [
|
||||
},
|
||||
];
|
||||
|
||||
function useScrollReveal() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('is-visible');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
|
||||
);
|
||||
el.querySelectorAll('.reveal').forEach((child) => observer.observe(child));
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
return ref;
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const containerRef = useScrollReveal();
|
||||
|
||||
return (
|
||||
<div className="min-h-full">
|
||||
<div className="min-h-full" ref={containerRef}>
|
||||
<style>{`
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(1.5rem);
|
||||
transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.reveal.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.reveal-d1 { transition-delay: 80ms; }
|
||||
.reveal-d2 { transition-delay: 160ms; }
|
||||
.reveal-d3 { transition-delay: 240ms; }
|
||||
.reveal-d4 { transition-delay: 320ms; }
|
||||
`}</style>
|
||||
<ContactModal
|
||||
isOpen={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
@@ -248,7 +288,7 @@ export default function Home() {
|
||||
{/* CTA */}
|
||||
<div className="flex flex-wrap gap-3 mb-14">
|
||||
<button
|
||||
onClick={() => setModalOpen(true)}
|
||||
onClick={() => { trackCTAClick('무료 상담 신청', '/'); setModalOpen(true); }}
|
||||
className="inline-flex items-center gap-2 bg-[#1a56db] hover:bg-[#1e4fc2] text-white px-8 py-4 rounded-xl font-bold text-sm transition-colors"
|
||||
>
|
||||
무료 상담 신청
|
||||
@@ -292,7 +332,7 @@ export default function Home() {
|
||||
══════════════════════════════════════ */}
|
||||
<section className="bg-white px-6 py-16 lg:px-14 lg:py-20">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="flex items-end justify-between mb-10 flex-wrap gap-4">
|
||||
<div className="reveal flex items-end justify-between mb-10 flex-wrap gap-4">
|
||||
<div>
|
||||
<p className="font-mono text-xs text-[#1a56db]/60 tracking-widest uppercase mb-2">
|
||||
Client Pain Points
|
||||
@@ -310,10 +350,10 @@ export default function Home() {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{PAIN_POINTS.map((p) => (
|
||||
{PAIN_POINTS.map((p, i) => (
|
||||
<div
|
||||
key={p.problem}
|
||||
className="border border-[#e2e8f0] rounded-2xl p-6 hover:border-[#cbd5e1] hover:shadow-sm transition-all bg-white"
|
||||
className={`reveal reveal-d${i + 1} border border-[#e2e8f0] rounded-2xl p-6 hover:border-[#cbd5e1] hover:shadow-sm transition-all bg-white`}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 ${p.color}`}>
|
||||
@@ -338,10 +378,10 @@ export default function Home() {
|
||||
══════════════════════════════════════ */}
|
||||
<section className="bg-[#04102b] px-6 py-16 lg:px-14 lg:py-20">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<p className="font-mono text-xs text-[#5ba4ff]/40 tracking-widest uppercase mb-3">
|
||||
<p className="reveal font-mono text-xs text-[#5ba4ff]/40 tracking-widest uppercase mb-3">
|
||||
About
|
||||
</p>
|
||||
<div className="grid lg:grid-cols-2 gap-10 lg:gap-16 items-start">
|
||||
<div className="reveal grid lg:grid-cols-2 gap-10 lg:gap-16 items-start">
|
||||
{/* 좌측: 스토리 */}
|
||||
<div>
|
||||
<h2
|
||||
@@ -432,7 +472,7 @@ export default function Home() {
|
||||
══════════════════════════════════════ */}
|
||||
<section className="bg-[#f8faff] px-6 py-16 lg:px-14 lg:py-20">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="mb-10">
|
||||
<div className="reveal mb-10">
|
||||
<p className="font-mono text-xs text-[#1a56db]/60 tracking-widest uppercase mb-2">
|
||||
Guarantee
|
||||
</p>
|
||||
@@ -444,7 +484,7 @@ export default function Home() {
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-px">
|
||||
<div className="reveal space-y-px">
|
||||
{PROMISES.map((p, i) => (
|
||||
<div
|
||||
key={p.number}
|
||||
@@ -481,7 +521,7 @@ export default function Home() {
|
||||
══════════════════════════════════════ */}
|
||||
<section className="bg-[#04102b] px-6 py-16 lg:px-14 lg:py-20">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="mb-10">
|
||||
<div className="reveal mb-10">
|
||||
<p className="font-mono text-xs text-[#5ba4ff]/40 tracking-widest uppercase mb-2">
|
||||
Live Portfolio
|
||||
</p>
|
||||
@@ -497,11 +537,11 @@ export default function Home() {
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
{LIVE_SERVICES.map((s) => (
|
||||
{LIVE_SERVICES.map((s, i) => (
|
||||
<Link
|
||||
key={s.name}
|
||||
href={s.url}
|
||||
className="group relative flex flex-col border border-white/8 hover:border-white/20 rounded-2xl p-6 transition-all hover:bg-white/3"
|
||||
className={`reveal reveal-d${i + 1} group relative flex flex-col border border-white/8 hover:border-white/20 rounded-2xl p-6 transition-all hover:bg-white/3`}
|
||||
>
|
||||
{/* 상단 */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
@@ -551,7 +591,7 @@ export default function Home() {
|
||||
══════════════════════════════════════ */}
|
||||
<section className="bg-white px-6 py-16 lg:px-14 lg:py-20">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="flex items-end justify-between flex-wrap gap-4 mb-8">
|
||||
<div className="reveal flex items-end justify-between flex-wrap gap-4 mb-8">
|
||||
<div>
|
||||
<p className="font-mono text-xs text-[#1a56db]/60 tracking-widest uppercase mb-2">Services</p>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]" style={{ wordBreak: 'keep-all' }}>
|
||||
@@ -566,7 +606,7 @@ export default function Home() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-[#f1f5f9]">
|
||||
<div className="reveal divide-y divide-[#f1f5f9]">
|
||||
{SERVICE_LIST.map((s) => (
|
||||
<Link
|
||||
key={s.href}
|
||||
@@ -617,7 +657,7 @@ export default function Home() {
|
||||
<section className="bg-[#04102b] px-6 py-20 lg:px-14">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* 무료 이벤트 배너 */}
|
||||
<div className="border border-white/8 rounded-2xl p-6 md:p-8 mb-10 flex flex-col md:flex-row items-start md:items-center justify-between gap-6">
|
||||
<div className="reveal border border-white/8 rounded-2xl p-6 md:p-8 mb-10 flex flex-col md:flex-row items-start md:items-center justify-between gap-6">
|
||||
<div>
|
||||
<div className="inline-flex items-center gap-2 bg-red-500/15 border border-red-500/20 text-red-400 text-xs font-bold px-3 py-1 rounded-full mb-3">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-red-400 animate-pulse" />
|
||||
@@ -639,7 +679,7 @@ export default function Home() {
|
||||
</div>
|
||||
|
||||
{/* 메인 CTA */}
|
||||
<div className="text-center">
|
||||
<div className="reveal text-center">
|
||||
<p className="font-mono text-xs text-[#5ba4ff]/40 tracking-widest uppercase mb-4">Get Started</p>
|
||||
<h2
|
||||
className="text-3xl md:text-5xl font-extrabold text-white mb-4 leading-tight"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
import ContactModal from '../../components/ContactModal';
|
||||
|
||||
@@ -162,12 +162,51 @@ const FAQ = [
|
||||
},
|
||||
];
|
||||
|
||||
function useScrollReveal() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('is-visible');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
|
||||
);
|
||||
el.querySelectorAll('.reveal').forEach((child) => observer.observe(child));
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
return ref;
|
||||
}
|
||||
|
||||
export default function AiKitPage() {
|
||||
const totalMonthlySaving = 27;
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const containerRef = useScrollReveal();
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-[#f0f4ff]">
|
||||
<div ref={containerRef} className="min-h-full bg-[#f0f4ff]">
|
||||
<style>{`
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(1.5rem);
|
||||
transition: opacity 0.7s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.reveal.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.reveal-d1 { transition-delay: 80ms; }
|
||||
.reveal-d2 { transition-delay: 160ms; }
|
||||
.reveal-d3 { transition-delay: 240ms; }
|
||||
.reveal-d4 { transition-delay: 320ms; }
|
||||
`}</style>
|
||||
<ContactModal
|
||||
isOpen={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
@@ -245,7 +284,7 @@ export default function AiKitPage() {
|
||||
{/* ─── 시간 낭비 가시화 섹션 ─── */}
|
||||
<div className="bg-white px-6 py-12 lg:px-12 border-b border-slate-100">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="reveal text-center mb-8">
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-slate-800 mb-2">
|
||||
지금 이 순간도 낭비되고 있는 당신의 시간
|
||||
</h2>
|
||||
@@ -253,7 +292,7 @@ export default function AiKitPage() {
|
||||
</div>
|
||||
|
||||
{/* 총합 카드 */}
|
||||
<div className="bg-red-50 border border-red-200 rounded-2xl p-6 mb-8">
|
||||
<div className="reveal bg-red-50 border border-red-200 rounded-2xl p-6 mb-8">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-bold text-red-600 mb-1">6가지 반복 업무를 혼자 할 때</p>
|
||||
@@ -273,7 +312,7 @@ export default function AiKitPage() {
|
||||
</div>
|
||||
|
||||
{/* 개별 도구 Before/After 바 차트 */}
|
||||
<div className="space-y-3">
|
||||
<div className="reveal space-y-3">
|
||||
{TOOLS.map((tool, i) => {
|
||||
const beforeVal = tool.before;
|
||||
const afterVal = tool.after;
|
||||
@@ -313,7 +352,7 @@ export default function AiKitPage() {
|
||||
{/* ─── "안 쓰면 생기는 실패 비용" 섹션 ─── */}
|
||||
<div className="px-6 py-12 lg:px-12 bg-[#0a0f2e]">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="reveal text-center mb-8">
|
||||
<div className="inline-flex items-center gap-2 bg-red-500/15 border border-red-500/30 text-red-400 text-xs font-extrabold px-4 py-1.5 rounded-full uppercase tracking-widest mb-3">
|
||||
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 9v4m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" /></svg>
|
||||
AI를 안 쓸 때 생기는 실패 비용
|
||||
@@ -324,7 +363,7 @@ export default function AiKitPage() {
|
||||
<p className="text-slate-400 text-sm">반복 업무를 수작업으로 할 때 실제로 발생하는 손실들</p>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="reveal grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{TOOLS.map((tool, i) => (
|
||||
<div key={i} className="bg-slate-900/60 border border-red-900/40 rounded-2xl p-5">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
@@ -342,7 +381,7 @@ export default function AiKitPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 bg-[#0a0f2e] border border-indigo-500/30 rounded-2xl p-6 text-center">
|
||||
<div className="reveal mt-8 bg-[#0a0f2e] border border-indigo-500/30 rounded-2xl p-6 text-center">
|
||||
<p className="text-white text-lg font-extrabold mb-1">
|
||||
실수 1번이 계약 1건을 날립니다.
|
||||
</p>
|
||||
@@ -356,7 +395,7 @@ export default function AiKitPage() {
|
||||
{/* ─── 포함 도구 ─── */}
|
||||
<div className="px-6 py-12 lg:px-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="reveal text-center mb-8">
|
||||
<div className="inline-flex items-center gap-2 bg-indigo-500/10 border border-indigo-500/25 text-indigo-500 text-xs font-extrabold px-4 py-1.5 rounded-full uppercase tracking-widest mb-3">
|
||||
6가지 AI 자동화 도구 포함
|
||||
</div>
|
||||
@@ -366,7 +405,7 @@ export default function AiKitPage() {
|
||||
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{TOOLS.map((tool, i) => (
|
||||
<div key={i} className="bg-white rounded-2xl p-5 border border-slate-200 hover:border-indigo-300 hover:shadow-lg transition-all group">
|
||||
<div key={i} className={`reveal reveal-d${(i % 3) + 1} bg-white rounded-2xl p-5 border border-slate-200 hover:border-indigo-300 hover:shadow-lg transition-all group`}>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="w-11 h-11 rounded-xl bg-indigo-50 border border-indigo-100 flex items-center justify-center text-indigo-500 group-hover:bg-indigo-100 transition-colors">
|
||||
{tool.icon}
|
||||
@@ -391,7 +430,7 @@ export default function AiKitPage() {
|
||||
</div>
|
||||
|
||||
{/* 업데이트 알림 */}
|
||||
<div className="mt-6 bg-indigo-50 border border-indigo-200 rounded-2xl p-5 flex items-start gap-4">
|
||||
<div className="reveal mt-6 bg-indigo-50 border border-indigo-200 rounded-2xl p-5 flex items-start gap-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-indigo-100 flex items-center justify-center text-indigo-600 flex-shrink-0">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
@@ -408,7 +447,7 @@ export default function AiKitPage() {
|
||||
{/* ─── 누구에게 필요한가 ─── */}
|
||||
<div className="px-6 py-10 lg:px-12 bg-white">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="reveal text-center mb-8">
|
||||
<h2 className="text-2xl font-extrabold text-slate-800">이런 분들이 가장 많이 씁니다</h2>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
@@ -454,7 +493,7 @@ export default function AiKitPage() {
|
||||
gain: '한 달치 콘텐츠 기획 → 10분 완성',
|
||||
},
|
||||
].map((item, i) => (
|
||||
<div key={i} className="p-5 rounded-2xl bg-slate-50 border border-slate-100">
|
||||
<div key={i} className={`reveal reveal-d${i + 1} p-5 rounded-2xl bg-slate-50 border border-slate-100`}>
|
||||
<div className="w-9 h-9 rounded-lg bg-indigo-100 text-indigo-600 flex items-center justify-center mb-3">{item.icon}</div>
|
||||
<p className="text-sm font-extrabold text-slate-800 mb-2">{item.title}</p>
|
||||
<p className="text-xs text-slate-500 italic leading-relaxed mb-3">{item.pain}</p>
|
||||
@@ -470,12 +509,12 @@ export default function AiKitPage() {
|
||||
{/* ─── 사용 후기 ─── */}
|
||||
<div className="px-6 py-10 lg:px-12">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="reveal text-center mb-8">
|
||||
<h2 className="text-2xl font-extrabold text-slate-800">실제 사용 후기</h2>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-3 gap-4">
|
||||
{TESTIMONIALS.map((t, i) => (
|
||||
<div key={i} className="bg-white rounded-2xl p-5 border border-slate-200 shadow-sm">
|
||||
<div key={i} className={`reveal reveal-d${i + 1} bg-white rounded-2xl p-5 border border-slate-200 shadow-sm`}>
|
||||
<div className="flex gap-0.5 mb-3">
|
||||
{Array.from({ length: t.rating }).map((_, j) => (
|
||||
<svg key={j} className="w-4 h-4 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
@@ -497,10 +536,10 @@ export default function AiKitPage() {
|
||||
{/* ─── FAQ ─── */}
|
||||
<div className="px-6 py-10 lg:px-12 bg-white">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="text-center mb-8">
|
||||
<div className="reveal text-center mb-8">
|
||||
<h2 className="text-2xl font-extrabold text-slate-800">자주 묻는 질문</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="reveal space-y-3">
|
||||
{FAQ.map((item, i) => (
|
||||
<div key={i} className="border border-slate-200 rounded-xl p-5">
|
||||
<p className="text-sm font-bold text-slate-800 mb-2">Q. {item.q}</p>
|
||||
@@ -513,7 +552,7 @@ export default function AiKitPage() {
|
||||
|
||||
{/* ─── 최하단 CTA ─── */}
|
||||
<div className="px-6 py-14 lg:px-12 bg-[#0a0f2e]" style={{ backgroundImage: 'repeating-linear-gradient(135deg, rgba(255,255,255,0.012) 0px, rgba(255,255,255,0.012) 1px, transparent 1px, transparent 40px)' }}>
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<div className="reveal max-w-2xl mx-auto text-center">
|
||||
{/* 마지막 카피: 기회비용 프레이밍 */}
|
||||
<p className="text-indigo-300/60 text-sm font-bold uppercase tracking-widest mb-3">구독 안 하면 내일도 동일합니다</p>
|
||||
<h2 className="text-2xl md:text-3xl font-extrabold text-white mb-3">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
import ContactModal from '../../components/ContactModal';
|
||||
import { trackCTAClick } from '../../../lib/gtag';
|
||||
|
||||
function useScrollReveal() {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@@ -188,6 +189,7 @@ export default function AutomationPage() {
|
||||
const containerRef = useScrollReveal();
|
||||
|
||||
const openModal = (service: string) => {
|
||||
trackCTAClick(service, '/services/automation');
|
||||
setModalService(service);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
import ContactModal from '../../components/ContactModal';
|
||||
import { trackCTAClick } from '../../../lib/gtag';
|
||||
const KAKAO_CHANNEL_URL = process.env.NEXT_PUBLIC_KAKAO_CHANNEL_URL ?? null;
|
||||
|
||||
function useScrollReveal() {
|
||||
@@ -372,6 +373,7 @@ export default function PromptPage() {
|
||||
const containerRef = useScrollReveal();
|
||||
|
||||
const openModal = (service: string) => {
|
||||
trackCTAClick(service, '/services/prompt');
|
||||
setModalService(service);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import Link from 'next/link';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import ContactModal from '../../components/ContactModal';
|
||||
import { trackCTAClick } from '../../../lib/gtag';
|
||||
|
||||
const samples = [
|
||||
{
|
||||
@@ -451,6 +452,7 @@ export default function WebsiteServicePage() {
|
||||
const [modalService, setModalService] = useState('홈페이지 제작');
|
||||
|
||||
const openModal = (service: string) => {
|
||||
trackCTAClick(service, '/services/website');
|
||||
setModalService(service);
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { trackToolDemo, trackCTAClick } from '../../lib/gtag';
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
도구 쇼케이스 — 리디자인 v2
|
||||
@@ -205,6 +206,7 @@ export default function ToolsShowcasePage() {
|
||||
<Link
|
||||
key={tool.id}
|
||||
href={tool.href}
|
||||
onClick={() => trackToolDemo(tool.title)}
|
||||
className={`group block tool-card bg-white rounded-2xl border border-slate-200 overflow-hidden active:scale-[0.99] reveal reveal-delay-${idx + 1}`}
|
||||
>
|
||||
<div className={`flex flex-col ${isEven ? 'md:flex-row-reverse' : 'md:flex-row'}`}>
|
||||
@@ -341,6 +343,7 @@ export default function ToolsShowcasePage() {
|
||||
</div>
|
||||
<Link
|
||||
href="/freelance#contact-form"
|
||||
onClick={() => trackCTAClick('무료 상담 신청하기', '/tools')}
|
||||
className="group inline-flex items-center gap-3 px-7 py-4 rounded-xl bg-blue-600 hover:bg-blue-500 active:scale-[0.98] text-white text-sm font-bold transition-all duration-300 shadow-lg shadow-blue-600/20 hover:shadow-blue-500/30 flex-shrink-0"
|
||||
>
|
||||
무료 상담 신청하기
|
||||
|
||||
47
lib/gtag.ts
Normal file
47
lib/gtag.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export const GA_ID = 'G-WG77RNHXRK';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
declare global { interface Window { gtag?: (...args: any[]) => void } }
|
||||
|
||||
export function trackEvent(
|
||||
eventName: string,
|
||||
params?: Record<string, string | number>,
|
||||
) {
|
||||
if (typeof window !== 'undefined' && typeof window.gtag === 'function') {
|
||||
window.gtag('event', eventName, params);
|
||||
}
|
||||
}
|
||||
|
||||
/** CTA 클릭 추적 */
|
||||
export function trackCTAClick(label: string, page?: string) {
|
||||
trackEvent('cta_click', {
|
||||
event_category: 'engagement',
|
||||
event_label: label,
|
||||
page_location: page || (typeof window !== 'undefined' ? window.location.pathname : ''),
|
||||
});
|
||||
}
|
||||
|
||||
/** 도구 체험/데모 클릭 */
|
||||
export function trackToolDemo(toolName: string) {
|
||||
trackEvent('tool_demo_click', {
|
||||
event_category: 'engagement',
|
||||
tool_name: toolName,
|
||||
});
|
||||
}
|
||||
|
||||
/** 무료 도구 다운로드 */
|
||||
export function trackDownload(toolName: string) {
|
||||
trackEvent('file_download', {
|
||||
event_category: 'conversion',
|
||||
file_name: toolName,
|
||||
});
|
||||
}
|
||||
|
||||
/** 외부 링크 클릭 (크몽 등) */
|
||||
export function trackOutboundClick(url: string, label: string) {
|
||||
trackEvent('outbound_click', {
|
||||
event_category: 'outbound',
|
||||
event_label: label,
|
||||
link_url: url,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user