From 95ac2e76a6aaad26b7433687a11f7c689bede29b Mon Sep 17 00:00:00 2001 From: soyeong Date: Sun, 23 Nov 2025 04:02:59 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20GA4=20User-ID=20=EC=A0=84=EC=86=A1=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EC=A7=80=EC=97=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - User-ID 설정 함수 추가 (setUserId) - trackEvent 함수에서 User-ID 자동 포함 - 로그인 시 User-ID 설정 (NicknamePage) - 로그아웃/탈퇴 시 User-ID 제거 - 검색 이벤트 전송 지연 처리 (GTM 처리 시간 확보) --- src/analytics/ga.ts | 19 ++++++++ .../home/HeaderToCarouselSection.tsx | 43 ++++++++++--------- src/hooks/useLogout.ts | 2 + src/hooks/useWithdraw.ts | 2 + src/pages/auth/NicknamePage.tsx | 2 + 5 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/analytics/ga.ts b/src/analytics/ga.ts index 1cceced..cb4ddc4 100644 --- a/src/analytics/ga.ts +++ b/src/analytics/ga.ts @@ -6,6 +6,7 @@ declare global { } let isInitialized = false; +let currentUserId: string | null = null; export const initGA = () => { // 중복 초기화 방지 @@ -20,6 +21,19 @@ export const initGA = () => { isInitialized = true; }; +// User-ID 설정 함수 +export const setUserId = (userId: string | null) => { + currentUserId = userId; + + if (typeof window !== 'undefined' && window.dataLayer) { + if (userId) { + window.dataLayer.push({ + user_id: userId, + }); + } + } +}; + export const trackPageView = (path: string) => { if (!isInitialized) { console.warn('GA4가 초기화되지 않았습니다. initGA()를 먼저 호출하세요.'); @@ -45,8 +59,13 @@ export const trackEvent = ( } if (typeof window !== 'undefined' && window.dataLayer) { + // 개발 환경에서만 debug_mode 활성화 + const isDevelopment = import.meta.env.DEV; + window.dataLayer.push({ event: eventName, + ...(isDevelopment && { debug_mode: true }), + ...(currentUserId && { user_id: currentUserId }), ...parameters, }); } diff --git a/src/components/home/HeaderToCarouselSection.tsx b/src/components/home/HeaderToCarouselSection.tsx index be7f4eb..f8a8a22 100644 --- a/src/components/home/HeaderToCarouselSection.tsx +++ b/src/components/home/HeaderToCarouselSection.tsx @@ -44,28 +44,31 @@ function HeaderToCarouselSection() { // 검색 이벤트 태깅 trackSearchStore(searchTerm, uniqueStores.length); - if ( - uniqueStores.length === 1 && - uniqueStores[0].name.toLowerCase().replace(/\s/g, '') === - searchTerm.toLowerCase().replace(/\s/g, '') - ) { - console.log('상세페이지로 이동:', uniqueStores[0].id); - navigate(`/store/${uniqueStores[0].id}`); - } else { - const isAddress = /동$|구$|역$/.test(searchTerm); - if (isAddress) { - navigate('/store-map', { state: { searchTerm } }); + // GTM이 이벤트를 처리할 시간을 주기 위해 약간의 지연 + setTimeout(() => { + if ( + uniqueStores.length === 1 && + uniqueStores[0].name.toLowerCase().replace(/\s/g, '') === + searchTerm.toLowerCase().replace(/\s/g, '') + ) { + console.log('상세페이지로 이동:', uniqueStores[0].id); + navigate(`/store/${uniqueStores[0].id}`); } else { - navigate('/store-map', { - state: { - searchTerm, - center: gpsLocation - ? { lat: gpsLocation.latitude, lng: gpsLocation.longitude } - : null, - }, - }); + const isAddress = /동$|구$|역$/.test(searchTerm); + if (isAddress) { + navigate('/store-map', { state: { searchTerm } }); + } else { + navigate('/store-map', { + state: { + searchTerm, + center: gpsLocation + ? { lat: gpsLocation.latitude, lng: gpsLocation.longitude } + : null, + }, + }); + } } - } + }, 100); } catch (error) { console.error('Search failed, navigating to map page as fallback', error); const isAddress = /동$|구$|역$/.test(inputValue); diff --git a/src/hooks/useLogout.ts b/src/hooks/useLogout.ts index a48758f..302aeac 100644 --- a/src/hooks/useLogout.ts +++ b/src/hooks/useLogout.ts @@ -1,5 +1,6 @@ import { useDispatch } from 'react-redux'; import { clearUser } from '@/store/userSlice'; +import { setUserId } from '@/analytics/ga'; export const useLogout = () => { const dispatch = useDispatch(); @@ -7,6 +8,7 @@ export const useLogout = () => { const handleLogout = () => { localStorage.removeItem('accessToken'); dispatch(clearUser()); + setUserId(null); // GA4 User-ID 제거 // window.location.reload(); }; diff --git a/src/hooks/useWithdraw.ts b/src/hooks/useWithdraw.ts index fcb0025..124bf8d 100644 --- a/src/hooks/useWithdraw.ts +++ b/src/hooks/useWithdraw.ts @@ -1,6 +1,7 @@ import { useDispatch } from 'react-redux'; import axiosInstance from '@/api/axiosInstance'; import { clearUser } from '@/store/userSlice'; +import { setUserId } from '@/analytics/ga'; export default function useWithdraw() { const dispatch = useDispatch(); @@ -9,6 +10,7 @@ export default function useWithdraw() { try { await axiosInstance.delete('/api/v1/user/me'); dispatch(clearUser()); + setUserId(null); // GA4 User-ID 제거 localStorage.removeItem('accessToken'); } catch (error) { console.error('회원 탈퇴 실패:', error); diff --git a/src/pages/auth/NicknamePage.tsx b/src/pages/auth/NicknamePage.tsx index 7e5972f..9cae998 100644 --- a/src/pages/auth/NicknamePage.tsx +++ b/src/pages/auth/NicknamePage.tsx @@ -6,6 +6,7 @@ import TopBar from '@/components/common/TopBar'; import { checkNicknameDuplicate, registerNickname } from '@/api/user'; import { useDispatch } from 'react-redux'; import { setUser } from '@/store/userSlice'; // ✅ ❷ 닉네임 저장용 액션 +import { setUserId } from '@/analytics/ga'; export default function NicknamePage() { const navigate = useNavigate(); @@ -67,6 +68,7 @@ export default function NicknamePage() { try { const result = await registerNickname(nickname); // 서버에서 등록 후 유저 정보 반환 dispatch(setUser({ id: result.email, nickname: result.nickname })); // nickname, email 받아서 Redux에 저장 + setUserId(result.email); // GA4 User-ID 설정 // 새로고침에도 유지하고 싶다면 localStorage에도 저장 // localStorage.setItem('nickname', result.nickname);