diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-07 01:44:45 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-07 01:44:45 +0000 |
| commit | 90f79a7a691943a496f67f01c1e493256070e4de (patch) | |
| tree | 37275fde3ae08c2bca384fbbc8eb378de7e39230 /lib/users/session/repository.ts | |
| parent | fbb3b7f05737f9571b04b0a8f4f15c0928de8545 (diff) | |
(대표님) 변경사항 20250707 10시 43분 - unstaged 변경사항 추가
Diffstat (limited to 'lib/users/session/repository.ts')
| -rw-r--r-- | lib/users/session/repository.ts | 460 |
1 files changed, 460 insertions, 0 deletions
diff --git a/lib/users/session/repository.ts b/lib/users/session/repository.ts new file mode 100644 index 00000000..a3b44fbf --- /dev/null +++ b/lib/users/session/repository.ts @@ -0,0 +1,460 @@ +// 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<string, { data: any; timestamp: number }>() +const CACHE_TTL = 5 * 60 * 1000 // 5분 캐시 + +export class SessionRepository { + // 임시 인증 세션 관리 (기존 메모리 저장소 대체) + static async createTempAuthSession(data: { + userId: number + email: string + authMethod: string + expiresAt: Date + }): Promise<string> { + 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<LoginSession> { + 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: string) { + 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<void> { + 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<typeof SessionRepository.recordPageVisit>[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 미만 + } + } + } +}
\ No newline at end of file |
