diff --git a/src/main/java/clap/server/adapter/inbound/web/task/TaskBoardController.java b/src/main/java/clap/server/adapter/inbound/web/task/TaskBoardController.java index b45e9264..954fa0a5 100644 --- a/src/main/java/clap/server/adapter/inbound/web/task/TaskBoardController.java +++ b/src/main/java/clap/server/adapter/inbound/web/task/TaskBoardController.java @@ -8,6 +8,7 @@ import clap.server.application.port.inbound.task.FilterTaskBoardUsecase; import clap.server.application.port.inbound.task.UpdateTaskBoardUsecase; import clap.server.application.port.inbound.task.GetTaskBoardUsecase; +import clap.server.application.port.inbound.task.UpdateTaskOrderAndStatusUsecase; import clap.server.common.annotation.architecture.WebAdapter; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -35,15 +36,16 @@ public class TaskBoardController { private final GetTaskBoardUsecase getTaskBoardUsecase; private final FilterTaskBoardUsecase filterTaskBoardUsecase; private final UpdateTaskBoardUsecase updateTaskBoardUsecase; + private final UpdateTaskOrderAndStatusUsecase updateTaskOrderAndStatus; @Operation(summary = "작업 보드 조회 API") @Secured({"ROLE_MANAGER"}) - @PostMapping + @GetMapping public ResponseEntity getTaskBoard(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int pageSize, - @Parameter(description = "완료 일자 조회 기준, yyyy-mm-dd 형식으로 입력합니다.") @RequestParam(required = false) + @Parameter(description = "작업 완료 일자 조회 기준, yyyy-mm-dd 형식으로 입력합니다.") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate untilDate, - @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "필터링 조회 request") @RequestBody(required = false) FilterTaskBoardRequest request, + @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "필터링 조회 request") @ModelAttribute FilterTaskBoardRequest request, @AuthenticationPrincipal SecurityUserDetails userInfo) { Pageable pageable = PageRequest.of(page, pageSize); if (request != null) { @@ -62,7 +64,7 @@ public void updateTaskBoard(@Parameter(description = "전환될 작업의 상태 if (status == null) { updateTaskBoardUsecase.updateTaskOrder(userInfo.getUserId(), request); } else { - updateTaskBoardUsecase.updateTaskOrderAndStatus(userInfo.getUserId(), request, status); + updateTaskOrderAndStatus.updateTaskOrderAndStatus(userInfo.getUserId(), request, status); } } diff --git a/src/main/java/clap/server/application/mapper/AttachmentMapper.java b/src/main/java/clap/server/application/mapper/AttachmentMapper.java index 217780eb..0753bf63 100644 --- a/src/main/java/clap/server/application/mapper/AttachmentMapper.java +++ b/src/main/java/clap/server/application/mapper/AttachmentMapper.java @@ -28,18 +28,6 @@ public static List toTaskAttachments(Task task, List .toList(); } - public static List toCommentAttachments(Task task, Comment comment, List files, List fileUrls) { - return IntStream.range(0, files.size()) - .mapToObj(i -> createCommentAttachment( - task, - comment, - files.get(i).getOriginalFilename(), - fileUrls.get(i), - files.get(i).getSize() - )) - .toList(); - } - public static List toAttachmentResponseList(List attachments) { return attachments.stream() diff --git a/src/main/java/clap/server/application/port/inbound/task/UpdateTaskBoardUsecase.java b/src/main/java/clap/server/application/port/inbound/task/UpdateTaskBoardUsecase.java index 3a7ebf9a..467f6d0b 100644 --- a/src/main/java/clap/server/application/port/inbound/task/UpdateTaskBoardUsecase.java +++ b/src/main/java/clap/server/application/port/inbound/task/UpdateTaskBoardUsecase.java @@ -1,9 +1,7 @@ package clap.server.application.port.inbound.task; import clap.server.adapter.inbound.web.dto.task.request.UpdateTaskOrderRequest; -import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; public interface UpdateTaskBoardUsecase { void updateTaskOrder(Long processorId, UpdateTaskOrderRequest request); - void updateTaskOrderAndStatus(Long processorId, UpdateTaskOrderRequest request, TaskStatus status); } diff --git a/src/main/java/clap/server/application/port/inbound/task/UpdateTaskOrderAndStatusUsecase.java b/src/main/java/clap/server/application/port/inbound/task/UpdateTaskOrderAndStatusUsecase.java new file mode 100644 index 00000000..a5a8c132 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/task/UpdateTaskOrderAndStatusUsecase.java @@ -0,0 +1,8 @@ +package clap.server.application.port.inbound.task; + +import clap.server.adapter.inbound.web.dto.task.request.UpdateTaskOrderRequest; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; + +public interface UpdateTaskOrderAndStatusUsecase { + void updateTaskOrderAndStatus(Long processorId, UpdateTaskOrderRequest request, TaskStatus status); +} diff --git a/src/main/java/clap/server/application/service/auth/ManageTokenService.java b/src/main/java/clap/server/application/service/auth/ManageTokenService.java index fecfdc5a..5a029d2d 100644 --- a/src/main/java/clap/server/application/service/auth/ManageTokenService.java +++ b/src/main/java/clap/server/application/service/auth/ManageTokenService.java @@ -11,14 +11,14 @@ import clap.server.domain.model.member.Member; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import java.time.Duration; import java.time.LocalDateTime; import java.util.function.Function; @RequiredArgsConstructor -@Component +@Service @Slf4j class ManageTokenService { private final JwtProvider accessTokenProvider; diff --git a/src/main/java/clap/server/application/service/comment/PostCommentService.java b/src/main/java/clap/server/application/service/comment/PostCommentService.java index 6eb15918..66590dcd 100644 --- a/src/main/java/clap/server/application/service/comment/PostCommentService.java +++ b/src/main/java/clap/server/application/service/comment/PostCommentService.java @@ -1,18 +1,17 @@ package clap.server.application.service.comment; import clap.server.adapter.inbound.web.dto.task.PostAndEditCommentRequest; -import clap.server.adapter.outbound.infrastructure.s3.S3UploadAdapter; import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; import clap.server.adapter.outbound.persistense.entity.task.constant.TaskHistoryType; -import clap.server.application.mapper.AttachmentMapper; import clap.server.application.port.inbound.comment.PostCommentUsecase; import clap.server.application.port.inbound.domain.MemberService; import clap.server.application.port.inbound.domain.TaskService; +import clap.server.application.port.outbound.s3.S3UploadPort; import clap.server.application.port.outbound.task.CommandAttachmentPort; import clap.server.application.port.outbound.task.CommandCommentPort; import clap.server.application.port.outbound.taskhistory.CommandTaskHistoryPort; -import clap.server.application.service.webhook.SendPushNotificationService; +import clap.server.application.service.webhook.SendNotificationService; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.common.constants.FilePathConstants; import clap.server.domain.model.member.Member; @@ -24,10 +23,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.util.ArrayList; -import java.util.List; - - @ApplicationService @RequiredArgsConstructor public class PostCommentService implements PostCommentUsecase { @@ -35,10 +30,10 @@ public class PostCommentService implements PostCommentUsecase { private final MemberService memberService; private final TaskService taskService; private final CommandCommentPort commandCommentPort; - private final S3UploadAdapter s3UploadAdapter; + private final S3UploadPort s3UploadPort; private final CommandAttachmentPort commandAttachmentPort; private final CommandTaskHistoryPort commandTaskHistoryPort; - private final SendPushNotificationService sendPushNotificationService; + private final SendNotificationService sendNotificationService; @Transactional @Override @@ -57,8 +52,7 @@ public void save(Long userId, Long taskId, PostAndEditCommentRequest request) { if (member.getMemberInfo().getRole() == MemberRole.ROLE_USER) { publishNotification(task.getProcessor(), task, comment.getContent(), member.getNickname()); - } - else { + } else { publishNotification(task.getRequester(), task, comment.getContent(), task.getProcessor().getNickname()); } } @@ -73,29 +67,27 @@ public void saveCommentAttachment(Long userId, Long taskId, MultipartFile file) if (Member.checkCommenter(task, member)) { Comment comment = Comment.createComment(member, task, null); Comment savedComment = commandCommentPort.saveComment(comment); - List files = new ArrayList<>(); - files.add(file); - saveAttachment(files, task, savedComment); + saveAttachment(file, task, savedComment); TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.COMMENT_FILE, task, null, member, savedComment); commandTaskHistoryPort.save(taskHistory); if (member.getMemberInfo().getRole() == MemberRole.ROLE_USER) { publishNotification(task.getProcessor(), task, "첨부파일", member.getNickname()); - } - else { + } else { publishNotification(task.getRequester(), task, "첨부파일", task.getProcessor().getNickname()); } } } - private void saveAttachment(List files, Task task, Comment comment) { - List fileUrls = s3UploadAdapter.uploadFiles(FilePathConstants.TASK_IMAGE, files); - List attachments = AttachmentMapper.toCommentAttachments(task, comment, files, fileUrls); - commandAttachmentPort.saveAll(attachments); + private void saveAttachment(MultipartFile file, Task task, Comment comment) { + String fileUrl = s3UploadPort.uploadSingleFile(FilePathConstants.TASK_COMMENT, file); + Attachment attachment = Attachment.createCommentAttachment(task, comment, file.getOriginalFilename(), fileUrl, file.getSize()); + commandAttachmentPort.save(attachment); } - private void publishNotification(Member receiver, Task task, String message, String commenterName){ - sendPushNotificationService.sendPushNotification(receiver, NotificationType.COMMENT, task, message, commenterName); + private void publishNotification(Member receiver, Task task, String message, String commenterName) { + sendNotificationService.sendPushNotification(receiver, NotificationType.COMMENT, task, message, commenterName); } + } diff --git a/src/main/java/clap/server/application/service/task/ApprovalTaskService.java b/src/main/java/clap/server/application/service/task/ApprovalTaskService.java index 5cd73d76..b8e2ff3b 100644 --- a/src/main/java/clap/server/application/service/task/ApprovalTaskService.java +++ b/src/main/java/clap/server/application/service/task/ApprovalTaskService.java @@ -13,20 +13,19 @@ import clap.server.application.port.inbound.task.ApprovalTaskUsecase; import clap.server.application.port.outbound.task.CommandTaskPort; import clap.server.application.port.outbound.taskhistory.CommandTaskHistoryPort; -import clap.server.application.service.webhook.SendPushNotificationService; +import clap.server.application.service.webhook.SendNotificationService; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.member.Member; import clap.server.domain.model.task.Category; import clap.server.domain.model.task.Label; import clap.server.domain.model.task.Task; import clap.server.domain.model.task.TaskHistory; +import clap.server.domain.policy.task.RequestedTaskUpdatePolicy; import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.List; - @ApplicationService @RequiredArgsConstructor @Transactional(readOnly = true) @@ -37,8 +36,9 @@ public class ApprovalTaskService implements ApprovalTaskUsecase { private final CategoryService categoryService; private final LabelService labelService; private final CommandTaskPort commandTaskPort; + private final RequestedTaskUpdatePolicy requestedTaskUpdatePolicy; private final CommandTaskHistoryPort commandTaskHistoryPort; - private final SendPushNotificationService sendPushNotificationService; + private final SendNotificationService sendNotificationService; @Override @Transactional @@ -49,15 +49,14 @@ public ApprovalTaskResponse approvalTaskByReviewer(Long reviewerId, Long taskId, Category category = categoryService.findById(approvalTaskRequest.categoryId()); Label label = labelService.findById(approvalTaskRequest.labelId()); + requestedTaskUpdatePolicy.validateTaskRequested(task); task.approveTask(reviewer, processor, approvalTaskRequest.dueDate(), category, label); TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.PROCESSOR_ASSIGNED, task, null, processor,null); commandTaskHistoryPort.save(taskHistory); - List receivers = new ArrayList<>(); - receivers.add(task.getRequester()); - receivers.add(task.getProcessor()); - + List receivers = List.of(reviewer, processor); publishNotification(receivers, task); + return TaskMapper.toApprovalTaskResponse(commandTaskPort.save(task)); } @@ -65,15 +64,15 @@ public ApprovalTaskResponse approvalTaskByReviewer(Long reviewerId, Long taskId, public FindApprovalFormResponse findApprovalForm(Long managerId, Long taskId) { memberService.findActiveMember(managerId); Task task = taskService.findById(taskId); - task.validateTaskRequested(); + requestedTaskUpdatePolicy.validateTaskRequested(task); return TaskMapper.toFindApprovalFormResponse(task); } private void publishNotification(List receivers, Task task){ - for (Member receiver : receivers) { - - sendPushNotificationService.sendPushNotification(receiver, NotificationType.PROCESSOR_ASSIGNED, + receivers.forEach(receiver -> { + sendNotificationService.sendPushNotification(receiver, NotificationType.PROCESSOR_ASSIGNED, task, task.getProcessor().getNickname(), null); - } + }); } + } diff --git a/src/main/java/clap/server/application/service/task/CreateTaskService.java b/src/main/java/clap/server/application/service/task/CreateTaskService.java index 83f37076..962bcb95 100644 --- a/src/main/java/clap/server/application/service/task/CreateTaskService.java +++ b/src/main/java/clap/server/application/service/task/CreateTaskService.java @@ -2,7 +2,6 @@ import clap.server.adapter.inbound.web.dto.task.CreateTaskRequest; import clap.server.adapter.inbound.web.dto.task.CreateTaskResponse; - import clap.server.adapter.outbound.infrastructure.s3.S3UploadAdapter; import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; import clap.server.application.mapper.AttachmentMapper; @@ -10,24 +9,23 @@ import clap.server.application.port.inbound.domain.CategoryService; import clap.server.application.port.inbound.domain.MemberService; import clap.server.application.port.inbound.task.CreateTaskUsecase; +import clap.server.application.port.outbound.s3.S3UploadPort; import clap.server.application.port.outbound.task.CommandAttachmentPort; import clap.server.application.port.outbound.task.CommandTaskPort; - -import clap.server.application.service.webhook.SendPushNotificationService; +import clap.server.application.service.webhook.SendNotificationService; import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.common.constants.FilePathConstants; import clap.server.domain.model.member.Member; import clap.server.domain.model.task.Attachment; import clap.server.domain.model.task.Category; -import clap.server.common.constants.FilePathConstants; import clap.server.domain.model.task.Task; import lombok.RequiredArgsConstructor; - -import org.springframework.context.ApplicationEventPublisher; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.util.List; + @ApplicationService @RequiredArgsConstructor public class CreateTaskService implements CreateTaskUsecase { @@ -36,9 +34,8 @@ public class CreateTaskService implements CreateTaskUsecase { private final CategoryService categoryService; private final CommandTaskPort commandTaskPort; private final CommandAttachmentPort commandAttachmentPort; - private final S3UploadAdapter s3UploadAdapter; - private final SendPushNotificationService sendPushNotificationService; - private final ApplicationEventPublisher applicationEventPublisher; + private final S3UploadPort s3UploadPort; + private final SendNotificationService sendNotificationService; @Override @Transactional @@ -51,29 +48,21 @@ public CreateTaskResponse createTask(Long requesterId, CreateTaskRequest createT commandTaskPort.save(savedTask); if (files != null) { - saveAttachments(files, savedTask); - } - + saveAttachments(files, savedTask);} publishNotification(savedTask); return TaskMapper.toCreateTaskResponse(savedTask); } private void saveAttachments(List files, Task task) { - List fileUrls = s3UploadAdapter.uploadFiles(FilePathConstants.TASK_IMAGE, files); + List fileUrls = s3UploadPort.uploadFiles(FilePathConstants.TASK_IMAGE, files); List attachments = AttachmentMapper.toTaskAttachments(task, files, fileUrls); commandAttachmentPort.saveAll(attachments); } private void publishNotification(Task task) { List reviewers = memberService.findReviewers(); + reviewers.forEach(reviewer -> {sendNotificationService.sendPushNotification(reviewer, NotificationType.TASK_REQUESTED, + task, null, null);}); - // 검토자들 각각에 대한 알림 생성 후 event 발행 - for (Member reviewer : reviewers) { - - sendPushNotificationService.sendPushNotification(reviewer, NotificationType.TASK_REQUESTED, - task, null, null); - } } - - } \ No newline at end of file diff --git a/src/main/java/clap/server/application/service/task/UpdateTaskBoardService.java b/src/main/java/clap/server/application/service/task/UpdateTaskBoardService.java index 25a45a6e..f7af4cf7 100644 --- a/src/main/java/clap/server/application/service/task/UpdateTaskBoardService.java +++ b/src/main/java/clap/server/application/service/task/UpdateTaskBoardService.java @@ -5,8 +5,11 @@ import clap.server.application.port.inbound.domain.MemberService; import clap.server.application.port.inbound.domain.TaskService; import clap.server.application.port.inbound.task.UpdateTaskBoardUsecase; +import clap.server.application.port.inbound.task.UpdateTaskOrderAndStatusUsecase; import clap.server.application.port.outbound.task.CommandTaskPort; import clap.server.application.port.outbound.task.LoadTaskPort; +import clap.server.domain.policy.task.TaskOrderCalculationPolicy; +import clap.server.domain.policy.task.ProcessorValidationPolicy; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.member.Member; import clap.server.domain.model.task.Task; @@ -19,21 +22,23 @@ @Slf4j @ApplicationService @RequiredArgsConstructor -class UpdateTaskBoardService implements UpdateTaskBoardUsecase { +class UpdateTaskBoardService implements UpdateTaskBoardUsecase, UpdateTaskOrderAndStatusUsecase { private final MemberService memberService; private final TaskService taskService; private final LoadTaskPort loadTaskPort; private final CommandTaskPort commandTaskPort; + private final TaskOrderCalculationPolicy taskOrderCalculationPolicy; + private final ProcessorValidationPolicy processorValidationPolicy; private Task findByIdAndStatus(Long taskId, TaskStatus status) { - return loadTaskPort.findByIdAndStatus(taskId, status).orElseThrow(()-> new ApplicationException(TaskErrorCode.TASK_NOT_FOUND)); + return loadTaskPort.findByIdAndStatus(taskId, status).orElseThrow(() -> new ApplicationException(TaskErrorCode.TASK_NOT_FOUND)); } /** * 작업(Task)의 순서를 업데이트하는 메서드 * * @param processorId 작업을 수행하는 멤버 ID - * @param request 순서 변경 요청 객체 + * @param request 순서 변경 요청 객체 */ @Override @Transactional @@ -42,148 +47,100 @@ public void updateTaskOrder(Long processorId, UpdateTaskOrderRequest request) { validateRequest(request, null); Member processor = memberService.findActiveMember(processorId); Task targetTask = taskService.findById(request.targetTaskId()); + processorValidationPolicy.validateProcessor(processorId, targetTask); // 가장 상위로 이동 if (request.prevTaskId() == 0) { - moveToTop(processorId, targetTask, request.nextTaskId()); + Task nextTask = findByIdAndStatus(request.targetTaskId(), targetTask.getTaskStatus()); + // 해당 상태에서 바로 앞에 있는 작업 찾기 + Task prevTask = loadTaskPort.findPrevOrderTaskByProcessorIdAndStatus(processorId, targetTask.getTaskStatus(), nextTask.getProcessorOrder()).orElse(null); + long newOrder = taskOrderCalculationPolicy.calculateOrderForTop(targetTask, nextTask); + updateNewTaskOrder(targetTask, newOrder); } // 가장 하위로 이동 else if (request.nextTaskId() == 0) { - moveToBottom(processorId, targetTask, request.prevTaskId()); + Task prevTask = findByIdAndStatus(request.targetTaskId(), targetTask.getTaskStatus()); + // 해당 상태에서 바로 뒤에 있는 작업 찾기 + Task nextTask = loadTaskPort.findNextOrderTaskByProcessorIdAndStatus(processorId, targetTask.getTaskStatus(), prevTask.getProcessorOrder()).orElse(null); + long newOrder = taskOrderCalculationPolicy.calculateOrderForBottom(prevTask, nextTask); + updateNewTaskOrder(targetTask, newOrder); } else { Task prevTask = findByIdAndStatus(request.prevTaskId(), targetTask.getTaskStatus()); Task nextTask = findByIdAndStatus(request.nextTaskId(), targetTask.getTaskStatus()); - - // 새로운 작업 순서 업데이트 - updateNewTaskOrder(processor.getMemberId(), targetTask, prevTask.getProcessorOrder(), nextTask.getProcessorOrder()); + long newOrder = taskOrderCalculationPolicy.calculateNewProcessorOrder(prevTask.getProcessorOrder(), nextTask.getProcessorOrder()); + updateNewTaskOrder(targetTask, newOrder); } } - /** - * 순서 변경 요청의 유효성을 검증하는 메서드 - */ - private static void validateRequest(UpdateTaskOrderRequest request, TaskStatus targetStatus) { - // 이전 및 다음 작업 ID가 모두 0인 경우 예외 발생 - if (request.prevTaskId() == 0 && request.nextTaskId() == 0) { - throw new ApplicationException(TaskErrorCode.INVALID_TASK_STATUS_TRANSITION); - } - - // 타겟 상태가 유효한지 검증 - if (targetStatus != null && !TaskStatus.getTaskBoardStatusList().contains(targetStatus)) { - throw new ApplicationException(TaskErrorCode.INVALID_TASK_STATUS_TRANSITION); - } - } /** * 새로운 processorOrder를 계산하고 저장하는 메서드 * - * @param processorId 작업을 수행하는 멤버 ID * @param targetTask 순서를 변경할 대상 작업 - * @param prevTaskOrder 이전 작업의 processorOrder 값 - * @param nextTaskOrder 다음 작업의 processorOrder 값 */ - private void updateNewTaskOrder(Long processorId, Task targetTask, Long prevTaskOrder, Long nextTaskOrder) { - targetTask.updateProcessorOrder(processorId, prevTaskOrder, nextTaskOrder); + private void updateNewTaskOrder(Task targetTask, Long newOrder) { + targetTask.updateProcessorOrder(newOrder); commandTaskPort.save(targetTask); } - /** - * 작업을 가장 상위로 이동시키는 메서드 - * - * @param processorId 작업을 수행하는 멤버 ID - * @param targetTask 순서를 변경할 대상 작업 - * @param nextTaskId 다음 작업의 ID - */ - private void moveToTop(Long processorId, Task targetTask, Long nextTaskId) { - // 다음 작업 찾기 - Task nextTask = findByIdAndStatus(nextTaskId, targetTask.getTaskStatus()); - - // 해당 상태에서 바로 앞에 있는 작업 찾기 - Task prevTask = loadTaskPort.findPrevOrderTaskByProcessorIdAndStatus(processorId, targetTask.getTaskStatus(), nextTask.getProcessorOrder()).orElse(null); - - Long prevTaskOrder = prevTask == null ? null : prevTask.getProcessorOrder(); - - updateNewTaskOrder(processorId, targetTask, prevTaskOrder, nextTask.getProcessorOrder()); - } - - /** - * 작업을 가장 하위로 이동시키는 메서드 - * - * @param processorId 작업을 수행하는 멤버 ID - * @param targetTask 순서를 변경할 대상 작업 - * @param prevTaskId 이전 작업의 ID - */ - private void moveToBottom(Long processorId, Task targetTask, Long prevTaskId) { - // 이전 작업 찾기 - Task prevTask = findByIdAndStatus(prevTaskId, targetTask.getTaskStatus()); - - // 해당 상태에서 바로 뒤에 있는 작업 찾기 - Task nextTask = loadTaskPort.findNextOrderTaskByProcessorIdAndStatus(processorId, targetTask.getTaskStatus(), prevTask.getProcessorOrder()).orElse(null); - - Long nextTaskOrder = nextTask == null ? null : nextTask.getProcessorOrder(); - - updateNewTaskOrder(processorId, targetTask, prevTask.getProcessorOrder(), nextTaskOrder); - } - /** * 작업의 상태와 순서를 동시에 변경하는 메서드 * - * @param processorId 작업을 수행하는 멤버 ID - * @param request 순서 변경 요청 객체 + * @param processorId 작업을 수행하는 멤버 ID + * @param request 순서 변경 요청 객체 * @param targetStatus 변경할 작업 상태 */ @Override @Transactional public void updateTaskOrderAndStatus(Long processorId, UpdateTaskOrderRequest request, TaskStatus targetStatus) { validateRequest(request, targetStatus); - Member processor = memberService.findActiveMember(processorId); - Task targetTask = taskService.findById(request.targetTaskId()); + processorValidationPolicy.validateProcessor(processorId, targetTask); if (request.prevTaskId() == 0) { - moveToTopWithStatusChange(processorId, targetTask, request.nextTaskId(), targetStatus); + Task nextTask = findByIdAndStatus(request.targetTaskId(), targetStatus); + // 해당 상태에서 바로 앞 있는 작업 찾기 + Task prevTask = loadTaskPort.findPrevOrderTaskByProcessorIdAndStatus(processorId, targetStatus, nextTask.getProcessorOrder()).orElse(null); + long newOrder = taskOrderCalculationPolicy.calculateOrderForTop(prevTask,nextTask); + updateNewTaskOrderAndStatus(targetStatus, targetTask, newOrder); } else if (request.nextTaskId() == 0) { - moveToBottomWithStatusChange(processorId, targetTask, request.prevTaskId(), targetStatus); + Task prevTask = findByIdAndStatus(request.targetTaskId(), targetStatus); + // 해당 상태에서 바로 뒤에 있는 작업 찾기 + Task nextTask = loadTaskPort.findNextOrderTaskByProcessorIdAndStatus(processorId, targetStatus, prevTask.getProcessorOrder()).orElse(null); + long newOrder = taskOrderCalculationPolicy.calculateOrderForBottom(prevTask, nextTask); + updateNewTaskOrderAndStatus(targetStatus, targetTask, newOrder); } else { Task prevTask = findByIdAndStatus(request.prevTaskId(), targetStatus); Task nextTask = findByIdAndStatus(request.nextTaskId(), targetStatus); - - updateNewTaskOrderAndStatus(targetStatus, targetTask, processor.getMemberId(), prevTask.getProcessorOrder(), nextTask.getProcessorOrder()); + long newOrder = taskOrderCalculationPolicy.calculateNewProcessorOrder(nextTask.getProcessorOrder(), prevTask.getProcessorOrder()); + updateNewTaskOrderAndStatus(targetStatus, targetTask, newOrder); } } /** * 작업의 상태와 순서를 업데이트하는 메서드 */ - private void updateNewTaskOrderAndStatus(TaskStatus targetStatus, Task targetTask, Long processorId, Long prevTaskOrder, Long nextTaskOrder) { - targetTask.updateProcessorOrder(processorId, prevTaskOrder, nextTaskOrder); + private void updateNewTaskOrderAndStatus(TaskStatus targetStatus, Task targetTask, long newOrder) { + targetTask.updateProcessorOrder(newOrder); targetTask.updateTaskStatus(targetStatus); commandTaskPort.save(targetTask); } /** - * 상태 변경을 포함하여 작업을 가장 상위로 이동하는 메서드 + * 순서 변경 요청의 유효성을 검증하는 메서드 */ - private void moveToTopWithStatusChange(Long processorId, Task targetTask, Long nextTaskId, TaskStatus targetStatus) { - Task nextTask = findByIdAndStatus(nextTaskId, targetStatus); - Task prevTask = loadTaskPort.findPrevOrderTaskByProcessorIdAndStatus(processorId, targetStatus, nextTask.getProcessorOrder()).orElse(null); - - Long prevTaskOrder = prevTask == null ? null : prevTask.getProcessorOrder(); + public void validateRequest(UpdateTaskOrderRequest request, TaskStatus targetStatus) { + // 이전 및 다음 작업 ID가 모두 0인 경우 예외 발생 + if (request.prevTaskId() == 0 && request.nextTaskId() == 0) { + throw new ApplicationException(TaskErrorCode.INVALID_TASK_STATUS_TRANSITION); + } - updateNewTaskOrderAndStatus(targetStatus, targetTask, processorId, prevTaskOrder, nextTask.getProcessorOrder()); + // 타겟 상태가 유효한지 검증 + if (targetStatus != null && !TaskStatus.getTaskBoardStatusList().contains(targetStatus)) { + throw new ApplicationException(TaskErrorCode.INVALID_TASK_STATUS_TRANSITION); + } } - /** - * 상태 변경을 포함하여 작업을 가장 하위로 이동하는 메서드 - */ - private void moveToBottomWithStatusChange(Long processorId, Task targetTask, Long prevTaskId, TaskStatus targetStatus) { - Task prevTask = findByIdAndStatus(prevTaskId, targetStatus); - Task nextTask = loadTaskPort.findNextOrderTaskByProcessorIdAndStatus(processorId, targetStatus, prevTask.getProcessorOrder()).orElse(null); - - Long nextTaskOrder = nextTask == null ? null : nextTask.getProcessorOrder(); - - updateNewTaskOrderAndStatus(targetStatus, targetTask, processorId, prevTask.getProcessorOrder(), nextTaskOrder); - } } diff --git a/src/main/java/clap/server/application/service/task/UpdateTaskService.java b/src/main/java/clap/server/application/service/task/UpdateTaskService.java index 2e939d29..7e661e20 100644 --- a/src/main/java/clap/server/application/service/task/UpdateTaskService.java +++ b/src/main/java/clap/server/application/service/task/UpdateTaskService.java @@ -1,7 +1,6 @@ package clap.server.application.service.task; import clap.server.adapter.inbound.web.dto.task.*; -import clap.server.adapter.outbound.infrastructure.s3.S3UploadAdapter; import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType; import clap.server.application.mapper.AttachmentMapper; import clap.server.application.mapper.TaskMapper; @@ -13,10 +12,11 @@ import clap.server.application.port.inbound.task.UpdateTaskProcessorUsecase; import clap.server.application.port.inbound.task.UpdateTaskStatusUsecase; import clap.server.application.port.inbound.task.UpdateTaskUsecase; +import clap.server.application.port.outbound.s3.S3UploadPort; import clap.server.application.port.outbound.task.CommandAttachmentPort; import clap.server.application.port.outbound.task.CommandTaskPort; import clap.server.application.port.outbound.task.LoadAttachmentPort; -import clap.server.application.service.webhook.SendPushNotificationService; +import clap.server.application.service.webhook.SendNotificationService; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.common.constants.FilePathConstants; import clap.server.domain.model.member.Member; @@ -31,7 +31,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.util.ArrayList; import java.util.List; @@ -43,13 +42,13 @@ public class UpdateTaskService implements UpdateTaskUsecase, UpdateTaskStatusUse private final MemberService memberService; private final CategoryService categoryService; private final TaskService taskService; - private final SendPushNotificationService sendPushNotificationService; + private final SendNotificationService sendNotificationService; private final CommandTaskPort commandTaskPort; private final LoadAttachmentPort loadAttachmentPort; private final LabelService labelService; private final CommandAttachmentPort commandAttachmentPort; - private final S3UploadAdapter s3UploadAdapter; + private final S3UploadPort s3UploadPort; @Override @Transactional @@ -74,9 +73,8 @@ public UpdateTaskResponse updateTaskState(Long memberId, Long taskId, UpdateTask Task task = taskService.findById(taskId); task.updateTaskStatus(updateTaskStatusRequest.taskStatus()); Task updateTask = commandTaskPort.save(task); - List receiver = new ArrayList<>(); - receiver.add(task.getRequester()); - publishNotification(receiver, updateTask, NotificationType.STATUS_SWITCHED, String.valueOf(updateTask.getTaskStatus())); + + publishNotification(updateTask, NotificationType.STATUS_SWITCHED, String.valueOf(updateTask.getTaskStatus())); return TaskMapper.toUpdateTaskResponse(updateTask); } @@ -90,11 +88,7 @@ public UpdateTaskResponse updateTaskProcessor(Long taskId, Long userId, UpdateTa task.updateProcessor(processor); Task updateTask = commandTaskPort.save(task); - List receivers = new ArrayList<>(); - receivers.add(updateTask.getRequester()); - receivers.add(updateTask.getProcessor()); - - publishNotification(receivers, updateTask, NotificationType.PROCESSOR_CHANGED, updateTask.getProcessor().getNickname()); + publishNotification(updateTask, NotificationType.PROCESSOR_CHANGED, updateTask.getProcessor().getNickname()); return TaskMapper.toUpdateTaskResponse(updateTask); } @@ -110,14 +104,12 @@ public UpdateTaskResponse updateTaskLabel(Long taskId, Long userId, UpdateTaskLa return TaskMapper.toUpdateTaskResponse(updatetask); } - - private void updateAttachments(List attachmentIdsToDelete, List files, Task task) { List attachmentsToDelete = validateAndGetAttachments(attachmentIdsToDelete, task); attachmentsToDelete.forEach(Attachment::softDelete); - if(files != null) { - List fileUrls = s3UploadAdapter.uploadFiles(FilePathConstants.TASK_IMAGE, files); + if (files != null) { + List fileUrls = s3UploadPort.uploadFiles(FilePathConstants.TASK_IMAGE, files); List attachments = AttachmentMapper.toTaskAttachments(task, files, fileUrls); commandAttachmentPort.saveAll(attachments); } @@ -131,10 +123,11 @@ private List validateAndGetAttachments(List attachmentIdsToDel return attachmentsOfTask; } - private void publishNotification(List receivers, Task task, NotificationType notificationType, String message){ - for (Member receiver : receivers) { - sendPushNotificationService.sendPushNotification(receiver, notificationType, + private void publishNotification(Task task, NotificationType notificationType, String message) { + List receivers = List.of(task.getRequester(), task.getProcessor()); + receivers.forEach(receiver -> { + sendNotificationService.sendPushNotification(receiver, notificationType, task, message, null); - } + }); } } diff --git a/src/main/java/clap/server/application/service/webhook/SendPushNotificationService.java b/src/main/java/clap/server/application/service/webhook/SendNotificationService.java similarity index 98% rename from src/main/java/clap/server/application/service/webhook/SendPushNotificationService.java rename to src/main/java/clap/server/application/service/webhook/SendNotificationService.java index 8c3b1d0d..dc31d418 100644 --- a/src/main/java/clap/server/application/service/webhook/SendPushNotificationService.java +++ b/src/main/java/clap/server/application/service/webhook/SendNotificationService.java @@ -17,7 +17,7 @@ @ApplicationService @RequiredArgsConstructor -public class SendPushNotificationService { +public class SendNotificationService { private final SendSseService sendSseService; private final SendAgitService sendAgitService; diff --git a/src/main/java/clap/server/common/annotation/architecture/Policy.java b/src/main/java/clap/server/common/annotation/architecture/Policy.java new file mode 100644 index 00000000..28ddfa87 --- /dev/null +++ b/src/main/java/clap/server/common/annotation/architecture/Policy.java @@ -0,0 +1,15 @@ +package clap.server.common.annotation.architecture; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface Policy { + @AliasFor(annotation = Component.class) + String value() default ""; +} \ No newline at end of file diff --git a/src/main/java/clap/server/common/constants/FilePathConstants.java b/src/main/java/clap/server/common/constants/FilePathConstants.java index 0b13ec57..3d2e6fd5 100644 --- a/src/main/java/clap/server/common/constants/FilePathConstants.java +++ b/src/main/java/clap/server/common/constants/FilePathConstants.java @@ -8,6 +8,7 @@ public enum FilePathConstants { TASK_IMAGE("task/image"), TASK_DOCUMENT("task/docs"), + TASK_COMMENT("task/comments"), MEMBER_IMAGE("member"), ; private final String path; diff --git a/src/main/java/clap/server/domain/model/task/Task.java b/src/main/java/clap/server/domain/model/task/Task.java index fd076ac7..50362b30 100644 --- a/src/main/java/clap/server/domain/model/task/Task.java +++ b/src/main/java/clap/server/domain/model/task/Task.java @@ -15,7 +15,7 @@ import java.time.format.DateTimeFormatter; import java.util.Objects; -import static clap.server.domain.model.task.constants.TaskProcessorOrderPolicy.DEFAULT_PROCESSOR_ORDER_GAP; +import static clap.server.domain.policy.task.TaskOrderGapPolicy.DEFAULT_PROCESSOR_ORDER_GAP; @Getter @SuperBuilder @@ -50,19 +50,12 @@ public void updateTask(Long requesterId, Category category, String title, String if(!Objects.equals(requesterId, this.requester.getMemberId() )) { throw new ApplicationException(TaskErrorCode.NOT_A_REQUESTER); } - validateTaskRequested(); this.category = category; this.title = title; this.description = description; this.taskCode = toTaskCode(category); } - public void validateTaskRequested() { - if (this.taskStatus != TaskStatus.REQUESTED) { - throw new DomainException(TaskErrorCode.TASK_STATUS_MISMATCH); - } - } - public void setInitialProcessorOrder() { if (this.processor == null) { this.processorOrder = this.taskId * DEFAULT_PROCESSOR_ORDER_GAP; @@ -85,7 +78,6 @@ public void updateLabel(Label label) { } public void approveTask(Member reviewer, Member processor, LocalDateTime dueDate, Category category, Label label) { - validateTaskRequested(); this.reviewer = reviewer; this.processor = processor; this.dueDate = dueDate; @@ -98,32 +90,7 @@ private static String toTaskCode(Category category) { return category.getMainCategory().getCode() + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmm")); } - public void updateProcessorOrder(Long processorId, Long prevTaskOrder, Long nextTaskOrder) { - if (!Objects.equals(processorId, this.processor.getMemberId())) { - throw new DomainException(TaskErrorCode.NOT_A_PROCESSOR); - } - long newProcessorOrder; - - // 최상위 이동: 가장 작은 processorOrder보다 더 작은 값 설정 - if (prevTaskOrder == null && nextTaskOrder != null) { - newProcessorOrder = nextTaskOrder - DEFAULT_PROCESSOR_ORDER_GAP; - } - // 최하위 이동: 가장 큰 processorOrder보다 더 큰 값 설정 - else if (prevTaskOrder != null && nextTaskOrder == null) { - newProcessorOrder = prevTaskOrder + DEFAULT_PROCESSOR_ORDER_GAP; - } - // 중간 위치로 이동: prevTask와 nextTask의 processorOrder 평균값 사용 - else if (prevTaskOrder != null && nextTaskOrder != null) { - if (nextTaskOrder - prevTaskOrder < 2) { - throw new DomainException(TaskErrorCode.INVALID_TASK_ORDER); - } - newProcessorOrder = (prevTaskOrder + nextTaskOrder) / 2; - } - // 기본값 (예외적인 상황 방지) - else { - newProcessorOrder = DEFAULT_PROCESSOR_ORDER_GAP; - } + public void updateProcessorOrder(long newProcessorOrder) { this.processorOrder = newProcessorOrder; } - } diff --git a/src/main/java/clap/server/domain/policy/task/ProcessorValidationPolicy.java b/src/main/java/clap/server/domain/policy/task/ProcessorValidationPolicy.java new file mode 100644 index 00000000..16f7bd73 --- /dev/null +++ b/src/main/java/clap/server/domain/policy/task/ProcessorValidationPolicy.java @@ -0,0 +1,17 @@ +package clap.server.domain.policy.task; + +import clap.server.common.annotation.architecture.Policy; +import clap.server.domain.model.task.Task; +import clap.server.exception.ApplicationException; +import clap.server.exception.code.TaskErrorCode; + +import java.util.Objects; + +@Policy +public class ProcessorValidationPolicy { + public void validateProcessor(Long processorId, Task targetTask) { + if (!Objects.equals(processorId, targetTask.getProcessor().getMemberId())) { + throw new ApplicationException(TaskErrorCode.NOT_A_PROCESSOR); + } + } +} diff --git a/src/main/java/clap/server/domain/policy/task/RequestedTaskUpdatePolicy.java b/src/main/java/clap/server/domain/policy/task/RequestedTaskUpdatePolicy.java new file mode 100644 index 00000000..ce1eb38a --- /dev/null +++ b/src/main/java/clap/server/domain/policy/task/RequestedTaskUpdatePolicy.java @@ -0,0 +1,16 @@ +package clap.server.domain.policy.task; + +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import clap.server.common.annotation.architecture.Policy; +import clap.server.domain.model.task.Task; +import clap.server.exception.DomainException; +import clap.server.exception.code.TaskErrorCode; + +@Policy +public class RequestedTaskUpdatePolicy { + public void validateTaskRequested(Task task) { + if (task.getTaskStatus() != TaskStatus.REQUESTED) { + throw new DomainException(TaskErrorCode.TASK_STATUS_MISMATCH); + } + } +} diff --git a/src/main/java/clap/server/domain/policy/task/TaskOrderCalculationPolicy.java b/src/main/java/clap/server/domain/policy/task/TaskOrderCalculationPolicy.java new file mode 100644 index 00000000..bd73e31e --- /dev/null +++ b/src/main/java/clap/server/domain/policy/task/TaskOrderCalculationPolicy.java @@ -0,0 +1,43 @@ +package clap.server.domain.policy.task; + +import clap.server.common.annotation.architecture.Policy; +import clap.server.domain.model.task.Task; +import clap.server.exception.DomainException; +import clap.server.exception.code.TaskErrorCode; + +import static clap.server.domain.policy.task.TaskOrderGapPolicy.DEFAULT_PROCESSOR_ORDER_GAP; + +@Policy +public class TaskOrderCalculationPolicy { + + public long calculateOrderForTop(Task prevTask, Task nextTask) { + Long prevTaskOrder = prevTask == null ? null : prevTask.getProcessorOrder(); + if (prevTaskOrder == null){ + return nextTask.getProcessorOrder() - DEFAULT_PROCESSOR_ORDER_GAP; + } + return calculateNewProcessorOrder(prevTaskOrder, nextTask.getProcessorOrder()); + } + + public long calculateOrderForBottom(Task prevTask, Task nextTask) { + Long nextTaskOrder = nextTask == null ? null : nextTask.getProcessorOrder(); + if (nextTaskOrder == null){ + return prevTask.getProcessorOrder() + DEFAULT_PROCESSOR_ORDER_GAP; + } + return calculateNewProcessorOrder(prevTask.getProcessorOrder(), nextTaskOrder); + } + + public long calculateNewProcessorOrder(Long prevTaskOrder, Long nextTaskOrder) { + if (prevTaskOrder != null && nextTaskOrder != null) { + if (nextTaskOrder - prevTaskOrder < 2) { + throw new DomainException(TaskErrorCode.INVALID_TASK_ORDER); + } + return (prevTaskOrder + nextTaskOrder) / 2; + } + // 기본값 (예외적인 상황 방지) + else { + return DEFAULT_PROCESSOR_ORDER_GAP; + } + } + +} + diff --git a/src/main/java/clap/server/domain/model/task/constants/TaskProcessorOrderPolicy.java b/src/main/java/clap/server/domain/policy/task/TaskOrderGapPolicy.java similarity index 56% rename from src/main/java/clap/server/domain/model/task/constants/TaskProcessorOrderPolicy.java rename to src/main/java/clap/server/domain/policy/task/TaskOrderGapPolicy.java index 78bc9cfc..bca343ea 100644 --- a/src/main/java/clap/server/domain/model/task/constants/TaskProcessorOrderPolicy.java +++ b/src/main/java/clap/server/domain/policy/task/TaskOrderGapPolicy.java @@ -1,8 +1,8 @@ -package clap.server.domain.model.task.constants; +package clap.server.domain.policy.task; import lombok.Getter; @Getter -public class TaskProcessorOrderPolicy { +public class TaskOrderGapPolicy { public static final long DEFAULT_PROCESSOR_ORDER_GAP = (long) Math.pow(2,6); } \ No newline at end of file