Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package clap.server.adapter.inbound.web.dto.task.request;

import io.swagger.v3.oas.annotations.media.Schema;

import java.util.List;

public record FilterTeamStatusRequest(
@Schema(description = "정렬 기준 (기여도순, 기본)", example = "기여도순")
String sortBy,

@Schema(description = "1차 카테고리 ID 목록", example = "[10, 20, 30]")
List<Long> mainCategoryIds,

@Schema(description = "2차 카테고리 ID 목록", example = "[1, 2, 3]")
List<Long> categoryIds,

@Schema(description = "작업 타이틀 검색", example = "타이틀1")
String taskTitle
) {
public FilterTeamStatusRequest {
sortBy = (sortBy == null || sortBy.isEmpty()) ? "기본" : sortBy;
mainCategoryIds = mainCategoryIds == null ? List.of() : mainCategoryIds;
categoryIds = categoryIds == null ? List.of() : categoryIds;
taskTitle = taskTitle == null ? "" : taskTitle;
}
Comment on lines +19 to +25
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

객체의 형태로 직접 파싱하실 경우 java validation을 사용하시면 좋을 것 같습니다.


// 카테고리 유효성 검사
public boolean isValid() {
// 1차 카테고리가 없으면 2차 카테고리는 선택할 수 없으므로
return mainCategoryIds.isEmpty() || !categoryIds.isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package clap.server.adapter.inbound.web.dto.task.response;

import com.querydsl.core.annotations.QueryProjection;

import java.util.List;

public record TeamMemberTaskResponse(
Long processorId,
String nickname,
String imageUrl,
String department,
int inProgressTaskCount,
int pendingTaskCount,
int totalTaskCount,
List<TaskItemResponse> tasks
) {
@QueryProjection
public TeamMemberTaskResponse {
tasks = (tasks == null) ? List.of() : tasks;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package clap.server.adapter.inbound.web.dto.task.response;

import java.util.List;

public record TeamStatusResponse(
List<TeamMemberTaskResponse> members
) {
public TeamStatusResponse(List<TeamMemberTaskResponse> members) {
this.members = (members == null) ? List.of() : members;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package clap.server.adapter.inbound.web.task;

import clap.server.adapter.inbound.web.dto.task.request.FilterTeamStatusRequest;

import clap.server.adapter.inbound.web.dto.task.response.TeamStatusResponse;
import clap.server.application.service.task.TeamStatusService;
import clap.server.common.annotation.architecture.WebAdapter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/team-status")
@RequiredArgsConstructor
@Tag(name = "팀 현황 조회 API")
@WebAdapter
public class TeamStatusController {

private final TeamStatusService teamStatusService;

@GetMapping("/filter")
public ResponseEntity<TeamStatusResponse> filterTeamStatus(@Valid@ModelAttribute FilterTeamStatusRequest filter) {
TeamStatusResponse response = teamStatusService.filterTeamStatus(filter);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import clap.server.adapter.inbound.web.dto.task.request.FilterTaskListRequest;
import clap.server.adapter.inbound.web.dto.task.request.FilterTaskBoardRequest;
import clap.server.adapter.inbound.web.dto.task.request.FilterTeamStatusRequest;
import clap.server.adapter.inbound.web.dto.task.response.TeamMemberTaskResponse;
import clap.server.adapter.outbound.persistense.entity.task.TaskEntity;
import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus;
import clap.server.adapter.outbound.persistense.mapper.TaskPersistenceMapper;
Expand Down Expand Up @@ -109,5 +111,10 @@ public Slice<Task> findTaskBoardByFilter(Long processorId, List<TaskStatus> stat
return new SliceImpl<>(taskList, pageable, hasNext);
}

@Override
public List<TeamMemberTaskResponse> findTeamStatus(Long memberId, FilterTeamStatusRequest filter) {
return taskRepository.findTeamStatus(memberId, filter);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import clap.server.adapter.outbound.persistense.mapper.common.PersistenceMapper;
import clap.server.domain.model.task.Task;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper(componentModel = "spring", uses = {MemberPersistenceMapper.class, LabelPersistenceMapper.class, CategoryPersistenceMapper.class})
public interface TaskPersistenceMapper extends PersistenceMapper<TaskEntity, Task> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import clap.server.adapter.inbound.web.dto.task.request.FilterTaskListRequest;
import clap.server.adapter.inbound.web.dto.task.request.FilterTaskBoardRequest;
import clap.server.adapter.inbound.web.dto.task.request.FilterTeamStatusRequest;
import clap.server.adapter.inbound.web.dto.task.response.TeamMemberTaskResponse;
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;
Expand All @@ -13,6 +15,7 @@
public interface TaskCustomRepository {

Page<TaskEntity> findTasksRequestedByUser(Long requesterId, Pageable pageable, FilterTaskListRequest findTaskListRequest);
List<TeamMemberTaskResponse> findTeamStatus(Long memberId, FilterTeamStatusRequest filter);
Page<TaskEntity> findPendingApprovalTasks(Pageable pageable, FilterTaskListRequest findTaskListRequest);
Page<TaskEntity> findAllTasks(Pageable pageable, FilterTaskListRequest findTaskListRequest);
List<TaskEntity> findTasksByFilter(Long processorId, List<TaskStatus> statuses, LocalDateTime localDateTime, FilterTaskBoardRequest request, Pageable pageable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

import clap.server.adapter.inbound.web.dto.task.request.FilterTaskListRequest;
import clap.server.adapter.inbound.web.dto.task.request.FilterTaskBoardRequest;
import clap.server.adapter.inbound.web.dto.task.request.FilterTeamStatusRequest;
import clap.server.adapter.inbound.web.dto.task.response.TaskItemResponse;
import clap.server.adapter.inbound.web.dto.task.response.TeamMemberTaskResponse;
import clap.server.adapter.outbound.persistense.entity.task.TaskEntity;
import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.DateTimePath;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
Expand All @@ -16,6 +21,7 @@

import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

import static clap.server.adapter.outbound.persistense.entity.task.QTaskEntity.taskEntity;
import static com.querydsl.core.types.Order.ASC;
Expand All @@ -26,7 +32,7 @@
public class TaskCustomRepositoryImpl implements TaskCustomRepository {

private final JPAQueryFactory queryFactory;

private final EntityManager entityManager;

@Override
public Page<TaskEntity> findTasksRequestedByUser(Long requesterId, Pageable pageable, FilterTaskListRequest filterTaskListRequest) {
Expand All @@ -50,6 +56,103 @@ public Page<TaskEntity> findTasksAssignedByManager(Long processorId, Pageable pa
return getTasksPage(pageable, builder, filterTaskListRequest.sortBy(), filterTaskListRequest.sortDirection());
}

@Override
public List<TeamMemberTaskResponse> findTeamStatus(Long memberId, FilterTeamStatusRequest filter) {
// 1. 담당자 목록을 가져옴 (페이징 제거)
List<Long> processorIds = queryFactory
.select(taskEntity.processor.memberId)
.from(taskEntity)
.groupBy(taskEntity.processor.memberId)
.orderBy("기여도순".equals(filter.sortBy()) ?
taskEntity.taskId.count().desc() :
taskEntity.processor.nickname.asc())
.fetch();

if (processorIds.isEmpty()) {
return List.of(); // 결과가 없으면 빈 리스트 반환
}

// 2. 담당자별 작업 조회 (페이징 제거)
List<TaskEntity> taskEntities = queryFactory
.selectFrom(taskEntity)
.where(taskEntity.processor.memberId.in(processorIds))
.fetch();

// 3. 담당자별 그룹핑
return taskEntities.stream()
.collect(Collectors.groupingBy(t -> t.getProcessor().getMemberId()))
.entrySet().stream()
.map(entry -> {
List<TaskItemResponse> taskResponses = entry.getValue().stream()
.map(taskEntity -> new TaskItemResponse(
taskEntity.getTaskId(),
taskEntity.getTaskCode(),
taskEntity.getTitle(),
taskEntity.getCategory().getMainCategory().getName(),
taskEntity.getCategory().getName(),
taskEntity.getRequester().getNickname(),
taskEntity.getRequester().getImageUrl(),
taskEntity.getRequester().getDepartment().getName(),
taskEntity.getProcessorOrder(),
taskEntity.getTaskStatus(),
taskEntity.getCreatedAt()
)).collect(Collectors.toList());

return new TeamMemberTaskResponse(
entry.getKey(),
entry.getValue().get(0).getProcessor().getNickname(),
entry.getValue().get(0).getProcessor().getImageUrl(),
entry.getValue().get(0).getProcessor().getDepartment().getName(),
(int) entry.getValue().stream().filter(t -> t.getTaskStatus() == TaskStatus.IN_PROGRESS).count(),
(int) entry.getValue().stream().filter(t -> t.getTaskStatus() == TaskStatus.PENDING_COMPLETED).count(),
entry.getValue().size(),
taskResponses
);
}).collect(Collectors.toList());
}



private String buildQueryString(FilterTeamStatusRequest filter) {
StringBuilder queryStr = new StringBuilder("SELECT t FROM TaskEntity t " +
"JOIN FETCH t.processor p " +
"WHERE (:memberId IS NULL OR p.memberId = :memberId) ");

if (!filter.taskTitle().isEmpty()) {
queryStr.append("AND t.title LIKE :title ");
}
if (!filter.mainCategoryIds().isEmpty()) {
queryStr.append("AND t.category.mainCategory.id IN :mainCategories ");
}
if (!filter.categoryIds().isEmpty()) {
queryStr.append("AND t.category.id IN :categories ");
}

if ("기여도순".equals(filter.sortBy())) {
queryStr.append("ORDER BY (SELECT COUNT(te) FROM TaskEntity te WHERE te.processor = p AND te.taskStatus IN ('IN_PROGRESS', 'PENDING_COMPLETED')) DESC");
} else {
queryStr.append("ORDER BY p.nickname ASC");
}

return queryStr.toString();
}

private boolean isValidTitle(FilterTeamStatusRequest filter) {
return filter.taskTitle() != null && !filter.taskTitle().isEmpty();
}

private void setQueryParameters(TypedQuery<TaskEntity> query, FilterTeamStatusRequest filter) {
if (isValidTitle(filter)) {
query.setParameter("title", "%" + filter.taskTitle() + "%");
}
if (!filter.mainCategoryIds().isEmpty()) {
query.setParameter("mainCategories", filter.mainCategoryIds());
}
if (!filter.categoryIds().isEmpty()) {
query.setParameter("categories", filter.categoryIds());
}
}

@Override
public Page<TaskEntity> findPendingApprovalTasks(Pageable pageable, FilterTaskListRequest filterTaskListRequest) {
BooleanBuilder builder = createFilter(filterTaskListRequest);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package clap.server.adapter.outbound.persistense.repository.task;


import clap.server.adapter.inbound.web.dto.task.request.FilterTeamStatusRequest;
import clap.server.adapter.inbound.web.dto.task.response.TeamMemberTaskResponse;
import clap.server.adapter.outbound.persistense.entity.task.TaskEntity;
import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus;
import io.lettuce.core.dynamic.annotation.Param;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
Expand Down Expand Up @@ -50,4 +54,9 @@ Slice<TaskEntity> findTasksWithTaskStatusAndCompletedAt(
Optional<TaskEntity> findTopByProcessor_MemberIdAndTaskStatusAndProcessorOrderAfterOrderByProcessorOrderDesc(
Long processorId, TaskStatus taskStatus, Long processorOrder);

@Query("SELECT t FROM TaskEntity t JOIN FETCH t.processor p WHERE (:memberId IS NULL OR p.memberId = :memberId) ")
Page<TeamMemberTaskResponse> findTeamStatus(@Param("memberId") Long memberId, FilterTeamStatusRequest filter, Pageable pageable);



}
4 changes: 3 additions & 1 deletion src/main/java/clap/server/application/mapper/TaskMapper.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package clap.server.application.mapper;


import clap.server.adapter.inbound.web.dto.task.response.TaskBoardResponse;
import clap.server.adapter.inbound.web.dto.task.response.TaskItemResponse;
import clap.server.adapter.inbound.web.dto.task.response.*;
import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus;
import clap.server.domain.model.task.Attachment;
Expand Down Expand Up @@ -131,7 +133,7 @@ public static TaskBoardResponse toSliceTaskItemResponse(Slice<Task> tasks) {
);
}

private static TaskItemResponse toTaskItemResponse(Task task) {
public static TaskItemResponse toTaskItemResponse(Task task) {
return new TaskItemResponse(
task.getTaskId(),
task.getTaskCode(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package clap.server.application.port.inbound.task;


import clap.server.adapter.inbound.web.dto.task.request.FilterTeamStatusRequest;
import clap.server.adapter.inbound.web.dto.task.response.TeamStatusResponse;
import org.springframework.data.domain.Pageable;

public interface FilterTeamStatusUsecase {
TeamStatusResponse filterTeamStatus(FilterTeamStatusRequest filter);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package clap.server.application.port.inbound.task;

import clap.server.adapter.inbound.web.dto.task.request.FilterTeamStatusRequest;
import clap.server.adapter.inbound.web.dto.task.response.TeamStatusResponse;
import org.springframework.data.domain.Pageable;

public interface LoadTeamStatusUsecase {
TeamStatusResponse getTeamStatus(Long memberId, FilterTeamStatusRequest filter, Pageable pageable);
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import clap.server.adapter.inbound.web.dto.task.request.FilterTaskListRequest;
import clap.server.adapter.inbound.web.dto.task.request.FilterTaskBoardRequest;
import clap.server.adapter.inbound.web.dto.task.request.FilterTeamStatusRequest;
import clap.server.adapter.inbound.web.dto.task.response.TeamMemberTaskResponse;
import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus;
import clap.server.domain.model.task.Task;
import org.springframework.data.domain.Page;
Expand Down Expand Up @@ -34,4 +36,6 @@ public interface LoadTaskPort {
Optional<Task> findNextOrderTaskByProcessorIdAndStatus(Long processorId, TaskStatus taskStatus, Long processorOrder);

Slice<Task> findTaskBoardByFilter(Long processorId, List<TaskStatus> statuses, LocalDateTime untilDateTime, FilterTaskBoardRequest request, Pageable pageable);
}

List<TeamMemberTaskResponse> findTeamStatus(Long memberId, FilterTeamStatusRequest filter);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package clap.server.application.service.task;

import clap.server.adapter.inbound.web.dto.task.request.FilterTeamStatusRequest;
import clap.server.adapter.inbound.web.dto.task.response.TeamMemberTaskResponse;
import clap.server.adapter.inbound.web.dto.task.response.TeamStatusResponse;
import clap.server.application.port.inbound.task.FilterTeamStatusUsecase;
import clap.server.application.port.inbound.task.LoadTeamStatusUsecase;
import clap.server.application.port.outbound.task.LoadTaskPort;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class TeamStatusService implements LoadTeamStatusUsecase, FilterTeamStatusUsecase {

private final LoadTaskPort loadTaskPort;

public TeamStatusService(LoadTaskPort loadTaskPort) {
this.loadTaskPort = loadTaskPort;
}

@Override
public TeamStatusResponse getTeamStatus(Long memberId, FilterTeamStatusRequest filter, Pageable pageable) {
List<TeamMemberTaskResponse> members = loadTaskPort.findTeamStatus(memberId, filter); // 페이징 처리
return new TeamStatusResponse(members);
}

@Override
public TeamStatusResponse filterTeamStatus(FilterTeamStatusRequest filter) {
List<TeamMemberTaskResponse> members = loadTaskPort.findTeamStatus(null, filter);
return new TeamStatusResponse(members);
}

}