Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
81ec436
CLAP-408 Feat: 진행중, 검토중 task에 대해 update 하는 member 도메인 정책 구현
joowojr Feb 13, 2025
3b512d4
CLAP-408 Feat: 회원 생성 및 활성화 메서드에 추가된 컬럼 세팅
joowojr Feb 13, 2025
13e1e00
CLAP-408 Feat: 상태 전환 및 담당자 변경될 시에 해당 담당자의 작업 수를 업데이트하는 서비스 클래스 추가
joowojr Feb 13, 2025
6008884
CLAP-408 Feat: 작업 승인, 종료, 상태 전환 시에 해당 담당자의 작업 수을 업데이트하는 로직 추가
joowojr Feb 13, 2025
7edc3b2
CLAP-408 Feat: 요청 취소 권한 요청자에게만 있도록 수정
joowojr Feb 13, 2025
05103e7
CLAP-408 Test: 변경 사항 test code에 반
joowojr Feb 13, 2025
9306b03
CLAP-408 Feat: 작업 수 업데이트 마이그레이션 파일 수정
joowojr Feb 13, 2025
27767da
CLAP-408 Refactor: 담당자 조회 시에 member의 정보에서 가져오도록 수정
joowojr Feb 13, 2025
a034916
Merge remote-tracking branch 'origin/develop' into CLAP-408
joowojr Feb 14, 2025
1a7f493
CLAP-408 Docs: 스웨거 명세 수정
joowojr Feb 14, 2025
24bdc69
Merge remote-tracking branch 'origin/develop' into CLAP-408
joowojr Feb 14, 2025
da08ed1
CLAP-408 Refactor: 팀 현황 조회 매핑 로직 매퍼 메서드로 이동
joowojr Feb 14, 2025
8dce9bf
CLAP-408 Refactor: member 엔티티의 컬럼을 통해 현재 진행중인 작업 수를 출력하도록 수정
joowojr Feb 14, 2025
0f02484
Merge branch 'develop' into CLAP-408
joowojr Feb 14, 2025
0b342a3
CLAP-408 Test: testcode 반영
joowojr Feb 14, 2025
c8bc8d2
CLAP-425 Refactor: task 테이블을 한 번만 스캔하여 조회하도록 수정
joowojr Feb 14, 2025
dfa2d4a
CLAP-408 Refactor: 알림 전송 및 history 저장의 기준으로 작업 업데이트 usecase 분리
joowojr Feb 14, 2025
a4db273
CLAP-408 Style: 코드 포맷팅 정리
joowojr Feb 14, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,4 @@ public record TeamStatusResponse(
int totalInReviewingTaskCount,
int totalTaskCount
) {
// 기존 생성자 (3개 파라미터)
public TeamStatusResponse(List<TeamTaskResponse> members, int totalInProgressTaskCount, int totalInReviewingTaskCount) {
this(
(members == null) ? List.of() : members,
totalInProgressTaskCount,
totalInReviewingTaskCount,
totalInProgressTaskCount + totalInReviewingTaskCount
);
}

// 추가된 생성자 (List만 받음)
public TeamStatusResponse(List<TeamTaskResponse> members) {
this(members, 0, 0); // 기본값을 사용하여 생성
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
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
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -25,10 +25,10 @@ private static TeamTaskResponse toTeamTaskResponse(Map.Entry<Long, List<Task>> 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(),
Expand Down Expand Up @@ -65,4 +65,13 @@ private static TeamTaskItemResponse.LabelInfo toLabelInfo(Task task) {
task.getLabel().getLabelColor()
) : null;
}

public static TeamStatusResponse toTeamStatusResponse(List<TeamTaskResponse> members, int totalInProgressTaskCount, int totalInReviewingTaskCount) {
return new TeamStatusResponse(
(members == null) ? List.of() : members,
totalInProgressTaskCount,
totalInReviewingTaskCount,
totalInProgressTaskCount + totalInReviewingTaskCount
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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<Member> receivers = List.of(task.getRequester(), processor);
Expand All @@ -71,7 +74,7 @@ public FindApprovalFormResponse findApprovalForm(Long managerId, Long taskId) {
return TaskResponseMapper.toFindApprovalFormResponse(task);
}

private void publishNotification(List<Member> receivers, Task task, String processorName){
private void publishNotification(List<Member> receivers, Task task, String processorName) {
receivers.forEach(receiver -> {
boolean isManager = receiver.getMemberInfo().getRole() == MemberRole.ROLE_MANAGER;
sendNotificationService.sendPushNotification(receiver, NotificationType.PROCESSOR_ASSIGNED,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -19,16 +17,14 @@
public class FindManagersService implements FindManagersUsecase {

private final MemberService memberService;
private final LoadTaskPort loadTaskPort;

@Transactional
@Override
public List<FindManagersResponse> findManagers() {
List<TaskStatus> targetStatuses = List.of(TaskStatus.IN_PROGRESS, TaskStatus.IN_REVIEWING);
List<Member> 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}


Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<MultipartFile> 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<Attachment> attachmentsToDelete = validateAndGetAttachments(request.attachmentsToDelete(), task);
attachmentsToDelete.stream()
.peek(Attachment::softDelete)
.forEach(commandAttachmentPort::save);
}

private void updateAttachments(List<MultipartFile> files, Task task) {
List<String> fileUrls = s3UploadPort.uploadFiles(FilePathConstants.TASK_FILE, files);
List<Attachment> attachments = AttachmentMapper.toTaskAttachments(task, files, fileUrls);
commandAttachmentPort.saveAll(attachments);
}

private static int getAttachmentCount(UpdateTaskRequest request, List<MultipartFile> 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<Attachment> validateAndGetAttachments(List<Long> attachmentIdsToDelete, Task task) {
List<Attachment> 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);
}

}
Loading