summaryrefslogtreecommitdiff
path: root/lib/notification/service.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-07-18 07:52:02 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-07-18 07:52:02 +0000
commit48a2255bfc45ffcfb0b39ffefdd57cbacf8b36df (patch)
tree0c88b7c126138233875e8d372a4e999e49c38a62 /lib/notification/service.ts
parent2ef02e27dbe639876fa3b90c30307dda183545ec (diff)
(대표님) 파일관리변경, 클라IP추적, 실시간알림, 미들웨어변경, 알림API
Diffstat (limited to 'lib/notification/service.ts')
-rw-r--r--lib/notification/service.ts342
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