'use server'; import { revalidatePath } from 'next/cache'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { headers } from 'next/headers'; import db from '@/db/db'; import { fileDownloadLogs, userDownloadStats, rfqAttachments } from '@/db/schema'; import { eq, and, gte, sql } from 'drizzle-orm'; import { createFileDownloadLogSchema, type CreateFileDownloadLogInput, type FileDownloadLog } from './validation'; import { z } from 'zod'; // 요청 정보 타입 interface RequestInfo { ip?: string; userAgent?: string; referer?: string; sessionId?: string; } // 헤더에서 요청 정보 추출 async function getRequestInfo(): Promise { const headersList = await headers(); return { ip: headersList.get('x-forwarded-for')?.split(',')[0] || headersList.get('x-real-ip') || headersList.get('cf-connecting-ip') || // Cloudflare undefined, userAgent: headersList.get('user-agent') || undefined, referer: headersList.get('referer') || undefined, sessionId: headersList.get('x-session-id') || undefined, // 커스텀 세션 ID }; } // 파일 다운로드 로그 생성 (메인 서버 액션) export async function createFileDownloadLog( input: CreateFileDownloadLogInput ): Promise<{ success: boolean; error?: string; logId?: number }> { try { const validatedInput = createFileDownloadLogSchema.parse(input); const session = await getServerSession(authOptions); if (!session?.user) { return { success: false, error: 'Unauthorized' }; } const requestInfo = await getRequestInfo(); const fileInfo = validatedInput.fileInfo; // 다운로드 로그 생성 const [newLog] = await db .insert(fileDownloadLogs) .values({ fileId: validatedInput.fileId, userId: session.user.id, userEmail: session.user.email || undefined, userName: session.user.name || undefined, userIP: requestInfo.ip, userAgent: requestInfo.userAgent, fileName: fileInfo?.fileName, filePath: fileInfo?.filePath, fileSize: fileInfo?.fileSize, success: validatedInput.success, errorMessage: validatedInput.errorMessage, sessionId: requestInfo.sessionId, requestId: validatedInput.requestId, referer: requestInfo.referer, downloadDurationMs: validatedInput.downloadDurationMs, downloadedAt: new Date(), }) .returning({ id: fileDownloadLogs.id }); // 성공한 다운로드인 경우 통계 업데이트 if (validatedInput.success && fileInfo?.fileSize) { await updateUserDownloadStats( session.user.id, fileInfo.fileSize, validatedInput.fileId ); } return { success: true, logId: newLog.id }; } catch (error) { console.error('파일 다운로드 로그 생성 실패:', error); if (error instanceof z.ZodError) { return { success: false, error: `입력값 검증 실패: ${error.errors.map(e => e.message).join(', ')}` }; } return { success: false, error: '로그 생성 중 오류가 발생했습니다.' }; } } // 사용자 다운로드 통계 업데이트 async function updateUserDownloadStats( userId: string, fileSize: number, fileId: number ): Promise { try { const today = new Date(); today.setHours(0, 0, 0, 0); // 오늘 날짜의 통계가 있는지 확인 const [existingStats] = await db .select() .from(userDownloadStats) .where( and( eq(userDownloadStats.userId, userId), gte(userDownloadStats.date, today) ) ) .limit(1); if (existingStats) { // 기존 통계 업데이트 await db .update(userDownloadStats) .set({ totalDownloads: sql`${userDownloadStats.totalDownloads} + 1`, totalBytes: sql`${userDownloadStats.totalBytes} + ${fileSize}`, lastDownloadAt: new Date(), // uniqueFiles는 별도 로직으로 계산 (복잡하므로 생략) }) .where(eq(userDownloadStats.id, existingStats.id)); } else { // 새 통계 생성 await db.insert(userDownloadStats).values({ userId, date: today, totalDownloads: 1, totalBytes: fileSize, uniqueFiles: 1, lastDownloadAt: new Date(), }); } } catch (error) { console.error('다운로드 통계 업데이트 실패:', error); // 통계 실패가 메인 기능에 영향을 주지 않도록 에러를 던지지 않음 } } // 사용자 다운로드 이력 조회 export async function getUserDownloadHistory( page: number = 1, limit: number = 20 ): Promise<{ logs: FileDownloadLog[], total: number, hasMore: boolean }> { try { const session = await getServerSession(authOptions); if (!session?.user) { throw new Error('Unauthorized'); } const offset = (page - 1) * limit; // 총 개수 조회 const [{ count }] = await db .select({ count: sql`count(*)` }) .from(fileDownloadLogs) .where(eq(fileDownloadLogs.userId, session.user.id)); // 로그 조회 const logs = await db .select() .from(fileDownloadLogs) .where(eq(fileDownloadLogs.userId, session.user.id)) .orderBy(sql`${fileDownloadLogs.downloadedAt} DESC`) .limit(limit) .offset(offset); return { logs, total: count, hasMore: offset + logs.length < count, }; } catch (error) { console.error('다운로드 이력 조회 실패:', error); throw new Error('다운로드 이력을 가져올 수 없습니다.'); } } // 사용자 다운로드 통계 조회 export async function getUserDownloadStats(days: number = 30): Promise<{ totalDownloads: number; totalBytes: number; uniqueFiles: number; dailyStats: Array<{ date: string; downloads: number; bytes: number; }>; }> { try { const session = await getServerSession(authOptions); if (!session?.user) { throw new Error('Unauthorized'); } const startDate = new Date(); startDate.setDate(startDate.getDate() - days); // 기간별 통계 조회 const dailyStats = await db .select({ date: userDownloadStats.date, downloads: userDownloadStats.totalDownloads, bytes: userDownloadStats.totalBytes, }) .from(userDownloadStats) .where( and( eq(userDownloadStats.userId, session.user.id), gte(userDownloadStats.date, startDate) ) ) .orderBy(userDownloadStats.date); // 총합 계산 const totals = dailyStats.reduce( (acc, stat) => ({ totalDownloads: acc.totalDownloads + stat.downloads, totalBytes: acc.totalBytes + stat.bytes, }), { totalDownloads: 0, totalBytes: 0 } ); // 고유 파일 수 계산 (기간 내) const [{ uniqueFiles }] = await db .select({ uniqueFiles: sql`count(distinct ${fileDownloadLogs.fileId})` }) .from(fileDownloadLogs) .where( and( eq(fileDownloadLogs.userId, session.user.id), eq(fileDownloadLogs.success, true), gte(fileDownloadLogs.downloadedAt, startDate) ) ); return { ...totals, uniqueFiles: uniqueFiles || 0, dailyStats: dailyStats.map(stat => ({ date: stat.date.toISOString().split('T')[0], downloads: stat.downloads, bytes: stat.bytes, })), }; } catch (error) { console.error('다운로드 통계 조회 실패:', error); throw new Error('다운로드 통계를 가져올 수 없습니다.'); } } // 관리자용: 전체 다운로드 로그 조회 (권한 확인 필요) export async function getAllDownloadLogs( page: number = 1, limit: number = 50, filters?: { userId?: string; success?: boolean; startDate?: Date; endDate?: Date; } ): Promise<{ logs: FileDownloadLog[], total: number, hasMore: boolean }> { try { const session = await getServerSession(authOptions); if (!session?.user || !session?.user?.roles.includes('admin') ) { throw new Error('관리자 권한이 필요합니다.'); } const offset = (page - 1) * limit; // 필터 조건 구성 const conditions = []; if (filters?.userId) { conditions.push(eq(fileDownloadLogs.userId, filters.userId)); } if (filters?.success !== undefined) { conditions.push(eq(fileDownloadLogs.success, filters.success)); } if (filters?.startDate) { conditions.push(gte(fileDownloadLogs.downloadedAt, filters.startDate)); } if (filters?.endDate) { conditions.push(sql`${fileDownloadLogs.downloadedAt} <= ${filters.endDate}`); } const whereClause = conditions.length > 0 ? and(...conditions) : undefined; // 총 개수 조회 const [{ count }] = await db .select({ count: sql`count(*)` }) .from(fileDownloadLogs) .where(whereClause); // 로그 조회 const logs = await db .select() .from(fileDownloadLogs) .where(whereClause) .orderBy(sql`${fileDownloadLogs.downloadedAt} DESC`) .limit(limit) .offset(offset); return { logs, total: count, hasMore: offset + logs.length < count, }; } catch (error) { console.error('전체 다운로드 로그 조회 실패:', error); throw new Error('다운로드 로그를 가져올 수 없습니다.'); } } // 편의 함수: 간단한 로그 생성 export async function logDownloadAttempt( fileId: number, success: boolean, errorMessage?: string, requestId?: string ): Promise { await createFileDownloadLog({ fileId, success, errorMessage, requestId, }); }