diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/task/response/TeamStatusResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/task/response/TeamStatusResponse.java index 0f33fe23..adb6e148 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/task/response/TeamStatusResponse.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/task/response/TeamStatusResponse.java @@ -8,18 +8,4 @@ public record TeamStatusResponse( int totalInReviewingTaskCount, int totalTaskCount ) { - // 기존 생성자 (3개 파라미터) - public TeamStatusResponse(List members, int totalInProgressTaskCount, int totalInReviewingTaskCount) { - this( - (members == null) ? List.of() : members, - totalInProgressTaskCount, - totalInReviewingTaskCount, - totalInProgressTaskCount + totalInReviewingTaskCount - ); - } - - // 추가된 생성자 (List만 받음) - public TeamStatusResponse(List members) { - this(members, 0, 0); // 기본값을 사용하여 생성 - } } diff --git a/src/main/java/clap/server/adapter/inbound/web/task/CancelTaskController.java b/src/main/java/clap/server/adapter/inbound/web/task/CancelTaskController.java index a5d1b9a2..72b12727 100644 --- a/src/main/java/clap/server/adapter/inbound/web/task/CancelTaskController.java +++ b/src/main/java/clap/server/adapter/inbound/web/task/CancelTaskController.java @@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; -@Tag(name = "02. Task [거부 & 종료]") +@Tag(name = "02. Task [종료]") @RequestMapping("/api/tasks") @RequiredArgsConstructor @WebAdapter @@ -20,7 +20,7 @@ public class CancelTaskController { private final CancelTaskUsecase cancelTaskUsecase; @Operation(summary = "작업 취소") - @Secured({"ROLE_USER","ROLE_MANAGER"}) + @Secured("ROLE_USER") @PatchMapping("/{taskId}/cancel") public void cancelTask(@PathVariable Long taskId, @AuthenticationPrincipal SecurityUserDetails userDetails) { cancelTaskUsecase.cancleTask(taskId, userDetails.getUserId()); diff --git a/src/main/java/clap/server/adapter/inbound/web/task/TerminateTaskController.java b/src/main/java/clap/server/adapter/inbound/web/task/TerminateTaskController.java index 36208ce6..b94a5b21 100644 --- a/src/main/java/clap/server/adapter/inbound/web/task/TerminateTaskController.java +++ b/src/main/java/clap/server/adapter/inbound/web/task/TerminateTaskController.java @@ -5,22 +5,23 @@ import clap.server.application.port.inbound.task.TerminateTaskUsecase; import clap.server.common.annotation.architecture.WebAdapter; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; -@Tag(name = "02. Task [거부 & 종료]") +@Tag(name = "02. Task [종료]") @WebAdapter @RequiredArgsConstructor @RequestMapping("/api/tasks") public class TerminateTaskController { private final TerminateTaskUsecase terminateTaskUsecase; - @Operation(summary = "작업 거부 및 종료") + @Operation(summary = "작업 반려 및 종료") @Secured({"ROLE_MANAGER"}) @PatchMapping("/{taskId}/terminate") public void terminateTask(@AuthenticationPrincipal SecurityUserDetails userInfo, diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepositoryImpl.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepositoryImpl.java index 4f89279b..397bd4b7 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepositoryImpl.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepositoryImpl.java @@ -3,12 +3,8 @@ import clap.server.adapter.inbound.web.dto.task.request.FilterTaskBoardRequest; import clap.server.adapter.inbound.web.dto.task.request.FilterTaskListRequest; import clap.server.adapter.inbound.web.dto.task.request.FilterTeamStatusRequest; -import clap.server.adapter.inbound.web.dto.task.request.SortBy; -import clap.server.adapter.inbound.web.dto.task.response.TeamTaskItemResponse; -import clap.server.adapter.inbound.web.dto.task.response.TeamTaskResponse; import clap.server.adapter.outbound.persistense.entity.task.TaskEntity; import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; -import clap.server.domain.model.task.Task; import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.dsl.DateTimePath; @@ -20,12 +16,8 @@ import org.springframework.stereotype.Repository; import java.time.LocalDateTime; -import java.util.Comparator; -import java.util.LinkedHashMap; import java.util.List; -import java.util.stream.Collectors; -import static clap.server.adapter.inbound.web.dto.task.request.SortBy.CONTRIBUTE; import static clap.server.adapter.outbound.persistense.entity.task.QTaskEntity.taskEntity; import static com.querydsl.core.types.Order.ASC; import static com.querydsl.core.types.Order.DESC; diff --git a/src/main/java/clap/server/application/mapper/response/TeamTaskResponseMapper.java b/src/main/java/clap/server/application/mapper/response/TeamTaskResponseMapper.java index 01c52d91..05aa2ffc 100644 --- a/src/main/java/clap/server/application/mapper/response/TeamTaskResponseMapper.java +++ b/src/main/java/clap/server/application/mapper/response/TeamTaskResponseMapper.java @@ -1,8 +1,8 @@ package clap.server.application.mapper.response; +import clap.server.adapter.inbound.web.dto.task.response.TeamStatusResponse; import clap.server.adapter.inbound.web.dto.task.response.TeamTaskItemResponse; import clap.server.adapter.inbound.web.dto.task.response.TeamTaskResponse; -import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; import clap.server.domain.model.task.Task; import java.util.LinkedHashMap; @@ -25,10 +25,10 @@ private static TeamTaskResponse toTeamTaskResponse(Map.Entry> e .map(TeamTaskResponseMapper::toTeamTaskItemResponse) .collect(Collectors.toList()); - int inProgressTaskCount = (int) entry.getValue().stream().filter(t -> t.getTaskStatus() == TaskStatus.IN_PROGRESS).count(); - int inReviewingTaskCount = (int) entry.getValue().stream().filter(t -> t.getTaskStatus() == TaskStatus.IN_REVIEWING).count(); - Task firstTask = entry.getValue().get(0); + int inProgressTaskCount = firstTask.getProcessor().getInProgressTaskCount(); + int inReviewingTaskCount = firstTask.getProcessor().getInReviewingTaskCount(); + return new TeamTaskResponse( entry.getKey(), firstTask.getProcessor().getNickname(), @@ -65,4 +65,13 @@ private static TeamTaskItemResponse.LabelInfo toLabelInfo(Task task) { task.getLabel().getLabelColor() ) : null; } + + public static TeamStatusResponse toTeamStatusResponse(List members, int totalInProgressTaskCount, int totalInReviewingTaskCount) { + return new TeamStatusResponse( + (members == null) ? List.of() : members, + totalInProgressTaskCount, + totalInReviewingTaskCount, + totalInProgressTaskCount + totalInReviewingTaskCount + ); + } } diff --git a/src/main/java/clap/server/application/service/admin/ManageMemberService.java b/src/main/java/clap/server/application/service/admin/ManageMemberService.java index cb2c59e4..45be8dbe 100644 --- a/src/main/java/clap/server/application/service/admin/ManageMemberService.java +++ b/src/main/java/clap/server/application/service/admin/ManageMemberService.java @@ -29,8 +29,7 @@ public void updateMemberInfo(Long adminId, Long memberId, UpdateMemberRequest re Member member = memberService.findById(memberId); Department department = loadDepartmentPort.findById(request.departmentId()).orElseThrow(() -> new ApplicationException(DepartmentErrorCode.DEPARTMENT_NOT_FOUND)); - - //TODO: 인프라팀만 담당자가 될 수 있도록 수정해야함 + member.getMemberInfo().updateMemberInfoByAdmin( request.name(), request.isReviewer(), department, request.role(), request.departmentRole()); 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 179bb257..7478fecb 100644 --- a/src/main/java/clap/server/application/service/task/ApprovalTaskService.java +++ b/src/main/java/clap/server/application/service/task/ApprovalTaskService.java @@ -6,6 +6,7 @@ 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.adapter.outbound.persistense.entity.task.constant.TaskStatus; import clap.server.application.mapper.response.TaskResponseMapper; import clap.server.application.port.inbound.domain.CategoryService; import clap.server.application.port.inbound.domain.LabelService; @@ -28,16 +29,16 @@ @ApplicationService @RequiredArgsConstructor -@Transactional(readOnly = true) public class ApprovalTaskService implements ApprovalTaskUsecase { - private final MemberService memberService; private final TaskService taskService; private final CategoryService categoryService; private final LabelService labelService; + private final RequestedTaskUpdatePolicy requestedTaskUpdatePolicy; private final CommandTaskHistoryPort commandTaskHistoryPort; private final SendNotificationService sendNotificationService; + private final UpdateProcessorTaskCountService updateProcessorTaskCountService; @Override @Transactional @@ -52,8 +53,10 @@ public ApprovalTaskResponse approvalTaskByReviewer(Long reviewerId, Long taskId, } requestedTaskUpdatePolicy.validateTaskRequested(task); + updateProcessorTaskCountService.handleTaskStatusChange(processor, TaskStatus.REQUESTED, TaskStatus.IN_PROGRESS); task.approveTask(reviewer, processor, approvalTaskRequest.dueDate(), category, label); - TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.PROCESSOR_ASSIGNED, task, null, processor,null); + + TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.PROCESSOR_ASSIGNED, task, null, processor, null); commandTaskHistoryPort.save(taskHistory); List receivers = List.of(task.getRequester(), processor); @@ -71,7 +74,7 @@ public FindApprovalFormResponse findApprovalForm(Long managerId, Long taskId) { return TaskResponseMapper.toFindApprovalFormResponse(task); } - private void publishNotification(List receivers, Task task, String processorName){ + private void publishNotification(List receivers, Task task, String processorName) { receivers.forEach(receiver -> { boolean isManager = receiver.getMemberInfo().getRole() == MemberRole.ROLE_MANAGER; sendNotificationService.sendPushNotification(receiver, NotificationType.PROCESSOR_ASSIGNED, diff --git a/src/main/java/clap/server/application/service/task/FindManagersService.java b/src/main/java/clap/server/application/service/task/FindManagersService.java index 285915f1..f988f0fc 100644 --- a/src/main/java/clap/server/application/service/task/FindManagersService.java +++ b/src/main/java/clap/server/application/service/task/FindManagersService.java @@ -1,10 +1,8 @@ package clap.server.application.service.task; import clap.server.adapter.inbound.web.dto.task.response.FindManagersResponse; -import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; import clap.server.application.port.inbound.domain.MemberService; import clap.server.application.port.inbound.task.FindManagersUsecase; -import clap.server.application.port.outbound.task.LoadTaskPort; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.member.Member; import jakarta.transaction.Transactional; @@ -19,16 +17,14 @@ public class FindManagersService implements FindManagersUsecase { private final MemberService memberService; - private final LoadTaskPort loadTaskPort; @Transactional @Override public List findManagers() { - List targetStatuses = List.of(TaskStatus.IN_PROGRESS, TaskStatus.IN_REVIEWING); List managers = memberService.findActiveManagers(); return managers.stream() .map(manager -> { - int remainingTasks = loadTaskPort.findTasksByMemberIdAndStatus(manager.getMemberId(), targetStatuses).size(); + int remainingTasks = manager.getInProgressTaskCount() + manager.getInReviewingTaskCount(); return toFindManagersResponse(manager, remainingTasks); }).toList(); } diff --git a/src/main/java/clap/server/application/service/task/TeamStatusService.java b/src/main/java/clap/server/application/service/task/TeamStatusService.java index 3c12aee9..6435abfa 100644 --- a/src/main/java/clap/server/application/service/task/TeamStatusService.java +++ b/src/main/java/clap/server/application/service/task/TeamStatusService.java @@ -35,7 +35,7 @@ public TeamStatusResponse filterTeamStatus(FilterTeamStatusRequest filter) { taskItemResponses.sort((a, b) -> b.totalTaskCount() - a.totalTaskCount()); else taskItemResponses.sort(Comparator.comparing(TeamTaskResponse::nickname)); - return new TeamStatusResponse(taskItemResponses, totalInProgressTaskCount, totalInReviewingTaskCount); + return TeamTaskResponseMapper.toTeamStatusResponse(taskItemResponses, totalInProgressTaskCount, totalInReviewingTaskCount); } diff --git a/src/main/java/clap/server/application/service/task/TerminateTaskService.java b/src/main/java/clap/server/application/service/task/TerminateTaskService.java index 8223e79f..d0d3700d 100644 --- a/src/main/java/clap/server/application/service/task/TerminateTaskService.java +++ b/src/main/java/clap/server/application/service/task/TerminateTaskService.java @@ -1,9 +1,8 @@ package clap.server.application.service.task; -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.port.inbound.domain.MemberService; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; import clap.server.application.port.inbound.domain.TaskService; import clap.server.application.port.inbound.task.TerminateTaskUsecase; import clap.server.application.port.outbound.taskhistory.CommandTaskHistoryPort; @@ -15,20 +14,20 @@ import lombok.RequiredArgsConstructor; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @ApplicationService @RequiredArgsConstructor @Transactional public class TerminateTaskService implements TerminateTaskUsecase { - private final MemberService memberService; private final TaskService taskService; private final CommandTaskHistoryPort commandTaskHistoryPort; private final SendNotificationService sendNotificationService; + private final UpdateProcessorTaskCountService updateProcessorTaskCountService; @Override public void terminateTask(Long memberId, Long taskId, String reason) { Task task = taskService.findById(taskId); + + updateProcessorTaskCountService.handleTaskStatusChange(task.getProcessor(), task.getTaskStatus(), TaskStatus.TERMINATED); task.terminateTask(); taskService.upsert(task); diff --git a/src/main/java/clap/server/application/service/task/UpdateProcessorTaskCountService.java b/src/main/java/clap/server/application/service/task/UpdateProcessorTaskCountService.java new file mode 100644 index 00000000..6ea44d88 --- /dev/null +++ b/src/main/java/clap/server/application/service/task/UpdateProcessorTaskCountService.java @@ -0,0 +1,29 @@ +package clap.server.application.service.task; + +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import clap.server.application.port.outbound.member.CommandMemberPort; +import clap.server.domain.model.member.Member; +import clap.server.domain.policy.task.ProcessorTaskCountPolicy; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UpdateProcessorTaskCountService { + + private final ProcessorTaskCountPolicy processorTaskCountPolicy; + private final CommandMemberPort commandMemberPort; + + public void handleTaskStatusChange(Member processor, TaskStatus oldStatus, TaskStatus newStatus) { + processorTaskCountPolicy.decrementTaskCount(processor, oldStatus); + processorTaskCountPolicy.incrementTaskCount(processor, newStatus); + commandMemberPort.save(processor); + } + + public void handleProcessorChange(Member oldProcessor, Member newProcessor, TaskStatus status) { + processorTaskCountPolicy.decrementTaskCount(oldProcessor, status); + processorTaskCountPolicy.incrementTaskCount(newProcessor, status); + commandMemberPort.save(oldProcessor); + commandMemberPort.save(newProcessor); + } +} diff --git a/src/main/java/clap/server/application/service/task/UpdateTaskContentService.java b/src/main/java/clap/server/application/service/task/UpdateTaskContentService.java new file mode 100644 index 00000000..d6a1e90c --- /dev/null +++ b/src/main/java/clap/server/application/service/task/UpdateTaskContentService.java @@ -0,0 +1,105 @@ +package clap.server.application.service.task; + +import clap.server.adapter.inbound.web.dto.task.request.UpdateTaskLabelRequest; +import clap.server.adapter.inbound.web.dto.task.request.UpdateTaskRequest; +import clap.server.application.mapper.AttachmentMapper; +import clap.server.application.port.inbound.domain.CategoryService; +import clap.server.application.port.inbound.domain.LabelService; +import clap.server.application.port.inbound.domain.MemberService; +import clap.server.application.port.inbound.domain.TaskService; +import clap.server.application.port.inbound.task.UpdateTaskLabelUsecase; +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.LoadAttachmentPort; +import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.common.constants.FilePathConstants; +import clap.server.domain.model.task.Attachment; +import clap.server.domain.model.task.Category; +import clap.server.domain.model.task.Label; +import clap.server.domain.model.task.Task; +import clap.server.exception.ApplicationException; +import clap.server.exception.code.TaskErrorCode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +import static clap.server.domain.policy.task.TaskPolicyConstants.TASK_MAX_FILE_COUNT; + +@ApplicationService +@RequiredArgsConstructor +@Slf4j +public class UpdateTaskContentService implements UpdateTaskLabelUsecase, UpdateTaskUsecase { + private final CategoryService categoryService; + private final MemberService memberService; + private final LabelService labelService; + private final TaskService taskService; + + private final LoadAttachmentPort loadAttachmentPort; + private final CommandAttachmentPort commandAttachmentPort; + private final S3UploadPort s3UploadPort; + + @Override + @Transactional + public void updateTask(Long requesterId, Long taskId, UpdateTaskRequest request, List files) { + memberService.findActiveMember(requesterId); + Category category = categoryService.findById(request.categoryId()); + Task task = taskService.findById(taskId); + int attachmentCount = getAttachmentCount(request, files, task); + + if (!request.attachmentsToDelete().isEmpty()) { + deleteAttachments(request, task); + } + if (files != null) { + updateAttachments(files, task); + } + task.updateTask(requesterId, category, request.title(), request.description(), attachmentCount); + taskService.upsert(task); + } + + private void deleteAttachments(UpdateTaskRequest request, Task task) { + List attachmentsToDelete = validateAndGetAttachments(request.attachmentsToDelete(), task); + attachmentsToDelete.stream() + .peek(Attachment::softDelete) + .forEach(commandAttachmentPort::save); + } + + private void updateAttachments(List files, Task task) { + List fileUrls = s3UploadPort.uploadFiles(FilePathConstants.TASK_FILE, files); + List attachments = AttachmentMapper.toTaskAttachments(task, files, fileUrls); + commandAttachmentPort.saveAll(attachments); + } + + private static int getAttachmentCount(UpdateTaskRequest request, List files, Task task) { + int attachmentToAdd = files == null ? 0 : files.size(); + int attachmentCount = task.getAttachmentCount() - request.attachmentsToDelete().size() + attachmentToAdd; + if (attachmentCount > TASK_MAX_FILE_COUNT) { + throw new ApplicationException(TaskErrorCode.FILE_COUNT_EXCEEDED); + } + return attachmentCount; + } + + private List validateAndGetAttachments(List attachmentIdsToDelete, Task task) { + List attachmentsOfTask = loadAttachmentPort.findAllByTaskIdAndAttachmentId(task.getTaskId(), attachmentIdsToDelete); + if (attachmentsOfTask.size() != attachmentIdsToDelete.size()) { + throw new ApplicationException(TaskErrorCode.TASK_ATTACHMENT_NOT_FOUND); + } + return attachmentsOfTask; + } + + @Transactional + @Override + public void updateTaskLabel(Long taskId, Long memberId, UpdateTaskLabelRequest request) { + memberService.findActiveMember(memberId); + memberService.findReviewer(memberId); + Task task = taskService.findById(taskId); + Label label = labelService.findById(request.labelId()); + + task.updateLabel(label); + taskService.upsert(task); + } + +} diff --git a/src/main/java/clap/server/application/service/task/UpdateTaskOrderAndStstusService.java b/src/main/java/clap/server/application/service/task/UpdateTaskOrderAndStstusService.java index ba1d5172..fac7a7f0 100644 --- a/src/main/java/clap/server/application/service/task/UpdateTaskOrderAndStstusService.java +++ b/src/main/java/clap/server/application/service/task/UpdateTaskOrderAndStstusService.java @@ -36,6 +36,7 @@ public class UpdateTaskOrderAndStstusService implements UpdateTaskOrderAndStatus private final SendNotificationService sendNotificationService; private final CommandTaskHistoryPort commandTaskHistoryPort; + private final UpdateProcessorTaskCountService updateProcessorTaskCountService; private final TaskOrderCalculationPolicy taskOrderCalculationPolicy; private final ProcessorValidationPolicy processorValidationPolicy; @@ -57,6 +58,7 @@ public void updateTaskOrderAndStatus(Long processorId, UpdateTaskOrderRequest re Task updatedTask; Task prevTask; Task nextTask; + updateProcessorTaskCountService.handleTaskStatusChange(processor, targetTask.getTaskStatus(), targetStatus); if (request.prevTaskId() == 0 && request.nextTaskId() == 0) { updatedTask = handleSingleTask(processorId, targetStatus, targetTask); 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 b7eace8a..bb6242ee 100644 --- a/src/main/java/clap/server/application/service/task/UpdateTaskService.java +++ b/src/main/java/clap/server/application/service/task/UpdateTaskService.java @@ -1,101 +1,61 @@ package clap.server.application.service.task; -import clap.server.adapter.inbound.web.dto.task.request.UpdateTaskLabelRequest; import clap.server.adapter.inbound.web.dto.task.request.UpdateTaskProcessorRequest; -import clap.server.adapter.inbound.web.dto.task.request.UpdateTaskRequest; 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.adapter.outbound.persistense.entity.task.constant.TaskStatus; -import clap.server.application.mapper.AttachmentMapper; -import clap.server.application.port.inbound.domain.CategoryService; -import clap.server.application.port.inbound.domain.LabelService; import clap.server.application.port.inbound.domain.MemberService; import clap.server.application.port.inbound.domain.TaskService; -import clap.server.application.port.inbound.task.UpdateTaskLabelUsecase; 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.LoadAttachmentPort; import clap.server.application.port.outbound.taskhistory.CommandTaskHistoryPort; 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.*; -import clap.server.common.constants.FilePathConstants; +import clap.server.domain.model.task.Task; +import clap.server.domain.model.task.TaskHistory; import clap.server.exception.ApplicationException; import clap.server.exception.code.TaskErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import static clap.server.domain.policy.task.TaskPolicyConstants.TASK_MAX_FILE_COUNT; +import static clap.server.domain.policy.task.TaskPolicyConstants.REMAINING_TASK_STATUS; import static clap.server.domain.policy.task.TaskPolicyConstants.TASK_UPDATABLE_STATUS; @ApplicationService @RequiredArgsConstructor @Slf4j -public class UpdateTaskService implements UpdateTaskUsecase, UpdateTaskStatusUsecase, UpdateTaskProcessorUsecase, UpdateTaskLabelUsecase { - +public class UpdateTaskService implements UpdateTaskStatusUsecase, UpdateTaskProcessorUsecase { private final MemberService memberService; - private final CategoryService categoryService; private final TaskService taskService; private final SendNotificationService sendNotificationService; - - private final LoadAttachmentPort loadAttachmentPort; - private final LabelService labelService; - private final CommandAttachmentPort commandAttachmentPort; + private final UpdateProcessorTaskCountService updateProcessorTaskCountService; private final CommandTaskHistoryPort commandTaskHistoryPort; - private final S3UploadPort s3UploadPort; - - @Override - @Transactional - public void updateTask(Long requesterId, Long taskId, UpdateTaskRequest request, List files) { - memberService.findActiveMember(requesterId); - Category category = categoryService.findById(request.categoryId()); - Task task = taskService.findById(taskId); - int attachmentCount = getAttachmentCount(request, files, task); - - if (!request.attachmentsToDelete().isEmpty()) { - List attachmentsToDelete = validateAndGetAttachments(request.attachmentsToDelete(), task); - attachmentsToDelete.stream() - .peek(Attachment::softDelete) - .forEach(commandAttachmentPort::save); - } - if (files != null) { - updateAttachments(files, task); - } - task.updateTask(requesterId, category, request.title(), request.description(), attachmentCount); - taskService.upsert(task); - } @Override @Transactional - public void updateTaskStatus(Long memberId, Long taskId, TaskStatus taskStatus) { + public void updateTaskStatus(Long memberId, Long taskId, TaskStatus targetTaskStatus) { memberService.findActiveMember(memberId); Task task = taskService.findById(taskId); - if (!TASK_UPDATABLE_STATUS.contains(taskStatus)) { + if (!TASK_UPDATABLE_STATUS.contains(targetTaskStatus)) { throw new ApplicationException(TaskErrorCode.TASK_STATUS_NOT_ALLOWED); } - if (!task.getTaskStatus().equals(taskStatus)) { - task.updateTaskStatus(taskStatus); - Task updateTask = taskService.upsert(task); - TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.STATUS_SWITCHED, task, taskStatus.getDescription(), null, null); - commandTaskHistoryPort.save(taskHistory); + if (!task.getTaskStatus().equals(targetTaskStatus)) { + updateProcessorTaskCountService.handleTaskStatusChange(task.getProcessor(), task.getTaskStatus(), targetTaskStatus); + task.updateTaskStatus(targetTaskStatus); + Task updatedTask = taskService.upsert(task); + + saveTaskHistory(TaskHistory.createTaskHistory(TaskHistoryType.STATUS_SWITCHED, task, targetTaskStatus.getDescription(), null, null)); List receivers = List.of(task.getRequester()); - publishNotification(receivers, updateTask, NotificationType.STATUS_SWITCHED, String.valueOf(updateTask.getTaskStatus())); + publishNotification(receivers, updatedTask, NotificationType.STATUS_SWITCHED, String.valueOf(updatedTask.getTaskStatus())); } } @@ -105,50 +65,23 @@ public void updateTaskProcessor(Long taskId, Long memberId, UpdateTaskProcessorR memberService.findActiveMember(memberId); Task task = taskService.findById(taskId); + Member processor = memberService.findActiveMember(request.processorId()); + if (REMAINING_TASK_STATUS.contains(task.getTaskStatus())) { + updateProcessorTaskCountService.handleProcessorChange(task.getProcessor(), processor, task.getTaskStatus()); + } task.updateProcessor(processor); - Task updateTask = taskService.upsert(task); - TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.PROCESSOR_CHANGED, task, null, processor, null); - commandTaskHistoryPort.save(taskHistory); + Task updatedTask = taskService.upsert(task); - List receivers = List.of(updateTask.getRequester()); - publishNotification(receivers, updateTask, NotificationType.PROCESSOR_CHANGED, processor.getNickname()); - } + saveTaskHistory(TaskHistory.createTaskHistory(TaskHistoryType.PROCESSOR_CHANGED, task, null, processor, null)); - @Transactional - @Override - public void updateTaskLabel(Long taskId, Long memberId, UpdateTaskLabelRequest request) { - memberService.findActiveMember(memberId); - memberService.findReviewer(memberId); - Task task = taskService.findById(taskId); - Label label = labelService.findById(request.labelId()); - - task.updateLabel(label); - taskService.upsert(task); + List receivers = List.of(updatedTask.getRequester()); + publishNotification(receivers, updatedTask, NotificationType.PROCESSOR_CHANGED, processor.getNickname()); } - private void updateAttachments(List files, Task task) { - List fileUrls = s3UploadPort.uploadFiles(FilePathConstants.TASK_FILE, files); - List attachments = AttachmentMapper.toTaskAttachments(task, files, fileUrls); - commandAttachmentPort.saveAll(attachments); - } - - private static int getAttachmentCount(UpdateTaskRequest request, List files, Task task) { - int attachmentToAdd = files == null ? 0 : files.size(); - int attachmentCount = task.getAttachmentCount() - request.attachmentsToDelete().size() + attachmentToAdd; - if (attachmentCount > TASK_MAX_FILE_COUNT) { - throw new ApplicationException(TaskErrorCode.FILE_COUNT_EXCEEDED); - } - return attachmentCount; - } - - private List validateAndGetAttachments(List attachmentIdsToDelete, Task task) { - List attachmentsOfTask = loadAttachmentPort.findAllByTaskIdAndAttachmentId(task.getTaskId(), attachmentIdsToDelete); - if (attachmentsOfTask.size() != attachmentIdsToDelete.size()) { - throw new ApplicationException(TaskErrorCode.TASK_ATTACHMENT_NOT_FOUND); - } - return attachmentsOfTask; + private void saveTaskHistory(TaskHistory taskHistory) { + commandTaskHistoryPort.save(taskHistory); } private void publishNotification(List receivers, Task task, NotificationType notificationType, String message) { diff --git a/src/main/java/clap/server/domain/model/member/Member.java b/src/main/java/clap/server/domain/model/member/Member.java index 62b57c47..237ba301 100644 --- a/src/main/java/clap/server/domain/model/member/Member.java +++ b/src/main/java/clap/server/domain/model/member/Member.java @@ -47,6 +47,8 @@ public static Member createMember(Member admin, MemberInfo memberInfo) { .imageUrl(null) .status(MemberStatus.PENDING) .password(null) + .inReviewingTaskCount(null) + .inProgressTaskCount(null) .build(); } @@ -59,6 +61,8 @@ public void resetPasswordAndActivateMember(String newEncodedPassword) { this.status = MemberStatus.ACTIVE; this.emailNotificationEnabled = true; this.kakaoworkNotificationEnabled = true; + this.inProgressTaskCount = 0; + this.inReviewingTaskCount = 0; } public String getNickname() { @@ -108,4 +112,18 @@ public void updateEmailEnabled() { public void register(Member admin) { this.admin = admin; } + + public void incrementInProgressTaskCount() { + this.inProgressTaskCount++; + } + public void incrementInReviewingTaskCount() { + this.inReviewingTaskCount++; + } + + public void decrementInProgressTaskCount() { + this.inProgressTaskCount--; + } + public void decrementInReviewingTaskCount() { + this.inReviewingTaskCount--; + } } diff --git a/src/main/java/clap/server/domain/policy/task/ProcessorTaskCountPolicy.java b/src/main/java/clap/server/domain/policy/task/ProcessorTaskCountPolicy.java new file mode 100644 index 00000000..e2056d08 --- /dev/null +++ b/src/main/java/clap/server/domain/policy/task/ProcessorTaskCountPolicy.java @@ -0,0 +1,26 @@ +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.member.Member; + +@Policy +public class ProcessorTaskCountPolicy { + + public void incrementTaskCount(Member processor, TaskStatus status) { + if (status == TaskStatus.IN_PROGRESS) { + processor.incrementInProgressTaskCount(); + } else if (status == TaskStatus.IN_REVIEWING) { + processor.incrementInReviewingTaskCount(); + } + } + + public void decrementTaskCount(Member processor, TaskStatus status) { + if (status == TaskStatus.IN_PROGRESS) { + processor.decrementInProgressTaskCount(); + } else if (status == TaskStatus.IN_REVIEWING) { + processor.decrementInReviewingTaskCount(); + } + } +} + diff --git a/src/main/java/clap/server/domain/policy/task/TaskPolicyConstants.java b/src/main/java/clap/server/domain/policy/task/TaskPolicyConstants.java index ff0e7028..d52e266d 100644 --- a/src/main/java/clap/server/domain/policy/task/TaskPolicyConstants.java +++ b/src/main/java/clap/server/domain/policy/task/TaskPolicyConstants.java @@ -21,4 +21,9 @@ public class TaskPolicyConstants { ); public static final int TASK_MAX_FILE_COUNT = 5; + + public static final List REMAINING_TASK_STATUS = List.of( + TaskStatus.IN_PROGRESS, + TaskStatus.IN_REVIEWING + ); } \ No newline at end of file diff --git a/src/main/resources/db/migration/dev/V20250214408__Modify_Task_Counts_From_Member.sql b/src/main/resources/db/migration/dev/V20250214408__Modify_Task_Counts_From_Member.sql new file mode 100644 index 00000000..a1e730aa --- /dev/null +++ b/src/main/resources/db/migration/dev/V20250214408__Modify_Task_Counts_From_Member.sql @@ -0,0 +1,17 @@ +START TRANSACTION; + +UPDATE member m + LEFT JOIN ( + SELECT processor_id, + SUM(CASE WHEN task_status = 'IN_PROGRESS' THEN 1 ELSE 0 END) AS in_progress_count, + SUM(CASE WHEN task_status = 'IN_REVIEWING' THEN 1 ELSE 0 END) AS in_reviewing_count + FROM task + GROUP BY processor_id + ) t ON m.member_id = t.processor_id +SET + m.in_progress_task_count = COALESCE(t.in_progress_count, 0), + m.in_reviewing_task_count = COALESCE(t.in_reviewing_count, 0) +WHERE m.status != 'DELETED'; + +COMMIT; + diff --git a/src/main/resources/db/migration/prod/V20250214408__Modify_Task_Counts_From_Member.sql b/src/main/resources/db/migration/prod/V20250214408__Modify_Task_Counts_From_Member.sql new file mode 100644 index 00000000..ad6ec657 --- /dev/null +++ b/src/main/resources/db/migration/prod/V20250214408__Modify_Task_Counts_From_Member.sql @@ -0,0 +1,16 @@ +START TRANSACTION; + +UPDATE member m + LEFT JOIN ( + SELECT processor_id, + SUM(CASE WHEN task_status = 'IN_PROGRESS' THEN 1 ELSE 0 END) AS in_progress_count, + SUM(CASE WHEN task_status = 'IN_REVIEWING' THEN 1 ELSE 0 END) AS in_reviewing_count + FROM task + GROUP BY processor_id + ) t ON m.member_id = t.processor_id +SET + m.in_progress_task_count = COALESCE(t.in_progress_count, 0), + m.in_reviewing_task_count = COALESCE(t.in_reviewing_count, 0) +WHERE m.status != 'DELETED'; + +COMMIT; diff --git a/src/test/java/clap/server/TestDataFactory.java b/src/test/java/clap/server/TestDataFactory.java index e485bb7b..8c547a13 100644 --- a/src/test/java/clap/server/TestDataFactory.java +++ b/src/test/java/clap/server/TestDataFactory.java @@ -29,6 +29,8 @@ public static Member createAdmin() { .status(MemberStatus.ACTIVE) .password("Password123!") .department(createDepartment()) + .inProgressTaskCount(0) + .inReviewingTaskCount(0) .build(); } @@ -43,6 +45,8 @@ public static Member createManagerWithReviewer() { .status(MemberStatus.ACTIVE) .password("Password456!") .department(createDepartment()) + .inProgressTaskCount(0) + .inReviewingTaskCount(0) .build(); } @@ -57,6 +61,8 @@ public static Member createManager() { .status(MemberStatus.ACTIVE) .password("Password789!") .department(createDepartment()) + .inProgressTaskCount(0) + .inReviewingTaskCount(0) .build(); } @@ -71,6 +77,8 @@ public static Member createUser() { .status(MemberStatus.ACTIVE) .password("Password000!") .department(createDepartment()) + .inProgressTaskCount(0) + .inReviewingTaskCount(0) .build(); } diff --git a/src/test/java/clap/server/application/service/task/ApprovalTaskServiceTest.java b/src/test/java/clap/server/application/service/task/ApprovalTaskServiceTest.java index e42af787..12facf2f 100644 --- a/src/test/java/clap/server/application/service/task/ApprovalTaskServiceTest.java +++ b/src/test/java/clap/server/application/service/task/ApprovalTaskServiceTest.java @@ -12,9 +12,7 @@ import clap.server.application.service.webhook.SendNotificationService; 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 clap.server.exception.DomainException; import clap.server.exception.code.TaskErrorCode; @@ -57,6 +55,9 @@ class ApprovalTaskServiceTest { @Mock private SendNotificationService sendNotificationService; + @Mock + private UpdateProcessorTaskCountService updateProcessorTaskCountService; + private Member reviewer, processor; private Task task; @@ -77,7 +78,7 @@ void approvalTask() { //given Long reviewerId = 2L; Long taskId = 1L; - ApprovalTaskRequest approvalTaskRequest = new ApprovalTaskRequest(2L, 2L, null, null); + ApprovalTaskRequest approvalTaskRequest = new ApprovalTaskRequest(2L, 3L, null, null); when(memberService.findReviewer(reviewerId)).thenReturn(reviewer); when(taskService.findById(taskId)).thenReturn(task); @@ -95,6 +96,31 @@ void approvalTask() { verify(requestedTaskUpdatePolicy).validateTaskRequested(task); } + @Test + @DisplayName("작업 승인 처리 - 담당자의 Task Count 증가") + void approvalTaskWithIncrementTaskCount() { + // given + Long reviewerId = 2L; + Long taskId = 1L; + ApprovalTaskRequest approvalTaskRequest = new ApprovalTaskRequest(2L, 3L, null, null); + + when(memberService.findReviewer(reviewerId)).thenReturn(reviewer); + when(taskService.findById(taskId)).thenReturn(task); + when(memberService.findActiveMember(approvalTaskRequest.processorId())).thenReturn(processor); + when(categoryService.findById(approvalTaskRequest.categoryId())).thenReturn(category); + when(taskService.upsert(task)).thenReturn(task); + + // when + ApprovalTaskResponse response = approvalTaskService.approvalTaskByReviewer(reviewerId, taskId, approvalTaskRequest); + + // then + assertThat(response).isNotNull(); + assertThat(response.taskId()).isEqualTo(task.getTaskId()); + assertThat(response.taskStatus()).isEqualTo(TaskStatus.IN_PROGRESS); + + verify(updateProcessorTaskCountService).handleTaskStatusChange(processor, TaskStatus.REQUESTED, TaskStatus.IN_PROGRESS); + } + @Test @DisplayName("작업 승인 처리 중 예외 - 상태 불일치") void approvalTask_throwsDomainException_whenTaskStatusIsNotRequested() {