diff --git a/src/main/java/clap/server/adapter/inbound/web/auth/ReissueTokenController.java b/src/main/java/clap/server/adapter/inbound/web/auth/ReissueTokenController.java new file mode 100644 index 00000000..8deff65a --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/auth/ReissueTokenController.java @@ -0,0 +1,26 @@ +package clap.server.adapter.inbound.web.auth; + +import clap.server.adapter.inbound.web.dto.auth.ReissueTokenResponse; +import clap.server.application.port.inbound.auth.ReissueTokenUsecase; +import clap.server.common.annotation.architecture.WebAdapter; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; + +@Tag(name = "토큰 재발급") +@WebAdapter +@RequiredArgsConstructor +@RequestMapping("/api/auths") +public class ReissueTokenController { + private final ReissueTokenUsecase reissueTokenUsecase; + + @Operation(summary = "토큰 재발급 API") + @PostMapping("/reissuance") + public ResponseEntity login(@RequestHeader String refreshToken) { + return ResponseEntity.ok(reissueTokenUsecase.reissueToken(refreshToken)); + } +} diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/admin/FindManagersResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/admin/FindManagersResponse.java new file mode 100644 index 00000000..26e64a60 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/admin/FindManagersResponse.java @@ -0,0 +1,23 @@ +package clap.server.adapter.inbound.web.dto.admin; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.List; + +@Getter +@AllArgsConstructor +public class FindManagersResponse { + + private Long memberId; + private String nickname; + private String imageUrl; + private int remainingTasks; + + public static List emptyListResponse() { + return List.of(); + } + +} + + diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/auth/ReissueTokenResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/auth/ReissueTokenResponse.java new file mode 100644 index 00000000..128abc3f --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/auth/ReissueTokenResponse.java @@ -0,0 +1,7 @@ +package clap.server.adapter.inbound.web.dto.auth; + +public record ReissueTokenResponse( + String accessToken, + String refreshToken +) { +} diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/statistics/PeriodType.java b/src/main/java/clap/server/adapter/inbound/web/dto/statistics/PeriodType.java new file mode 100644 index 00000000..0df867ec --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/statistics/PeriodType.java @@ -0,0 +1,28 @@ +package clap.server.adapter.inbound.web.dto.statistics; + +import clap.server.exception.StatisticsException; +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import static clap.server.exception.code.StatisticsErrorCode.STATISTICS_BAD_REQUEST; + +@Getter +@RequiredArgsConstructor +public enum PeriodType { + DAY("day"), + WEEK("week"), + MONTH("month"); + + private final String type; + + @JsonCreator + public static PeriodType from(String input) { + for (PeriodType periodType : PeriodType.values()) { + if (periodType.getType().equals(input)) { + return periodType; + } + } + throw new StatisticsException(STATISTICS_BAD_REQUEST); + } +} diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/statistics/StatisticsResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/statistics/StatisticsResponse.java new file mode 100644 index 00000000..200f09ab --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/statistics/StatisticsResponse.java @@ -0,0 +1,5 @@ +package clap.server.adapter.inbound.web.dto.statistics; + +public record StatisticsResponse(String key, long count) { + +} \ No newline at end of file diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/statistics/StatisticsType.java b/src/main/java/clap/server/adapter/inbound/web/dto/statistics/StatisticsType.java new file mode 100644 index 00000000..faf7ae29 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/statistics/StatisticsType.java @@ -0,0 +1,27 @@ +package clap.server.adapter.inbound.web.dto.statistics; + +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + + +@Getter +@RequiredArgsConstructor +public enum StatisticsType { + REQUEST_BY_PERIOD("request-by-period"), + PROCESS_BY_PERIOD("process-by-period"), + REQUEST_BY_CATEGORY("request-by-category"), + PROCESS_BY_MANAGER("process-by-manager"); + + private final String type; + + @JsonCreator + public static StatisticsType from(String input) { + for (StatisticsType statisticsType : StatisticsType.values()) { + if (statisticsType.getType().equals(input)) { + return statisticsType; + } + } + return null; + } +} diff --git a/src/main/java/clap/server/adapter/inbound/web/member/ManagerController.java b/src/main/java/clap/server/adapter/inbound/web/member/ManagerController.java new file mode 100644 index 00000000..8354dd05 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/member/ManagerController.java @@ -0,0 +1,30 @@ +package clap.server.adapter.inbound.web.member; + +import clap.server.application.port.inbound.domain.FindManagersUsecase; +import clap.server.adapter.inbound.web.dto.admin.FindManagersResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/manager") +@RequiredArgsConstructor +public class ManagerController { + + private final FindManagersUsecase findManagersUsecase; + + @GetMapping + public List findManagers() { + + List managers = findManagersUsecase.execute(); + + if (managers.isEmpty()) { + return FindManagersResponse.emptyListResponse(); + } + + return managers; + } +} diff --git a/src/main/java/clap/server/adapter/inbound/web/statistics/FindStatisticsController.java b/src/main/java/clap/server/adapter/inbound/web/statistics/FindStatisticsController.java index 6be9e4c7..91ac152f 100644 --- a/src/main/java/clap/server/adapter/inbound/web/statistics/FindStatisticsController.java +++ b/src/main/java/clap/server/adapter/inbound/web/statistics/FindStatisticsController.java @@ -1,7 +1,11 @@ package clap.server.adapter.inbound.web.statistics; +import clap.server.adapter.inbound.web.dto.statistics.PeriodType; +import clap.server.adapter.inbound.web.dto.statistics.StatisticsResponse; +import clap.server.adapter.inbound.web.dto.statistics.StatisticsType; import clap.server.application.port.inbound.statistics.*; import clap.server.common.annotation.architecture.WebAdapter; +import clap.server.exception.StatisticsException; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -9,7 +13,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; -import java.util.Map; +import java.util.List; + +import static clap.server.exception.code.StatisticsErrorCode.STATISTICS_BAD_REQUEST; @Tag(name = "작업 관련 통계") @WebAdapter @@ -20,30 +26,49 @@ public class FindStatisticsController { private final FindPeriodTaskProcessUsecase findPeriodTaskProcessUsecase; private final FindCategoryTaskRequestUsecase findCategoryTaskRequestUsecase; private final FindSubCategoryTaskRequestUsecase findSubCategoryTaskRequestUsecase; - private final ManagerTaskProcessUsecase managerTaskProcessUsecase; - - @GetMapping(value = "/task-requests-by-period") - public ResponseEntity> aggregatePeriodTaskRequest(@RequestParam String period) { - return ResponseEntity.ok(findPeriodTaskRequestUsecase.aggregatePeriodTaskRequest(period)); - } - - @GetMapping("/task-processed-by-period") - public ResponseEntity> aggregatePeriodTaskProcess(@RequestParam String period) { - return ResponseEntity.ok(findPeriodTaskProcessUsecase.aggregatePeriodTaskProcess(period)); - } - - @GetMapping("/task-requests-by-category") - public ResponseEntity> aggregateCategoryTaskRequest(@RequestParam String period) { - return ResponseEntity.ok(findCategoryTaskRequestUsecase.aggregateCategoryTaskRequest(period)); - } + private final FindManagerTaskProcessUsecase findManagerTaskProcessUsecase; - @GetMapping("/task-requests-by-subcategory") - public ResponseEntity> aggregateSubCategoryTaskRequest(@RequestParam String period, @RequestParam String mainCategory) { - return ResponseEntity.ok(findSubCategoryTaskRequestUsecase.aggregateSubCategoryTaskRequest(period, mainCategory)); + @GetMapping + public ResponseEntity> aggregateTaskStatistics(@RequestParam PeriodType periodType, @RequestParam StatisticsType statisticsType) { + switch (statisticsType) { + case REQUEST_BY_PERIOD -> + ResponseEntity.ok(findPeriodTaskRequestUsecase + .aggregatePeriodTaskRequest(periodType.getType()) + .entrySet() + .stream() + .map(result -> new StatisticsResponse(result.getKey(), result.getValue())) + .toList()); + case PROCESS_BY_PERIOD -> + ResponseEntity.ok(findPeriodTaskProcessUsecase + .aggregatePeriodTaskProcess(periodType.getType()) + .entrySet() + .stream() + .map(result -> new StatisticsResponse(result.getKey(), result.getValue())) + .toList()); + case REQUEST_BY_CATEGORY -> + ResponseEntity.ok(findCategoryTaskRequestUsecase.aggregateCategoryTaskRequest(periodType.getType()) + .entrySet() + .stream() + .map(result -> new StatisticsResponse(result.getKey(), result.getValue())) + .toList()); + case PROCESS_BY_MANAGER -> + ResponseEntity.ok(findManagerTaskProcessUsecase + .aggregateManagerTaskProcess(periodType.getType()) + .entrySet() + .stream() + .map(result -> new StatisticsResponse(result.getKey(), result.getValue())) + .toList()); + } + throw new StatisticsException(STATISTICS_BAD_REQUEST); } - @GetMapping("/tasks-processed-by-manager") - public ResponseEntity> aggregateSubCategoryTaskRequest(@RequestParam String period) { - return ResponseEntity.ok(managerTaskProcessUsecase.aggregateManagerTaskProcess(period)); + @GetMapping("/subcategory") + public ResponseEntity> aggregateSubCategoryTaskRequest(@RequestParam String period, @RequestParam String mainCategory) { + return ResponseEntity.ok(findSubCategoryTaskRequestUsecase + .aggregateSubCategoryTaskRequest(period, mainCategory) + .entrySet() + .stream() + .map(result -> new StatisticsResponse(result.getKey(), result.getValue())) + .toList()); } } \ No newline at end of file diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/elastic/PeriodConfig.java b/src/main/java/clap/server/adapter/outbound/infrastructure/elastic/PeriodConfig.java index 1d1b9bf7..6515fc4c 100644 --- a/src/main/java/clap/server/adapter/outbound/infrastructure/elastic/PeriodConfig.java +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/elastic/PeriodConfig.java @@ -4,15 +4,27 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; + @Getter @RequiredArgsConstructor public enum PeriodConfig { - DAY(1, CalendarInterval.Hour, 11, 19), - WEEK(14, CalendarInterval.Day, 0, 10); + DAY(1, CalendarInterval.Hour, 11, 16), + WEEK(7, CalendarInterval.Day, 0, 10), + MONTH(-1, CalendarInterval.Day, 0, 10); private final long daysToSubtract; private final CalendarInterval calendarInterval; private final int substringStart; private final int substringEnd; -} + // MONTH에 대해 동적으로 daysToSubtract를 계산하는 메서드 + public long getDaysToSubtract() { + if (this == MONTH) { + return ChronoUnit.DAYS.between(LocalDate.now().minusMonths(1), LocalDate.now()); + } + return daysToSubtract; + } + +} \ No newline at end of file diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/elastic/TaskDocumentAdapter.java b/src/main/java/clap/server/adapter/outbound/infrastructure/elastic/TaskDocumentAdapter.java index 7d68bf31..85caa97e 100644 --- a/src/main/java/clap/server/adapter/outbound/infrastructure/elastic/TaskDocumentAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/elastic/TaskDocumentAdapter.java @@ -50,7 +50,7 @@ public Map findCategoryTaskRequestByPeriod(String period) { PeriodConfig periodConfig = PeriodConfig.valueOf(period.toUpperCase()); NativeQuery query = buildCategoryTaskRequestQuery(periodConfig); - return getCategoryTaskResults(executeQuery(query)); + return getNonPeriodTaskResults(executeQuery(query), "category_task"); } @Override @@ -58,7 +58,7 @@ public Map findSubCategoryTaskRequestByPeriod(String period, Strin PeriodConfig periodConfig = PeriodConfig.valueOf(period.toUpperCase()); NativeQuery query = buildSubCategoryTaskRequestQuery(periodConfig, mainCategory); - return getCategoryTaskResults(executeQuery(query)); + return getNonPeriodTaskResults(executeQuery(query), "category_task"); } @Override @@ -66,7 +66,7 @@ public Map findManagerTaskProcessByPeriod(String period) { PeriodConfig periodConfig = PeriodConfig.valueOf(period.toUpperCase()); NativeQuery query = buildManagerTaskProcessQuery(periodConfig); - return getManagerTaskResults(executeQuery(query)); + return getNonPeriodTaskResults(executeQuery(query), "manager_task"); } private NativeQuery buildPeriodTaskRequestQuery(PeriodConfig config) { @@ -188,25 +188,9 @@ private Map getPeriodTaskResults(ElasticsearchAggregations aggrega ); } - private Map getCategoryTaskResults(ElasticsearchAggregations aggregations) { + private Map getNonPeriodTaskResults(ElasticsearchAggregations aggregations, String name) { return new TreeMap<>( - aggregations.get("category_task") - .aggregation() - .getAggregate() - .sterms() - .buckets() - .array() - .stream() - .collect(Collectors.toMap( - bucket -> bucket.key().stringValue(), - MultiBucketBase::docCount - )) - ); - } - - private Map getManagerTaskResults(ElasticsearchAggregations aggregations) { - return new TreeMap<>( - aggregations.get("manager_task") + aggregations.get(name) .aggregation() .getAggregate() .sterms() diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenEntity.java b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenEntity.java index 2479d079..c71200ac 100644 --- a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenEntity.java +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenEntity.java @@ -1,5 +1,6 @@ package clap.server.adapter.outbound.infrastructure.redis.refresh; +import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; @@ -8,6 +9,7 @@ @Getter @RedisHash("refreshToken") +@Builder @ToString(of = {"memberId", "token", "ttl"}) @EqualsAndHashCode(of = {"memberId", "token"}) public class RefreshTokenEntity { diff --git a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenMapper.java b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenMapper.java index 52115015..c9a95792 100644 --- a/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenMapper.java +++ b/src/main/java/clap/server/adapter/outbound/infrastructure/redis/refresh/RefreshTokenMapper.java @@ -1,12 +1,10 @@ package clap.server.adapter.outbound.infrastructure.redis.refresh; - -import clap.server.adapter.outbound.persistense.mapper.MemberPersistenceMapper; import clap.server.domain.model.auth.RefreshToken; import org.mapstruct.InheritInverseConfiguration; import org.mapstruct.Mapper; -@Mapper(componentModel = "spring", uses = {MemberPersistenceMapper.class}) +@Mapper(componentModel = "spring") public interface RefreshTokenMapper { @InheritInverseConfiguration RefreshToken toDomain(final RefreshTokenEntity entity); diff --git a/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java index d1a0dd67..3dc1fb46 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java @@ -1,12 +1,21 @@ package clap.server.adapter.outbound.persistense; import clap.server.adapter.outbound.persistense.entity.member.MemberEntity; +import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus; import clap.server.adapter.outbound.persistense.mapper.MemberPersistenceMapper; import clap.server.adapter.outbound.persistense.repository.member.MemberRepository; import clap.server.application.port.outbound.member.CommandMemberPort; import clap.server.application.port.outbound.member.LoadMemberPort; import clap.server.common.annotation.architecture.PersistenceAdapter; +import clap.server.domain.model.task.Task; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus ; +import clap.server.adapter.outbound.persistense.entity.task.TaskEntity; +import clap.server.adapter.outbound.persistense.repository.task.TaskRepository; +import clap.server.adapter.outbound.persistense.mapper.TaskPersistenceMapper; +import java.util.stream.Collectors; +import java.util.List; + import clap.server.domain.model.member.Member; import lombok.RequiredArgsConstructor; @@ -14,9 +23,12 @@ @PersistenceAdapter @RequiredArgsConstructor -public class MemberPersistenceAdapter implements LoadMemberPort, CommandMemberPort { + public class MemberPersistenceAdapter implements LoadMemberPort, CommandMemberPort { private final MemberRepository memberRepository; private final MemberPersistenceMapper memberPersistenceMapper; + private final TaskRepository taskRepository; + private final TaskPersistenceMapper taskPersistenceMapper; + @Override public Optional findById(final Long id) { @@ -41,4 +53,29 @@ public void save(final Member member) { MemberEntity memberEntity = memberPersistenceMapper.toEntity(member); memberRepository.save(memberEntity); } + + @Override + public List findActiveManagers() { + List memberEntities = memberRepository.findByRoleAndStatus(MemberRole.valueOf("ROLE_MANAGER"), MemberStatus.ACTIVE); + return memberEntities.stream() + .map(memberPersistenceMapper::toDomain) + .collect(Collectors.toList()); + } + + @Override + public int getRemainingTasks(Long memberId) { + List targetStatuses = List.of(TaskStatus.IN_PROGRESS, TaskStatus.PENDING_COMPLETED); + return findTasksByMemberIdAndStatus(memberId, targetStatuses).size(); + } + + + @Override + public List findTasksByMemberIdAndStatus(Long memberId, List taskStatuses) { + List taskEntities = taskRepository.findByProcessor_MemberIdAndTaskStatusIn(memberId, taskStatuses); + return taskEntities.stream() + .map(taskPersistenceMapper::toDomain) + .collect(Collectors.toList()); + } + } + diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java index 5d345651..892f0a88 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java @@ -1,15 +1,23 @@ package clap.server.adapter.outbound.persistense.repository.member; import clap.server.adapter.outbound.persistense.entity.member.MemberEntity; +import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository public interface MemberRepository extends JpaRepository { + + + List findByRoleAndStatus(MemberRole role, MemberStatus status); + + Optional findByStatusAndMemberId(MemberStatus memberStatus, Long memberId); Optional findByNickname(String nickname); -} \ No newline at end of file +} + diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskRepository.java index ee3bc0d2..46bd8bd2 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskRepository.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskRepository.java @@ -1,20 +1,45 @@ package clap.server.adapter.outbound.persistense.repository.task; +import clap.server.adapter.inbound.web.dto.task.FindTaskListRequest; 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.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; + +import org.springframework.stereotype.Repository; + +import java.nio.channels.FileChannel; +import java.time.LocalDateTime; +import java.util.Collection; + import org.springframework.stereotype.Repository; import java.time.LocalDateTime; + import java.util.List; @Repository public interface TaskRepository extends JpaRepository, TaskCustomRepository { + @Query("select t from TaskEntity t join fetch t.processor p" + " where t.updatedAt between :updatedAtAfter and :updatedAtBefore") + List findYesterdayTaskByUpdatedAtIsBetween( @Param("updatedAtAfter") LocalDateTime updatedAtAfter, @Param("updatedAtBefore") LocalDateTime updatedAtBefore ); -} \ No newline at end of file + + + // 'processor'의 'id'로 검색하기 + List findByProcessor_MemberIdAndTaskStatusIn(Long memberId, Collection taskStatuses); +} + + + + + + + + diff --git a/src/main/java/clap/server/application/mapper/MemberMapper.java b/src/main/java/clap/server/application/mapper/MemberMapper.java index 628be123..f7f14d34 100644 --- a/src/main/java/clap/server/application/mapper/MemberMapper.java +++ b/src/main/java/clap/server/application/mapper/MemberMapper.java @@ -13,4 +13,4 @@ public static Member toMember(MemberInfo memberInfo) { .memberInfo(memberInfo) .build(); } -} +} \ No newline at end of file diff --git a/src/main/java/clap/server/application/mapper/response/AuthResponseMapper.java b/src/main/java/clap/server/application/mapper/response/AuthResponseMapper.java index f5545efa..3c9ddbdd 100644 --- a/src/main/java/clap/server/application/mapper/response/AuthResponseMapper.java +++ b/src/main/java/clap/server/application/mapper/response/AuthResponseMapper.java @@ -2,6 +2,8 @@ import clap.server.adapter.inbound.web.dto.auth.LoginResponse; import clap.server.adapter.inbound.web.dto.auth.MemberInfoResponse; +import clap.server.adapter.inbound.web.dto.auth.ReissueTokenResponse; +import clap.server.domain.model.auth.CustomJwts; import clap.server.domain.model.member.Member; public class AuthResponseMapper { @@ -27,4 +29,11 @@ public static MemberInfoResponse toMemberInfoResponse(Member member) { member.getStatus() ); } + + public static ReissueTokenResponse toReissueTokenResponse(final CustomJwts jwtTokens) { + return new ReissueTokenResponse( + jwtTokens.accessToken(), + jwtTokens.refreshToken() + ); + } } diff --git a/src/main/java/clap/server/application/port/inbound/auth/ReissueTokenUsecase.java b/src/main/java/clap/server/application/port/inbound/auth/ReissueTokenUsecase.java new file mode 100644 index 00000000..a9df09e2 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/auth/ReissueTokenUsecase.java @@ -0,0 +1,7 @@ +package clap.server.application.port.inbound.auth; + +import clap.server.adapter.inbound.web.dto.auth.ReissueTokenResponse; + +public interface ReissueTokenUsecase { + ReissueTokenResponse reissueToken(String oldRefreshToken); +} diff --git a/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecase.java b/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecase.java new file mode 100644 index 00000000..57327d16 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecase.java @@ -0,0 +1,8 @@ +package clap.server.application.port.inbound.domain; + +import clap.server.adapter.inbound.web.dto.admin.FindManagersResponse; +import java.util.List; + +public interface FindManagersUsecase { + List execute(); +} diff --git a/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecaseImpl.java b/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecaseImpl.java new file mode 100644 index 00000000..0148fb72 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/domain/FindManagersUsecaseImpl.java @@ -0,0 +1,41 @@ +package clap.server.application.port.inbound.domain; + +import clap.server.adapter.inbound.web.dto.admin.FindManagersResponse; +import clap.server.domain.model.member.Member; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class FindManagersUsecaseImpl implements FindManagersUsecase { + + private final MemberService memberService; + + @Override + @Transactional + public List execute() { + + List managers = memberService.findActiveManagers(); + + if (managers.isEmpty()) { + return FindManagersResponse.emptyListResponse(); // 빈 리스트 반환 + } + + return managers.stream().map(manager -> { + int remainingTasks = memberService.getRemainingTasks(manager.getMemberId()); + String nickname = memberService.getMemberNickname(manager.getMemberId()); + String imageUrl = memberService.getMemberImageUrl(manager.getMemberId()); + + return new FindManagersResponse( + manager.getMemberId(), + nickname, + imageUrl, + remainingTasks + ); + }).collect(Collectors.toList()); + } +} diff --git a/src/main/java/clap/server/application/port/inbound/domain/MemberService.java b/src/main/java/clap/server/application/port/inbound/domain/MemberService.java index 47de39c7..a3d31fd3 100644 --- a/src/main/java/clap/server/application/port/inbound/domain/MemberService.java +++ b/src/main/java/clap/server/application/port/inbound/domain/MemberService.java @@ -1,12 +1,16 @@ package clap.server.application.port.inbound.domain; import clap.server.application.port.outbound.member.LoadMemberPort; -import clap.server.exception.ApplicationException; import clap.server.domain.model.member.Member; +import clap.server.domain.model.task.Task; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import clap.server.exception.ApplicationException; import clap.server.exception.code.MemberErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.List; + @Service @RequiredArgsConstructor public class MemberService { @@ -14,11 +18,38 @@ public class MemberService { public Member findById(Long memberId) { return loadMemberPort.findById(memberId).orElseThrow( - ()-> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); + () -> new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND)); } - + public Member findActiveMember(Long memberId) { return loadMemberPort.findActiveMemberById(memberId).orElseThrow( - ()-> new ApplicationException(MemberErrorCode.ACTIVE_MEMBER_NOT_FOUND)); + () -> new ApplicationException(MemberErrorCode.ACTIVE_MEMBER_NOT_FOUND)); + } + + public int getRemainingTasks(Long memberId) { + List targetStatuses = List.of(TaskStatus.IN_PROGRESS, TaskStatus.PENDING_COMPLETED); + return loadMemberPort.findTasksByMemberIdAndStatus(memberId, targetStatuses).size(); + } + + public String getMemberNickname(Long memberId) { + Member member = findById(memberId); + if (member.getMemberInfo() == null) { + throw new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND); + } + return member.getMemberInfo().getNickname(); + } + + public String getMemberImageUrl(Long memberId) { + Member member = findById(memberId); + return member.getImageUrl() != null ? member.getImageUrl() : "default-image-url"; + } + + public List findActiveManagers() { + List activeManagers = loadMemberPort.findActiveManagers(); + + if (activeManagers.isEmpty()) { + return List.of(); + } + return activeManagers; } } diff --git a/src/main/java/clap/server/application/port/inbound/statistics/ManagerTaskProcessUsecase.java b/src/main/java/clap/server/application/port/inbound/statistics/FindManagerTaskProcessUsecase.java similarity index 75% rename from src/main/java/clap/server/application/port/inbound/statistics/ManagerTaskProcessUsecase.java rename to src/main/java/clap/server/application/port/inbound/statistics/FindManagerTaskProcessUsecase.java index 68026e7a..84e31050 100644 --- a/src/main/java/clap/server/application/port/inbound/statistics/ManagerTaskProcessUsecase.java +++ b/src/main/java/clap/server/application/port/inbound/statistics/FindManagerTaskProcessUsecase.java @@ -2,6 +2,6 @@ import java.util.Map; -public interface ManagerTaskProcessUsecase { +public interface FindManagerTaskProcessUsecase { Map aggregateManagerTaskProcess(String period); } diff --git a/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java b/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java index 5962d38a..2d719663 100644 --- a/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java +++ b/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java @@ -1,7 +1,9 @@ package clap.server.application.port.outbound.member; import clap.server.domain.model.member.Member; - +import clap.server.domain.model.task.Task; // Task 클래스 임포트 확인 +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; // TaskStatus 임포트 +import java.util.List; import java.util.Optional; public interface LoadMemberPort { @@ -9,5 +11,12 @@ public interface LoadMemberPort { Optional findActiveMemberById(Long id); + List findActiveManagers(); + + List findTasksByMemberIdAndStatus(Long memberId, List taskStatuses); + + int getRemainingTasks(Long memberId); + Optional findByNickname(String nickname); + } diff --git a/src/main/java/clap/server/application/service/auth/AuthService.java b/src/main/java/clap/server/application/service/auth/AuthService.java index ee960017..7015a047 100644 --- a/src/main/java/clap/server/application/service/auth/AuthService.java +++ b/src/main/java/clap/server/application/service/auth/AuthService.java @@ -4,6 +4,7 @@ import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus; import clap.server.application.mapper.response.AuthResponseMapper; import clap.server.application.port.inbound.auth.AuthUsecase; +import clap.server.application.port.outbound.auth.CommandRefreshTokenPort; import clap.server.application.port.outbound.member.LoadMemberPort; import clap.server.common.annotation.architecture.ApplicationService; import clap.server.domain.model.auth.CustomJwts; @@ -18,6 +19,7 @@ @RequiredArgsConstructor class AuthService implements AuthUsecase { private final LoadMemberPort loadMemberPort; + private final CommandRefreshTokenPort commandRefreshTokenPort; private final IssueTokenService issueTokenService; private final PasswordEncoder passwordEncoder; @@ -29,22 +31,25 @@ public LoginResponse login(String nickname, String password) { validatePassword(password, member.getPassword()); if (member.getStatus().equals(MemberStatus.APPROVAL_REQUEST)) { - String temporaryToken = issueTokenService.createTemporaryToken(member); + String temporaryToken = issueTokenService.issueTemporaryToken(member.getMemberId()); return AuthResponseMapper.toLoginResponse( temporaryToken, null, member ); } else { - CustomJwts jwtTokens = issueTokenService.createToken(member); + CustomJwts jwtTokens = issueTokenService.issueTokens(member); + commandRefreshTokenPort.save( + issueTokenService.issueRefreshToken(member.getMemberId()) + ); return AuthResponseMapper.toLoginResponse( jwtTokens.accessToken(), jwtTokens.refreshToken(), member ); } } - private void validatePassword(String inputPassword, String encodedPassword) { if (!passwordEncoder.matches(inputPassword, encodedPassword)) { throw new ApplicationException(MemberErrorCode.MEMBER_NOT_FOUND); } } + } diff --git a/src/main/java/clap/server/application/service/auth/IssueTokenService.java b/src/main/java/clap/server/application/service/auth/IssueTokenService.java index 42b9d348..2b8770dd 100644 --- a/src/main/java/clap/server/application/service/auth/IssueTokenService.java +++ b/src/main/java/clap/server/application/service/auth/IssueTokenService.java @@ -1,22 +1,21 @@ package clap.server.application.service.auth; +import clap.server.adapter.outbound.jwt.JwtClaims; import clap.server.adapter.outbound.jwt.access.AccessTokenClaim; import clap.server.adapter.outbound.jwt.access.temporary.TemporaryTokenClaim; import clap.server.adapter.outbound.jwt.refresh.RefreshTokenClaim; -import clap.server.application.port.outbound.auth.CommandRefreshTokenPort; +import clap.server.adapter.outbound.jwt.refresh.RefreshTokenClaimKeys; import clap.server.application.port.outbound.auth.JwtProvider; -import clap.server.application.port.outbound.auth.LoadRefreshTokenPort; import clap.server.domain.model.auth.CustomJwts; import clap.server.domain.model.auth.RefreshToken; import clap.server.domain.model.member.Member; -import clap.server.exception.AuthException; -import clap.server.exception.code.AuthErrorCode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.time.Duration; import java.time.LocalDateTime; +import java.util.function.Function; @RequiredArgsConstructor @Component @@ -25,56 +24,49 @@ class IssueTokenService { private final JwtProvider accessTokenProvider; private final JwtProvider refreshTokenProvider; private final JwtProvider temporaryTokenProvider; - private final LoadRefreshTokenPort loadRefreshTokenPort; - private final CommandRefreshTokenPort commandRefreshTokenPort; - public CustomJwts createToken(Member member) { + public CustomJwts issueTokens(Member member) { String accessToken = accessTokenProvider.createToken(AccessTokenClaim.of(member.getMemberId())); String refreshToken = refreshTokenProvider.createToken(RefreshTokenClaim.of(member.getMemberId())); - - commandRefreshTokenPort.save( - RefreshToken.of( - member.getMemberId(), refreshToken, - toSeconds(refreshTokenProvider.getExpiredDate(refreshToken)) - ) - ); - return CustomJwts.of(accessToken, refreshToken); } - public String createTemporaryToken(Member member) { - return temporaryTokenProvider.createToken(TemporaryTokenClaim.of(member.getMemberId())); + public String issueAccessToken(Long memberId) { + return accessTokenProvider.createToken(AccessTokenClaim.of(memberId)); } - private long toSeconds(LocalDateTime expiredDate) { - return Duration.between(LocalDateTime.now(), expiredDate).getSeconds(); - } - public RefreshToken refresh( - Long memberId, - String oldRefreshToken, - String newRefreshToken - ) throws IllegalArgumentException, IllegalStateException { - RefreshToken refreshToken = loadRefreshTokenPort.findByMemberId(memberId).orElseThrow( - ()-> new AuthException(AuthErrorCode.REFRESH_TOKEN_NOT_FOUND) + public RefreshToken issueRefreshToken(Long memberId) { + String refreshToken = refreshTokenProvider.createToken(RefreshTokenClaim.of(memberId)); + return RefreshToken.of( + memberId, refreshToken, + toSeconds(refreshTokenProvider.getExpiredDate(refreshToken)) ); - validateToken(oldRefreshToken, refreshToken); + } + + public String issueTemporaryToken(Long memberId) { + return temporaryTokenProvider.createToken(TemporaryTokenClaim.of(memberId)); + } - refreshToken.rotation(newRefreshToken); - commandRefreshTokenPort.save(refreshToken); - return refreshToken; + public Long resolveRefreshToken(String refreshToken) { + JwtClaims claims = refreshTokenProvider.parseJwtClaimsFromToken(refreshToken); + + return getClaimValue(claims, + RefreshTokenClaimKeys.USER_ID.getValue(), + Long::parseLong); } - private void validateToken(String oldRefreshToken, RefreshToken refreshToken) { - if (isTakenAway(oldRefreshToken, refreshToken.getToken())) { - commandRefreshTokenPort.delete(refreshToken); - throw new AuthException(AuthErrorCode.REFRESH_TOKEN_MISMATCHED); - } + private long toSeconds(LocalDateTime expiredDate) { + return Duration.between(LocalDateTime.now(), expiredDate).getSeconds(); } - private boolean isTakenAway(String requestRefreshToken, String expectedRefreshToken) { - return !requestRefreshToken.equals(expectedRefreshToken); + private static T getClaimValue(JwtClaims jwtClaims, String key, Function converter) { + Object value = jwtClaims.getClaims().get(key); + if (value != null) { + return converter.apply(value.toString()); + } + return null; } } diff --git a/src/main/java/clap/server/application/service/auth/ReissueTokenService.java b/src/main/java/clap/server/application/service/auth/ReissueTokenService.java new file mode 100644 index 00000000..022dd9e2 --- /dev/null +++ b/src/main/java/clap/server/application/service/auth/ReissueTokenService.java @@ -0,0 +1,68 @@ +package clap.server.application.service.auth; + +import clap.server.adapter.inbound.web.dto.auth.ReissueTokenResponse; +import clap.server.application.port.inbound.auth.ReissueTokenUsecase; +import clap.server.application.port.outbound.auth.CommandRefreshTokenPort; +import clap.server.application.port.outbound.auth.LoadRefreshTokenPort; +import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.domain.model.auth.CustomJwts; +import clap.server.domain.model.auth.RefreshToken; +import clap.server.exception.AuthException; +import clap.server.exception.code.AuthErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +import static clap.server.application.mapper.response.AuthResponseMapper.toReissueTokenResponse; + +@ApplicationService +@RequiredArgsConstructor +class ReissueTokenService implements ReissueTokenUsecase { + private final IssueTokenService issueTokenService; + private final LoadRefreshTokenPort loadRefreshTokenPort; + private final CommandRefreshTokenPort commandRefreshTokenPort; + + @Transactional + public ReissueTokenResponse reissueToken(String oldRefreshToken) { + Long memberId = issueTokenService.resolveRefreshToken(oldRefreshToken); + RefreshToken newRefreshToken; + try { + newRefreshToken = refresh(memberId, oldRefreshToken, + issueTokenService.issueRefreshToken(memberId).getToken()); + } catch (IllegalArgumentException e) { + throw new AuthException(AuthErrorCode.EXPIRED_TOKEN); + } catch (IllegalStateException e) { + throw new AuthException(AuthErrorCode.TAKEN_AWAY_TOKEN); + } + + String newAccessToken = issueTokenService.issueAccessToken(memberId); + CustomJwts tokens = CustomJwts.of(newAccessToken, newRefreshToken.getToken()); + return toReissueTokenResponse(tokens); + } + + private RefreshToken refresh( + Long memberId, + String oldRefreshToken, + String newRefreshToken + ) throws IllegalArgumentException, IllegalStateException { + RefreshToken refreshToken = loadRefreshTokenPort.findByMemberId(memberId).orElseThrow( + () -> new AuthException(AuthErrorCode.REFRESH_TOKEN_NOT_FOUND) + ); + validateToken(oldRefreshToken, refreshToken); + + refreshToken.rotation(newRefreshToken); + commandRefreshTokenPort.save(refreshToken); + return refreshToken; + } + + private void validateToken(String oldRefreshToken, RefreshToken refreshToken) { + if (isTakenAway(oldRefreshToken, refreshToken.getToken())) { + commandRefreshTokenPort.delete(refreshToken); + throw new AuthException(AuthErrorCode.REFRESH_TOKEN_MISMATCHED); + } + } + + private boolean isTakenAway(String requestRefreshToken, String expectedRefreshToken) { + return !requestRefreshToken.equals(expectedRefreshToken); + } + +} diff --git a/src/main/java/clap/server/application/statistics/ManagerTaskProcessService.java b/src/main/java/clap/server/application/statistics/FindManagerTaskProcessService.java similarity index 74% rename from src/main/java/clap/server/application/statistics/ManagerTaskProcessService.java rename to src/main/java/clap/server/application/statistics/FindManagerTaskProcessService.java index c06141c0..0ae2ae46 100644 --- a/src/main/java/clap/server/application/statistics/ManagerTaskProcessService.java +++ b/src/main/java/clap/server/application/statistics/FindManagerTaskProcessService.java @@ -1,6 +1,6 @@ package clap.server.application.statistics; -import clap.server.application.port.inbound.statistics.ManagerTaskProcessUsecase; +import clap.server.application.port.inbound.statistics.FindManagerTaskProcessUsecase; import clap.server.application.port.outbound.task.TaskDocumentPort; import clap.server.common.annotation.architecture.ApplicationService; import lombok.RequiredArgsConstructor; @@ -9,7 +9,7 @@ @ApplicationService @RequiredArgsConstructor -public class ManagerTaskProcessService implements ManagerTaskProcessUsecase { +public class FindManagerTaskProcessService implements FindManagerTaskProcessUsecase { private final TaskDocumentPort taskDocumentPort; @Override diff --git a/src/main/java/clap/server/application/statistics/FindPeriodTaskProcessService.java b/src/main/java/clap/server/application/statistics/FindPeriodTaskProcessService.java index 02595e52..7b94469e 100644 --- a/src/main/java/clap/server/application/statistics/FindPeriodTaskProcessService.java +++ b/src/main/java/clap/server/application/statistics/FindPeriodTaskProcessService.java @@ -3,6 +3,7 @@ import clap.server.application.port.inbound.statistics.FindPeriodTaskProcessUsecase; import clap.server.application.port.outbound.task.TaskDocumentPort; import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.domain.statistics.Statistics; import lombok.RequiredArgsConstructor; import java.util.Map; @@ -14,6 +15,9 @@ public class FindPeriodTaskProcessService implements FindPeriodTaskProcessUsecas @Override public Map aggregatePeriodTaskProcess(String period) { + if (period.equals("week") || period.equals("month")) { + return Statistics.transformToWeekdayStatistics(taskDocumentPort.findPeriodTaskProcessByPeriod(period)); + } return taskDocumentPort.findPeriodTaskProcessByPeriod(period); } } diff --git a/src/main/java/clap/server/application/statistics/FindPeriodTaskRequestService.java b/src/main/java/clap/server/application/statistics/FindPeriodTaskRequestService.java index 287c33fb..f003dffd 100644 --- a/src/main/java/clap/server/application/statistics/FindPeriodTaskRequestService.java +++ b/src/main/java/clap/server/application/statistics/FindPeriodTaskRequestService.java @@ -3,6 +3,7 @@ import clap.server.application.port.inbound.statistics.FindPeriodTaskRequestUsecase; import clap.server.application.port.outbound.task.TaskDocumentPort; import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.domain.statistics.Statistics; import lombok.RequiredArgsConstructor; import java.util.Map; @@ -14,6 +15,9 @@ public class FindPeriodTaskRequestService implements FindPeriodTaskRequestUsecas @Override public Map aggregatePeriodTaskRequest(String period) { + if (period.equals("week") || period.equals("month")) { + return Statistics.transformToWeekdayStatistics(taskDocumentPort.findPeriodTaskRequestByPeriod(period)); + } return taskDocumentPort.findPeriodTaskRequestByPeriod(period); } } 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 1e9b7ce8..a40773eb 100644 --- a/src/main/java/clap/server/domain/model/member/Member.java +++ b/src/main/java/clap/server/domain/model/member/Member.java @@ -45,4 +45,8 @@ public void resetPasswordAndActivateMember(String newEncodedPassword) { this.password = newEncodedPassword; this.status = MemberStatus.ACTIVE; } + public String getNickname() { + return memberInfo != null ? memberInfo.getNickname() : null; + } + } diff --git a/src/main/java/clap/server/domain/model/member/MemberInfo.java b/src/main/java/clap/server/domain/model/member/MemberInfo.java index 69a6b55c..31917d52 100644 --- a/src/main/java/clap/server/domain/model/member/MemberInfo.java +++ b/src/main/java/clap/server/domain/model/member/MemberInfo.java @@ -27,4 +27,5 @@ public MemberInfo(String name, String email, String nickname, boolean isReviewer this.role = role; this.departmentRole = departmentRole; } + } \ No newline at end of file diff --git a/src/main/java/clap/server/domain/statistics/Statistics.java b/src/main/java/clap/server/domain/statistics/Statistics.java new file mode 100644 index 00000000..8df5790b --- /dev/null +++ b/src/main/java/clap/server/domain/statistics/Statistics.java @@ -0,0 +1,31 @@ +package clap.server.domain.statistics; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Statistics { + public static Map transformToWeekdayStatistics(Map statistics) { + TreeMap result = new TreeMap<>(); + + for (Entry statistic : statistics.entrySet()) { + String stringDate = statistic.getKey(); + LocalDate date = LocalDate.parse(stringDate); + + if (!(date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY)) { + result.put(stringDate.substring(6, 10).replace("-", "월 ") + "일", statistic.getValue()); + } + } + + return result; + + } +} diff --git a/src/main/java/clap/server/exception/ExceptionAdvice.java b/src/main/java/clap/server/exception/ExceptionAdvice.java index d36b3e2a..af782c06 100644 --- a/src/main/java/clap/server/exception/ExceptionAdvice.java +++ b/src/main/java/clap/server/exception/ExceptionAdvice.java @@ -3,6 +3,7 @@ import clap.server.exception.code.AuthErrorCode; import clap.server.exception.code.BaseErrorCode; import clap.server.exception.code.CommonErrorCode; +import clap.server.exception.code.StatisticsErrorCode; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; @@ -170,4 +171,16 @@ public ResponseEntity handleAccessDeniedException(AccessDeniedException AuthErrorCode.FORBIDDEN.getMessage() ); } + + @ExceptionHandler(StatisticsException.class) + public ResponseEntity handleAccessDeniedException(StatisticsException e, WebRequest request) { + return handleExceptionInternalFalse( + e, + StatisticsErrorCode.STATISTICS_BAD_REQUEST, + HttpHeaders.EMPTY, + HttpStatus.BAD_REQUEST, + request, + StatisticsErrorCode.STATISTICS_BAD_REQUEST.getMessage() + ); + } } diff --git a/src/main/java/clap/server/exception/StatisticsException.java b/src/main/java/clap/server/exception/StatisticsException.java new file mode 100644 index 00000000..e06b9776 --- /dev/null +++ b/src/main/java/clap/server/exception/StatisticsException.java @@ -0,0 +1,10 @@ +package clap.server.exception; + +import clap.server.exception.code.BaseErrorCode; + +public class StatisticsException extends BaseException { + + public StatisticsException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/clap/server/exception/code/StatisticsErrorCode.java b/src/main/java/clap/server/exception/code/StatisticsErrorCode.java new file mode 100644 index 00000000..8a808121 --- /dev/null +++ b/src/main/java/clap/server/exception/code/StatisticsErrorCode.java @@ -0,0 +1,15 @@ +package clap.server.exception.code; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum StatisticsErrorCode implements BaseErrorCode{ + STATISTICS_BAD_REQUEST(HttpStatus.BAD_REQUEST, "STATISTICS_001", "잘못된 통계 조회 파라미터 입력."); + + private final HttpStatus httpStatus; + private final String customCode; + private final String message; +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 90830453..ef3f186c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -9,7 +9,7 @@ spring: - auth.yml - optional:classpath:env.properties application: - name: taskflow + name: taskflow2 elasticsearch: uris: ${ELASTIC_URI:127.0.0.1:9200} @@ -50,4 +50,4 @@ logging: spring.config.activate.on-profile: prod logging: level: - root: INFO + root: INFO \ No newline at end of file diff --git a/src/main/resources/env.properties b/src/main/resources/env.properties new file mode 100644 index 00000000..c3456735 --- /dev/null +++ b/src/main/resources/env.properties @@ -0,0 +1,31 @@ +# application.properties + +spring.config.import=classpath:env.properties + +# MySQL Configuration +spring.datasource.url=${DB_URL} +spring.datasource.username=${DB_USERNAME} +spring.datasource.password=${DB_PASSWORD} + +# Application Profile +APPLICATION_PORT=8080 + +# Redis Configuration +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= + +# Swagger Configuration +SWAGGER_SERVER_URL=http://localhost:8080 + +# MySQL Configuration +MYSQL_HOST=localhost +MYSQL_PORT=3306 +MYSQL_USERNAME=root +MYSQL_PASSWORD=km7971na +MYSQL_DATABASE=taskflow2 + +# Additional Environment-specific Configurations +DB_URL=jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE} +DB_USERNAME=${MYSQL_USERNAME} +DB_PASSWORD=${MYSQL_PASSWORD} diff --git a/src/main/resources/mysql.yml b/src/main/resources/mysql.yml index 7655ec4e..366aaec9 100644 --- a/src/main/resources/mysql.yml +++ b/src/main/resources/mysql.yml @@ -10,7 +10,7 @@ spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://${DATABASE_HOST:localhost}:${DATABASE_PORT:3306}/${DATABASE_NAME:taskflow}?characterEncoding=UTF-8&serverTimezone=Asia/Seoul&autoReconnect=true + url: jdbc:mysql://${DATABASE_HOST:localhost}:${DATABASE_PORT:3306}/${DATABASE_NAME:taskflow2}?characterEncoding=UTF-8&serverTimezone=Asia/Seoul&autoReconnect=true username: ${DATABASE_USERNAME} password: ${DATABASE_PASSWORD} data-source-properties: diff --git a/src/test/java/clap/server/TaskflowApplicationTests.java b/src/test/java/clap/server/TaskflowApplicationTests.java index 41766b64..65422b39 100644 --- a/src/test/java/clap/server/TaskflowApplicationTests.java +++ b/src/test/java/clap/server/TaskflowApplicationTests.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.elasticsearch.ElasticsearchContainer; @@ -24,19 +23,26 @@ class TaskflowApplicationTests { private MemberRepository memberRepository; @Container -// @ServiceConnection - public static ElasticsearchContainer ES_CONTAINER = new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:7.17.5"); - - @Container - @ServiceConnection - public static RedisContainer REDIS_CONTAINER = new RedisContainer(DockerImageName.parse("redis:6.2.6")); + public static ElasticsearchContainer ES_CONTAINER = new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:7.17.5") + .withReuse(true); @DynamicPropertySource - static void configureProperties(DynamicPropertyRegistry registry) { + static void elasticProperties(DynamicPropertyRegistry registry) { // Elasticsearch 설정 registry.add("spring.elasticsearch.uris", ES_CONTAINER::getHttpHostAddress); } + @Container + public static RedisContainer REDIS_CONTAINER = new RedisContainer(DockerImageName.parse("redis:6.2.6")) + .withReuse(true); + + @DynamicPropertySource + static void redisProperties(DynamicPropertyRegistry registry) { + // redis 설정 + registry.add("spring.data.redis.host", REDIS_CONTAINER::getHost); + registry.add("spring.data.redis.port", () -> REDIS_CONTAINER.getMappedPort(6379)); + } + @Test void contextLoads() { assertThat(memberRepository.findAll()).isEmpty(); diff --git a/src/test/java/clap/server/adapter/inbound/web/admin/MemberControllerTest.java b/src/test/java/clap/server/adapter/inbound/web/admin/MemberControllerTest.java new file mode 100644 index 00000000..14a1689e --- /dev/null +++ b/src/test/java/clap/server/adapter/inbound/web/admin/MemberControllerTest.java @@ -0,0 +1,210 @@ +package clap.server.adapter.inbound.web.admin; + +import clap.server.adapter.outbound.persistense.entity.member.DepartmentEntity; +import clap.server.adapter.outbound.persistense.entity.member.MemberEntity; +import clap.server.adapter.outbound.persistense.entity.member.constant.DepartmentStatus; +import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; +import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus; +import clap.server.adapter.outbound.persistense.entity.task.CategoryEntity; +import clap.server.adapter.outbound.persistense.entity.task.LabelEntity; +import clap.server.adapter.outbound.persistense.entity.task.TaskEntity; +import clap.server.adapter.outbound.persistense.entity.task.constant.LabelType; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import clap.server.config.elastic.ElasticsearchConfig; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.annotation.Rollback; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.testcontainers.elasticsearch.ElasticsearchContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Import(ElasticsearchConfig.class) +@SpringBootTest +@Transactional +@AutoConfigureMockMvc +@Testcontainers +public class MemberControllerTest { + + @Container + public static ElasticsearchContainer ES_CONTAINER = new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:7.17.5") + .withReuse(true); + + @DynamicPropertySource + static void elasticProperties(DynamicPropertyRegistry registry) { + // Elasticsearch 설정 + registry.add("spring.elasticsearch.uris", ES_CONTAINER::getHttpHostAddress); + } + + @Autowired + private MockMvc mockMvc; + + @Autowired + private EntityManager entityManager; + + @BeforeEach + @Transactional + @Rollback(false) + public void setupTestData() { + // 부서 추가 + DepartmentEntity department = DepartmentEntity.builder() + .code("DEPT001") + .name("IT Department") + .status(DepartmentStatus.ACTIVE) + .build(); + entityManager.persist(department); // department 먼저 persist + + // 관리자 추가 + MemberEntity admin = MemberEntity.builder() + .name("Admin User") + .email("admin@example.com") + .nickname("Admin1") + .isReviewer(false) + .role(MemberRole.ROLE_ADMIN) + .departmentRole("Admin") + .status(MemberStatus.ACTIVE) + .password("admin123") + .imageUrl("http://example.com/admin.jpg") + .notificationEnabled(true) + .department(department) // 부서 할당 + .build(); + entityManager.persist(admin); + + // 카테고리 추가 + CategoryEntity category = CategoryEntity.builder() + .code("CATEGORY001") + .name("Development") + .descriptionExample("Development tasks category") + .isDeleted(false) + .admin(admin) + .build(); + entityManager.persist(category); + + // 라벨 추가 + LabelEntity label = LabelEntity.builder() + .labelType(LabelType.EMERGENCY) + .admin(admin) + .isDeleted(false) + .build(); + entityManager.persist(label); + + // 관리자 (manager) 추가 + MemberEntity manager1 = MemberEntity.builder() // manager1 정의 + .name("Manager1") + .email("manager1@example.com") + .nickname("Manager1") + .isReviewer(true) + .role(MemberRole.ROLE_MANAGER) // 관리자로 설정 + .departmentRole("Manager") + .status(MemberStatus.ACTIVE) + .password("manager123") + .imageUrl("http://example.com/manager1.jpg") + .notificationEnabled(true) + .department(department) // 부서 할당 + .build(); + entityManager.persist(manager1); // manager1 저장 + + // 태스크 추가 + TaskEntity task = TaskEntity.builder() + .taskCode("TASK001") + .title("Task Title") + .description("Task Description") + .category(category) // 카테고리 연결 + .requester(admin) + .taskStatus(TaskStatus.PENDING_COMPLETED) + .processorOrder(1) + .reviewer(admin) + .processor(manager1) // 여기서 manager1을 processor로 설정 + .label(label) + .dueDate(LocalDateTime.now().plusDays(7)) + .completedAt(null) + .build(); + entityManager.persist(task); + + // 일반 사용자 (user) 추가 + MemberEntity user = MemberEntity.builder() + .name("User1") + .email("user1@example.com") + .nickname("User1") + .isReviewer(false) + .role(MemberRole.ROLE_USER) + .departmentRole("User") + .status(MemberStatus.ACTIVE) + .password("user123") + .imageUrl("http://example.com/user1.jpg") + .notificationEnabled(true) + .department(department) // 부서 할당 + .build(); + entityManager.persist(user); + // 두 번째 관리자 추가 + MemberEntity manager2 = MemberEntity.builder() + .name("Manager2") + .email("manager2@example.com") + .nickname("Manager2") + .isReviewer(true) + .role(MemberRole.ROLE_MANAGER) + .departmentRole("Manager") + .status(MemberStatus.ACTIVE) + .password("manager123") + .imageUrl("http://example.com/manager2.jpg") + .notificationEnabled(true) + .department(department) + .build(); + entityManager.persist(manager2); // 두 번째 관리자 저장 + + // 추가로 5명의 사용자 생성 + List members = new ArrayList<>(); + for (int i = 2; i <= 5; i++) { + members.add(MemberEntity.builder() + .name("User" + i) + .email("user" + i + "@example.com") + .nickname("User" + i) + .isReviewer(i % 2 == 0) + .role(MemberRole.ROLE_USER) + .departmentRole("DepartmentUser") + .status(MemberStatus.ACTIVE) + .password("user123") + .imageUrl("http://example.com/user" + i + ".jpg") + .notificationEnabled(i % 2 != 0) + .department(department) // 부서 할당 + .build()); + } + + // 멤버들을 데이터베이스에 저장 + members.forEach(entityManager::persist); + } + + + @Test + @WithMockUser(username = "admin", roles = "ADMIN") + public void testFindManagers() throws Exception { + mockMvc.perform(get("/manager")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].nickname").value("Manager1")) + .andExpect(jsonPath("$[0].imageUrl").value("http://example.com/manager1.jpg")) + .andExpect(jsonPath("$[0].remainingTasks").value(1)) + .andExpect(jsonPath("$[1].nickname").value("Manager2")) // 추가된 관리자 + .andExpect(jsonPath("$[1].imageUrl").value("http://example.com/manager2.jpg")) + .andExpect(jsonPath("$[1].remainingTasks").value(0)) // 예시로 추가된 관리자들의 데이터를 검증 + .andDo(print()); + } + +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index a0c7628e..ba283b05 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -1,24 +1,27 @@ spring: - # H2 Setting Info (H2 Console에 접속하기 위한 설정정보 입력) + # H2 설정 정보 (H2 Console에 접속하기 위한 설정) h2: console: enabled: false # H2 Console을 사용할지 여부 (H2 Console은 H2 Database를 UI로 제공해주는 기능) path: /h2 # H2 Console의 Path - # Database Setting Info (Database를 H2로 사용하기 위해 H2연결 정보 입력) + # Database 설정 정보 (H2 연동 정보) datasource: - driver-class-name: org.h2.Driver # Database를 H2로 사용하겠다. + driver-class-name: org.h2.Driver # H2 Database 사용 url: jdbc:h2:mem:testdb # H2 접속 정보 - username: taskflow # H2 접속 시 입력할 username 정보 (원하는 것으로 입력) - password: # H2 접속 시 입력할 password 정보 (원하는 것으로 입력) + username: taskflow # H2 접속 시 입력할 username 정보 + password: # H2 접속 시 입력할 password 정보 jpa: hibernate: ddl-auto: create + testcontainers: + beans: + startup: parallel + swagger: server: url: http://localhost:8080 - jwt: secret-key: access-token: exampleSecretKeyForTFSystemAccessSecretKeyTestForPadding @@ -33,3 +36,4 @@ password: policy: length: 12 characters: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+" +