Skip to content

Commit e96a305

Browse files
authored
Merge pull request #46 from SWYP-mingling/chore/ga4-event
2 parents 32a9f89 + b45ddf4 commit e96a305

5 files changed

Lines changed: 121 additions & 8 deletions

File tree

app/create/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ export default function Page() {
145145

146146
// --- [GA4 이벤트 전송 로직 추가] ---
147147
if (typeof window !== 'undefined') {
148-
// 1. 브라우저 식별자(browser_id) 확인 및 생성 (Get or Create)
148+
// 브라우저 식별자(browser_id) 확인 및 생성 (Get or Create)
149149
let browserId = localStorage.getItem('browser_id');
150150
if (!browserId) {
151151
// 없으면 새로 발급해서 브라우저에 각인!
@@ -154,7 +154,10 @@ export default function Page() {
154154
localStorage.setItem('browser_id', browserId);
155155
}
156156

157-
// 2. 방 만든 브라우저가 누구인지 식별자를 담아서 이벤트 전송
157+
// 방장임을 증명하는 마패(로컬스토리지) 발급!
158+
localStorage.setItem(`is_host_${meetingId}`, 'true');
159+
160+
// 방 만든 브라우저가 누구인지 식별자를 담아서 이벤트 전송
158161
sendGAEvent('event', 'url_created', {
159162
meeting_url_id: meetingId,
160163
participant_count_expected: capacity,

app/meeting/[id]/page.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import MeetingInfoSection from '@/components/meeting/MeetingInfoSection';
1515
import { useToast } from '@/hooks/useToast';
1616
import Toast from '@/components/ui/toast';
1717
import { getMeetingUserId, removeMeetingUserId } from '@/lib/storage';
18+
import { sendGAEvent } from '@next/third-parties/google';
1819

1920
interface StationInfo {
2021
line: string;
@@ -73,8 +74,33 @@ export default function Page() {
7374
}
7475
}, [isError, error, id]);
7576

77+
// GA4 전송 전용 도우미 함수
78+
const trackShareEvent = () => {
79+
if (typeof window !== 'undefined') {
80+
let browserId = localStorage.getItem('browser_id');
81+
if (!browserId) {
82+
browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
83+
localStorage.setItem('browser_id', browserId);
84+
}
85+
86+
sendGAEvent('event', 'share_link', {
87+
meeting_url_id: id,
88+
location: 'creation_complete',
89+
browser_id: browserId,
90+
});
91+
}
92+
};
93+
94+
// 공유하기 버튼 전용 핸들러
7695
const handleShareClick = (e: React.MouseEvent<HTMLButtonElement>) => {
7796
openModal('SHARE', { meetingId: id }, e);
97+
trackShareEvent();
98+
};
99+
100+
// 재촉하기 버튼 전용 핸들러
101+
const handleNudgeClick = (e: React.MouseEvent<HTMLButtonElement>) => {
102+
openModal('NUDGE', { meetingId: id }, e);
103+
trackShareEvent();
78104
};
79105

80106
const handleSelectStation = (stationName: string | null) => {
@@ -109,6 +135,23 @@ export default function Page() {
109135
{
110136
onSuccess: () => {
111137
refetch();
138+
139+
if (typeof window !== 'undefined') {
140+
let browserId = localStorage.getItem('browser_id');
141+
if (!browserId) {
142+
browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
143+
localStorage.setItem('browser_id', browserId);
144+
}
145+
146+
const isHost = localStorage.getItem(`is_host_${id}`) === 'true';
147+
const userRole = isHost ? 'host' : 'participant';
148+
149+
sendGAEvent('event', 'departure_location_submitted', {
150+
meeting_url_id: id,
151+
user_cookie_id: browserId,
152+
role: userRole,
153+
});
154+
}
112155
},
113156
onError: () => {
114157
setErrorMessage('출발지 등록에 실패했습니다.');
@@ -244,7 +287,7 @@ export default function Page() {
244287
<button
245288
type="button"
246289
className="bg-blue-5 hover:bg-blue-8 flex h-21 w-full cursor-pointer items-center justify-between rounded p-4 text-left text-white transition-transform active:scale-[0.98]"
247-
onClick={(e) => openModal('NUDGE', { meetingId: id }, e)}
290+
onClick={handleNudgeClick}
248291
>
249292
<div className="flex flex-col gap-0.5">
250293
<span className="text-lg leading-[1.44] font-semibold">

app/result/[id]/page.tsx

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { getMeetingUserId } from '@/lib/storage';
1111
import { useQueryClient } from '@tanstack/react-query';
1212
import Loading from '@/components/loading/loading';
1313
import { getRandomHexColor } from '@/lib/color';
14+
import { sendGAEvent } from '@next/third-parties/google';
1415

1516
export default function Page() {
1617
const queryClient = useQueryClient();
@@ -159,6 +160,26 @@ export default function Page() {
159160
router.replace(`/meeting/${id}`);
160161
};
161162

163+
const handleResultShareClick = (e: React.MouseEvent<HTMLButtonElement>) => {
164+
// 1. GA 데이터 먼저 전송!
165+
if (typeof window !== 'undefined') {
166+
let browserId = localStorage.getItem('browser_id');
167+
if (!browserId) {
168+
browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
169+
localStorage.setItem('browser_id', browserId);
170+
}
171+
172+
sendGAEvent('event', 'share_link', {
173+
meeting_url_id: id,
174+
location: 'place_list', // PM님 명세: 결과 리스트 페이지
175+
browser_id: browserId,
176+
});
177+
}
178+
179+
// 2. 안전하게 전송 후 모달 띄우기
180+
openModal('SHARE', { meetingId: id }, e);
181+
};
182+
162183
useEffect(() => {
163184
clearRelatedCache();
164185

@@ -266,7 +287,7 @@ export default function Page() {
266287
<div
267288
key={result.id}
268289
onClick={() => setSelectedResultId(result.id)}
269-
className={`flex cursor-pointer flex-col gap-3.75 rounded-[4px] border bg-white p-5 ${
290+
className={`flex cursor-pointer flex-col gap-3.75 rounded border bg-white p-5 ${
270291
selectedResultId === result.id
271292
? 'border-blue-5 border-2'
272293
: 'border-gray-2 hover:bg-gray-1'
@@ -296,7 +317,7 @@ export default function Page() {
296317
<div className="flex gap-2">
297318
<button
298319
onClick={handleRecommendClick}
299-
className="bg-gray-8 hover:bg-gray-9 h-10 flex-1 cursor-pointer rounded-[4px] text-[15px] font-normal text-white transition-colors"
320+
className="bg-gray-8 hover:bg-gray-9 h-10 flex-1 cursor-pointer rounded text-[15px] font-normal text-white transition-colors"
300321
type="button"
301322
>
302323
주변 장소 추천
@@ -314,7 +335,7 @@ export default function Page() {
314335
e
315336
);
316337
}}
317-
className="bg-gray-1 hover:bg-gray-2 text-blue-5 h-10 flex-1 cursor-pointer rounded-[4px] text-[15px] font-normal transition-colors"
338+
className="bg-gray-1 hover:bg-gray-2 text-blue-5 h-10 flex-1 cursor-pointer rounded text-[15px] font-normal transition-colors"
318339
type="button"
319340
>
320341
환승 경로 보기
@@ -328,7 +349,7 @@ export default function Page() {
328349
</div>
329350

330351
<button
331-
onClick={(e) => openModal('SHARE', { meetingId: id }, e)}
352+
onClick={handleResultShareClick}
332353
className="bg-blue-5 hover:bg-blue-8 absolute right-5 bottom-0 left-5 flex h-12 items-center justify-center gap-2.5 rounded text-lg font-semibold text-white transition-transform active:scale-[0.98] md:right-0 md:left-0"
333354
>
334355
<Image src="/icon/share-white.svg" alt="공유 아이콘" width={20} height={20} />

components/join/joinForm.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useIsLoggedIn } from '@/hooks/useIsLoggedIn';
1010
import { setMeetingUserId } from '@/lib/storage';
1111
import Image from 'next/image';
1212
import { getRandomHexColor } from '@/lib/color';
13+
import { sendGAEvent } from '@next/third-parties/google';
1314

1415
interface JoinFormProps {
1516
meetingId: string;
@@ -65,6 +66,28 @@ export default function JoinForm({ meetingId }: JoinFormProps) {
6566
e.preventDefault();
6667
if (!isFormValid || !meetingId) return;
6768

69+
// ⭐️ 1. 비즈니스 로직 실행 전 GA 이벤트 선(先) 전송!
70+
if (typeof window !== 'undefined') {
71+
let browserId = localStorage.getItem('browser_id');
72+
if (!browserId) {
73+
browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
74+
localStorage.setItem('browser_id', browserId);
75+
}
76+
77+
// ⭐️ 개발자님의 완벽한 엣지케이스 방어 로직!
78+
// 로컬스토리지에 '이 방의 생성자(방장)'라는 징표가 있는지 확인
79+
const isHost = localStorage.getItem(`is_host_${meetingId}`) === 'true';
80+
const userRole = isHost ? 'host' : 'participant';
81+
82+
sendGAEvent('event', 'participant_registration', {
83+
meeting_url_id: meetingId,
84+
user_cookie_id: browserId, // 생성 때 썼던 그 browserId와 일치하게 됨!
85+
role: userRole, // 완벽하게 방장/참여자 구분 완료
86+
remember_me: isRemembered ? 'yes' : 'no',
87+
});
88+
}
89+
90+
// ⭐️ 2. 기존 참여 API 호출 로직
6891
try {
6992
// @ts-ignore
7093
const result = await participantEnter.mutateAsync({

components/share/shareContent.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Link from 'next/link';
55
import Toast from '@/components/ui/toast';
66
import { useShareMeeting } from '@/hooks/api/query/useShareMeeting';
77
import Image from 'next/image';
8+
import { sendGAEvent } from '@next/third-parties/google';
89

910
interface ShareContentProps {
1011
id: string;
@@ -14,6 +15,28 @@ export default function ShareContent({ id }: ShareContentProps) {
1415
// useParams() 대신 부모(Page)에서 전달받은 id 사용
1516
const { shareUrl, isError, isLoading, handleCopyLink, isVisible } = useShareMeeting(id);
1617

18+
// 복사 버튼 클릭 시 실행할 래퍼 함수 생성
19+
const handleCopyClickWithGA = () => {
20+
handleCopyLink();
21+
22+
// 2. GA4 이벤트 전송 로직 인라인 처리
23+
if (typeof window !== 'undefined') {
24+
// 브라우저 식별자(browser_id) 확인 및 생성 (Get or Create)
25+
let browserId = localStorage.getItem('browser_id');
26+
if (!browserId) {
27+
const randomStr = Math.random().toString(36).substring(2, 15);
28+
browserId = `bid_${randomStr}${Date.now().toString(36)}`;
29+
localStorage.setItem('browser_id', browserId);
30+
}
31+
32+
sendGAEvent('event', 'share_link', {
33+
meeting_url_id: id,
34+
location: 'creation_complete',
35+
browser_id: browserId,
36+
});
37+
}
38+
};
39+
1740
if (isError) notFound();
1841
if (isLoading) return null;
1942

@@ -44,7 +67,7 @@ export default function ShareContent({ id }: ShareContentProps) {
4467
/>
4568
<button
4669
type="button"
47-
onClick={handleCopyLink}
70+
onClick={handleCopyClickWithGA}
4871
className="bg-gray-1 text-gray-6 border-gray-1 hover:bg-gray-2 cursor-pointer rounded-r-sm border px-3.5 py-3 text-sm font-semibold whitespace-nowrap transition-colors"
4972
>
5073
복사

0 commit comments

Comments
 (0)