diff options
Diffstat (limited to 'components/tracking')
| -rw-r--r-- | components/tracking/page-visit-tracker.tsx | 207 |
1 files changed, 207 insertions, 0 deletions
diff --git a/components/tracking/page-visit-tracker.tsx b/components/tracking/page-visit-tracker.tsx new file mode 100644 index 00000000..09dfc03e --- /dev/null +++ b/components/tracking/page-visit-tracker.tsx @@ -0,0 +1,207 @@ +// components/tracking/page-visit-tracker.tsx +"use client" + +import { useEffect, useRef } from 'react' +import { useSession } from 'next-auth/react' +import { usePathname } from 'next/navigation' + +interface PageVisitTrackerProps { + children: React.ReactNode +} + +export function PageVisitTracker({ children }: PageVisitTrackerProps) { + const { data: session } = useSession() + const pathname = usePathname() + const startTimeRef = useRef<number>(Date.now()) + const isTrackingRef = useRef<boolean>(false) + + // 추적 제외 경로 판단 + const shouldExcludeFromTracking = (path: string): boolean => { + const excludePaths = [ + '/api', + '/_next', + '/favicon.ico', + '/robots.txt', + '/sitemap.xml' + ] + + const excludeExtensions = [ + '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', + '.css', '.js', '.woff', '.woff2', '.ttf', '.eot' + ] + + return excludePaths.some(exclude => path.startsWith(exclude)) || + excludeExtensions.some(ext => path.includes(ext)) + } + + // 디바이스 타입 감지 + const getDeviceType = (): string => { + if (typeof window === 'undefined') return 'unknown' + + const width = window.innerWidth + if (width < 768) return 'mobile' + if (width < 1024) return 'tablet' + return 'desktop' + } + + // 브라우저 정보 추출 + const getBrowserInfo = () => { + if (typeof navigator === 'undefined') return { name: 'unknown', os: 'unknown' } + + const userAgent = navigator.userAgent + let browserName = 'unknown' + let osName = 'unknown' + + // 간단한 브라우저 감지 + if (userAgent.includes('Chrome')) browserName = 'Chrome' + else if (userAgent.includes('Firefox')) browserName = 'Firefox' + else if (userAgent.includes('Safari')) browserName = 'Safari' + else if (userAgent.includes('Edge')) browserName = 'Edge' + + // 간단한 OS 감지 + if (userAgent.includes('Windows')) osName = 'Windows' + else if (userAgent.includes('Mac')) osName = 'macOS' + else if (userAgent.includes('Linux')) osName = 'Linux' + else if (userAgent.includes('Android')) osName = 'Android' + else if (userAgent.includes('iOS')) osName = 'iOS' + + return { name: browserName, os: osName } + } + + // 페이지 제목 추출 + const getPageTitle = (route: string): string => { + if (typeof document !== 'undefined' && document.title) { + return document.title + } + + // 경로 기반 제목 매핑 + const titleMap: Record<string, string> = { + '/': 'Home', + '/dashboard': 'Dashboard', + '/profile': 'Profile', + '/settings': 'Settings', + } + + // 언어 코드 제거 후 매핑 + const cleanRoute = route.replace(/^\/[a-z]{2}/, '') || '/' + return titleMap[cleanRoute] || cleanRoute + } + + // 페이지 방문 추적 + const trackPageVisit = async (route: string) => { + if (shouldExcludeFromTracking(route) || isTrackingRef.current) { + return + } + + isTrackingRef.current = true + + try { + const browserInfo = getBrowserInfo() + + const trackingData = { + route, + pageTitle: getPageTitle(route), + referrer: document.referrer || null, + deviceType: getDeviceType(), + browserName: browserInfo.name, + osName: browserInfo.os, + screenResolution: `${screen.width}x${screen.height}`, + windowSize: `${window.innerWidth}x${window.innerHeight}`, + language: navigator.language, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + timestamp: new Date().toISOString(), + userAgent: navigator.userAgent, + } + + // 백그라운드로 추적 API 호출 (에러 시에도 메인 기능에 영향 없음) + fetch('/api/tracking/page-visit', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(trackingData), + // 백그라운드 요청으로 설정 + keepalive: true, + }).catch(error => { + console.error('Page visit tracking failed:', error) + }) + + } catch (error) { + console.error('Client-side tracking error:', error) + } finally { + isTrackingRef.current = false + } + } + + // 체류 시간 기록 + const trackPageDuration = async (route: string, duration: number) => { + if (shouldExcludeFromTracking(route) || duration < 5) { + return // 5초 미만은 기록하지 않음 + } + + try { + const data = JSON.stringify({ + route, + duration, + timestamp: new Date().toISOString(), + }) + + // navigator.sendBeacon 사용 (페이지 이탈 시에도 안전하게 전송) + if (navigator.sendBeacon) { + navigator.sendBeacon('/api/tracking/page-duration', data) + } else { + // sendBeacon 미지원 시 fallback + fetch('/api/tracking/page-duration', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: data, + keepalive: true, + }).catch(() => { + // 페이지 이탈 시 에러는 무시 + }) + } + } catch (error) { + console.error('Duration tracking error:', error) + } + } + + useEffect(() => { + // 페이지 변경 시 추적 + startTimeRef.current = Date.now() + trackPageVisit(pathname) + + // 페이지 이탈 시 체류 시간 기록 + return () => { + const duration = Math.floor((Date.now() - startTimeRef.current) / 1000) + trackPageDuration(pathname, duration) + } + }, [pathname]) + + // 브라우저 닫기/새로고침 시 체류 시간 기록 + useEffect(() => { + const handleBeforeUnload = () => { + const duration = Math.floor((Date.now() - startTimeRef.current) / 1000) + trackPageDuration(pathname, duration) + } + + // visibility change 이벤트로 탭 변경 감지 + const handleVisibilityChange = () => { + if (document.visibilityState === 'hidden') { + const duration = Math.floor((Date.now() - startTimeRef.current) / 1000) + trackPageDuration(pathname, duration) + } else { + startTimeRef.current = Date.now() // 탭 복귀 시 시간 리셋 + } + } + + window.addEventListener('beforeunload', handleBeforeUnload) + document.addEventListener('visibilitychange', handleVisibilityChange) + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload) + document.removeEventListener('visibilitychange', handleVisibilityChange) + } + }, [pathname]) + + return <>{children}</> +}
\ No newline at end of file |
