From 5d2fd4be1fe2bda1bc57a9aacb27c9de43f07e7d Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 3 Apr 2026 07:34:17 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20GA4=20=EC=A0=84=ED=99=98=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EC=B6=94=EC=A0=81=20+=20=EC=A0=84=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20?= =?UTF-8?q?=EB=A6=AC=EB=B9=8C=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/components/ContactForm.tsx | 5 +++ app/components/ContactModal.tsx | 12 +---- app/freelance/page.tsx | 68 +++++++++++++++++++++++------ app/page.tsx | 74 +++++++++++++++++++++++-------- app/services/ai-kit/page.tsx | 75 ++++++++++++++++++++++++-------- app/services/automation/page.tsx | 2 + app/services/prompt/page.tsx | 2 + app/services/website/page.tsx | 2 + app/tools/page.tsx | 3 ++ lib/gtag.ts | 47 ++++++++++++++++++++ 10 files changed, 230 insertions(+), 60 deletions(-) create mode 100644 lib/gtag.ts diff --git a/app/components/ContactForm.tsx b/app/components/ContactForm.tsx index 8310273..898ba59 100644 --- a/app/components/ContactForm.tsx +++ b/app/components/ContactForm.tsx @@ -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) { diff --git a/app/components/ContactModal.tsx b/app/components/ContactModal.tsx index 5bbb136..3bdf7e7 100644 --- a/app/components/ContactModal.tsx +++ b/app/components/ContactModal.tsx @@ -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) => { - 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'); diff --git a/app/freelance/page.tsx b/app/freelance/page.tsx index f1cb61e..993758c 100644 --- a/app/freelance/page.tsx +++ b/app/freelance/page.tsx @@ -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(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 ( -
+
+ {/* ─── Hero ─── */}
-
+

PORTFOLIO

직접 개발한 프로젝트

실제 운영 중인 서비스와 납품 완료 프로젝트입니다

-
+
{portfolio.map((item) => (
{/* 추가 문구 */} -
+

위 프로젝트 외에도 다양한 프로젝트 경험이 있습니다 ·{' '} 포트폴리오 전체 요청 @@ -370,13 +410,13 @@ export default function FreelancePage() { {/* ─── 고객 후기 ─── */}

-
+

REVIEWS

실제 의뢰인 후기

숫자보다 실제 말이 더 정직합니다

-
+
{testimonials.map((t) => (
-
+
무료 상담 시작하기 @@ -432,14 +472,14 @@ export default function FreelancePage() { {/* ─── 진행 프로세스 ─── */}
-
+

PROCESS

진행 프로세스

투명하고 체계적인 6단계로 진행됩니다

{/* Vertical timeline */} -
+
{/* connecting line */}
@@ -499,7 +539,7 @@ export default function FreelancePage() {
{/* Tech Stack */} -
+

개발 가능 기술 스택

@@ -527,7 +567,7 @@ export default function FreelancePage() { {/* 신뢰 포인트 */}
-
+

CONTACT

프로젝트 문의

개발사 연락 두절로 손해 본 경험 있으신가요? 여기선 계약서부터 시작합니다.

-
+
{/* 왼쪽: 간단 안내 */}
diff --git a/app/page.tsx b/app/page.tsx index 9fd97d7..21ede33 100644 --- a/app/page.tsx +++ b/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(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 ( -
+
+ setModalOpen(false)} @@ -248,7 +288,7 @@ export default function Home() { {/* CTA */}
-
+
{SERVICE_LIST.map((s) => (
{/* 무료 이벤트 배너 */} -
+
@@ -639,7 +679,7 @@ export default function Home() {
{/* 메인 CTA */} -
+

Get Started

(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 ( -
+
+ setModalOpen(false)} @@ -245,7 +284,7 @@ export default function AiKitPage() { {/* ─── 시간 낭비 가시화 섹션 ─── */}
-
+

지금 이 순간도 낭비되고 있는 당신의 시간

@@ -253,7 +292,7 @@ export default function AiKitPage() {
{/* 총합 카드 */} -
+

6가지 반복 업무를 혼자 할 때

@@ -273,7 +312,7 @@ export default function AiKitPage() {
{/* 개별 도구 Before/After 바 차트 */} -
+
{TOOLS.map((tool, i) => { const beforeVal = tool.before; const afterVal = tool.after; @@ -313,7 +352,7 @@ export default function AiKitPage() { {/* ─── "안 쓰면 생기는 실패 비용" 섹션 ─── */}
-
+
AI를 안 쓸 때 생기는 실패 비용 @@ -324,7 +363,7 @@ export default function AiKitPage() {

반복 업무를 수작업으로 할 때 실제로 발생하는 손실들

-
+
{TOOLS.map((tool, i) => (
@@ -342,7 +381,7 @@ export default function AiKitPage() { ))}
-
+

실수 1번이 계약 1건을 날립니다.

@@ -356,7 +395,7 @@ export default function AiKitPage() { {/* ─── 포함 도구 ─── */}
-
+
6가지 AI 자동화 도구 포함
@@ -366,7 +405,7 @@ export default function AiKitPage() {
{TOOLS.map((tool, i) => ( -
+
{tool.icon} @@ -391,7 +430,7 @@ export default function AiKitPage() {
{/* 업데이트 알림 */} -
+
@@ -408,7 +447,7 @@ export default function AiKitPage() { {/* ─── 누구에게 필요한가 ─── */}
-
+

이런 분들이 가장 많이 씁니다

@@ -454,7 +493,7 @@ export default function AiKitPage() { gain: '한 달치 콘텐츠 기획 → 10분 완성', }, ].map((item, i) => ( -
+
{item.icon}

{item.title}

{item.pain}

@@ -470,12 +509,12 @@ export default function AiKitPage() { {/* ─── 사용 후기 ─── */}
-
+

실제 사용 후기

{TESTIMONIALS.map((t, i) => ( -
+
{Array.from({ length: t.rating }).map((_, j) => ( @@ -497,10 +536,10 @@ export default function AiKitPage() { {/* ─── FAQ ─── */}
-
+

자주 묻는 질문

-
+
{FAQ.map((item, i) => (

Q. {item.q}

@@ -513,7 +552,7 @@ export default function AiKitPage() { {/* ─── 최하단 CTA ─── */}
-
+
{/* 마지막 카피: 기회비용 프레이밍 */}

구독 안 하면 내일도 동일합니다

diff --git a/app/services/automation/page.tsx b/app/services/automation/page.tsx index b5de640..851a0db 100644 --- a/app/services/automation/page.tsx +++ b/app/services/automation/page.tsx @@ -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(null); @@ -188,6 +189,7 @@ export default function AutomationPage() { const containerRef = useScrollReveal(); const openModal = (service: string) => { + trackCTAClick(service, '/services/automation'); setModalService(service); setModalOpen(true); }; diff --git a/app/services/prompt/page.tsx b/app/services/prompt/page.tsx index a220c91..244c48a 100644 --- a/app/services/prompt/page.tsx +++ b/app/services/prompt/page.tsx @@ -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); }; diff --git a/app/services/website/page.tsx b/app/services/website/page.tsx index c02f562..6c551a8 100644 --- a/app/services/website/page.tsx +++ b/app/services/website/page.tsx @@ -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); }; diff --git a/app/tools/page.tsx b/app/tools/page.tsx index 870065e..bbfd729 100644 --- a/app/tools/page.tsx +++ b/app/tools/page.tsx @@ -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() { 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}`} >
@@ -341,6 +343,7 @@ export default function ToolsShowcasePage() {
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" > 무료 상담 신청하기 diff --git a/lib/gtag.ts b/lib/gtag.ts new file mode 100644 index 0000000..23a7896 --- /dev/null +++ b/lib/gtag.ts @@ -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, +) { + 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, + }); +}