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:
2026-04-03 07:34:17 +09:00
parent c7bf0253e3
commit 5d2fd4be1f
10 changed files with 230 additions and 60 deletions

View File

@@ -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">