diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/task/request/FilterTaskBoardRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/task/request/FilterTaskBoardRequest.java new file mode 100644 index 00000000..bde8cfe4 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/task/request/FilterTaskBoardRequest.java @@ -0,0 +1,21 @@ +package clap.server.adapter.inbound.web.dto.task.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record FilterTaskBoardRequest( + @Schema(description = "라벨 ID") + Long labelId, + + @Schema(description = "1차 카테고리 ID, ** 2차 카테고리로 검색할 시에는 1차 카테고리 값은 넣지 않습니다.") + Long mainCategoryId, + + @Schema(description = "2차 카테고리 ID") + Long subCategoryId, + + @Schema(description = "작업 제목", example = "작업 제목") + String title, + + @Schema(description = "요청자 닉네임", example = "atom.park") + String requesterNickname +) { +} diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/task/response/TaskItemResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/task/response/TaskItemResponse.java index 8fa05701..a67406d6 100644 --- a/src/main/java/clap/server/adapter/inbound/web/dto/task/response/TaskItemResponse.java +++ b/src/main/java/clap/server/adapter/inbound/web/dto/task/response/TaskItemResponse.java @@ -7,6 +7,7 @@ public record TaskItemResponse( Long taskId, String taskCode, + String title, String mainCategoryName, String categoryName, String requesterNickname, 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 00e7b505..748f8191 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 @@ -1,17 +1,20 @@ package clap.server.adapter.inbound.web.task; import clap.server.adapter.inbound.security.SecurityUserDetails; +import clap.server.adapter.inbound.web.dto.task.request.FilterTaskBoardRequest; import clap.server.adapter.inbound.web.dto.task.request.UpdateTaskOrderRequest; import clap.server.adapter.inbound.web.dto.task.response.TaskBoardResponse; import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import clap.server.application.port.inbound.task.FilterTaskBoardUsecase; import clap.server.application.port.inbound.task.UpdateTaskBoardUsecase; -import clap.server.application.port.inbound.task.TaskBoardUsecase; +import clap.server.application.port.inbound.task.GetTaskBoardUsecase; import clap.server.common.annotation.architecture.WebAdapter; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.format.annotation.DateTimeFormat; @@ -21,24 +24,30 @@ import java.time.LocalDate; -@Tag(name = "02. Task [담당자]", description = " 작업 보드 API") +@Slf4j +@Tag(name = "02. Task [담당자]") @WebAdapter @RestController @RequiredArgsConstructor @RequestMapping("/api/task-board") public class TaskBoardController { - private final TaskBoardUsecase taskBoardUsecase; + private final GetTaskBoardUsecase getTaskBoardUsecase; + private final FilterTaskBoardUsecase filterTaskBoardUsecase; private final UpdateTaskBoardUsecase updateTaskBoardUsecase; @Operation(summary = "작업 보드 조회 API") - @GetMapping + @PostMapping 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, @AuthenticationPrincipal SecurityUserDetails userInfo) { Pageable pageable = PageRequest.of(page, pageSize); - return ResponseEntity.ok(taskBoardUsecase.getTaskBoards(userInfo.getUserId(), untilDate, pageable)); + if (request != null) { + return ResponseEntity.ok(filterTaskBoardUsecase.getTaskBoardByFilter(userInfo.getUserId(), untilDate, request, pageable)); + } else + return ResponseEntity.ok(getTaskBoardUsecase.getTaskBoards(userInfo.getUserId(), untilDate, pageable)); } @Operation(summary = "작업 보드 순서 및 상태 변경 API") diff --git a/src/main/java/clap/server/adapter/outbound/persistense/TaskPersistenceAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/TaskPersistenceAdapter.java index 745049d3..91149156 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/TaskPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/TaskPersistenceAdapter.java @@ -4,6 +4,7 @@ import clap.server.adapter.inbound.web.dto.task.FilterPendingApprovalResponse; import clap.server.adapter.inbound.web.dto.task.FilterRequestedTasksResponse; import clap.server.adapter.inbound.web.dto.task.FilterTaskListRequest; +import clap.server.adapter.inbound.web.dto.task.request.FilterTaskBoardRequest; import clap.server.adapter.inbound.web.dto.task.*; import clap.server.adapter.outbound.persistense.entity.task.TaskEntity; import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; @@ -19,6 +20,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; import java.time.LocalDateTime; import java.util.List; @@ -27,7 +29,7 @@ @Slf4j @PersistenceAdapter @RequiredArgsConstructor -public class TaskPersistenceAdapter implements CommandTaskPort , LoadTaskPort { +public class TaskPersistenceAdapter implements CommandTaskPort, LoadTaskPort { private final TaskRepository taskRepository; private final TaskPersistenceMapper taskPersistenceMapper; @@ -84,21 +86,37 @@ public List findYesterdayTaskByDate(LocalDateTime now) { } @Override - public Page findAllTasks (Pageable pageable, FilterTaskListRequest filterTaskListRequest) { + public Page findAllTasks(Pageable pageable, FilterTaskListRequest filterTaskListRequest) { Page taskList = taskRepository.findAllTasks(pageable, filterTaskListRequest) .map(taskPersistenceMapper::toDomain); return taskList.map(TaskMapper::toFilterAllTasksResponse); } + @Override - public Optional findPrevOrderTaskByProcessorIdAndStatus(Long processorId, TaskStatus taskStatus, Long processorOrder){ + public Optional findPrevOrderTaskByProcessorIdAndStatus(Long processorId, TaskStatus taskStatus, Long processorOrder) { Optional taskEntity = taskRepository.findTopByProcessor_MemberIdAndTaskStatusAndProcessorOrderLessThanOrderByProcessorOrderDesc(processorId, taskStatus, processorOrder); return taskEntity.map(taskPersistenceMapper::toDomain); } @Override - public Optional findNextOrderTaskByProcessorIdAndStatus(Long processorId, TaskStatus taskStatus, Long processorOrder){ - Optional taskEntity = taskRepository.findTopByProcessor_MemberIdAndTaskStatusAndProcessorOrderAfterOrderByProcessorOrderDesc(processorId, taskStatus, processorOrder); + public Optional findNextOrderTaskByProcessorIdAndStatus(Long processorId, TaskStatus taskStatus, Long processorOrder) { + Optional taskEntity = taskRepository.findTopByProcessor_MemberIdAndTaskStatusAndProcessorOrderAfterOrderByProcessorOrderDesc(processorId, taskStatus, processorOrder); return taskEntity.map(taskPersistenceMapper::toDomain); } + @Override + public Slice findTaskBoardByFilter(Long processorId, List statuses, LocalDateTime untilDate, FilterTaskBoardRequest request, Pageable pageable) { + List taskList = new java.util.ArrayList<>(taskRepository.findTasksByFilter(processorId, statuses, untilDate, request, pageable) + .stream() + .map(taskPersistenceMapper::toDomain) + .toList()); + + boolean hasNext = taskList.size() > pageable.getPageSize(); + if (hasNext) { + taskList.remove(taskList.size() - 1); + } + return new SliceImpl<>(taskList, pageable, hasNext); + } + + } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepository.java index fc974623..9b7607d5 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepository.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepository.java @@ -1,14 +1,21 @@ package clap.server.adapter.outbound.persistense.repository.task; import clap.server.adapter.inbound.web.dto.task.FilterTaskListRequest; +import clap.server.adapter.inbound.web.dto.task.request.FilterTaskBoardRequest; import clap.server.adapter.outbound.persistense.entity.task.TaskEntity; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import java.time.LocalDateTime; +import java.util.List; public interface TaskCustomRepository { Page findTasksRequestedByUser(Long requesterId, Pageable pageable, FilterTaskListRequest findTaskListRequest); Page findPendingApprovalTasks(Pageable pageable, FilterTaskListRequest findTaskListRequest); Page findAllTasks(Pageable pageable, FilterTaskListRequest findTaskListRequest); + List findTasksByFilter(Long processorId, List statuses, LocalDateTime localDateTime, FilterTaskBoardRequest request, Pageable pageable); Page findTasksAssignedByManager(Long processorId, Pageable pageable, FilterTaskListRequest findTaskListRequest); } 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 a95c2f0a..991efd1d 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 @@ -1,6 +1,7 @@ package clap.server.adapter.outbound.persistense.repository.task; import clap.server.adapter.inbound.web.dto.task.FilterTaskListRequest; +import clap.server.adapter.inbound.web.dto.task.request.FilterTaskBoardRequest; import clap.server.adapter.outbound.persistense.entity.task.TaskEntity; import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; import com.querydsl.core.BooleanBuilder; @@ -8,9 +9,8 @@ import com.querydsl.core.types.dsl.DateTimePath; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; -import org.springframework.data.domain.Pageable; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.*; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; @@ -19,6 +19,7 @@ import static clap.server.adapter.outbound.persistense.entity.task.QTaskEntity.taskEntity; import static com.querydsl.core.types.Order.*; +@Slf4j @Repository @RequiredArgsConstructor public class TaskCustomRepositoryImpl implements TaskCustomRepository { @@ -69,6 +70,47 @@ public Page findAllTasks(Pageable pageable, FilterTaskListRequest fi return getTasksPage(pageable, whereClause, filterTaskListRequest.orderRequest().sortBy(), filterTaskListRequest.orderRequest().sortDirection()); } + @Override + public List findTasksByFilter(Long processorId, List statuses, LocalDateTime untilDateTime, FilterTaskBoardRequest request, Pageable pageable) { + BooleanBuilder whereClause = createTaskBoardFilter(processorId, statuses, untilDateTime, request); + return queryFactory + .selectFrom(taskEntity) + .where(whereClause) + .orderBy(taskEntity.processorOrder.asc()) + .limit(pageable.getPageSize() + 1) + .offset(pageable.getOffset()) + .fetch(); + } + + private BooleanBuilder createTaskBoardFilter(Long processorId, List statuses, LocalDateTime untilDateTime, FilterTaskBoardRequest request) { + BooleanBuilder whereClause = new BooleanBuilder(); + + whereClause.and(taskEntity.processor.memberId.eq(processorId)); + whereClause.and(taskEntity.taskStatus.in(statuses)); + whereClause.and(taskEntity.finishedAt.isNull().or(taskEntity.finishedAt.loe(untilDateTime))); + + if (request.labelId() != null) { + whereClause.and(taskEntity.label.labelId.eq(request.labelId())); + } + if (request.mainCategoryId() != null) { + whereClause.and(taskEntity.category.mainCategory.categoryId.eq(request.mainCategoryId())); + } + if (request.subCategoryId() != null) { + whereClause.and(taskEntity.category.categoryId.eq(request.subCategoryId())); + } + if (request.title() != null && !request.title().isEmpty()) { + String titleFilter = "%" + request.title() + "%"; + whereClause.and(taskEntity.title.like(titleFilter)); + } + if (request.requesterNickname() != null && !request.requesterNickname().isEmpty()) { + String nicknameFilter = "%" + request.requesterNickname().toLowerCase() + "%"; + whereClause.and(taskEntity.requester.nickname.lower().like(nicknameFilter)); + + } + + return whereClause; + } + private BooleanBuilder createFilter(FilterTaskListRequest request) { BooleanBuilder whereClause = new BooleanBuilder(); if (request.term() != null) { @@ -90,6 +132,7 @@ private BooleanBuilder createFilter(FilterTaskListRequest request) { return whereClause; } + private Page getTasksPage(Pageable pageable, BooleanBuilder whereClause, String sortBy, String sortDirection) { OrderSpecifier orderSpecifier = getOrderSpecifier(sortBy, sortDirection); diff --git a/src/main/java/clap/server/application/mapper/TaskMapper.java b/src/main/java/clap/server/application/mapper/TaskMapper.java index 3cfce29a..2a88dab0 100644 --- a/src/main/java/clap/server/application/mapper/TaskMapper.java +++ b/src/main/java/clap/server/application/mapper/TaskMapper.java @@ -132,6 +132,7 @@ private static TaskItemResponse toTaskItemResponse(Task task) { return new TaskItemResponse( task.getTaskId(), task.getTaskCode(), + task.getTitle(), task.getCategory().getMainCategory().getName(), task.getCategory().getName(), task.getRequester().getNickname(), diff --git a/src/main/java/clap/server/application/port/inbound/task/FilterTaskBoardUsecase.java b/src/main/java/clap/server/application/port/inbound/task/FilterTaskBoardUsecase.java new file mode 100644 index 00000000..a245ca28 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/task/FilterTaskBoardUsecase.java @@ -0,0 +1,11 @@ +package clap.server.application.port.inbound.task; + +import clap.server.adapter.inbound.web.dto.task.request.FilterTaskBoardRequest; +import clap.server.adapter.inbound.web.dto.task.response.TaskBoardResponse; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDate; + +public interface FilterTaskBoardUsecase { + TaskBoardResponse getTaskBoardByFilter(Long processorId, LocalDate untilDate, FilterTaskBoardRequest request, Pageable pageable); +} diff --git a/src/main/java/clap/server/application/port/inbound/task/TaskBoardUsecase.java b/src/main/java/clap/server/application/port/inbound/task/GetTaskBoardUsecase.java similarity index 79% rename from src/main/java/clap/server/application/port/inbound/task/TaskBoardUsecase.java rename to src/main/java/clap/server/application/port/inbound/task/GetTaskBoardUsecase.java index 073f94b9..ccf7dce9 100644 --- a/src/main/java/clap/server/application/port/inbound/task/TaskBoardUsecase.java +++ b/src/main/java/clap/server/application/port/inbound/task/GetTaskBoardUsecase.java @@ -5,6 +5,6 @@ import java.time.LocalDate; -public interface TaskBoardUsecase { - TaskBoardResponse getTaskBoards(Long processorId, LocalDate untilDate, Pageable pageable); +public interface GetTaskBoardUsecase { + TaskBoardResponse getTaskBoards(Long processorId, LocalDate untilDate,Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/clap/server/application/port/outbound/task/LoadTaskPort.java b/src/main/java/clap/server/application/port/outbound/task/LoadTaskPort.java index c6628d79..f400e726 100644 --- a/src/main/java/clap/server/application/port/outbound/task/LoadTaskPort.java +++ b/src/main/java/clap/server/application/port/outbound/task/LoadTaskPort.java @@ -5,6 +5,7 @@ import clap.server.adapter.inbound.web.dto.task.FilterPendingApprovalResponse; import clap.server.adapter.inbound.web.dto.task.FilterRequestedTasksResponse; import clap.server.adapter.inbound.web.dto.task.FilterTaskListRequest; +import clap.server.adapter.inbound.web.dto.task.request.FilterTaskBoardRequest; import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; import clap.server.domain.model.task.Task; import org.springframework.data.domain.Page; @@ -35,4 +36,6 @@ public interface LoadTaskPort { Optional findPrevOrderTaskByProcessorIdAndStatus(Long processorId, TaskStatus taskStatus, Long processorOrder); Optional findNextOrderTaskByProcessorIdAndStatus(Long processorId, TaskStatus taskStatus, Long processorOrder); + + Slice findTaskBoardByFilter(Long processorId, List statuses, LocalDateTime untilDateTime, FilterTaskBoardRequest request, Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/clap/server/application/service/task/TaskBoardService.java b/src/main/java/clap/server/application/service/task/GetTaskBoardService.java similarity index 60% rename from src/main/java/clap/server/application/service/task/TaskBoardService.java rename to src/main/java/clap/server/application/service/task/GetTaskBoardService.java index e2332833..16a3569f 100644 --- a/src/main/java/clap/server/application/service/task/TaskBoardService.java +++ b/src/main/java/clap/server/application/service/task/GetTaskBoardService.java @@ -1,14 +1,17 @@ package clap.server.application.service.task; +import clap.server.adapter.inbound.web.dto.task.request.FilterTaskBoardRequest; import clap.server.adapter.inbound.web.dto.task.response.TaskBoardResponse; import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; import clap.server.application.mapper.TaskMapper; import clap.server.application.port.inbound.domain.MemberService; -import clap.server.application.port.inbound.task.TaskBoardUsecase; +import clap.server.application.port.inbound.task.FilterTaskBoardUsecase; +import clap.server.application.port.inbound.task.GetTaskBoardUsecase; import clap.server.application.port.outbound.task.LoadTaskPort; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.task.Task; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.transaction.annotation.Transactional; @@ -17,20 +20,29 @@ import java.time.LocalDateTime; import java.util.List; +@Slf4j @ApplicationService @RequiredArgsConstructor -class TaskBoardService implements TaskBoardUsecase { +@Transactional(readOnly = true) +class GetTaskBoardService implements GetTaskBoardUsecase, FilterTaskBoardUsecase { private final static List VIEWABLE_STATUSES = TaskStatus.getTaskBoardStatusList(); private final MemberService memberService; private final LoadTaskPort loadTaskPort; @Override - @Transactional(readOnly = true) public TaskBoardResponse getTaskBoards(Long processorId, LocalDate untilDate, Pageable pageable) { - memberService.findById(processorId); + memberService.findActiveMember(processorId); LocalDateTime untilDateTime = untilDate == null ? LocalDate.now().plusDays(1).atStartOfDay() : untilDate.plusDays(1).atStartOfDay(); Slice tasks = loadTaskPort.findByProcessorAndStatus(processorId, VIEWABLE_STATUSES, untilDateTime, pageable); return TaskMapper.toSliceTaskItemResponse(tasks); } + + @Override + public TaskBoardResponse getTaskBoardByFilter(Long processorId, LocalDate untilDate, FilterTaskBoardRequest request, Pageable pageable) { + memberService.findActiveMember(processorId); + LocalDateTime untilDateTime = untilDate == null ? LocalDate.now().plusDays(1).atStartOfDay() : untilDate.plusDays(1).atStartOfDay(); + Slice tasks = loadTaskPort.findTaskBoardByFilter(processorId, VIEWABLE_STATUSES, untilDateTime, request, pageable); + return TaskMapper.toSliceTaskItemResponse(tasks); + } }