// lib/notification/service.ts import db from '@/db/db'; import { notifications, type NewNotification, type Notification } from '@/db/schema'; import { eq, and, desc, count, gt } from 'drizzle-orm'; // 알림 생성 export async function createNotification(data: Omit) { const [notification] = await db .insert(notifications) .values({ ...data, createdAt: new Date(), }) .returning(); return notification; } // 여러 알림 한번에 생성 (벌크 생성) export async function createNotifications(data: Omit[]) { if (data.length === 0) return []; const notificationsData = data.map(item => ({ ...item, createdAt: new Date(), })); const createdNotifications = await db .insert(notifications) .values(notificationsData) .returning(); return createdNotifications; } // 사용자의 알림 목록 조회 (페이지네이션 포함) export async function getUserNotifications( userId: string, options: { limit?: number; offset?: number; unreadOnly?: boolean; } = {} ) { const { limit = 20, offset = 0, unreadOnly = false } = options; const whereConditions = [eq(notifications.userId, userId)]; if (unreadOnly) { whereConditions.push(eq(notifications.isRead, false)); } const userNotifications = await db .select() .from(notifications) .where(and(...whereConditions)) .orderBy(desc(notifications.createdAt)) .limit(limit) .offset(offset); return userNotifications; } // 읽지 않은 알림 개수 조회 export async function getUnreadNotificationCount(userId: string) { const [result] = await db .select({ count: count() }) .from(notifications) .where( and( eq(notifications.userId, userId), eq(notifications.isRead, false) ) ); return result.count; } // 알림 읽음 처리 export async function markNotificationAsRead(notificationId: string, userId: string) { const [updatedNotification] = await db .update(notifications) .set({ isRead: true, readAt: new Date() }) .where( and( eq(notifications.id, notificationId), eq(notifications.userId, userId) ) ) .returning(); return updatedNotification; } // 모든 알림 읽음 처리 export async function markAllNotificationsAsRead(userId: string) { const updatedNotifications = await db .update(notifications) .set({ isRead: true, readAt: new Date() }) .where( and( eq(notifications.userId, userId), eq(notifications.isRead, false) ) ) .returning(); return updatedNotifications; } // 특정 알림 삭제 export async function deleteNotification(notificationId: string, userId: string) { const [deletedNotification] = await db .delete(notifications) .where( and( eq(notifications.id, notificationId), eq(notifications.userId, userId) ) ) .returning(); return deletedNotification; } // 최근 알림 조회 (실시간 업데이트용) export async function getLatestNotifications(userId: string, since?: Date) { const whereConditions = [eq(notifications.userId, userId)]; if (since) { whereConditions.push(gt(notifications.createdAt, since)); } const latestNotifications = await db .select() .from(notifications) .where(and(...whereConditions)) .orderBy(desc(notifications.createdAt)) .limit(10); return latestNotifications; } // ========================= // 알림 템플릿 및 헬퍼 함수들 // ========================= export const NotificationTemplates = { // 프로젝트 관련 projectAssigned: (projectName: string, projectId: string): Omit => ({ title: '새 프로젝트가 할당되었습니다', message: `"${projectName}" 프로젝트가 회원님에게 할당되었습니다.`, type: 'assignment', relatedRecordId: projectId, relatedRecordType: 'project', isRead: false, }), // 작업 관련 taskAssigned: (taskName: string, taskId: string): Omit => ({ title: '새 작업이 할당되었습니다', message: `"${taskName}" 작업이 회원님에게 할당되었습니다.`, type: 'assignment', relatedRecordId: taskId, relatedRecordType: 'task', isRead: false, }), // 주문 관련 orderStatusChanged: (orderNumber: string, status: string, orderId: string): Omit => ({ title: '주문 상태가 변경되었습니다', message: `주문 ${orderNumber}의 상태가 "${status}"로 변경되었습니다.`, type: 'status_change', relatedRecordId: orderId, relatedRecordType: 'order', isRead: false, }), // 평가 관련 evaluationDocumentRequested: (evaluationYear: number, evaluationRound: string, dueDate?: string): Omit => ({ title: '협력업체 평가 자료 제출 요청', message: `${evaluationYear}년 ${evaluationRound} 협력업체 평가 자료 제출이 요청되었습니다.${dueDate ? ` 마감일: ${dueDate}` : ''}`, type: 'evaluation_request', relatedRecordId: null, relatedRecordType: 'evaluation', isRead: false, }), evaluationRequestCompleted: (vendorCount: number, evaluationYear: number, evaluationRound: string): Omit => ({ title: '평가 자료 요청이 완료되었습니다', message: `${vendorCount}개 협력업체에게 ${evaluationYear}년 ${evaluationRound} 평가 자료 요청이 완료되었습니다.`, type: 'evaluation_admin', relatedRecordId: null, relatedRecordType: 'evaluation', isRead: false, }), evaluationSubmitted: (vendorName: string, evaluationYear: number, evaluationRound: string, submissionId: string): Omit => ({ title: '협력업체 평가가 제출되었습니다', message: `${vendorName}에서 ${evaluationYear}년 ${evaluationRound} 평가를 제출했습니다.`, type: 'evaluation_submission', relatedRecordId: submissionId, relatedRecordType: 'evaluation_submission', isRead: false, }), // 승인 관련 approvalRequired: (documentName: string, documentId: string): Omit => ({ title: '승인이 필요합니다', message: `"${documentName}" 문서의 승인이 필요합니다.`, type: 'approval', relatedRecordId: documentId, relatedRecordType: 'document', isRead: false, }), // 마감일 관련 deadlineReminder: (taskName: string, daysLeft: number, taskId: string): Omit => ({ title: '마감일 알림', message: `"${taskName}" 작업의 마감일이 ${daysLeft}일 남았습니다.`, type: 'deadline', relatedRecordId: taskId, relatedRecordType: 'task', isRead: false, }), // 시스템 공지 systemAnnouncement: (title: string, message: string): Omit => ({ title, message, type: 'announcement', relatedRecordId: null, relatedRecordType: 'system', isRead: false, }), }; // ========================= // 비즈니스 로직별 편의 함수들 // ========================= // 프로젝트 할당 알림 export async function notifyProjectAssignment(userId: string, projectName: string, projectId: string) { return await createNotification({ userId, ...NotificationTemplates.projectAssigned(projectName, projectId), }); } // 작업 할당 알림 export async function notifyTaskAssignment(userId: string, taskName: string, taskId: string) { return await createNotification({ userId, ...NotificationTemplates.taskAssigned(taskName, taskId), }); } // 주문 상태 변경 알림 export async function notifyOrderStatusChange(userId: string, orderNumber: string, status: string, orderId: string) { return await createNotification({ userId, ...NotificationTemplates.orderStatusChanged(orderNumber, status, orderId), }); } // 평가 자료 요청 알림 (벤더에게) export async function notifyEvaluationDocumentRequest( userIds: string[], evaluationYear: number, evaluationRound: string, dueDate?: string ) { const template = NotificationTemplates.evaluationDocumentRequested(evaluationYear, evaluationRound, dueDate); const notificationsData = userIds.map(userId => ({ userId, ...template, })); return await createNotifications(notificationsData); } // 평가 요청 완료 알림 (관리자에게) export async function notifyEvaluationRequestCompleted( adminUserIds: string[], vendorCount: number, evaluationYear: number, evaluationRound: string ) { const template = NotificationTemplates.evaluationRequestCompleted(vendorCount, evaluationYear, evaluationRound); const notificationsData = adminUserIds.map(userId => ({ userId, ...template, })); return await createNotifications(notificationsData); } // 평가 제출 알림 (평가 담당자에게) export async function notifyEvaluationSubmission( evaluatorUserIds: string[], vendorName: string, evaluationYear: number, evaluationRound: string, submissionId: string ) { const template = NotificationTemplates.evaluationSubmitted(vendorName, evaluationYear, evaluationRound, submissionId); const notificationsData = evaluatorUserIds.map(userId => ({ userId, ...template, })); return await createNotifications(notificationsData); } // 승인 요청 알림 export async function notifyApprovalRequired(userId: string, documentName: string, documentId: string) { return await createNotification({ userId, ...NotificationTemplates.approvalRequired(documentName, documentId), }); } // 시스템 공지 (전체 사용자) export async function broadcastSystemAnnouncement(userIds: string[], title: string, message: string) { const template = NotificationTemplates.systemAnnouncement(title, message); const notificationsData = userIds.map(userId => ({ userId, ...template, })); return await createNotifications(notificationsData); }