diff --git a/package-lock.json b/package-lock.json index e36d34f..da3c8b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1021,7 +1021,8 @@ "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.2.tgz", "integrity": "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w==", "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/@electric-sql/pglite-socket": { "version": "0.0.6", @@ -2638,8 +2639,7 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "devOptional": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dayjs": { "version": "1.11.19", @@ -3158,6 +3158,7 @@ "integrity": "sha512-BIdolzGpDO9MQ4nu3AUuDwHZZ+KViNm+EZ75Ae55eMXMqLVhDFqEMXxtUe9Qh8hjL+pIna/frs2j6Y2yD5Ua/g==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -3961,6 +3962,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "7.2.0", "@prisma/dev": "0.17.0", @@ -4220,8 +4222,7 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "devOptional": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/semver": { "version": "7.7.3", diff --git a/src/controllers/task.controller.js b/src/controllers/task.controller.js index a7e5953..0a1f644 100644 --- a/src/controllers/task.controller.js +++ b/src/controllers/task.controller.js @@ -1,8 +1,11 @@ import taskService from "../services/task.service.js"; import { createTaskRequestDTO } from "../dtos/task.dto.js"; import { updateTaskRequestDTO } from "../dtos/task.dto.js"; +import { taskDetailResponseDTO } from "../dtos/task.dto.js"; +import { taskListResponseDTO } from "../dtos/task.dto.js"; class TaskController { + // 과제 생성 async createTask(req, res, next) { try { const taskRequest = createTaskRequestDTO(req.body); @@ -19,6 +22,8 @@ class TaskController { } } + + // 과제 수정 async updateTask(req, res, next) { try { const { taskId } = req.params; @@ -36,6 +41,7 @@ class TaskController { } } + // 과제 삭제 async deleteTask(req, res, next) { try { const { taskId } = req.params; @@ -50,6 +56,45 @@ class TaskController { next(error); } } + + // 과제 세부 사항 조회 + async getTaskDetail(req, res, next) { + try { + const { taskId } = req.params; + const task = await taskService.getTaskDetail(parseInt(taskId)); + + res.status(200).json({ + resultType: "SUCCESS", + message: "서버가 요청을 성공적으로 처리하였습니다.", + data: taskDetailResponseDTO(task) + }); + } catch (error) { + next(error); + } + } + + // 과제 목록 조회 + 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 tasks = await taskService.getTaskList(queryParams); + + res.status(200).json({ + resultType: "SUCCESS", + message: "서버가 요청을 성공적으로 처리하였습니다.", + data: taskListResponseDTO(tasks) + }); + } catch (error) { + next(error); + } + } // 세부 TASK 완료 처리 API async updateSubTaskStatus(req, res, next) { @@ -103,3 +148,4 @@ class TaskController { } export default new TaskController(); + diff --git a/src/dtos/task.dto.js b/src/dtos/task.dto.js index 3f23f6e..313ac8c 100644 --- a/src/dtos/task.dto.js +++ b/src/dtos/task.dto.js @@ -4,6 +4,7 @@ export const createTaskRequestDTO = (data) => { folderId: data.folderId, deadline: new Date(data.deadline), type: data.type === "팀" ? "TEAM" : "PERSONAL", + status: "PENDING", subTasks: (data.subTasks || []).map(st => ({ title: st.title, endDate: new Date(st.deadline) @@ -25,4 +26,70 @@ export const updateTaskRequestDTO = (data) => { })), references: data.references || [] }; +}; + +export const taskDetailResponseDTO = (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)}`; + + // 진행률 계산 (완료된 세부 과제 수 / 전체 세부 과제 수) + const totalSubTasks = task.subTasks?.length || 0; + const completedSubTasks = task.subTasks?.filter(st => st.status === 'COMPLETED').length || 0; + const progressRate = totalSubTasks > 0 ? Math.round((completedSubTasks / totalSubTasks) * 100) : 0; + + return { + taskId: task.id, + title: task.title, + type: task.type === "TEAM" ? "팀" : "개인", + 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' ? '완료' : '진행중', + isAlarm: st.isAlarm || false, + commentCount: st._count?.comments || 0, + assigneeName: st.assigneeName || "미지정" + })) || [], + communications: task.communications?.map(c => ({ + 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 + })) || [] + }; +}; + +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 { + taskId: task.id, + folderId: task.folderId, + folderTitle: task.folder?.title || "미지정", + title: task.title, + type: task.type === "TEAM" ? "팀" : "개인", + deadline: task.deadline.toISOString().split('T')[0].replace(/-/g, '.'), + dDay: dDay, + progressRate: task.progressRate // 서비스에서 계산된 값 사용 + }; + }); }; \ No newline at end of file diff --git a/src/repositories/task.repository.js b/src/repositories/task.repository.js index a3e8193..d9898f2 100644 --- a/src/repositories/task.repository.js +++ b/src/repositories/task.repository.js @@ -21,6 +21,45 @@ class TaskRepository { return await tx.task.update({ where: { id }, data }); } + // 과제 세부 사항 조회 + async findTaskDetail(id) { + return await prisma.task.findUnique({ + where: { id }, + nclude: { + subTasks: { + include: { + _count: { + select: { comments: true } + } + } + }, + references: true, + logs: true, + communications: true + } + }); + } + + // 과제 목록 조회 + async findAllTasks({ type, folderId, sort }) { + const query = { + where: {}, + include: { + folder: true, + subTasks: true + } + }; + + if (type) query.where.type = type === "팀" ? "TEAM" : "INDIVIDUAL"; + if (folderId) query.where.folderId = parseInt(folderId); + + if (sort === '마감일순' || !sort) { + query.orderBy = { deadline: 'asc' }; + } + + return await prisma.task.findMany(query); +} + // 세부 과제 일괄 삭제 async deleteAllSubTasks(taskId, tx) { return await tx.subTask.deleteMany({ where: { taskId } }); diff --git a/src/routes/task.route.js b/src/routes/task.route.js index 56e4671..4a187e2 100644 --- a/src/routes/task.route.js +++ b/src/routes/task.route.js @@ -3,6 +3,21 @@ import taskController from "../controllers/task.controller.js"; const router = express.Router(); +// POST /api/v1/task -- 과제 생성 +router.post("/", taskController.createTask); + +// PATCH /api/v1/task/:taskId -- 과제 수정 +router.patch("/:taskId", taskController.updateTask); + +// DELETE /api/v1/task/:taskId -- 과제 삭제 +router.delete("/:taskId", taskController.deleteTask); + +// GET /api/v1/task/:taskId -- 과제 세부 사항 조회 +router.get("/:taskId", taskController.getTaskDetail); + +// GET /api/v1/task?sort=우선순위 -- 과제 목록 조회 +router.get("/", taskController.getTasks); + // GET /api/v1/task -- 과제 생성 router.get("/", taskController.createTask); diff --git a/src/services/task.service.js b/src/services/task.service.js index 05a9a12..98563fd 100644 --- a/src/services/task.service.js +++ b/src/services/task.service.js @@ -105,6 +105,82 @@ class TaskService { } } + async getTaskDetail(taskId) { + const task = await taskRepository.findTaskDetail(taskId); + + if (!task) { + throw new NotFoundError("과제를 찾을 수 없음"); + } + + return task; + } + + async getTaskList(queryParams) { + const tasks = await taskRepository.findAllTasks(queryParams); + + // 각 과제의 진행률을 미리 계산하여 객체에 추가 + const tasksWithProgress = tasks.map(task => { + const totalSubTasks = task.subTasks?.length || 0; + const completedSubTasks = task.subTasks?.filter( + st => st.status === 'COMPLETED' || st.status === '완료' + ).length || 0; + + const progressRate = totalSubTasks > 0 + ? Math.round((completedSubTasks / totalSubTasks) * 100) + : 0; + + return { ...task, progressRate }; + }); + + if (queryParams.sort === '진척도순') { + tasksWithProgress.sort((a, b) => b.progressRate - a.progressRate); + } + + return tasksWithProgress; + } + + // 과제 삭제 + async removeTask(taskId) { + // 과제 존재 여부 확인 + const currentTask = await taskRepository.findTaskById(taskId); + if (!currentTask) { + throw new NotFoundError("삭제하려는 과제가 존재하지 않습니다."); + } + + // 과제 삭제 실행 + return await taskRepository.deleteTask(taskId); + } + + // 세부 TASK 완료 처리 API + async updateSubTaskStatus(subTaskId, status) { + try { + // 서브태스크 존재 여부 확인 + const existingTask = await prisma.SubTask.findUnique({ + where: { id: parseInt(subTaskId) }, + }); + + if (!existingTask) { + const error = new Error('해당하는 세부 태스크를 찾을 수 없습니다.'); + error.status = 404; + throw error; + } + + // 상태 업데이트(프리지마 모델명은 대소문자 구분!) + const updatedTask = await prisma.SubTask.update({ + where: { id: parseInt(subTaskId) }, + data: { + status: status === 'COMPLETE' ? 'COMPLETED' : 'PENDING', + updatedAt: new Date() + }, + }); + + return updatedTask; + } catch (error) { + console.error('Error updating subtask status:', error); + throw error; + } + } + // 세부task 날짜 변경 API async updateSubTaskDeadline(subTaskId, deadline) { try {