diff --git a/src/controllers/task.controller.js b/src/controllers/task.controller.js index d4dee2c..671c487 100644 --- a/src/controllers/task.controller.js +++ b/src/controllers/task.controller.js @@ -1,4 +1,5 @@ import taskService from "../services/task.service.js"; +import { uploadToS3 } from '../middlewares/upload.middleware.js'; import { TaskRequestDTO, TaskResponseDTO } from "../dtos/task.dto.js"; import { BadRequestError } from "../errors/custom.error.js"; @@ -40,21 +41,46 @@ class TaskController { // 과제 수정 async updateTask(req, res, next) { - try { - const { taskId } = req.params; - const taskRequest = TaskRequestDTO.toUpdate(req.body); - - const result = await taskService.modifyTask(parseInt(taskId), taskRequest); + try { + const { taskId } = req.params; + + let customFileNames = []; + if (req.body.fileNames) { + const rawNames = req.body.fileNames; + if (typeof rawNames === 'string' && rawNames.startsWith('[')) { + customFileNames = JSON.parse(rawNames); + } else if (typeof rawNames === 'string') { + customFileNames = rawNames.split(',').map(name => name.trim()); + } else { + customFileNames = rawNames; + } + } - res.status(200).json({ - resultType: "SUCCESS", - message: "요청이 성공적으로 처리되었습니다.", - data: result - }); - } catch (error) { - next(error); + let fileReferences = []; + if (req.files && req.files.length > 0) { + for (let i = 0; i < req.files.length; i++) { + const file = req.files[i]; + const fileUrl = await uploadToS3(file); + + fileReferences.push({ + name: (customFileNames && customFileNames[i]) ? customFileNames[i] : file.originalname, + fileUrl: fileUrl + }); + } } + + const taskRequest = TaskRequestDTO.toUpdate(req.body, fileReferences); + const result = await taskService.modifyTask(parseInt(taskId), taskRequest); + + res.status(200).json({ + resultType: "SUCCESS", + message: "과제가 성공적으로 수정되었습니다.", + data: result + }); + } catch (error) { + next(error); } +} // 과제 삭제 async deleteTask(req, res, next) { @@ -163,7 +189,8 @@ class TaskController { // 팀원 정보 수정 (역할 변경) async updateTeamMember(req, res, next) { try { - const { taskId, memberId } = req.params; + const taskId = req.body.taskId || req.params.taskId; + const memberId = req.body.memberId || req.params.memberId; const { role } = req.body; const result = await taskService.modifyMemberRole( diff --git a/src/dtos/task.dto.js b/src/dtos/task.dto.js index 208a366..d5f774d 100644 --- a/src/dtos/task.dto.js +++ b/src/dtos/task.dto.js @@ -59,18 +59,34 @@ export class TaskRequestDTO { } // 과제 수정 - static toUpdate(data) { + static toUpdate(data, uploadedFiles = []) { + const subTasks = typeof data.subTasks === 'string' ? JSON.parse(data.subTasks) : (data.subTasks || []); + const existingRefs = typeof data.references === 'string' ? JSON.parse(data.references) : (data.references || []); + return { title: data.title, - folderId: data.folderId, - deadline: data.deadline ? new Date(data.deadline) : undefined, - type: data.type === "TEAM" ? "TEAM" : (data.type === "PERSONAL" ? "PERSONAL" : undefined), - subTasks: (data.subTasks || []).map(st => ({ - title: st.title, - endDate: st.endDate ? new Date(st.endDate) : new Date(), - status: st.status || "PENDING" - })), - references: data.references || [] + folderId: data.folderId ? Number(data.folderId) : undefined, + deadline: data.deadline, + type: (data.type === "TEAM" || data.type === "팀") ? "TEAM" : "PERSONAL", + subTasks: subTasks + .filter(st => st !== null && st !== undefined) // 빈 객체 제거 + .map(st => ({ + title: st.title || "제목 없음", + endDate: st.endDate ? new Date(st.endDate) : new Date(), + status: st.status || "PENDING" + })), + references: [ + ...existingRefs.map(ref => ({ + name: ref.name, + url: ref.url || null, + fileUrl: ref.fileUrl || ref.file_url || null + })), + ...uploadedFiles.map(file => ({ + name: file.name, + url: null, + fileUrl: file.fileUrl || file.file_url + })) + ] }; } } diff --git a/src/routes/task.route.js b/src/routes/task.route.js index 8df3e53..ac87a9f 100644 --- a/src/routes/task.route.js +++ b/src/routes/task.route.js @@ -1,7 +1,12 @@ import express from "express"; +import multer from "multer"; + import taskController from "../controllers/task.controller.js"; import authenticate from "../middlewares/authenticate.middleware.js"; +const upload = multer({ storage: multer.memoryStorage() }); + + const router = express.Router(); // 완료된 과제 @@ -14,7 +19,7 @@ router.post("/", authenticate, taskController.createTask); router.patch("/priority", authenticate, taskController.updateTaskPriorities); // PATCH /api/v1/task/:taskId -- 과제 수정 -router.patch("/:taskId", authenticate, taskController.updateTask); +router.patch("/:taskId", authenticate, upload.array('files'), taskController.updateTask); // DELETE /api/v1/task/:taskId -- 과제 삭제 router.delete("/:taskId", authenticate, taskController.deleteTask); diff --git a/src/services/task.service.js b/src/services/task.service.js index a323547..09a728e 100644 --- a/src/services/task.service.js +++ b/src/services/task.service.js @@ -139,7 +139,7 @@ class TaskService { } // 과제 수정 - async modifyTask(taskId, data) { + async modifyTask(taskId, data = {}) { const { subTasks, references, folderId, ...taskData } = data; // 과제 존재 여부 확인 @@ -205,9 +205,12 @@ class TaskService { } // 자료 갱신 - await taskRepository.deleteAllReferences(taskId, tx); - if (references?.length > 0) { - await taskRepository.addReferences(taskId, references, tx); + if (references) { + await taskRepository.deleteAllReferences(taskId, tx); + + if (references.length > 0) { + await taskRepository.addReferences(taskId, references, tx); + } } return { taskId: updatedTask.id }; diff --git a/src/swagger/swagger.yml b/src/swagger/swagger.yml index 1a36a17..f38b1a5 100644 --- a/src/swagger/swagger.yml +++ b/src/swagger/swagger.yml @@ -1302,7 +1302,11 @@ paths: tags: - Task summary: 과제 수정 - description: 과제의 기본 정보 및 세부 태스크를 수정합니다. + description: | + 과제 정보와 여러 개의 파일을 수정합니다. + - **files**: 여러 개의 파일을 한꺼번에 선택할 수 있습니다. + - **fileNames**: 업로드한 파일 순서대로 이름을 쉽표(,)로 구분해서 적어주세요. + (예: `보고서최종, 참고자료1`) security: - bearerAuth: [] parameters: @@ -1314,7 +1318,7 @@ paths: requestBody: required: true content: - application/json: + multipart/form-data: schema: type: object properties: @@ -1337,37 +1341,23 @@ paths: type: integer example: 2 subTasks: - type: array - items: - type: object - properties: - title: - type: string - example: "세부과제 1" - endDate: - type: string - format: date - example: "2026-05-10" - status: - type: string - example: "COMPLETED" - isAlarm: - type: boolean - example: true - assigneeId: - type: integer - example: 5 + type: string + description: "세부 과제 배열 (JSON string)" + example: '[{"title": "세부과제 1", "endDate": "2026-05-10", "status": "COMPLETED", "isAlarm": true, "assigneeId": 5}]' references: - type: array + type: string + description: "기존 유지할 자료 목록 (JSON string)" + example: '[{"name": "자료1", "url": "https://mariadb.org/documentation/"}]' + fileNames: + type: string + description: "파일 원본 이름 대신 저장할 이름 (쉼표 구분)" + example: "과제발표자료, 참고도면" + files: + type: array items: - type: object - properties: - name: - type: string - example: "자료1" - url: - type: string - example: "https://mariadb.org/documentation/" + type: string + format: binary + description: "업로드할 파일들을 모두 선택하세요." responses: '200': description: 과제 수정 성공 @@ -1764,7 +1754,14 @@ paths: application/json: schema: type: object + required: [taskId, memberId, role] properties: + taskId: + type: integer + example: 1 + memberId: + type: integer + example: 5 role: type: integer description: "0: 일반, 1: 관리자"