// 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(Date.now()) const isTrackingRef = useRef(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 = { '/': '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} }