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
60 changes: 39 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,25 +174,43 @@ npm start
<br>

## 📝 주요 API 목록

### 🔐 OAuth Domain
| 기능명 | Method | Endpoint | 설명 |
| :--- | :---: | :--- | :--- |
| 카카오 로그인 | GET | `/api/v1/auth/kakao` | 카카오 OAuth 로그인 요청 |
| Access Token 재발급 | POST | `/api/v1/auth/refresh` | Refresh Token 기반 Access Token 재발급 |

### 📋 Task Domain
| 기능명 | Method | Endpoint | 설명 |
| :--- | :---: | :--- | :--- |
| 과제 생성 | POST | `/api/v1/task` | 개인/팀 과제 생성 |
| 과제 목록 조회 | GET | `/api/v1/task` | 필터링(개인/팀/마감일/진척도 등) 기반 과제 조회 |
| 과제 상세조회 | GET | `/api/v1/task/{taskId}` | 과제 + 세부과제 + 자료 + 커뮤니케이션 통합 조회 |
| 과제 수정 | PATCH | `/api/v1/task/{taskId}` | 과제 정보 수정 |
| 과제 삭제 | DELETE | `/api/v1/task/{taskId}` | 과제 삭제 |
| 세부 과제 상태 변경 | PATCH | `/api/v1/task/subtask/{subTaskId}/status` | 세부과제 완료/진행 상태 변경 |
| 팀원 초대 링크 생성 | POST | `/api/v1/task/{taskId}/invitation` | 팀 과제 초대 URL 생성 |

### 🔔 Alarm Domain
| 기능명 | Method | Endpoint | 설명 |
| :--- | :---: | :--- | :--- |
| 알림 목록 조회 | GET | `/api/v1/alarm` | 현재 사용자 알림 목록 조회 |
| 알림 읽기 처리 | PATCH | `/api/v1/alarm/{alarmId}` | 특정 알림 읽음 처리 |
| 알림 전체 삭제 | DELETE | `/api/v1/alarm/all` | 모든 알림 삭제 |
| 알림 설정 변경 | PATCH | `/api/v1/alarm/settings/task` | 사용자 알림 설정 변경 |

### 👤 User Domain
| 기능명 | Method | Endpoint | 설명 |
| :--- | :---: | :--- | :--- |
| 내 정보 조회 | GET | `/api/v1/user/me` | 로그인 사용자 정보 조회 |
| 프로필 수정 | PATCH | `/api/v1/user/profile` | 닉네임 등 사용자 정보 수정 |
| 폴더 생성 | POST | `/api/v1/user/folder` | 사용자 폴더 생성 |
| 폴더 조회 | GET | `/api/v1/user/folder` | 사용자 폴더 목록 조회 |

### 📂 Reference Domain
| 기능명 | Method | Endpoint | 설명 |
| --- | --- | --- | --- |
| 카카오 로그인 | GET | /api/v1/auth/kakao | 카카오 OAuth 로그인 요청 |
| Access Token 재발급 | POST | /api/v1/auth/refresh | Refresh Token 기반 Access Token 재발급 |
| 과제 생성 | POST | /api/v1/task | 개인/팀 과제 생성 |
| 과제 목록 조회 | GET | /api/v1/task | 필터링(개인/팀/마감일/진척도 등) 기반 과제 조회 |
| 과제 상세조회 | GET | /api/v1/task/{taskId} | 과제 + 세부과제 + 자료 + 커뮤니케이션 통합 조회 |
| 과제 수정 | PATCH | /api/v1/task/{taskId} | 과제 정보 수정 |
| 과제 삭제 | DELETE | /api/v1/task/{taskId} | 과제 삭제 |
| 세부 과제 상태 변경 | PATCH | /api/v1/task/subtask/{subTaskId}/status | 세부과제 완료/진행 상태 변경 |
| 팀원 초대 링크 생성 | POST | /api/v1/task/{taskId}/invitation | 팀 과제 초대 URL 생성 |
| 알림 목록 조회 | GET | /api/v1/alarm | 현재 사용자 알림 목록 조회 |
| 알림 읽기 처리 | PATCH | /api/v1/alarm/{alarmId} | 특정 알림 읽음 처리 |
| 알림 전체 삭제 | DELETE | /api/v1/alarm/all | 모든 알림 삭제 |
| 알림 설정 변경 | PATCH | /api/v1/alarm/settings/task | 사용자 알림 설정 변경 |
| 내 정보 조회 | GET | /api/v1/user/me | 로그인 사용자 정보 조회 |
| 프로필 수정 | PATCH | /api/v1/user/profile | 닉네임 등 사용자 정보 수정 |
| 폴더 생성 | POST | /api/v1/user/folder | 사용자 폴더 생성 |
| 폴더 조회 | GET | /api/v1/user/folder | 사용자 폴더 목록 조회 |
| 자료 추가 | POST | /api/v1/reference/data/{taskId} | URL 또는 파일 자료 업로드 |
| 커뮤니케이션 생성 | POST | /api/v1/reference/communication/{taskId} | 과제별 커뮤니케이션 생성 |
| 회의록 생성 | POST | /api/v1/reference/log/{taskId} | 과제 회의록 작성 |
| :--- | :---: | :--- | :--- |
| 자료 추가 | POST | `/api/v1/reference/data/{taskId}` | URL 또는 파일 자료 업로드 |
| 커뮤니케이션 생성 | POST | `/api/v1/reference/communication/{taskId}` | 과제별 커뮤니케이션 생성 |
| 회의록 생성 | POST | `/api/v1/reference/log/{taskId}` | 과제 회의록 작성 |
53 changes: 40 additions & 13 deletions src/controllers/task.controller.js
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down
36 changes: 26 additions & 10 deletions src/dtos/task.dto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}))
]
};
}
}
Expand Down
7 changes: 6 additions & 1 deletion src/routes/task.route.js
Original file line number Diff line number Diff line change
@@ -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();

// 완료된 과제
Expand All @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions src/services/folder.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ class FolderService {
if (!folder) throw new NotFoundError("FOLDER_NOT_FOUND", "해당 폴더를 찾을 수 없습니다.");
if (folder.userId !== userId) throw new ForbiddenError("FORBIDDEN", "수정 권한이 없습니다.");

if (folder.folderTitle === "팀") {
throw new BadRequestError("PROTECTED_FOLDER", "'팀' 폴더는 수정할 수 없습니다.");
}

const updateData = FolderDto.updateBodyToFolderDto(body);

if (Object.keys(updateData).length === 0) {
Expand All @@ -55,6 +59,10 @@ class FolderService {
if (!folder) throw new NotFoundError("FOLDER_NOT_FOUND", "해당 폴더를 찾을 수 없습니다.");
if (folder.userId !== userId) throw new ForbiddenError("FORBIDDEN", "삭제 권한이 없습니다.");

if (folder.folderTitle === "팀") {
throw new BadRequestError("PROTECTED_FOLDER", "'팀' 폴더는 삭제할 수 없습니다.");
}

await folderRepository.removeFolder(userId, folderId);
}
}
Expand Down
42 changes: 38 additions & 4 deletions src/services/task.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,37 @@ class TaskService {
if (!folder) throw new NotFoundError("존재하지 않는 폴더입니다.");
}

let targetFolderId = folderId; // 최종적으로 저장될 폴더 ID

if (taskData.type === 'TEAM') {
// 1. 팀 과제라면 무조건 '팀' 폴더를 찾습니다.
const teamFolder = await prisma.folder.findFirst({
where: {
userId: userId,
folderTitle: "팀"
}
});

if (!teamFolder) {
throw new NotFoundError("팀 과제 전용 폴더를 찾을 수 없습니다.");
}

// 2. 강제로 '팀' 폴더 ID로
targetFolderId = teamFolder.id;

} else if (folderId) {
// 3. 개인 과제인데 폴더를 선택한 경우 -> 유효성 및 권한 검사
const folder = await taskRepository.findFolderById(folderId);

if (!folder) {
throw new NotFoundError("존재하지 않는 폴더입니다.");
}

if (folder.userId !== userId) {
throw new ForbiddenError("권한이 없는 폴더입니다.");
}
}

return await prisma.$transaction(async (tx) => {
// 과제 생성
const newTask = await taskRepository.createTask({ ...taskData, folderId }, tx);
Expand Down Expand Up @@ -139,7 +170,7 @@ class TaskService {
}

// 과제 수정
async modifyTask(taskId, data) {
async modifyTask(taskId, data = {}) {
const { subTasks, references, folderId, ...taskData } = data;

// 과제 존재 여부 확인
Expand Down Expand Up @@ -205,9 +236,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 };
Expand Down
Loading