From 44bdb81a60d3a44ba7e379f3c20fe6d8fb284339 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 7 Jul 2025 08:24:16 +0000
Subject: (대표님) 변경사항 20250707 12시 30분
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/tracking/page-visit-tracker.tsx | 207 +++++++++++++++++++++++++++++
 1 file changed, 207 insertions(+)
 create mode 100644 components/tracking/page-visit-tracker.tsx

(limited to 'components/tracking/page-visit-tracker.tsx')

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
-- 
cgit v1.2.3

