diff --git a/.DS_Store b/.DS_Store index dcaa276..ccdc01b 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 315fa1d..a06ac6a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -96,7 +96,7 @@ model UserAlarm { //시간 관련 alarmDate DateTime @map("alarm_date") @db.DateTime // 알람 보낼 시간(기본값: 24시간 후) - createdAt DateTime @default(now()) @map("created_at") @db.DateTime // 알람 생성 시간 + createdAt DateTime @default(now()) @map("created_at") @db.DateTime // 알람 생성 시간 한국시간으로 // 관계 설정 (Relations) user User @relation("UserAlarms", fields: [userId], references: [id], onDelete: Restrict) // 유저와 연결 diff --git a/src/controllers/task.controller.js b/src/controllers/task.controller.js index 8655309..8badb50 100644 --- a/src/controllers/task.controller.js +++ b/src/controllers/task.controller.js @@ -8,8 +8,8 @@ class TaskController { // 완료된 과제 조회 async getCompletedTasks(req, res, next) { try { - const userId = req.user.id; - + const userId = req.user.id; + const result = await taskService.getCompletedTasks(userId); res.status(200).json({ @@ -22,11 +22,13 @@ class TaskController { } } + // 과제 생성 async createTask(req, res, next) { try { + const userId = req.user.id; // 사용자 ID 가져오기 const taskRequest = createTaskRequestDTO(req.body); - - const result = await taskService.registerTask(taskRequest); + + const result = await taskService.registerTask(userId, taskRequest); res.status(201).json({ resultType: "SUCCESS", @@ -34,17 +36,16 @@ class TaskController { data: result }); } catch (error) { - next(error); + next(error); } } - // 과제 수정 async updateTask(req, res, next) { try { const { taskId } = req.params; const taskRequest = updateTaskRequestDTO(req.body); - + const result = await taskService.modifyTask(parseInt(taskId), taskRequest); res.status(200).json({ @@ -80,9 +81,9 @@ class TaskController { const task = await taskService.getTaskDetail(parseInt(taskId)); res.status(200).json({ - resultType: "SUCCESS", - message: "서버가 요청을 성공적으로 처리하였습니다.", - data: taskDetailResponseDTO(task) + resultType: "SUCCESS", + message: "서버가 요청을 성공적으로 처리하였습니다.", + data: taskDetailResponseDTO(task) }); } catch (error) { next(error); @@ -92,65 +93,65 @@ class TaskController { // 과제 목록 조회 async getTasks(req, res, next) { try { - console.log("쿼리 내용:", req.query); - - const queryParams = { - type: req.query.type, - sort: req.query.sort, - folderId: req.query.folderId || req.query.folder_id || req.query.folderld, - }; - const userId = req.user.id; - - const tasks = await taskService.getTaskList(userId, queryParams); - - res.status(200).json({ - resultType: "SUCCESS", - message: "서버가 요청을 성공적으로 처리하였습니다.", - data: taskListResponseDTO(tasks) - }); + console.log("쿼리 내용:", req.query); + + const queryParams = { + type: req.query.type, + sort: req.query.sort, + folderId: req.query.folderId || req.query.folder_id || req.query.folderld, + }; + const userId = req.user.id; + + const tasks = await taskService.getTaskList(userId, queryParams); + + res.status(200).json({ + resultType: "SUCCESS", + message: "서버가 요청을 성공적으로 처리하였습니다.", + data: taskListResponseDTO(tasks) + }); } catch (error) { - next(error); + next(error); } } // 우선 순위 변경 async updateTaskPriorities(req, res, next) { try { - const userId = req.user.id; - const { orderedTasks } = req.body; + const userId = req.user.id; + const { orderedTasks } = req.body; - await taskService.updatePriorities(userId, orderedTasks); + await taskService.updatePriorities(userId, orderedTasks); - res.status(200).json({ - resultType: "SUCCESS", - message: "과제 우선순위가 일괄 변경되었습니다.", - data: null - }); + res.status(200).json({ + resultType: "SUCCESS", + message: "과제 우선순위가 일괄 변경되었습니다.", + data: null + }); } catch (error) { - next(error); + next(error); } } - + // 팀원 정보 수정 async updateTeamMember(req, res, next) { try { - const {taskId, memberId} = req.params; - const {role} = req.body; + const { taskId, memberId } = req.params; + const { role } = req.body; const result = await taskService.modifyMemberRole( - parseInt(taskId), - parseInt(memberId), - role + parseInt(taskId), + parseInt(memberId), + role ); res.status(200).json({ resultType: "SUCCESS", message: "요청이 성공적으로 처리되었습니다.", data: { - member_id: result.id, - user_id: result.userId, - task_id: result.taskId, - role: result.role ? 1 : 0, + member_id: result.id, + user_id: result.userId, + task_id: result.taskId, + role: result.role ? 1 : 0, } }); } catch (error) { @@ -196,7 +197,7 @@ class TaskController { const responseData = { resultType: 'SUCCESS', message: '마감 기한이 변경되었습니다.', - data: { + data: { sub_task_id: updatedTask.id, end_date: updatedTask.endDate.toISOString().split('T')[0] } @@ -210,13 +211,13 @@ class TaskController { status: error.status, errorCode: error.errorCode }); - + // 에러 객체에 상태 코드가 없으면 500으로 설정 if (!error.status) { error.status = 500; error.errorCode = 'INTERNAL_SERVER_ERROR'; } - + next(error); } } @@ -250,7 +251,7 @@ class TaskController { statusCode: error.statusCode || error.status, errorCode: error.errorCode }); - + // 에러 객체에 상태 코드가 없으면 500으로 설정 if (!error.statusCode && !error.status) { error.statusCode = 500; @@ -259,7 +260,7 @@ class TaskController { // 이전 버전과의 호환성을 위해 status가 있으면 statusCode로 복사 error.statusCode = error.status; } - + next(error); } } @@ -284,6 +285,36 @@ class TaskController { next(error); } } + + // 초대 코드로 팀 참여 + async joinTaskByInviteCode(req, res, next) { + try { + const userId = req.user.id; + const { inviteCode } = req.body; + + if (!inviteCode || typeof inviteCode !== 'string') { + return res.status(400).json({ + resultType: "FAIL", + message: "초대 코드는 필수입니다.", + data: null + }); + } + + const result = await taskService.joinTaskByInviteCode(userId, inviteCode); + + res.status(200).json({ + resultType: "SUCCESS", + message: "팀에 성공적으로 참여했습니다.", + data: { + task_id: result.taskId, + task_title: result.taskTitle, + member_id: result.memberId + } + }); + } catch (error) { + next(error); + } + } } diff --git a/src/dtos/task.dto.js b/src/dtos/task.dto.js index cea44cb..6354aa8 100644 --- a/src/dtos/task.dto.js +++ b/src/dtos/task.dto.js @@ -3,11 +3,11 @@ export const createTaskRequestDTO = (data) => { title: data.title, folderId: data.folderId, deadline: new Date(data.deadline), - type: data.type === "팀" ? "TEAM" : "PERSONAL", + type: data.type === "팀" ? "TEAM" : "PERSONAL", status: "PENDING", subTasks: (data.subTasks || []).map(st => ({ title: st.title, - endDate: new Date(st.deadline) + endDate: new Date(st.deadline) })), references: data.references || [] }; @@ -42,56 +42,56 @@ export const taskDetailResponseDTO = (task) => { const progressRate = totalSubTasks > 0 ? Math.round((completedSubTasks / totalSubTasks) * 100) : 0; return { - taskId: task.id, - title: task.title, - type: task.type === "TEAM" ? "TEAM" : "INDIVIDUAL", - deadline: task.deadline.toISOString().split('T')[0], - dDay: dDay, - progressRate: progressRate, + taskId: task.id, + title: task.title, + type: task.type === "TEAM" ? "TEAM" : "INDIVIDUAL", + deadline: task.deadline.toISOString().split('T')[0], + dDay: dDay, + progressRate: progressRate, subTasks: task.subTasks?.map(st => ({ - subTaskId: st.id, - title: st.title, - deadline: st.endDate?.toISOString().split('T')[0] || null, - status: st.status === 'COMPLETED' ? 'COMPLETED' : 'PROGRASS', + subTaskId: st.id, + title: st.title, + deadline: st.endDate?.toISOString().split('T')[0] || null, + status: st.status === 'COMPLETED' ? 'COMPLETED' : 'PROGRASS', isAlarm: st.isAlarm || false, - commentCount: st._count?.comments || 0, - assigneeName: st.assigneeName || "PENDING" + commentCount: st._count?.comments || 0, + assigneeName: st.assigneeName || "PENDING" })) || [], communications: task.communications?.map(c => ({ - name: c.name, - url: c.url + name: c.name, + url: c.url })) || [], meetingLogs: task.logs?.map(log => ({ logId: log.id, title: log.title })) || [], references: task.references?.map(r => ({ - name: r.name, - url: r.url + name: r.name, + url: r.url })) || [] }; }; export const taskListResponseDTO = (tasks) => { - return tasks.map(task => { - // D-Day 계산 - const today = new Date(); - const deadlineDate = new Date(task.deadline); - const diffTime = deadlineDate - today; - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - const dDay = diffDays === 0 ? "D-Day" : diffDays > 0 ? `D-${diffDays}` : `D+${Math.abs(diffDays)}`; + return tasks.map(task => { + // D-Day 계산 + const today = new Date(); + const deadlineDate = new Date(task.deadline); + const diffTime = deadlineDate - today; + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + const dDay = diffDays === 0 ? "D-Day" : diffDays > 0 ? `D-${diffDays}` : `D+${Math.abs(diffDays)}`; - return { - taskId: task.id, - folderId: task.folderId, - folderTitle: task.folder?.title || "PENDING", - title: task.title, - type: task.type === "TEAM" ? "TEAM" : "INDIVIDUAL", - deadline: task.deadline.toISOString().split('T')[0].replace(/-/g, '.'), - dDay: dDay, - progressRate: task.progressRate // 서비스에서 계산된 값 사용 - }; - }); + return { + taskId: task.id, + folderId: task.folderId, + folderTitle: task.folder?.title || "PENDING", + title: task.title, + type: task.type === "TEAM" ? "TEAM" : "INDIVIDUAL", + deadline: task.deadline.toISOString().split('T')[0].replace(/-/g, '.'), + dDay: dDay, + progressRate: task.progressRate // 서비스에서 계산된 값 사용 + }; + }); }; export const responseFromCompletedTasks = (tasks) => { @@ -100,11 +100,11 @@ export const responseFromCompletedTasks = (tasks) => { taskId: task.id, title: task.title, deadline: task.deadline ? new Date(task.deadline).toISOString().split('T')[0] : null, - - type: task.type === "TEAM" ? "팀" : "개인", - + + type: task.type === "TEAM" ? "팀" : "개인", + status: task.status === 'COMPLETED' ? '완료' : task.status, - + folderId: task.folder ? task.folder.id : null, folderTitle: task.folder ? task.folder.folderTitle : null, color: task.folder ? task.folder.color : null, diff --git a/src/repositories/alarm.repository.js b/src/repositories/alarm.repository.js index bc5a9f6..c88960a 100644 --- a/src/repositories/alarm.repository.js +++ b/src/repositories/alarm.repository.js @@ -1,6 +1,7 @@ import prisma from "../db.config.js"; import dayjs from "dayjs"; + export const findAlarmsByUserId = async (userId, options = {}) => { // 기본값 적용 const { @@ -31,6 +32,36 @@ export const findAlarmsByUserId = async (userId, options = {}) => { return alarms; }; +// 과제 알림 생성 +export const createTaskAlarm = async (userId, taskId, taskTitle, alarmDate, tx = prisma) => { + return await tx.userAlarm.create({ + data: { + userId, + taskId, + title: "과제 생성 알림", + alarmContent: `${taskTitle} 과제가 생성되었습니다`, + alarmDate, + isRead: false, + createdAt: new Date(Date.now() + 9 * 60 * 60 * 1000), + }, + }); +}; +// 세부과제 알림 생성 +export const createSubTaskAlarm = async (userId, taskId, subTaskId, subTaskTitle, alarmDate, tx = prisma) => { + return await tx.userAlarm.create({ + data: { + userId, + taskId, + subTaskId, + title: "세부과제 생성 알림", + alarmContent: `${subTaskTitle} 세부과제가 생성되었습니다`, + alarmDate, + isRead: false, + createdAt: new Date(Date.now() + 9 * 60 * 60 * 1000), + }, + }); +}; + // 개별 알림 삭제 export const deleteAlarmById = async (alarmId) => { return await prisma.userAlarm.delete({ @@ -135,3 +166,13 @@ export const updateAlarmReadStatusRepository = async (alarmId, isRead) => { }, }); }; + +// 세부과제 알림 삭제 (특정 세부과제의 특정 사용자 알림 삭제) +export const deleteSubTaskAlarm = async (userId, subTaskId) => { + return await prisma.userAlarm.deleteMany({ + where: { + userId, + subTaskId, + }, + }); +}; diff --git a/src/repositories/task.repository.js b/src/repositories/task.repository.js index 01dd4f2..3335e5d 100644 --- a/src/repositories/task.repository.js +++ b/src/repositories/task.repository.js @@ -7,7 +7,7 @@ class TaskRepository { folder: { userId: userId, }, - + status: 'COMPLETED', }, include: { @@ -47,13 +47,13 @@ class TaskRepository { subTasks: { include: { _count: { - select: { comments: true } + select: { comments: true } } } }, references: true, - logs: true, - communications: true + logs: true, + communications: true } }); } @@ -65,9 +65,9 @@ class TaskRepository { where: {}, include: { folder: true, - subTasks: true, + subTasks: true, priorities: { - where: { userId: userId } + where: { userId: userId } } } }; @@ -101,32 +101,43 @@ class TaskRepository { } else if (sort === 'PROGRASSRATE') { return processedTasks.sort((a, b) => b.progress - a.progress); } - + return processedTasks; } // 우선 순위 변경 async upsertTaskPriority(userId, taskId, rank, tx = prisma) { - return await tx.taskPirority.upsert({ + return await tx.taskPriority.upsert({ where: { - userId_taskId: { + userId_taskId: { userId: userId, taskId: taskId } }, update: { - rank: rank + rank: rank }, create: { userId: userId, taskId: taskId, - rank: rank + rank: rank } }); } + // 멤버 생성 + async createMember(userId, taskId, role, tx = prisma) { + return await tx.member.create({ + data: { + userId, + taskId, + role: role ? true : false, // true: member, false: owner + }, + }); + } + // 멤버 존재 여부 확인 - async findMemberInTask (taskId, memberId) { + async findMemberInTask(taskId, memberId) { return await prisma.member.findFirst({ where: { id: memberId, @@ -142,8 +153,8 @@ class TaskRepository { taskId: taskId, id: { not: excludeMemberId } // 대상 멤버는 제외 }, - data: { - role: true + data: { + role: true } }); } @@ -151,8 +162,8 @@ class TaskRepository { // 멤버 역할 업데이트 async updateMemberRole(memberId, isMember) { return await prisma.member.update({ - where: {id: memberId}, - data: {role: isMember} + where: { id: memberId }, + data: { role: isMember } }); } @@ -192,7 +203,7 @@ class TaskRepository { // 1년 후 만료일로 설정 const oneYearLater = new Date(); oneYearLater.setFullYear(oneYearLater.getFullYear() + 1); - + return await tx.task.update({ where: { id: taskId }, data: { diff --git a/src/routes/task.route.js b/src/routes/task.route.js index 3ae135e..45abf6a 100644 --- a/src/routes/task.route.js +++ b/src/routes/task.route.js @@ -57,5 +57,11 @@ router.post( authenticate, taskController.generateInviteCode ); +// 초대 코드로 팀 참여 API +router.post( + '/join', + authenticate, + taskController.joinTaskByInviteCode +); export default router; \ No newline at end of file diff --git a/src/services/task.service.js b/src/services/task.service.js index d4294b7..258b8cf 100644 --- a/src/services/task.service.js +++ b/src/services/task.service.js @@ -1,8 +1,10 @@ import taskRepository from "../repositories/task.repository.js"; import { BadRequestError, NotFoundError, ForbiddenError } from "../errors/custom.error.js"; -import { getUserData } from "../repositories/user.repository.js"; +import { getUserData } from "../repositories/user.repository.js"; import { responseFromCompletedTasks } from "../dtos/task.dto.js"; import { prisma } from "../db.config.js"; +import { createTaskAlarm, createSubTaskAlarm, deleteSubTaskAlarm } from "../repositories/alarm.repository.js"; +import { calculateAlarmDate } from "../utils/calculateAlarmDate.js"; class TaskService { async getCompletedTasks(userId) { @@ -15,25 +17,108 @@ class TaskService { return responseFromCompletedTasks(tasks); } - + // 과제 등록 - async registerTask(data) { + async registerTask(userId, data) { const { subTasks, references, folderId, ...taskData } = data; if (!taskData.title) throw new BadRequestError("과제명은 필수입니다."); - const folder = await taskRepository.findFolderById(folderId); - if (!folder) throw new NotFoundError("존재하지 않는 폴더입니다."); + // folderId가 있을 때만 폴더 존재 여부 확인 + if (folderId) { + const folder = await taskRepository.findFolderById(folderId); + if (!folder) throw new NotFoundError("존재하지 않는 폴더입니다."); + } return await prisma.$transaction(async (tx) => { // 과제 생성 const newTask = await taskRepository.createTask({ ...taskData, folderId }, tx); + // 과제 생성자를 owner로 멤버에 자동 추가 + await taskRepository.createMember(userId, newTask.id, false, tx); // false = owner + + // 과제 알림 생성 + if (newTask.isAlarm) { + // 팀 과제인 경우: 멤버 모두에게 알림 생성 + if (newTask.type === 'TEAM') { + // 멤버 조회 (생성자 포함) + const members = await tx.member.findMany({ + where: { taskId: newTask.id }, + include: { user: true }, + }); + + if (members.length > 0) { + // 모든 멤버에게 알림 생성 + const alarmPromises = members.map(async (member) => { + const user = member.user; + const alarmHours = user.taskAlarm || 24; + const alarmDate = calculateAlarmDate(newTask.deadline, alarmHours); + + return createTaskAlarm( + member.userId, + newTask.id, + newTask.title, + alarmDate, + tx + ); + }); + await Promise.all(alarmPromises); + } + } else { + // 개인 과제인 경우: 생성자에게만 알림 생성 + const creator = await tx.user.findUnique({ + where: { id: userId }, + select: { taskAlarm: true }, + }); + + if (creator) { + const alarmHours = creator.taskAlarm || 24; + const alarmDate = calculateAlarmDate(newTask.deadline, alarmHours); + + await createTaskAlarm( + userId, + newTask.id, + newTask.title, + alarmDate, + tx + ); + } + } + } + // 하위 데이터 저장 - if (subTasks.length > 0) { + if (subTasks && subTasks.length > 0) { await taskRepository.addSubTasks(newTask.id, subTasks, tx); + + // 세부과제 생성 후 알림 생성 + const createdSubTasksList = await tx.subTask.findMany({ + where: { taskId: newTask.id }, + include: { assignee: true }, + }); + + for (const subTask of createdSubTasksList) { + // 세부과제 담당자에게 알림 생성 + if (subTask.isAlarm && subTask.assigneeId) { + const assignee = subTask.assignee; + if (assignee) { + const alarmHours = assignee.taskAlarm || 24; + const alarmDate = new Date(subTask.endDate); + alarmDate.setHours(alarmDate.getHours() - alarmHours); + + await createSubTaskAlarm( + subTask.assigneeId, + subTask.taskId, + subTask.id, + subTask.title, + alarmDate, + tx + ); + } + } + } } - if (references.length > 0) { + + if (references && references.length > 0) { await taskRepository.addReferences(newTask.id, references, tx); } @@ -49,7 +134,7 @@ class TaskService { const currentTask = await taskRepository.findTaskById(taskId); if (!currentTask) throw new NotFoundError("수정하려는 과제가 존재하지 않습니다."); - // 폴더 변경 시 유효성 체크 + // 폴더 if (folderId) { const folder = await taskRepository.findFolderById(folderId); if (!folder) throw new NotFoundError("변경하려는 폴더가 존재하지 않습니다."); @@ -64,6 +149,32 @@ class TaskService { await taskRepository.deleteAllSubTasks(taskId, tx); if (subTasks?.length > 0) { await taskRepository.addSubTasks(taskId, subTasks, tx); + + // 새로 생성된 세부과제에 대한 알림 생성 + const createdSubTasksList = await tx.subTask.findMany({ + where: { taskId }, + include: { assignee: true }, + }); + + for (const subTask of createdSubTasksList) { + // 세부과제 담당자에게 알림 생성 + if (subTask.isAlarm && subTask.assigneeId) { + const assignee = subTask.assignee; + if (assignee) { + const alarmHours = assignee.taskAlarm || 24; + const alarmDate = calculateAlarmDate(subTask.endDate, alarmHours); + + await createSubTaskAlarm( + subTask.assigneeId, + subTask.taskId, + subTask.id, + subTask.title, + alarmDate, + tx + ); + } + } + } } // 자료 갱신 @@ -91,7 +202,7 @@ class TaskService { // 과제 세부 사항 조회 async getTaskDetail(taskId) { const task = await taskRepository.findTaskDetail(taskId); - + if (!task) { throw new NotFoundError("과제를 찾을 수 없음"); } @@ -113,7 +224,7 @@ class TaskService { return tasks; } - + // 세부 TASK 완료 처리 API async updateSubTaskStatus(subTaskId, status) { try { @@ -144,8 +255,8 @@ class TaskService { throw error; } } - - + + // 세부task 날짜 변경 API async updateSubTaskDeadline(subTaskId, deadline) { @@ -164,7 +275,7 @@ class TaskService { if (!existingTask) { const error = new Error('해당하는 세부 태스크를 찾을 수 없습니다.'); - error.statusCode = 404; + error.statusCode = 404; error.errorCode = 'SUBTASK_NOT_FOUND'; throw error; } @@ -198,7 +309,7 @@ class TaskService { // 세부 TASK 담당자 설정 API async setSubTaskAssignee(subTaskId, assigneeId) { console.log('Service - subTaskId:', subTaskId, 'assigneeId:', assigneeId); - + try { // ID 유효성 검사 const parsedSubTaskId = parseInt(subTaskId); @@ -207,27 +318,20 @@ class TaskService { error.statusCode = 400; throw error; } - + // 서브태스크와 관련된 Task, Member 정보 조회 const existingTask = await prisma.subTask.findUnique({ where: { id: parsedSubTaskId }, include: { task: { include: { - members: { - include: { - user: true - } - }, - folder: true + members: true, } }, assignee: true } }); - console.log('Existing task with members:', JSON.stringify(existingTask, null, 2)); - if (!existingTask) { const error = new Error('해당하는 세부 태스크를 찾을 수 없습니다.'); error.statusCode = 404; @@ -237,6 +341,7 @@ class TaskService { const task = existingTask.task; const isTeamTask = task.type === 'TEAM'; + const previousAssigneeId = existingTask.assigneeId; // 이전 담당자 ID 저장 // assigneeId가 있는 경우에만 멤버 확인 if (assigneeId) { @@ -271,32 +376,67 @@ class TaskService { } } - // 담당자 업데이트 (assigneeId가 null이면 담당자 해제) - const updatedTask = await prisma.subTask.update({ - where: { id: parsedSubTaskId }, - data: { - assigneeId: assigneeId ? parseInt(assigneeId) : null, - updatedAt: new Date() - }, - select: { - id: true, - assigneeId: true + // 트랜잭션으로 담당자 업데이트 및 알림 생성/삭제 + return await prisma.$transaction(async (tx) => { + // 이전 담당자가 있고, 담당자가 변경되는 경우 이전 담당자의 알림 삭제 + if (previousAssigneeId && previousAssigneeId !== parseInt(assigneeId || 0)) { + await deleteSubTaskAlarm(previousAssigneeId, parsedSubTaskId); } - }); - console.log('Updated task:', updatedTask); + // 담당자 업데이트 (assigneeId가 null이면 담당자 해제) + const updatedTask = await tx.subTask.update({ + where: { id: parsedSubTaskId }, + data: { + assigneeId: assigneeId ? parseInt(assigneeId) : null, + updatedAt: new Date() + }, + include: { + task: true + } + }); - return { - subTaskId: updatedTask.id, - assigneeId: updatedTask.assigneeId - }; + // 담당자가 해제된 경우 (assigneeId가 null) 이전 담당자의 알림 삭제 + if (!assigneeId && previousAssigneeId) { + await deleteSubTaskAlarm(previousAssigneeId, parsedSubTaskId, tx); + } + + // 담당자가 새로 설정되었고, 세부과제 알림이 켜져있으면 알림 생성 + if (assigneeId && updatedTask.isAlarm && previousAssigneeId !== parseInt(assigneeId)) { + const newAssignee = await tx.user.findUnique({ + where: { id: parseInt(assigneeId) }, + select: { taskAlarm: true }, + }); + + if (newAssignee) { + // 사용자의 taskAlarm 설정에 따라 알림 시간 계산 (기본 24시간 전) + const alarmHours = newAssignee.taskAlarm || 24; + const alarmDate = calculateAlarmDate(updatedTask.endDate, alarmHours); + + await createSubTaskAlarm( + parseInt(assigneeId), + parseInt(updatedTask.taskId), + parseInt(updatedTask.id), + updatedTask.title, + alarmDate, + tx + ); + } + } + + console.log('Updated task:', updatedTask); + + return { + subTaskId: updatedTask.id, + assigneeId: updatedTask.assigneeId + }; + }); } catch (error) { console.error('Error in setSubTaskAssignee service:', { message: error.message, stack: error.stack, statusCode: error.statusCode }); - + // 상태 코드가 이미 설정된 에러는 그대로 전파 if (error.statusCode) { // 404 에러의 경우 errorCode가 없으면 추가 @@ -305,7 +445,7 @@ class TaskService { } throw error; } - + // 그 외의 에러는 500 에러로 처리 error.statusCode = 500; error.errorCode = 'INTERNAL_SERVER_ERROR'; @@ -354,6 +494,74 @@ class TaskService { invite_expired: result.inviteExpiredAt }; } + // ... existing code ... + + // 초대 코드로 팀 참여 + async joinTaskByInviteCode(userId, inviteCode) { + // 초대 코드로 과제 찾기 + const task = await prisma.task.findFirst({ + where: { + inviteCode: inviteCode, + type: 'TEAM', // 팀 과제만 가능 + }, + }); + + if (!task) { + throw new NotFoundError("INVALID_INVITE_CODE", "유효하지 않은 초대 코드입니다."); + } + + // 초대 코드 만료일 확인 + if (task.inviteExpiredAt && new Date() > new Date(task.inviteExpiredAt)) { + throw new BadRequestError("EXPIRED_INVITE_CODE", "만료된 초대 코드입니다."); + } + + // 이미 멤버인지 확인 + const existingMember = await prisma.member.findFirst({ + where: { + taskId: task.id, + userId: userId, + }, + }); + + if (existingMember) { + throw new BadRequestError("ALREADY_MEMBER", "이미 팀 멤버입니다."); + } + + // 트랜잭션으로 멤버 추가 및 알림 생성 + return await prisma.$transaction(async (tx) => { + // 멤버 추가 (role: true = member) + const newMember = await taskRepository.createMember(userId, task.id, true, tx); + + // 과제 알림이 켜져있으면 알림 생성 + if (task.isAlarm) { + const user = await tx.user.findUnique({ + where: { id: userId }, + select: { taskAlarm: true }, + }); + + if (user) { + const alarmHours = user.taskAlarm || 24; + const alarmDate = calculateAlarmDate(task.deadline, alarmHours); + + await createTaskAlarm( + userId, + task.id, + task.title, + alarmDate, + tx + ); + } + } + + return { + taskId: task.id, + taskTitle: task.title, + memberId: newMember.id, + }; + }); + } + + } export default new TaskService(); \ No newline at end of file diff --git a/src/utils/calculateAlarmDate.js b/src/utils/calculateAlarmDate.js new file mode 100644 index 0000000..67fc226 --- /dev/null +++ b/src/utils/calculateAlarmDate.js @@ -0,0 +1,34 @@ +//한국 시간으로 설정 +import dayjs from "dayjs"; +import timezone from "dayjs/plugin/timezone.js"; +import utc from "dayjs/plugin/utc.js"; + +dayjs.extend(utc); +dayjs.extend(timezone); + +// 한국 시간 기준으로 알림 시간 계산 헬퍼 함수 +export const calculateAlarmDate = (deadline, alarmHours) => { + // deadline을 날짜 문자열로 변환 (YYYY-MM-DD) + let dateStr; + + if (deadline instanceof Date) { + // Date 객체에서 날짜만 추출 (로컬 시간 기준) + const year = deadline.getFullYear(); + const month = String(deadline.getMonth() + 1).padStart(2, '0'); + const day = String(deadline.getDate() + 1).padStart(2, '0'); + dateStr = `${year}-${month}-${day}`; + } else { + // 이미 문자열인 경우 + dateStr = deadline.toString().split('T')[0]; // 시간 부분 제거 + } + + // 한국 시간 자정(00:00:00 KST)으로 파싱 + const kstDeadline = dayjs.tz(dateStr, "YYYY-MM-DD", "Asia/Seoul"); + + // 알림 시간 계산 (한국 시간 기준) + const alarmDateKST = kstDeadline.subtract(alarmHours, "hour"); + + + // 한국 시간을 Date 객체로 변환 + return new Date(alarmDateKST.toDate().getTime() + 9 * 60 * 60 * 1000); +}; \ No newline at end of file