summaryrefslogtreecommitdiff
path: root/components/tracking
diff options
context:
space:
mode:
Diffstat (limited to 'components/tracking')
-rw-r--r--components/tracking/page-visit-tracker.tsx207
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