diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-18 07:52:02 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-18 07:52:02 +0000 |
| commit | 48a2255bfc45ffcfb0b39ffefdd57cbacf8b36df (patch) | |
| tree | 0c88b7c126138233875e8d372a4e999e49c38a62 /lib/notification/service.ts | |
| parent | 2ef02e27dbe639876fa3b90c30307dda183545ec (diff) | |
(대표님) 파일관리변경, 클라IP추적, 실시간알림, 미들웨어변경, 알림API
Diffstat (limited to 'lib/notification/service.ts')
| -rw-r--r-- | lib/notification/service.ts | 342 |
1 files changed, 342 insertions, 0 deletions
diff --git a/lib/notification/service.ts b/lib/notification/service.ts new file mode 100644 index 00000000..f0018113 --- /dev/null +++ b/lib/notification/service.ts @@ -0,0 +1,342 @@ +// 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<NewNotification, 'id' | 'createdAt'>) { + const [notification] = await db + .insert(notifications) + .values({ + ...data, + createdAt: new Date(), + }) + .returning(); + + return notification; +} + +// 여러 알림 한번에 생성 (벌크 생성) +export async function createNotifications(data: Omit<NewNotification, 'id' | 'createdAt'>[]) { + 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<NewNotification, 'userId' | 'id' | 'createdAt'> => ({ + title: '새 프로젝트가 할당되었습니다', + message: `"${projectName}" 프로젝트가 회원님에게 할당되었습니다.`, + type: 'assignment', + relatedRecordId: projectId, + relatedRecordType: 'project', + isRead: false, + }), + + // 작업 관련 + taskAssigned: (taskName: string, taskId: string): Omit<NewNotification, 'userId' | 'id' | 'createdAt'> => ({ + title: '새 작업이 할당되었습니다', + message: `"${taskName}" 작업이 회원님에게 할당되었습니다.`, + type: 'assignment', + relatedRecordId: taskId, + relatedRecordType: 'task', + isRead: false, + }), + + // 주문 관련 + orderStatusChanged: (orderNumber: string, status: string, orderId: string): Omit<NewNotification, 'userId' | 'id' | 'createdAt'> => ({ + title: '주문 상태가 변경되었습니다', + message: `주문 ${orderNumber}의 상태가 "${status}"로 변경되었습니다.`, + type: 'status_change', + relatedRecordId: orderId, + relatedRecordType: 'order', + isRead: false, + }), + + // 평가 관련 + evaluationDocumentRequested: (evaluationYear: number, evaluationRound: string, dueDate?: string): Omit<NewNotification, 'userId' | 'id' | 'createdAt'> => ({ + title: '협력업체 평가 자료 제출 요청', + message: `${evaluationYear}년 ${evaluationRound} 협력업체 평가 자료 제출이 요청되었습니다.${dueDate ? ` 마감일: ${dueDate}` : ''}`, + type: 'evaluation_request', + relatedRecordId: null, + relatedRecordType: 'evaluation', + isRead: false, + }), + + evaluationRequestCompleted: (vendorCount: number, evaluationYear: number, evaluationRound: string): Omit<NewNotification, 'userId' | 'id' | 'createdAt'> => ({ + title: '평가 자료 요청이 완료되었습니다', + message: `${vendorCount}개 협력업체에게 ${evaluationYear}년 ${evaluationRound} 평가 자료 요청이 완료되었습니다.`, + type: 'evaluation_admin', + relatedRecordId: null, + relatedRecordType: 'evaluation', + isRead: false, + }), + + evaluationSubmitted: (vendorName: string, evaluationYear: number, evaluationRound: string, submissionId: string): Omit<NewNotification, 'userId' | 'id' | 'createdAt'> => ({ + title: '협력업체 평가가 제출되었습니다', + message: `${vendorName}에서 ${evaluationYear}년 ${evaluationRound} 평가를 제출했습니다.`, + type: 'evaluation_submission', + relatedRecordId: submissionId, + relatedRecordType: 'evaluation_submission', + isRead: false, + }), + + // 승인 관련 + approvalRequired: (documentName: string, documentId: string): Omit<NewNotification, 'userId' | 'id' | 'createdAt'> => ({ + title: '승인이 필요합니다', + message: `"${documentName}" 문서의 승인이 필요합니다.`, + type: 'approval', + relatedRecordId: documentId, + relatedRecordType: 'document', + isRead: false, + }), + + // 마감일 관련 + deadlineReminder: (taskName: string, daysLeft: number, taskId: string): Omit<NewNotification, 'userId' | 'id' | 'createdAt'> => ({ + title: '마감일 알림', + message: `"${taskName}" 작업의 마감일이 ${daysLeft}일 남았습니다.`, + type: 'deadline', + relatedRecordId: taskId, + relatedRecordType: 'task', + isRead: false, + }), + + // 시스템 공지 + systemAnnouncement: (title: string, message: string): Omit<NewNotification, 'userId' | 'id' | 'createdAt'> => ({ + 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); +}
\ No newline at end of file |
