// lib/session/repository.ts import db from '@/db/db' import { loginSessions, tempAuthSessions, pageVisits, type NewLoginSession, type NewTempAuthSession, type NewPageVisit, type LoginSession } from '@/db/schema' import { eq, and, desc, lt } from 'drizzle-orm' import { v4 as uuidv4 } from 'uuid' // 성능 최적화를 위한 캐시 const sessionCache = new Map() const CACHE_TTL = 5 * 60 * 1000 // 5분 캐시 export class SessionRepository { // 임시 인증 세션 관리 (기존 메모리 저장소 대체) static async createTempAuthSession(data: { userId: number email: string authMethod: string expiresAt: Date }): Promise { const tempAuthKey = `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` try { await db.insert(tempAuthSessions).values({ tempAuthKey, userId: data.userId, email: data.email, authMethod: data.authMethod, expiresAt: data.expiresAt, }) return tempAuthKey } catch (error) { console.error('Failed to create temp auth session:', error) throw error } } static async getTempAuthSession(tempAuthKey: string) { try { const result = await db .select() .from(tempAuthSessions) .where( and( eq(tempAuthSessions.tempAuthKey, tempAuthKey), eq(tempAuthSessions.isUsed, false) ) ) .limit(1) const session = result[0] if (!session || new Date() > session.expiresAt) { return null } return session } catch (error) { console.error('Failed to get temp auth session:', error) return null } } static async markTempAuthSessionAsUsed(tempAuthKey: string) { try { await db .update(tempAuthSessions) .set({ isUsed: true }) .where(eq(tempAuthSessions.tempAuthKey, tempAuthKey)) } catch (error) { console.error('Failed to mark temp auth session as used:', error) } } static async cleanupExpiredTempSessions() { try { await db .delete(tempAuthSessions) .where(lt(tempAuthSessions.expiresAt, new Date())) } catch (error) { console.error('Failed to cleanup expired temp sessions:', error) } } // 로그인 세션 관리 static async createLoginSession(data: { userId: number ipAddress: string userAgent?: string authMethod: string sessionExpiredAt?: Date nextAuthSessionId?: string }): Promise { try { const sessionData: NewLoginSession = { userId: data.userId, ipAddress: data.ipAddress, userAgent: data.userAgent, authMethod: data.authMethod, sessionExpiredAt: data.sessionExpiredAt, nextAuthSessionId: data.nextAuthSessionId, } const result = await db.insert(loginSessions).values(sessionData).returning() // 캐시에서 해당 사용자의 활성 세션 정보 제거 sessionCache.delete(`active_session_${data.userId}`) return result[0] } catch (error) { console.error('Failed to create login session:', error) throw error } } static async updateLoginSession(sessionId: string, updates: { lastActivityAt?: Date sessionExpiredAt?: Date logoutAt?: Date isActive?: boolean }) { try { await db .update(loginSessions) .set({ ...updates, updatedAt: new Date() }) .where(eq(loginSessions.id, sessionId)) // 캐시 무효화 (세션이 업데이트되었으므로) for (const [key] of sessionCache) { if (key.includes(sessionId)) { sessionCache.delete(key) } } } catch (error) { console.error('Failed to update login session:', error) } } // 캐시를 활용한 활성 세션 조회 static async getActiveSessionByUserId(userId: number) { const cacheKey = `active_session_${userId}` const cached = sessionCache.get(cacheKey) // 캐시가 유효한 경우 반환 if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.data } try { const result = await db .select() .from(loginSessions) .where( and( eq(loginSessions.userId, userId), eq(loginSessions.isActive, true) ) ) .orderBy(desc(loginSessions.loginAt)) .limit(1) const session = result[0] || null // 캐시에 저장 sessionCache.set(cacheKey, { data: session, timestamp: Date.now() }) return session } catch (error) { console.error('Failed to get active session:', error) return null } } static async logoutSession(sessionId: string) { try { await db .update(loginSessions) .set({ logoutAt: new Date(), isActive: false, updatedAt: new Date() }) .where(eq(loginSessions.id, sessionId)) // 캐시에서 관련된 세션 정보 제거 for (const [key] of sessionCache) { if (key.includes(sessionId)) { sessionCache.delete(key) } } } catch (error) { console.error('Failed to logout session:', error) } } static async logoutAllUserSessions(userId: number) { try { await db .update(loginSessions) .set({ logoutAt: new Date(), isActive: false, updatedAt: new Date() }) .where( and( eq(loginSessions.userId, userId), eq(loginSessions.isActive, true) ) ) // 해당 사용자의 캐시 제거 sessionCache.delete(`active_session_${userId}`) } catch (error) { console.error('Failed to logout all user sessions:', error) } } // 배치 처리를 위한 페이지 방문 기록 (성능 최적화) private static visitQueue: NewPageVisit[] = [] private static isProcessingQueue = false static async recordPageVisit(data: { userId?: number sessionId?: string route: string pageTitle?: string referrer?: string ipAddress: string userAgent?: string queryParams?: string deviceType?: string browserName?: string osName?: string }) { const visitData: NewPageVisit = { userId: data.userId, sessionId: data.sessionId, route: data.route, pageTitle: data.pageTitle, referrer: data.referrer, ipAddress: data.ipAddress, userAgent: data.userAgent, queryParams: data.queryParams, deviceType: data.deviceType, browserName: data.browserName, osName: data.osName, } // 큐에 추가 this.visitQueue.push(visitData) // 큐가 20개 이상이거나 3초마다 배치 처리 if (this.visitQueue.length >= 20 || !this.isProcessingQueue) { this.processVisitQueue() } } // 배치 처리로 성능 최적화 private static async processVisitQueue() { if (this.isProcessingQueue || this.visitQueue.length === 0) { return } this.isProcessingQueue = true try { const batch = this.visitQueue.splice(0, 100) // 최대 100개씩 처리 if (batch.length > 0) { await db.insert(pageVisits).values(batch) } } catch (error) { console.error('Failed to process visit queue:', error) } finally { this.isProcessingQueue = false // 더 처리할 데이터가 있다면 재귀 호출 if (this.visitQueue.length > 0) { setTimeout(() => this.processVisitQueue(), 100) } } } // 3초마다 큐 처리 (백그라운드) static { if (typeof setInterval !== 'undefined') { setInterval(() => { this.processVisitQueue() }, 3000) } } // 세션 활동 업데이트 (논블로킹, 에러 무시) static updateSessionActivity(sessionId: string): Promise { return new Promise((resolve) => { // 비동기로 실행하되 메인 플로우를 블로킹하지 않음 setImmediate(async () => { try { await this.updateLoginSession(sessionId, { lastActivityAt: new Date() }) } catch (error) { // 에러를 로그만 남기고 무시 console.error('Failed to update session activity (non-blocking):', error) } resolve() }) }) } static async updatePageVisitDuration(visitId: string, duration: number) { try { await db .update(pageVisits) .set({ duration }) .where(eq(pageVisits.id, visitId)) } catch (error) { console.error('Failed to update page visit duration:', error) } } // 캐시 정리 (메모리 관리) static cleanupCache() { const now = Date.now() for (const [key, value] of sessionCache) { if (now - value.timestamp > CACHE_TTL) { sessionCache.delete(key) } } } // 모니터링을 위한 통계 정보 제공 static getRepositoryStats() { return { cacheSize: sessionCache.size, queueSize: this.visitQueue?.length || 0, cacheTTL: CACHE_TTL, isProcessingQueue: this.isProcessingQueue } } // 캐시 크기 조회 (모니터링용) static getCacheSize(): number { return sessionCache.size } // 큐 크기 조회 (모니터링용) static getQueueSize(): number { return this.visitQueue?.length || 0 } // 정기적인 캐시 정리 (10분마다) static { if (typeof setInterval !== 'undefined') { setInterval(() => { this.cleanupCache() }, 10 * 60 * 1000) } } } // 에러 처리를 위한 래퍼 함수들 export const safeSessionOperations = { async recordPageVisit(data: Parameters[0]) { try { await SessionRepository.recordPageVisit(data) } catch (error) { console.error('Safe page visit recording failed:', error) } }, async updateSessionActivity(sessionId: string) { try { await SessionRepository.updateSessionActivity(sessionId) } catch (error) { console.error('Safe session activity update failed:', error) } }, async getActiveSession(userId: number) { try { return await SessionRepository.getActiveSessionByUserId(userId) } catch (error) { console.error('Safe get active session failed:', error) return null } } } // lib/session/monitoring.ts - 성능 모니터링 (수정된 버전) export class SessionMonitoring { private static metrics = { pageVisitRecords: 0, sessionUpdates: 0, cacheHits: 0, cacheMisses: 0, errors: 0 } static incrementMetric(metric: keyof typeof this.metrics) { this.metrics[metric]++ } static getMetrics() { return { ...this.metrics } } static resetMetrics() { Object.keys(this.metrics).forEach(key => { this.metrics[key as keyof typeof this.metrics] = 0 }) } // 성능 통계 로깅 (수정된 버전) static logPerformanceStats() { const repoStats = SessionRepository.getRepositoryStats() console.log('Session Repository Performance:', { ...this.metrics, cacheHitRate: this.metrics.cacheHits / (this.metrics.cacheHits + this.metrics.cacheMisses) * 100 || 0, ...repoStats // cacheSize, queueSize 등 포함 }) } // 상세 성능 리포트 생성 static getDetailedPerformanceReport() { const repoStats = SessionRepository.getRepositoryStats() const totalRequests = this.metrics.cacheHits + this.metrics.cacheMisses return { metrics: this.getMetrics(), repository: repoStats, performance: { cacheHitRate: totalRequests > 0 ? (this.metrics.cacheHits / totalRequests) * 100 : 0, errorRate: this.metrics.pageVisitRecords > 0 ? (this.metrics.errors / this.metrics.pageVisitRecords) * 100 : 0, queueUtilization: repoStats.queueSize / 100 * 100, // 100이 최대 큐 크기라고 가정 }, status: { healthy: this.metrics.errors / Math.max(this.metrics.pageVisitRecords, 1) < 0.01, // 1% 미만 에러율 cacheEfficient: totalRequests > 0 ? (this.metrics.cacheHits / totalRequests) > 0.8 : true, // 80% 이상 캐시 히트율 queueManageable: repoStats.queueSize < 50 // 큐 크기가 50 미만 } } } }