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 { useState, useEffect, Suspense } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
import { trackEvent } from '../../lib/gtag';
|
||||||
|
|
||||||
function ContactFormInner() {
|
function ContactFormInner() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -36,6 +37,10 @@ function ContactFormInner() {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (!response.ok) throw new Error(data.error || '문의 전송에 실패했습니다.');
|
if (!response.ok) throw new Error(data.error || '문의 전송에 실패했습니다.');
|
||||||
setStatus('success');
|
setStatus('success');
|
||||||
|
trackEvent('generate_lead', {
|
||||||
|
event_category: 'contact',
|
||||||
|
event_label: formData.service,
|
||||||
|
});
|
||||||
setFormData({ name: '', phone: '', email: '', service: '외주 개발 문의', message: '' });
|
setFormData({ name: '', phone: '', email: '', service: '외주 개발 문의', message: '' });
|
||||||
setTimeout(() => setStatus('idle'), 5000);
|
setTimeout(() => setStatus('idle'), 5000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { trackEvent } from '../../lib/gtag';
|
||||||
|
|
||||||
interface ContactModalProps {
|
interface ContactModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -63,17 +64,6 @@ export default function ContactModal({
|
|||||||
setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));
|
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) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setStatus('loading');
|
setStatus('loading');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import ContactForm from '../components/ContactForm';
|
import ContactForm from '../components/ContactForm';
|
||||||
|
|
||||||
/* ─── Data ─── */
|
/* ─── 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 ─── */
|
/* ─── Main Page ─── */
|
||||||
export default function FreelancePage() {
|
export default function FreelancePage() {
|
||||||
const [_contactPreset] = useState('');
|
const [_contactPreset] = useState('');
|
||||||
|
const containerRef = useScrollReveal();
|
||||||
|
|
||||||
return (
|
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 ─── */}
|
{/* ─── Hero ─── */}
|
||||||
<div
|
<div
|
||||||
@@ -293,13 +333,13 @@ export default function FreelancePage() {
|
|||||||
{/* ─── 포트폴리오 ─── */}
|
{/* ─── 포트폴리오 ─── */}
|
||||||
<div className="px-6 py-12 lg:px-12">
|
<div className="px-6 py-12 lg:px-12">
|
||||||
<div className="max-w-5xl mx-auto">
|
<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>
|
<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>
|
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]">직접 개발한 프로젝트</h2>
|
||||||
<p className="text-slate-500 text-sm mt-2">실제 운영 중인 서비스와 납품 완료 프로젝트입니다</p>
|
<p className="text-slate-500 text-sm mt-2">실제 운영 중인 서비스와 납품 완료 프로젝트입니다</p>
|
||||||
</div>
|
</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) => (
|
{portfolio.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={item.title}
|
key={item.title}
|
||||||
@@ -358,7 +398,7 @@ export default function FreelancePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 추가 문구 */}
|
{/* 추가 문구 */}
|
||||||
<div className="mt-6 text-center">
|
<div className="reveal mt-6 text-center">
|
||||||
<p className="text-slate-400 text-sm">
|
<p className="text-slate-400 text-sm">
|
||||||
위 프로젝트 외에도 다양한 프로젝트 경험이 있습니다 ·{' '}
|
위 프로젝트 외에도 다양한 프로젝트 경험이 있습니다 ·{' '}
|
||||||
<a href="mailto:bgg8988@gmail.com" className="text-[#1a56db] hover:underline font-medium">포트폴리오 전체 요청</a>
|
<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="px-6 pb-12 lg:px-12">
|
||||||
<div className="max-w-5xl mx-auto">
|
<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>
|
<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>
|
<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>
|
<p className="text-slate-500 text-sm mt-2" style={{ wordBreak: 'keep-all' }}>숫자보다 실제 말이 더 정직합니다</p>
|
||||||
</div>
|
</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) => (
|
{testimonials.map((t) => (
|
||||||
<div
|
<div
|
||||||
key={t.name}
|
key={t.name}
|
||||||
@@ -420,7 +460,7 @@ export default function FreelancePage() {
|
|||||||
* 의뢰인 동의 하에 게시된 후기입니다. 전체 대화 내역 공개 요청 시 제공 가능합니다.
|
* 의뢰인 동의 하에 게시된 후기입니다. 전체 대화 내역 공개 요청 시 제공 가능합니다.
|
||||||
</p>
|
</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 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>
|
</a>
|
||||||
@@ -432,14 +472,14 @@ export default function FreelancePage() {
|
|||||||
{/* ─── 진행 프로세스 ─── */}
|
{/* ─── 진행 프로세스 ─── */}
|
||||||
<div className="px-6 pb-12 lg:px-12">
|
<div className="px-6 pb-12 lg:px-12">
|
||||||
<div className="max-w-3xl mx-auto">
|
<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>
|
<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>
|
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]">진행 프로세스</h2>
|
||||||
<p className="text-slate-500 text-sm mt-2">투명하고 체계적인 6단계로 진행됩니다</p>
|
<p className="text-slate-500 text-sm mt-2">투명하고 체계적인 6단계로 진행됩니다</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Vertical timeline */}
|
{/* Vertical timeline */}
|
||||||
<div className="relative">
|
<div className="reveal relative">
|
||||||
{/* connecting line */}
|
{/* connecting line */}
|
||||||
<div className="absolute left-6 top-6 bottom-6 w-px bg-[#dbe8ff]" />
|
<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">
|
<div className="max-w-5xl mx-auto grid md:grid-cols-2 gap-5">
|
||||||
|
|
||||||
{/* Tech Stack */}
|
{/* 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="flex items-center gap-2 mb-4">
|
||||||
<div className="w-1 h-5 bg-[#1a56db] rounded-full" />
|
<div className="w-1 h-5 bg-[#1a56db] rounded-full" />
|
||||||
<h3 className="font-bold text-[#04102b] text-sm">개발 가능 기술 스택</h3>
|
<h3 className="font-bold text-[#04102b] text-sm">개발 가능 기술 스택</h3>
|
||||||
@@ -527,7 +567,7 @@ export default function FreelancePage() {
|
|||||||
|
|
||||||
{/* 신뢰 포인트 */}
|
{/* 신뢰 포인트 */}
|
||||||
<div
|
<div
|
||||||
className="rounded-2xl border border-[#1a3a7a] p-6"
|
className="reveal reveal-d2 rounded-2xl border border-[#1a3a7a] p-6"
|
||||||
style={{
|
style={{
|
||||||
background: '#04102b',
|
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)',
|
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 id="contact-form" className="px-6 pb-14 lg:px-12">
|
||||||
<div className="max-w-5xl mx-auto">
|
<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>
|
<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>
|
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]">프로젝트 문의</h2>
|
||||||
<p className="text-slate-500 text-sm mt-2">개발사 연락 두절로 손해 본 경험 있으신가요? 여기선 계약서부터 시작합니다.</p>
|
<p className="text-slate-500 text-sm mt-2">개발사 연락 두절로 손해 본 경험 있으신가요? 여기선 계약서부터 시작합니다.</p>
|
||||||
</div>
|
</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="md:col-span-2 space-y-4">
|
||||||
<div className="bg-white rounded-2xl border border-[#dbe8ff] p-5">
|
<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';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import ContactModal from './components/ContactModal';
|
import ContactModal from './components/ContactModal';
|
||||||
|
import { trackCTAClick } from '../lib/gtag';
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════
|
||||||
쟁승메이드 홈페이지 — 리뉴얼 v2
|
쟁승메이드 홈페이지 — 리뉴얼 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() {
|
export default function Home() {
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const containerRef = useScrollReveal();
|
||||||
|
|
||||||
return (
|
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
|
<ContactModal
|
||||||
isOpen={modalOpen}
|
isOpen={modalOpen}
|
||||||
onClose={() => setModalOpen(false)}
|
onClose={() => setModalOpen(false)}
|
||||||
@@ -248,7 +288,7 @@ export default function Home() {
|
|||||||
{/* CTA */}
|
{/* CTA */}
|
||||||
<div className="flex flex-wrap gap-3 mb-14">
|
<div className="flex flex-wrap gap-3 mb-14">
|
||||||
<button
|
<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"
|
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">
|
<section className="bg-white px-6 py-16 lg:px-14 lg:py-20">
|
||||||
<div className="max-w-5xl mx-auto">
|
<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>
|
<div>
|
||||||
<p className="font-mono text-xs text-[#1a56db]/60 tracking-widest uppercase mb-2">
|
<p className="font-mono text-xs text-[#1a56db]/60 tracking-widest uppercase mb-2">
|
||||||
Client Pain Points
|
Client Pain Points
|
||||||
@@ -310,10 +350,10 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
{PAIN_POINTS.map((p) => (
|
{PAIN_POINTS.map((p, i) => (
|
||||||
<div
|
<div
|
||||||
key={p.problem}
|
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="flex items-start gap-4">
|
||||||
<div className={`w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 ${p.color}`}>
|
<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">
|
<section className="bg-[#04102b] px-6 py-16 lg:px-14 lg:py-20">
|
||||||
<div className="max-w-5xl mx-auto">
|
<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
|
About
|
||||||
</p>
|
</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>
|
<div>
|
||||||
<h2
|
<h2
|
||||||
@@ -432,7 +472,7 @@ export default function Home() {
|
|||||||
══════════════════════════════════════ */}
|
══════════════════════════════════════ */}
|
||||||
<section className="bg-[#f8faff] px-6 py-16 lg:px-14 lg:py-20">
|
<section className="bg-[#f8faff] px-6 py-16 lg:px-14 lg:py-20">
|
||||||
<div className="max-w-5xl mx-auto">
|
<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">
|
<p className="font-mono text-xs text-[#1a56db]/60 tracking-widest uppercase mb-2">
|
||||||
Guarantee
|
Guarantee
|
||||||
</p>
|
</p>
|
||||||
@@ -444,7 +484,7 @@ export default function Home() {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-px">
|
<div className="reveal space-y-px">
|
||||||
{PROMISES.map((p, i) => (
|
{PROMISES.map((p, i) => (
|
||||||
<div
|
<div
|
||||||
key={p.number}
|
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">
|
<section className="bg-[#04102b] px-6 py-16 lg:px-14 lg:py-20">
|
||||||
<div className="max-w-5xl mx-auto">
|
<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">
|
<p className="font-mono text-xs text-[#5ba4ff]/40 tracking-widest uppercase mb-2">
|
||||||
Live Portfolio
|
Live Portfolio
|
||||||
</p>
|
</p>
|
||||||
@@ -497,11 +537,11 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-3 gap-4">
|
<div className="grid md:grid-cols-3 gap-4">
|
||||||
{LIVE_SERVICES.map((s) => (
|
{LIVE_SERVICES.map((s, i) => (
|
||||||
<Link
|
<Link
|
||||||
key={s.name}
|
key={s.name}
|
||||||
href={s.url}
|
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">
|
<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">
|
<section className="bg-white px-6 py-16 lg:px-14 lg:py-20">
|
||||||
<div className="max-w-5xl mx-auto">
|
<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>
|
<div>
|
||||||
<p className="font-mono text-xs text-[#1a56db]/60 tracking-widest uppercase mb-2">Services</p>
|
<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' }}>
|
<h2 className="text-2xl md:text-3xl font-extrabold text-[#04102b]" style={{ wordBreak: 'keep-all' }}>
|
||||||
@@ -566,7 +606,7 @@ export default function Home() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divide-y divide-[#f1f5f9]">
|
<div className="reveal divide-y divide-[#f1f5f9]">
|
||||||
{SERVICE_LIST.map((s) => (
|
{SERVICE_LIST.map((s) => (
|
||||||
<Link
|
<Link
|
||||||
key={s.href}
|
key={s.href}
|
||||||
@@ -617,7 +657,7 @@ export default function Home() {
|
|||||||
<section className="bg-[#04102b] px-6 py-20 lg:px-14">
|
<section className="bg-[#04102b] px-6 py-20 lg:px-14">
|
||||||
<div className="max-w-5xl mx-auto">
|
<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>
|
||||||
<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">
|
<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" />
|
<span className="w-1.5 h-1.5 rounded-full bg-red-400 animate-pulse" />
|
||||||
@@ -639,7 +679,7 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 메인 CTA */}
|
{/* 메인 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>
|
<p className="font-mono text-xs text-[#5ba4ff]/40 tracking-widest uppercase mb-4">Get Started</p>
|
||||||
<h2
|
<h2
|
||||||
className="text-3xl md:text-5xl font-extrabold text-white mb-4 leading-tight"
|
className="text-3xl md:text-5xl font-extrabold text-white mb-4 leading-tight"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import ContactModal from '../../components/ContactModal';
|
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() {
|
export default function AiKitPage() {
|
||||||
const totalMonthlySaving = 27;
|
const totalMonthlySaving = 27;
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const containerRef = useScrollReveal();
|
||||||
|
|
||||||
return (
|
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
|
<ContactModal
|
||||||
isOpen={modalOpen}
|
isOpen={modalOpen}
|
||||||
onClose={() => setModalOpen(false)}
|
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="bg-white px-6 py-12 lg:px-12 border-b border-slate-100">
|
||||||
<div className="max-w-5xl mx-auto">
|
<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 className="text-2xl md:text-3xl font-extrabold text-slate-800 mb-2">
|
||||||
지금 이 순간도 낭비되고 있는 당신의 시간
|
지금 이 순간도 낭비되고 있는 당신의 시간
|
||||||
</h2>
|
</h2>
|
||||||
@@ -253,7 +292,7 @@ export default function AiKitPage() {
|
|||||||
</div>
|
</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 className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-bold text-red-600 mb-1">6가지 반복 업무를 혼자 할 때</p>
|
<p className="text-sm font-bold text-red-600 mb-1">6가지 반복 업무를 혼자 할 때</p>
|
||||||
@@ -273,7 +312,7 @@ export default function AiKitPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 개별 도구 Before/After 바 차트 */}
|
{/* 개별 도구 Before/After 바 차트 */}
|
||||||
<div className="space-y-3">
|
<div className="reveal space-y-3">
|
||||||
{TOOLS.map((tool, i) => {
|
{TOOLS.map((tool, i) => {
|
||||||
const beforeVal = tool.before;
|
const beforeVal = tool.before;
|
||||||
const afterVal = tool.after;
|
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="px-6 py-12 lg:px-12 bg-[#0a0f2e]">
|
||||||
<div className="max-w-5xl mx-auto">
|
<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">
|
<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>
|
<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를 안 쓸 때 생기는 실패 비용
|
AI를 안 쓸 때 생기는 실패 비용
|
||||||
@@ -324,7 +363,7 @@ export default function AiKitPage() {
|
|||||||
<p className="text-slate-400 text-sm">반복 업무를 수작업으로 할 때 실제로 발생하는 손실들</p>
|
<p className="text-slate-400 text-sm">반복 업무를 수작업으로 할 때 실제로 발생하는 손실들</p>
|
||||||
</div>
|
</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) => (
|
{TOOLS.map((tool, i) => (
|
||||||
<div key={i} className="bg-slate-900/60 border border-red-900/40 rounded-2xl p-5">
|
<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">
|
<div className="flex items-start gap-3 mb-3">
|
||||||
@@ -342,7 +381,7 @@ export default function AiKitPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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">
|
<p className="text-white text-lg font-extrabold mb-1">
|
||||||
실수 1번이 계약 1건을 날립니다.
|
실수 1번이 계약 1건을 날립니다.
|
||||||
</p>
|
</p>
|
||||||
@@ -356,7 +395,7 @@ export default function AiKitPage() {
|
|||||||
{/* ─── 포함 도구 ─── */}
|
{/* ─── 포함 도구 ─── */}
|
||||||
<div className="px-6 py-12 lg:px-12">
|
<div className="px-6 py-12 lg:px-12">
|
||||||
<div className="max-w-5xl mx-auto">
|
<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">
|
<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 자동화 도구 포함
|
6가지 AI 자동화 도구 포함
|
||||||
</div>
|
</div>
|
||||||
@@ -366,7 +405,7 @@ export default function AiKitPage() {
|
|||||||
|
|
||||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{TOOLS.map((tool, i) => (
|
{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="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">
|
<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}
|
{tool.icon}
|
||||||
@@ -391,7 +430,7 @@ export default function AiKitPage() {
|
|||||||
</div>
|
</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">
|
<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">
|
<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" />
|
<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="px-6 py-10 lg:px-12 bg-white">
|
||||||
<div className="max-w-5xl mx-auto">
|
<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>
|
<h2 className="text-2xl font-extrabold text-slate-800">이런 분들이 가장 많이 씁니다</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
@@ -454,7 +493,7 @@ export default function AiKitPage() {
|
|||||||
gain: '한 달치 콘텐츠 기획 → 10분 완성',
|
gain: '한 달치 콘텐츠 기획 → 10분 완성',
|
||||||
},
|
},
|
||||||
].map((item, i) => (
|
].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>
|
<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-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>
|
<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="px-6 py-10 lg:px-12">
|
||||||
<div className="max-w-5xl mx-auto">
|
<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>
|
<h2 className="text-2xl font-extrabold text-slate-800">실제 사용 후기</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid sm:grid-cols-3 gap-4">
|
<div className="grid sm:grid-cols-3 gap-4">
|
||||||
{TESTIMONIALS.map((t, i) => (
|
{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">
|
<div className="flex gap-0.5 mb-3">
|
||||||
{Array.from({ length: t.rating }).map((_, j) => (
|
{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">
|
<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 ─── */}
|
{/* ─── FAQ ─── */}
|
||||||
<div className="px-6 py-10 lg:px-12 bg-white">
|
<div className="px-6 py-10 lg:px-12 bg-white">
|
||||||
<div className="max-w-3xl mx-auto">
|
<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>
|
<h2 className="text-2xl font-extrabold text-slate-800">자주 묻는 질문</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div className="reveal space-y-3">
|
||||||
{FAQ.map((item, i) => (
|
{FAQ.map((item, i) => (
|
||||||
<div key={i} className="border border-slate-200 rounded-xl p-5">
|
<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>
|
<p className="text-sm font-bold text-slate-800 mb-2">Q. {item.q}</p>
|
||||||
@@ -513,7 +552,7 @@ export default function AiKitPage() {
|
|||||||
|
|
||||||
{/* ─── 최하단 CTA ─── */}
|
{/* ─── 최하단 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="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>
|
<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">
|
<h2 className="text-2xl md:text-3xl font-extrabold text-white mb-3">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import ContactModal from '../../components/ContactModal';
|
import ContactModal from '../../components/ContactModal';
|
||||||
|
import { trackCTAClick } from '../../../lib/gtag';
|
||||||
|
|
||||||
function useScrollReveal() {
|
function useScrollReveal() {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
@@ -188,6 +189,7 @@ export default function AutomationPage() {
|
|||||||
const containerRef = useScrollReveal();
|
const containerRef = useScrollReveal();
|
||||||
|
|
||||||
const openModal = (service: string) => {
|
const openModal = (service: string) => {
|
||||||
|
trackCTAClick(service, '/services/automation');
|
||||||
setModalService(service);
|
setModalService(service);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import ContactModal from '../../components/ContactModal';
|
import ContactModal from '../../components/ContactModal';
|
||||||
|
import { trackCTAClick } from '../../../lib/gtag';
|
||||||
const KAKAO_CHANNEL_URL = process.env.NEXT_PUBLIC_KAKAO_CHANNEL_URL ?? null;
|
const KAKAO_CHANNEL_URL = process.env.NEXT_PUBLIC_KAKAO_CHANNEL_URL ?? null;
|
||||||
|
|
||||||
function useScrollReveal() {
|
function useScrollReveal() {
|
||||||
@@ -372,6 +373,7 @@ export default function PromptPage() {
|
|||||||
const containerRef = useScrollReveal();
|
const containerRef = useScrollReveal();
|
||||||
|
|
||||||
const openModal = (service: string) => {
|
const openModal = (service: string) => {
|
||||||
|
trackCTAClick(service, '/services/prompt');
|
||||||
setModalService(service);
|
setModalService(service);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import ContactModal from '../../components/ContactModal';
|
import ContactModal from '../../components/ContactModal';
|
||||||
|
import { trackCTAClick } from '../../../lib/gtag';
|
||||||
|
|
||||||
const samples = [
|
const samples = [
|
||||||
{
|
{
|
||||||
@@ -451,6 +452,7 @@ export default function WebsiteServicePage() {
|
|||||||
const [modalService, setModalService] = useState('홈페이지 제작');
|
const [modalService, setModalService] = useState('홈페이지 제작');
|
||||||
|
|
||||||
const openModal = (service: string) => {
|
const openModal = (service: string) => {
|
||||||
|
trackCTAClick(service, '/services/website');
|
||||||
setModalService(service);
|
setModalService(service);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { trackToolDemo, trackCTAClick } from '../../lib/gtag';
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════
|
||||||
도구 쇼케이스 — 리디자인 v2
|
도구 쇼케이스 — 리디자인 v2
|
||||||
@@ -205,6 +206,7 @@ export default function ToolsShowcasePage() {
|
|||||||
<Link
|
<Link
|
||||||
key={tool.id}
|
key={tool.id}
|
||||||
href={tool.href}
|
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}`}
|
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'}`}>
|
<div className={`flex flex-col ${isEven ? 'md:flex-row-reverse' : 'md:flex-row'}`}>
|
||||||
@@ -341,6 +343,7 @@ export default function ToolsShowcasePage() {
|
|||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/freelance#contact-form"
|
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"
|
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