Skip to content

Commit 03b8f26

Browse files
authored
Merge pull request #48 from SWYP-mingling/chore/convert-to-object
refactor: 전역 트래킹 코드를 dataLayer.push 객체 형태로 변경
2 parents 211b186 + 846ba2d commit 03b8f26

7 files changed

Lines changed: 52 additions & 70 deletions

File tree

app/create/page.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useCreateMeeting } from '@/hooks/api/mutation/useCreateMeeting';
77
import type { MeetingCreateRequest } from '@/types/api';
88
import { useToast } from '@/hooks/useToast';
99
import Toast from '@/components/ui/toast';
10-
import { sendGAEvent } from '@next/third-parties/google';
10+
import { pushDataLayer } from '@/lib/gtm';
1111

1212
export default function Page() {
1313
const [meetingName, setMeetingName] = useState('');
@@ -157,8 +157,9 @@ export default function Page() {
157157
// 방장임을 증명하는 마패(로컬스토리지) 발급!
158158
localStorage.setItem(`is_host_${meetingId}`, 'true');
159159

160-
// 방 만든 브라우저가 누구인지 식별자를 담아서 이벤트 전송
161-
sendGAEvent('event', 'url_created', {
160+
// ⭐️ 방 만든 브라우저가 누구인지 식별자를 담아서 이벤트 전송 (dataLayer 직접 Push)
161+
pushDataLayer({
162+
event: 'url_created', // 객체 내부의 키로 이벤트명 삽입
162163
meeting_url_id: meetingId,
163164
participant_count_expected: capacity,
164165
browser_id: browserId,
@@ -168,8 +169,8 @@ export default function Page() {
168169
// -----------------------------------
169170

170171
// purposes를 localStorage에 저장 (장소 추천 카테고리로 사용)
171-
const purposes = getPurposes();
172-
if (purposes.length > 0) {
172+
const purposesStr = getPurposes();
173+
if (purposesStr.length > 0) {
173174
// meetingType 저장 (회의 또는 친목)
174175
if (meetingType) {
175176
localStorage.setItem(`meeting_${meetingId}_meetingType`, meetingType);

app/meeting/[id]/page.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ 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';
18+
import { pushDataLayer } from '@/lib/gtm';
19+
1920
interface StationInfo {
2021
line: string;
2122
name: string;
@@ -73,7 +74,7 @@ export default function Page() {
7374
}
7475
}, [isError, error, id]);
7576

76-
// GA4 전송 전용 도우미 함수
77+
// GA4 전송 전용 도우미 함수 (GTM 친화적)
7778
const trackShareEvent = () => {
7879
if (typeof window !== 'undefined') {
7980
let browserId = localStorage.getItem('browser_id');
@@ -82,7 +83,8 @@ export default function Page() {
8283
localStorage.setItem('browser_id', browserId);
8384
}
8485

85-
sendGAEvent('event', 'share_link', {
86+
pushDataLayer({
87+
event: 'share_link',
8688
meeting_url_id: id,
8789
location: 'creation_complete',
8890
browser_id: browserId,
@@ -145,7 +147,8 @@ export default function Page() {
145147
const isHost = localStorage.getItem(`is_host_${id}`) === 'true';
146148
const userRole = isHost ? 'host' : 'participant';
147149

148-
sendGAEvent('event', 'departure_location_submitted', {
150+
pushDataLayer({
151+
event: 'departure_location_submitted',
149152
meeting_url_id: id,
150153
user_cookie_id: browserId,
151154
role: userRole,
@@ -176,7 +179,8 @@ export default function Page() {
176179
const userRole = isHost ? 'host' : 'participant';
177180
const browserId = localStorage.getItem('browser_id');
178181

179-
sendGAEvent('event', 'midpoint_calculated', {
182+
pushDataLayer({
183+
event: 'midpoint_calculated',
180184
meeting_url_id: id,
181185
browser_id: browserId,
182186
role: userRole,

app/recommend/page.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import Image from 'next/image';
66
import KakaoMapRecommend from '@/components/map/kakaoMapRecommend';
77
import { useRecommend } from '@/hooks/api/query/useRecommend';
88
import { useCheckMeeting } from '@/hooks/api/query/useCheckMeeting';
9-
import { sendGAEvent } from '@next/third-parties/google';
9+
import { pushDataLayer } from '@/lib/gtm';
1010

1111
function RecommendContent() {
1212
const router = useRouter();
@@ -147,14 +147,15 @@ function RecommendContent() {
147147
) => {
148148
e.stopPropagation();
149149

150-
// 카카오맵에서 보기 클릭 시 GA 전송 (external_map_opened)
150+
// ⭐️ 카카오맵에서 보기 클릭 시 GA 전송 (GTM 친화적 dataLayer 직접 Push)
151151
if (typeof window !== 'undefined' && meetingId && place) {
152152
const browserId = localStorage.getItem('browser_id');
153153
const isHost = localStorage.getItem(`is_host_${meetingId}`) === 'true';
154154
const userRole = isHost ? 'host' : 'participant';
155155
const candidateId = `place_${String(place.id).padStart(2, '0')}`;
156156

157-
sendGAEvent('event', 'external_map_opened', {
157+
pushDataLayer({
158+
event: 'external_map_opened',
158159
meeting_url_id: meetingId,
159160
user_cookie_id: browserId,
160161
role: userRole,
@@ -169,12 +170,14 @@ function RecommendContent() {
169170
}
170171
};
171172

172-
// 장소 리스트 중 하나 클릭 시 GA 전송 (place_list_viewed)
173+
// ⭐️ 장소 리스트 중 하나 클릭 시 GA 전송 (GTM 친화적 dataLayer 직접 Push)
173174
const handlePlaceClick = (place: (typeof places)[0]) => {
174175
setSelectedPlaceId(place.id);
175-
if (meetingId) {
176+
if (typeof window !== 'undefined' && meetingId) {
176177
const candidateId = `place_${String(place.id).padStart(2, '0')}`;
177-
sendGAEvent('event', 'place_list_viewed', {
178+
179+
pushDataLayer({
180+
event: 'place_list_viewed',
178181
meeting_url_id: meetingId,
179182
candidate_id: candidateId,
180183
place_category: place.category,

app/result/[id]/page.tsx

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +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';
14+
import { pushDataLayer } from '@/lib/gtm';
1515

1616
export default function Page() {
1717
const queryClient = useQueryClient();
@@ -149,37 +149,6 @@ export default function Page() {
149149

150150
const [selectedResultId, setSelectedResultId] = useState<number>(1);
151151

152-
// 중간지점 후보 조회 GA 이벤트
153-
const trackMidpointCandidateViewed = useCallback(
154-
(candidateRankOrder: number, candidateId: string) => {
155-
if (typeof window === 'undefined' || !id) return;
156-
const browserId = localStorage.getItem('browser_id');
157-
const isHost = localStorage.getItem(`is_host_${id}`) === 'true';
158-
const userRole = isHost ? 'host' : 'participant';
159-
160-
sendGAEvent('event', 'midpoint_candidate_viewed', {
161-
meeting_url_id: id,
162-
user_cookie_id: browserId,
163-
role: userRole,
164-
candidate_rank_order: candidateRankOrder,
165-
candidate_id: candidateId,
166-
});
167-
},
168-
[id]
169-
);
170-
171-
// 장소 리스트에서 결과보기 페이지로 돌아왔을 때 midpoint_candidate_viewed 전송
172-
useEffect(() => {
173-
if (typeof window === 'undefined' || !id || locationResults.length === 0) return;
174-
const fromRecommend = sessionStorage.getItem(`from_recommend_${id}`);
175-
if (fromRecommend !== '1') return;
176-
177-
sessionStorage.removeItem(`from_recommend_${id}`);
178-
const selected = locationResults.find((r) => r.id === selectedResultId) ?? locationResults[0];
179-
const candidateId = `mid_${selected.endStation.replace(/\s+/g, '_')}`;
180-
trackMidpointCandidateViewed(selected.id, candidateId);
181-
}, [id, locationResults, selectedResultId, trackMidpointCandidateViewed]);
182-
183152
// 뒤로 가기 클릭 시 캐시 데이터 무효화
184153
const clearRelatedCache = useCallback(() => {
185154
queryClient.removeQueries({ queryKey: ['midpoint', id] });
@@ -192,15 +161,16 @@ export default function Page() {
192161
};
193162

194163
const handleResultShareClick = (e: React.MouseEvent<HTMLButtonElement>) => {
195-
// 1. GA 데이터 먼저 전송!
164+
// 1. GA 데이터 먼저 전송! (GTM 친화적 dataLayer 직접 Push)
196165
if (typeof window !== 'undefined') {
197166
let browserId = localStorage.getItem('browser_id');
198167
if (!browserId) {
199168
browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
200169
localStorage.setItem('browser_id', browserId);
201170
}
202171

203-
sendGAEvent('event', 'share_link', {
172+
pushDataLayer({
173+
event: 'share_link',
204174
meeting_url_id: id,
205175
location: 'place_list', // PM님 명세: 결과 리스트 페이지
206176
browser_id: browserId,
@@ -311,20 +281,13 @@ export default function Page() {
311281
if (meetingType) params.append('meetingType', meetingType);
312282
if (categoryParam) params.append('category', categoryParam);
313283

314-
if (typeof window !== 'undefined') {
315-
sessionStorage.setItem(`from_recommend_${id}`, '1');
316-
}
317284
router.push(`/recommend?${params.toString()}`);
318285
};
319286

320287
return (
321288
<div
322289
key={result.id}
323-
onClick={() => {
324-
setSelectedResultId(result.id);
325-
const candidateId = `mid_${result.endStation.replace(/\s+/g, '_')}`;
326-
trackMidpointCandidateViewed(result.id, candidateId);
327-
}}
290+
onClick={() => setSelectedResultId(result.id)}
328291
className={`flex cursor-pointer flex-col gap-3.75 rounded border bg-white p-5 ${
329292
selectedResultId === result.id
330293
? 'border-blue-5 border-2'

components/join/joinForm.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +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';
13+
import { pushDataLayer } from '@/lib/gtm';
1414

1515
interface JoinFormProps {
1616
meetingId: string;
@@ -66,23 +66,23 @@ export default function JoinForm({ meetingId }: JoinFormProps) {
6666
e.preventDefault();
6767
if (!isFormValid || !meetingId) return;
6868

69-
// ⭐️ 1. 비즈니스 로직 실행 전 GA 이벤트 선(先) 전송!
69+
// ⭐️ 1. 비즈니스 로직 실행 전 GA 이벤트 선(先) 전송! (dataLayer 방식 적용)
7070
if (typeof window !== 'undefined') {
7171
let browserId = localStorage.getItem('browser_id');
7272
if (!browserId) {
7373
browserId = `bid_${Math.random().toString(36).substring(2, 15)}${Date.now().toString(36)}`;
7474
localStorage.setItem('browser_id', browserId);
7575
}
7676

77-
// ⭐️ 개발자님의 완벽한 엣지케이스 방어 로직!
78-
// 로컬스토리지에 '이 방의 생성자(방장)'라는 징표가 있는지 확인
77+
// 방장/참여자 구분 로직
7978
const isHost = localStorage.getItem(`is_host_${meetingId}`) === 'true';
8079
const userRole = isHost ? 'host' : 'participant';
8180

82-
sendGAEvent('event', 'participant_registration', {
81+
pushDataLayer({
82+
event: 'participant_registration',
8383
meeting_url_id: meetingId,
84-
user_cookie_id: browserId, // 생성 때 썼던 그 browserId와 일치하게 됨!
85-
role: userRole, // 완벽하게 방장/참여자 구분 완료
84+
user_cookie_id: browserId,
85+
role: userRole,
8686
remember_me: isRemembered ? 'yes' : 'no',
8787
});
8888
}

components/share/shareContent.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +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';
8+
import { pushDataLayer } from '@/lib/gtm';
99

1010
interface ShareContentProps {
1111
id: string;
@@ -17,9 +17,7 @@ export default function ShareContent({ id }: ShareContentProps) {
1717

1818
// 복사 버튼 클릭 시 실행할 래퍼 함수 생성
1919
const handleCopyClickWithGA = () => {
20-
handleCopyLink();
21-
22-
// 2. GA4 이벤트 전송 로직 인라인 처리
20+
// 1. GA4 이벤트 전송 로직 (GTM 친화적 순수 객체 형태 & 무조건 먼저 실행!)
2321
if (typeof window !== 'undefined') {
2422
// 브라우저 식별자(browser_id) 확인 및 생성 (Get or Create)
2523
let browserId = localStorage.getItem('browser_id');
@@ -29,12 +27,17 @@ export default function ShareContent({ id }: ShareContentProps) {
2927
localStorage.setItem('browser_id', browserId);
3028
}
3129

32-
sendGAEvent('event', 'share_link', {
30+
// dataLayer 직접 Push (이벤트명을 키값으로!)
31+
pushDataLayer({
32+
event: 'share_link',
3333
meeting_url_id: id,
3434
location: 'creation_complete',
3535
browser_id: browserId,
3636
});
3737
}
38+
39+
// 2. 안전하게 전송 후 클립보드 복사 로직 실행
40+
handleCopyLink();
3841
};
3942

4043
if (isError) notFound();

lib/gtm.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const pushDataLayer = (data: Record<string, unknown>) => {
2+
if (typeof window !== 'undefined') {
3+
const w = window as unknown as { dataLayer: object[] };
4+
5+
w.dataLayer = w.dataLayer || [];
6+
w.dataLayer.push(data);
7+
}
8+
};

0 commit comments

Comments
 (0)