Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions src/controllers/task.controller.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -19,6 +22,8 @@ class TaskController {
}
}


// 과제 수정
async updateTask(req, res, next) {
try {
const { taskId } = req.params;
Expand All @@ -36,6 +41,7 @@ class TaskController {
}
}

// 과제 삭제
async deleteTask(req, res, next) {
try {
const { taskId } = req.params;
Expand All @@ -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) {
Expand Down Expand Up @@ -103,3 +148,4 @@ class TaskController {
}

export default new TaskController();

67 changes: 67 additions & 0 deletions src/dtos/task.dto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 // 서비스에서 계산된 값 사용
};
});
};
39 changes: 39 additions & 0 deletions src/repositories/task.repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } });
Expand Down
15 changes: 15 additions & 0 deletions src/routes/task.route.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
76 changes: 76 additions & 0 deletions src/services/task.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down