diff --git a/package.json b/package.json index ffa6197..3296b61 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "author": "", "type": "module", "main": "index.js", + "prisma": { + "seed": "node prisma/seed.js" + }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node src/index.js", @@ -26,7 +29,8 @@ "migrate:dev": "prisma migrate dev", "migrate:deploy": "prisma migrate deploy", "migrate:reset": "prisma migrate reset --force", - "migrate:status": "prisma migrate status" + "migrate:status": "prisma migrate status", + "seed": "node prisma/seed.js" }, "dependencies": { "@prisma/adapter-mariadb": "^7.2.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 32468a0..315fa1d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -30,7 +30,7 @@ enum TaskType { TEAM } -model TaskPirority { +model TaskPriority { id Int @id @default(autoincrement()) @map("priority_id") userId Int @map("user_id") taskId Int @map("task_id") @@ -43,7 +43,7 @@ model TaskPirority { task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) // 과제 삭제 시 우선순위도 삭제됨 @@unique([userId, taskId]) - @@map("TaskPirority") + @@map("TaskPriority") } // 사용자 (User) @@ -74,7 +74,7 @@ model User { memberships Member[] @relation("UserMembers") // 유저가 참여한 팀(멤버) 기록 (팀과제일 경우) comments Comment[] @relation("UserComments") // 유저가 쓴 댓글들 SubTasks SubTask[] @relation("Assignee") // 유저에게 할당된 세부과제 (개인과제일 경우 모두 자신에게 할당됨) - taskPriorities TaskPirority[] @relation("UserTaskPriorities") // 유저의 과제 우선순위들 + taskPriorities TaskPriority[] @relation("UserTaskPriorities") // 유저의 과제 우선순위들 @@unique([provider, providerId]) @@index([deletedAt]) @@ -143,7 +143,7 @@ model Task { logs Log[] communications Communication[] userAlarms UserAlarm[] - priorities TaskPirority[] // 과제 우선순위 + priorities TaskPriority[] // 과제 우선순위 @@map("Task") } diff --git a/prisma/seed.js b/prisma/seed.js new file mode 100644 index 0000000..49569bd --- /dev/null +++ b/prisma/seed.js @@ -0,0 +1,461 @@ +// prisma/seed.js +import { prisma } from "../src/db.config.js"; +import dayjs from "dayjs"; +import dotenv from "dotenv"; +import jwt from "jsonwebtoken"; + +async function main() { + console.log("🌱 시드 데이터 생성 시작..."); + + // 기존 데이터 삭제 (순서 중요: 외래키 관계 고려) + console.log("🗑️ 기존 데이터 삭제 중..."); + await prisma.userAlarm.deleteMany(); + await prisma.taskPriority.deleteMany(); + await prisma.comment.deleteMany(); + await prisma.subTask.deleteMany(); + await prisma.member.deleteMany(); + await prisma.task.deleteMany(); + await prisma.folder.deleteMany(); + await prisma.user.deleteMany(); + + // 1. User 생성 + console.log("👤 유저 생성 중..."); + const users = await Promise.all([ + prisma.user.create({ + data: { + nickname: "홍길동", + phoneNum: "010-1234-5678", + email: "hong@example.com", + deadlineAlarm: 24, + taskAlarm: 24, + }, + }), + prisma.user.create({ + data: { + nickname: "김철수", + phoneNum: "010-2345-6789", + email: "kim@example.com", + deadlineAlarm: 12, + taskAlarm: 12, + }, + }), + prisma.user.create({ + data: { + nickname: "이영희", + phoneNum: "010-3456-7890", + email: "lee@example.com", + deadlineAlarm: 48, + taskAlarm: 48, + }, + }), + ]); + + console.log(`✅ ${users.length}명의 유저 생성 완료`); + + // 2. Folder 생성 + console.log("📁 폴더 생성 중..."); + const folders = await Promise.all([ + prisma.folder.create({ + data: { + userId: users[0].id, + folderTitle: "개인 프로젝트", + color: "#FF5733", + }, + }), + prisma.folder.create({ + data: { + userId: users[0].id, + folderTitle: "업무", + color: "#33C3F0", + }, + }), + prisma.folder.create({ + data: { + userId: users[1].id, + folderTitle: "학습", + color: "#28A745", + }, + }), + prisma.folder.create({ + data: { + userId: users[2].id, + folderTitle: "개인", + color: "#FFC107", + }, + }), + ]); + + console.log(`✅ ${folders.length}개의 폴더 생성 완료`); + + // 3. Task 생성 (개인 과제 + 팀 과제) + console.log("📝 과제 생성 중..."); + const now = dayjs(); + + // 개인 과제들 + const personalTasks = await Promise.all([ + prisma.task.create({ + data: { + folderId: folders[0].id, + title: "포트폴리오 웹사이트 제작", + deadline: now.add(7, "day").toDate(), + type: "PERSONAL", + status: "PROGRESS", + isAlarm: true, + }, + }), + prisma.task.create({ + data: { + folderId: folders[0].id, + title: "독서 목표 달성", + deadline: now.add(14, "day").toDate(), + type: "PERSONAL", + status: "PENDING", + isAlarm: true, + }, + }), + prisma.task.create({ + data: { + folderId: folders[1].id, + title: "회의 자료 준비", + deadline: now.add(2, "day").toDate(), + type: "PERSONAL", + status: "PENDING", + isAlarm: false, + }, + }), + ]); + + // 팀 과제 + const teamTask = await prisma.task.create({ + data: { + folderId: folders[2].id, + title: "팀 프로젝트 개발", + deadline: now.add(30, "day").toDate(), + type: "TEAM", + status: "PROGRESS", + isAlarm: true, + inviteCode: "TEAM123", + inviteExpiredAt: now.add(7, "day").toDate(), + }, + }); + + console.log(`✅ ${personalTasks.length + 1}개의 과제 생성 완료`); + + // 4. Member 생성 (팀 과제 멤버) + console.log("👥 멤버 생성 중..."); + await Promise.all([ + // 개인 과제는 본인이 멤버 + prisma.member.create({ + data: { + userId: users[0].id, + taskId: personalTasks[0].id, + role: false, // owner + }, + }), + prisma.member.create({ + data: { + userId: users[0].id, + taskId: personalTasks[1].id, + role: false, + }, + }), + prisma.member.create({ + data: { + userId: users[1].id, + taskId: personalTasks[2].id, + role: false, + }, + }), + // 팀 과제 멤버들 + prisma.member.create({ + data: { + userId: users[1].id, + taskId: teamTask.id, + role: false, // owner + }, + }), + prisma.member.create({ + data: { + userId: users[0].id, + taskId: teamTask.id, + role: true, // member + }, + }), + prisma.member.create({ + data: { + userId: users[2].id, + taskId: teamTask.id, + role: true, // member + }, + }), + ]); + + console.log("✅ 멤버 생성 완료"); + + // 5. SubTask 생성 + console.log("📋 세부과제 생성 중..."); + const subTasks = []; + + // 첫 번째 개인 과제의 세부과제들 + subTasks.push( + await prisma.subTask.create({ + data: { + taskId: personalTasks[0].id, + assigneeId: users[0].id, + title: "디자인 완료", + endDate: now.add(3, "day").toDate(), + status: "PROGRESS", + isAlarm: true, + }, + }), + await prisma.subTask.create({ + data: { + taskId: personalTasks[0].id, + assigneeId: users[0].id, + title: "프론트엔드 개발", + endDate: now.add(5, "day").toDate(), + status: "PENDING", + isAlarm: true, + }, + }), + await prisma.subTask.create({ + data: { + taskId: personalTasks[0].id, + title: "백엔드 API 개발", + endDate: now.add(7, "day").toDate(), + status: "PENDING", + isAlarm: false, + }, + }) + ); + + // 두 번째 개인 과제의 세부과제 + subTasks.push( + await prisma.subTask.create({ + data: { + taskId: personalTasks[1].id, + assigneeId: users[0].id, + title: "책 3권 읽기", + endDate: now.add(10, "day").toDate(), + status: "PENDING", + isAlarm: true, + }, + }) + ); + + // 팀 과제의 세부과제들 + subTasks.push( + await prisma.subTask.create({ + data: { + taskId: teamTask.id, + assigneeId: users[0].id, + title: "기획서 작성", + endDate: now.add(5, "day").toDate(), + status: "COMPLETED", + isAlarm: true, + }, + }), + await prisma.subTask.create({ + data: { + taskId: teamTask.id, + assigneeId: users[1].id, + title: "데이터베이스 설계", + endDate: now.add(10, "day").toDate(), + status: "PROGRESS", + isAlarm: true, + }, + }), + await prisma.subTask.create({ + data: { + taskId: teamTask.id, + assigneeId: users[2].id, + title: "API 개발", + endDate: now.add(15, "day").toDate(), + status: "PENDING", + isAlarm: true, + }, + }) + ); + + console.log(`✅ ${subTasks.length}개의 세부과제 생성 완료`); + + // 6. TaskPriority 생성 (과제 우선순위) + console.log("⭐ 우선순위 생성 중..."); + await Promise.all([ + prisma.taskPriority.create({ + data: { + userId: users[0].id, + taskId: personalTasks[0].id, + rank: 1, + }, + }), + prisma.taskPriority.create({ + data: { + userId: users[0].id, + taskId: personalTasks[1].id, + rank: 2, + }, + }), + prisma.taskPriority.create({ + data: { + userId: users[1].id, + taskId: personalTasks[2].id, + rank: 1, + }, + }), + prisma.taskPriority.create({ + data: { + userId: users[1].id, + taskId: teamTask.id, + rank: 1, + }, + }), + prisma.taskPriority.create({ + data: { + userId: users[0].id, + taskId: teamTask.id, + rank: 3, + }, + }), + prisma.taskPriority.create({ + data: { + userId: users[2].id, + taskId: teamTask.id, + rank: 1, + }, + }), + ]); + + console.log("✅ 우선순위 생성 완료"); + + // 7. UserAlarm 생성 (다양한 알림 상태) + console.log("🔔 알림 생성 중..."); + const alarms = []; + + // 과거 알림 (읽음/안읽음) + alarms.push( + await prisma.userAlarm.create({ + data: { + userId: users[0].id, + taskId: personalTasks[0].id, + title: "과제 마감 임박", + alarmContent: "포트폴리오 웹사이트 제작 마감이 3일 남았습니다", + isRead: true, + alarmDate: now.subtract(1, "day").toDate(), + }, + }), + await prisma.userAlarm.create({ + data: { + userId: users[0].id, + taskId: personalTasks[0].id, + subTaskId: subTasks[0].id, + title: "세부과제 마감 알림", + alarmContent: "디자인 완료 마감이 임박했습니다", + isRead: false, + alarmDate: now.subtract(2, "hours").toDate(), + }, + }) + ); + + // 현재/미래 알림 + alarms.push( + await prisma.userAlarm.create({ + data: { + userId: users[0].id, + taskId: personalTasks[1].id, + title: "과제 생성 알림", + alarmContent: "독서 목표 달성 과제가 생성되었습니다", + isRead: false, + alarmDate: now.add(1, "hour").toDate(), + }, + }), + await prisma.userAlarm.create({ + data: { + userId: users[1].id, + taskId: teamTask.id, + subTaskId: subTasks[4].id, + title: "세부과제 할당", + alarmContent: "데이터베이스 설계가 할당되었습니다", + isRead: false, + alarmDate: now.add(2, "hours").toDate(), + }, + }), + await prisma.userAlarm.create({ + data: { + userId: users[2].id, + taskId: teamTask.id, + subTaskId: subTasks[5].id, + title: "세부과제 할당", + alarmContent: "API 개발이 할당되었습니다", + isRead: false, + alarmDate: now.add(3, "hours").toDate(), + }, + }), + await prisma.userAlarm.create({ + data: { + userId: users[0].id, + taskId: personalTasks[0].id, + subTaskId: subTasks[1].id, + title: "세부과제 마감 알림", + alarmContent: "프론트엔드 개발 마감이 임박했습니다", + isRead: false, + alarmDate: now.add(1, "day").toDate(), + }, + }), + await prisma.userAlarm.create({ + data: { + userId: users[1].id, + taskId: personalTasks[2].id, + title: "과제 마감 알림", + alarmContent: "회의 자료 준비 마감이 임박했습니다", + isRead: false, + alarmDate: now.add(1, "day").toDate(), + }, + }) + ); + + console.log(`✅ ${alarms.length}개의 알림 생성 완료`); + + console.log("\n✨ 시드 데이터 생성 완료!"); + console.log("\n📊 생성된 데이터:"); + console.log(` - 유저: ${users.length}명`); + console.log(` - 폴더: ${folders.length}개`); + console.log( + ` - 과제: ${personalTasks.length + 1}개 (개인 ${ + personalTasks.length + }개, 팀 1개)` + ); + console.log(` - 세부과제: ${subTasks.length}개`); + console.log(` - 알림: ${alarms.length}개`); + console.log("\n🔑 테스트용 유저 정보:"); + users.forEach((user, index) => { + console.log( + ` ${index + 1}. ${user.nickname} (ID: ${user.id}, Email: ${user.email})` + ); + }); + // 🔑 테스트용 JWT 토큰 생성 + const jwtSecret = process.env.JWT_SECRET || "dev-secret"; + + console.log("\n🔑 테스트용 JWT 토큰 (Authorization 헤더에 사용):"); + users.forEach((user, index) => { + const token = jwt.sign( + { id: user.id }, + jwtSecret, + { expiresIn: "7d" } // 필요에 따라 수정 + ); + + console.log( + ` ${index + 1}. ${user.nickname} (ID: ${user.id}, Email: ${user.email})` + ); + console.log(` Authorization: Bearer ${token}\n`); + }); +} + +main() + .catch((e) => { + console.error("❌ 시드 데이터 생성 실패:", e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/src/controllers/alarm.controller.js b/src/controllers/alarm.controller.js index 26acfb3..8cefa35 100644 --- a/src/controllers/alarm.controller.js +++ b/src/controllers/alarm.controller.js @@ -1,7 +1,7 @@ import { BadRequestError } from "../errors/custom.error.js"; import * as AlarmService from "../services/alarm.service.js"; -// 알람 목록 조회 +// ✅ GET /v1/api/alarm - 알람 목록 조회 export const handleAlarmList = async (req, res) => { const userId = req.user.id; // req.user는 로그인 미들웨어에서 설정됨 @@ -17,7 +17,7 @@ export const handleAlarmList = async (req, res) => { return res.success(result, "알림 목록 조회 성공"); }; -// 개별 알림 삭제 +// ✅ DELETE /v1/api/alarm/:alarmId - 개별 알림 삭제 export const handleAlarmDelete = async (req, res) => { // userId (임시 - 로그인 미들웨어 생성 후 req.user.id 사용) const userId = req.user.id; @@ -42,7 +42,7 @@ export const handleAlarmDelete = async (req, res) => { return res.success(null, "개별 알림 삭제 성공"); }; -// 전체 알림 삭제 +// ✅ DELETE /v1/api/alarm - 전체 알림 삭제 export const handleAlarmDeleteAll = async (req, res) => { const userId = req.user.id; @@ -53,7 +53,7 @@ export const handleAlarmDeleteAll = async (req, res) => { return res.success(null, "알림 전체 삭제 성공"); }; -// 최종 마감 알림 수정 +// ✅ PATCH /v1/api/alarm/deadline - 최종 마감 알림 수정 export const handleAlarmUpdateDeadline = async (req, res) => { const userId = req.user.id; const deadlineAlarm = req.body.deadlineAlarm; @@ -72,7 +72,7 @@ export const handleAlarmUpdateDeadline = async (req, res) => { return res.success(result, "최종 마감 시간의 알림 전송 시간을 변경했습니다."); }; -// Task 마감 알림 수정 +// ✅ Task 마감 알림 수정 export const handleAlarmUpdateTask = async (req, res) => { const userId = req.user.id; const taskAlarm = req.body.taskAlarm; @@ -90,3 +90,60 @@ export const handleAlarmUpdateTask = async (req, res) => { // 성공 응답 (이미지 명세에 맞게) return res.success(result, "과제 마감 시간의 알림 전송 시간을 변경했습니다."); }; + +// ✅ 과제 알림 여부 설정 +export const handleAlarmUpdateTaskStatus = async (req, res) => { + const userId = req.user.id; + const taskId = req.params.taskId; + const isAlarm = req.body.isAlarm; + + const parsedTaskId = parseInt(taskId); + + // Service 호출 + const result = await AlarmService.updateTaskAlarmStatus( + userId, + parsedTaskId, + isAlarm + ); + + // 성공 응답 (이미지 명세에 맞게) + return res.success(result, "과제에 대한 알림 여부를 변경하였습니다.."); +}; + +// ✅ PATCH /v1/api/alarm/subtask/:subTaskId - subtask 알림 여부 설정 +export const handleAlarmUpdateSubtaskStatus = async (req, res) => { + const userId = req.user.id; + const subTaskId = req.params.subtaskId; + const isAlarm = req.body.isAlarm; + + const parsedSubTaskId = parseInt(subTaskId); + + // Service 호출 + const result = await AlarmService.updateSubtaskAlarmStatus( + userId, + parsedSubTaskId, + isAlarm + ); + + // 성공 응답 (이미지 명세에 맞게) + return res.success(result, "세부과제에 대한 알림 여부를 변경하였습니다.."); +}; + +// ✅ PATCH /v1/api/alarm/read/:alarmId - 알림 읽음 처리 +export const handleAlarmUpdateAlarmReadStatus = async (req, res) => { + const userId = req.user.id; + const alarmId = req.params.alarmId; + const isRead = req.body.isRead; + + const parsedAlarmId = parseInt(alarmId); + + // Service 호출 + const result = await AlarmService.updateAlarmReadStatus( + userId, + parsedAlarmId, + isRead + ); + + // 성공 응답 (이미지 명세에 맞게) + return res.success(result, "알림 읽음 처리 성공"); +}; diff --git a/src/dtos/alarm.dto.js b/src/dtos/alarm.dto.js index 41591fe..0cfda26 100644 --- a/src/dtos/alarm.dto.js +++ b/src/dtos/alarm.dto.js @@ -2,7 +2,7 @@ export const alarmListResponseDto = (alarms, hasNextPage = false) => { return { alarmList: alarms.map((alarm) => ({ - noticeId: alarm.id, // 알람 ID + alarmId: alarm.id, // 알람 ID title: alarm.title, // 알람 제목 alarmContent: alarm.alarmContent, // 알람 내용 isRead: alarm.isRead, // 0: off, 1: on @@ -34,3 +34,46 @@ export const updateTaskAlarmDto = (data) => { }, }; }; + +// 과제 알림 여부 설정 +export const updateTaskAlarmStatusDto = (data) => { + return { + task: { + taskId: data.taskId, + title: data.title, + deadline: data.deadline, + isAlarm: data.isAlarm, + updatedAt: data.updatedAt, + }, + }; +}; + +// 세부과제 알림 여부 설정 +export const updateSubtaskAlarmStatusDto = (data) => { + return { + subtask: { + subTaskId: data.subTaskId, + assigneeId: data.assigneeId, + taskId: data.taskId, + title: data.title, + endDate: data.endDate, + isAlarm: data.isAlarm, + updatedAt: data.updatedAt, + }, + }; +}; + +// 알림 읽음 처리 +export const updateAlarmReadStatusDto = (data) => { + return { + alarm: { + alarmId: data.alarmId, + userId: data.userId, + taskId: data.taskId, + subTaskId: data.subTaskId, + title: data.title, + alarmContent: data.alarmContent, + isRead: data.isRead, + }, + }; +}; diff --git a/src/repositories/alarm.repository.js b/src/repositories/alarm.repository.js index 1fa30eb..bc5a9f6 100644 --- a/src/repositories/alarm.repository.js +++ b/src/repositories/alarm.repository.js @@ -83,3 +83,55 @@ export const updateTaskAlarm = async (userId, taskAlarm) => { }, }); }; + +// 과제 알림 여부 설정 +export const updateTaskAlarmStatusRepository = async (taskId, isAlarm) => { + return await prisma.task.update({ + where: { id: taskId }, + data: { isAlarm }, + select: { + id: true, + title: true, + deadline: true, + isAlarm: true, + updatedAt: true, + }, + }); +}; + +// 세부과제 알림 여부 설정 +export const updateSubtaskAlarmStatusRepository = async ( + subTaskId, + isAlarm +) => { + return await prisma.subTask.update({ + where: { id: subTaskId }, + data: { isAlarm }, + select: { + id: true, + assigneeId: true, + taskId: true, + title: true, + endDate: true, + isAlarm: true, + updatedAt: true, + }, + }); +}; + +// 알림 읽음 처리 +export const updateAlarmReadStatusRepository = async (alarmId, isRead) => { + return await prisma.userAlarm.update({ + where: { id: alarmId }, + data: { isRead }, + select: { + id: true, + userId: true, + taskId: true, + subTaskId: true, + title: true, + alarmContent: true, + isRead: true, + }, + }); +}; diff --git a/src/routes/alarm.route.js b/src/routes/alarm.route.js index 7a45e27..ebd0884 100644 --- a/src/routes/alarm.route.js +++ b/src/routes/alarm.route.js @@ -3,8 +3,11 @@ import { handleAlarmDelete, handleAlarmDeleteAll, handleAlarmList, + handleAlarmUpdateAlarmReadStatus, handleAlarmUpdateDeadline, + handleAlarmUpdateSubtaskStatus, handleAlarmUpdateTask, + handleAlarmUpdateTaskStatus, } from "../controllers/alarm.controller.js"; import authenticate from "../middlewares/authenticate.middleware.js"; @@ -18,10 +21,23 @@ router.delete("/:alarmId", authenticate, handleAlarmDelete); // DELETE /v1/api/alarm - 전체 알림 삭제 (모든 알림) router.delete("/", authenticate, handleAlarmDeleteAll); -// PATCH /v1/api/alarm/deadline - 최종 마감 알림 수정 -router.patch("/deadline", authenticate, handleAlarmUpdateDeadline); +// PATCH /v1/api/alarm/settings/deadline - 최종 마감 알림 수정 +router.patch("/settings/deadline", authenticate, handleAlarmUpdateDeadline); -// PATCH /v1/api/alarm/task - 과제 마감 알림 수정 -router.patch("/task", authenticate, handleAlarmUpdateTask); +// PATCH /v1/api/alarm/settings/task - 과제 마감 알림 수정 +router.patch("/settings/task", authenticate, handleAlarmUpdateTask); + +// PATCH /v1/api/alarm/subtask/:subTaskId - 세부과제 알림 여부 수정 +router.patch( + "/subtask/:subtaskId", + authenticate, + handleAlarmUpdateSubtaskStatus +); + +// PATCH /v1/api/alarm/task/:taskId - 과제 알림 여부 수정 +router.patch("/task/:taskId", authenticate, handleAlarmUpdateTaskStatus); + +// PATCH /v1/api/alarm/:alarmId - 알림 읽음 처리 +router.patch("/:alarmId", authenticate, handleAlarmUpdateAlarmReadStatus); export default router; diff --git a/src/services/alarm.service.js b/src/services/alarm.service.js index 4cec6ac..f7dbdb6 100644 --- a/src/services/alarm.service.js +++ b/src/services/alarm.service.js @@ -5,35 +5,26 @@ import { deleteAllAlarmsByUserId, updateDeadlineAlarm, updateTaskAlarm, + updateTaskAlarmStatusRepository, + updateSubtaskAlarmStatusRepository, + updateAlarmReadStatusRepository, } from "../repositories/alarm.repository.js"; import { alarmListResponseDto, + updateAlarmReadStatusDto, updateDeadlineAlarmDto, + updateSubtaskAlarmStatusDto, updateTaskAlarmDto, + updateTaskAlarmStatusDto, } from "../dtos/alarm.dto.js"; -import { NotFoundError, ForbiddenError } from "../errors/custom.error.js"; +import { + NotFoundError, + ForbiddenError, + BadRequestError, +} from "../errors/custom.error.js"; import prisma from "../db.config.js"; export const getAlarms = async (userId, cursor, limit, orderBy, order) => { - //미들웨어 생성 전 유저 검증 로직 service에서 처리 => 미들웨어 생성 후 삭제 예정 (9~24줄) - // 유저 존재 및 탈퇴 여부 확인 - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { - id: true, - deletedAt: true, - }, - }); - if (!user) { - throw new NotFoundError("USER_NOT_FOUND", "사용자를 찾을 수 없습니다."); - } - if (user.deletedAt !== null) { - throw new ForbiddenError( - "USER_DELETED", - "탈퇴한 유저는 알림을 조회할 수 없습니다." - ); - } - // 알람 조회 const alarms = await findAlarmsByUserId(userId, { cursor, @@ -63,26 +54,6 @@ export const getAlarms = async (userId, cursor, limit, orderBy, order) => { // 개별 알림 삭제 export const deleteAlarm = async (userId, alarmId) => { - // 유저 검증 (임시 - 로그인 미들웨어 생성 후 삭제 예정) - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { - id: true, - deletedAt: true, - }, - }); - - if (!user) { - throw new NotFoundError("USER_NOT_FOUND", "사용자를 찾을 수 없습니다."); - } - - if (user.deletedAt !== null) { - throw new ForbiddenError( - "USER_DELETED", - "탈퇴한 유저는 알림을 삭제할 수 없습니다." - ); - } - // 알림 존재 여부 확인 const alarm = await findAlarmById(alarmId); @@ -109,26 +80,6 @@ export const deleteAlarm = async (userId, alarmId) => { // 전체 알림 삭제 export const deleteAllAlarms = async (userId) => { - // 유저 검증 (임시 - 로그인 미들웨어 생성 후 삭제 예정) - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { - id: true, - deletedAt: true, - }, - }); - - if (!user) { - throw new NotFoundError("USER_NOT_FOUND", "사용자를 찾을 수 없습니다."); - } - - if (user.deletedAt !== null) { - throw new ForbiddenError( - "USER_DELETED", - "탈퇴한 유저는 알림을 삭제할 수 없습니다." - ); - } - // 유저의 모든 알림 삭제 await deleteAllAlarmsByUserId(userId); @@ -137,65 +88,166 @@ export const deleteAllAlarms = async (userId) => { // 최종 마감 알림 수정 export const updateDeadline = async (userId, deadlineAlarm) => { - // 유저 검증 (임시 - 로그인 미들웨어 생성 후 삭제 예정) - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { - id: true, - deletedAt: true, + // 최종 마감 알림 수정 (Repository 호출) + const updatedUser = await updateDeadlineAlarm(userId, deadlineAlarm); + + // DTO 변환 + return updateDeadlineAlarmDto({ userId, ...updatedUser }); +}; + +// Task 마감 알림 수정 +export const updateTask = async (userId, taskAlarm) => { + // Task 마감 알림 수정 (Repository 호출) + const updatedUser = await updateTaskAlarm(userId, taskAlarm); + + // DTO 변환 + return updateTaskAlarmDto({ userId, ...updatedUser }); +}; + +// ✅ 과제 알림 여부 설정 +export const updateTaskAlarmStatus = async (userId, taskId, isAlarm) => { + if (!taskId || isNaN(parseInt(taskId))) { + throw new BadRequestError( + "INVALID_PARAMS", + "params는 숫자로 보내야합니다." + ); + } + + // taskId가 유효한 숫자인지 확인 + if (!Number.isInteger(taskId) || taskId <= 0) { + throw new BadRequestError( + "INVALID_TASK_ID", + "유효하지 않은 과제 ID입니다." + ); + } + // 과제 존재 여부 및 소유권 확인 + const task = await prisma.task.findUnique({ + where: { id: taskId }, + include: { + members: { + where: { userId: userId }, + }, }, }); - if (!user) { - throw new NotFoundError("USER_NOT_FOUND", "사용자를 찾을 수 없습니다."); + if (!task) { + throw new NotFoundError("TASK_NOT_FOUND", "과제를 찾을 수 없습니다."); } - if (user.deletedAt !== null) { + // 개인 과제인 경우 본인만, 팀 과제인 경우 멤버만 수정 가능 + const isOwner = task.members.some( + (m) => m.userId === userId && m.role === false + ); + const isMember = task.members.some((m) => m.userId === userId); + + if (!isOwner && !isMember) { throw new ForbiddenError( - "USER_DELETED", - "탈퇴한 유저는 알림을 수정할 수 없습니다." + "TASK_ACCESS_DENIED", + "해당 과제에 접근할 권한이 없습니다." ); } - // 최종 마감 알림 수정 (Repository 호출) - const updatedUser = await updateDeadlineAlarm(userId, deadlineAlarm); + if (typeof isAlarm !== "boolean") { + throw new BadRequestError( + "INVALID_BODY", + "Body의 isAlarm 데이터는 boolean 형식으로 보내야합니다." + ); + } + + // 과제 알림 여부 설정 (Repository 호출) + const updatedTask = await updateTaskAlarmStatusRepository(taskId, isAlarm); // DTO 변환 - return updateDeadlineAlarmDto({ - userId: updatedUser.id, - nickname: updatedUser.nickname, - deadlineAlarm: updatedUser.deadlineAlarm, - }); + return updateTaskAlarmStatusDto(updatedTask); }; -// Task 마감 알림 수정 -export const updateTask = async (userId, taskAlarm) => { - // 유저 검증 (임시 - 로그인 미들웨어 생성 후 삭제 예정) - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { - id: true, - deletedAt: true, +// ✅ 세부과제 알림 여부 설정 +export const updateSubtaskAlarmStatus = async (userId, subTaskId, isAlarm) => { + // subTaskId가 유효한 숫자인지 확인 + if (!Number.isInteger(subTaskId) || subTaskId <= 0) { + throw new BadRequestError( + "INVALID_PARAMETER", + "params의 subTaskId는 숫자로 보내야합니다." + ); + } + // 세부과제 존재 여부 및 과제에 속한 맴버인지 확인 + const subTask = await prisma.subTask.findUnique({ + where: { id: subTaskId }, + include: { + task: { + include: { + members: true, // 모든 멤버 가져오기 (where 제거) + }, + }, }, }); - if (!user) { - throw new NotFoundError("USER_NOT_FOUND", "사용자를 찾을 수 없습니다."); + // 세부과제 존재 여부 확인 + if (!subTask) { + throw new NotFoundError( + "SUB_TASK_NOT_FOUND", + "요청하신 subTaskId가 DB에 존재하지 않습니다." + ); } - - if (user.deletedAt !== null) { + // 해당 과제의 멤버인지 확인 + const task = subTask.task; + const isOwner = task.members.some( + (m) => m.userId === userId && m.role === false + ); + const isMember = task.members.some((m) => m.userId === userId); + + if (!isOwner && !isMember) { throw new ForbiddenError( - "USER_DELETED", - "탈퇴한 유저는 알림을 수정할 수 없습니다." + "SUBTASK_ACCESS_DENIED", + "해당 세부과제에 접근할 권한이 없습니다." ); } - // Task 마감 알림 수정 (Repository 호출) - const updatedUser = await updateTaskAlarm(userId, taskAlarm); + if (typeof isAlarm !== "boolean") { + throw new BadRequestError( + "INVALID_BODY", + "Body의 isAlarm 데이터는 boolean 형식으로 보내야합니다." + ); + } + // 세부과제 알림 여부 설정 (Repository 호출) + const updatedSubTask = await updateSubtaskAlarmStatusRepository( + subTaskId, + isAlarm + ); + + // updatedSubTask가 null인 경우 체크 + if (!updatedSubTask) { + throw new NotFoundError( + "SUBTASK_UPDATE_FAILED", + "세부과제 업데이트에 실패했습니다." + ); + } // DTO 변환 - return updateTaskAlarmDto({ - userId: updatedUser.id, - nickname: updatedUser.nickname, - taskAlarm: updatedUser.taskAlarm, - }); + return updateSubtaskAlarmStatusDto(updatedSubTask); +}; + +// 알림 읽음 처리 +export const updateAlarmReadStatus = async (userId, alarmId, isRead) => { + // 알림 존재 여부 확인 + const alarm = await findAlarmById(alarmId); + if (!alarm) { + throw new NotFoundError("ALARM_NOT_FOUND", "알림을 찾을 수 없습니다."); + } + // 알림 소유자 확인 (본인의 알림인지) + if (alarm.userId !== userId) { + throw new ForbiddenError( + "ALARM_ACCESS_DENIED", + "해당 알림에 접근할 권한이 없습니다." + ); + } + if (typeof isRead !== "boolean") { + throw new BadRequestError( + "INVALID_BODY", + "Body의 isRead 데이터는 boolean 형식으로 보내야합니다." + ); + } + + // 알림 읽음 처리 (Repository 호출) + const updatedAlarm = await updateAlarmReadStatusRepository(alarmId, isRead); + return updateAlarmReadStatusDto({ userId, ...updatedAlarm }); };