summaryrefslogtreecommitdiff
path: root/lib/file-download-log
diff options
context:
space:
mode:
Diffstat (limited to 'lib/file-download-log')
-rw-r--r--lib/file-download-log/service.ts353
-rw-r--r--lib/file-download-log/validation.ts37
2 files changed, 390 insertions, 0 deletions
diff --git a/lib/file-download-log/service.ts b/lib/file-download-log/service.ts
new file mode 100644
index 00000000..b2350782
--- /dev/null
+++ b/lib/file-download-log/service.ts
@@ -0,0 +1,353 @@
+'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<RequestInfo> {
+ 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<void> {
+ 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<number>`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<number>`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<number>`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<void> {
+ await createFileDownloadLog({
+ fileId,
+ success,
+ errorMessage,
+ requestId,
+ });
+} \ No newline at end of file
diff --git a/lib/file-download-log/validation.ts b/lib/file-download-log/validation.ts
new file mode 100644
index 00000000..bf888e9c
--- /dev/null
+++ b/lib/file-download-log/validation.ts
@@ -0,0 +1,37 @@
+import { fileDownloadLogs } from "@/db/schema";
+import { createInsertSchema, createSelectSchema } from "drizzle-zod";
+import { z } from 'zod';
+
+// Zod 스키마
+export const fileDownloadLogInsertSchema = createInsertSchema(fileDownloadLogs);
+export const fileDownloadLogSelectSchema = createSelectSchema(fileDownloadLogs);
+
+export type FileDownloadLog = typeof fileDownloadLogs.$inferSelect;
+export type NewFileDownloadLog = typeof fileDownloadLogs.$inferInsert;
+
+// validation.ts에 추가할 타입
+interface FileInfo {
+ fileName?: string;
+ filePath?: string;
+ fileSize?: number;
+ }
+
+ // 수정된 입력 스키마
+export const createFileDownloadLogSchema = z.object({
+ fileId: z.number(),
+ success: z.boolean(),
+ errorMessage: z.string().optional(),
+ requestId: z.string().optional(),
+ downloadDurationMs: z.number().optional(),
+ // 파일 정보를 직접 받을 수 있도록 추가
+ fileInfo: z.object({
+ fileName: z.string().optional(),
+ filePath: z.string().optional(),
+ fileSize: z.number().optional(),
+ }).optional(),
+ // 또는 다른 테이블에서 조회할 수 있도록
+ fileSource: z.enum(['rfqAttachments', 'documents', 'uploads']).optional(),
+ });
+
+
+export type CreateFileDownloadLogInput = z.infer<typeof createFileDownloadLogSchema>;