diff --git a/src/main/java/clap/server/adapter/inbound/security/filter/LoginAttemptFilter.java b/src/main/java/clap/server/adapter/inbound/security/filter/LoginAttemptFilter.java index c011a3d3..2fd65036 100644 --- a/src/main/java/clap/server/adapter/inbound/security/filter/LoginAttemptFilter.java +++ b/src/main/java/clap/server/adapter/inbound/security/filter/LoginAttemptFilter.java @@ -1,5 +1,6 @@ package clap.server.adapter.inbound.security.filter; +import clap.server.application.port.inbound.auth.CheckAccountLockStatusUseCase; import clap.server.application.service.auth.LoginAttemptService; import clap.server.exception.AuthException; import jakarta.servlet.FilterChain; @@ -25,7 +26,7 @@ @Slf4j public class LoginAttemptFilter extends OncePerRequestFilter { - private final LoginAttemptService loginAttemptService; + private final CheckAccountLockStatusUseCase checkAccountLockStatusUseCase; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -34,7 +35,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse if (request.getRequestURI().equals(LOGIN_ENDPOINT)) { String clientIp = getClientIp(request); - loginAttemptService.checkAccountIsLocked(clientIp); + checkAccountLockStatusUseCase.checkAccountIsLocked(clientIp); } } catch (AuthException e) { diff --git a/src/main/java/clap/server/application/port/inbound/auth/CheckAccountLockStatusUseCase.java b/src/main/java/clap/server/application/port/inbound/auth/CheckAccountLockStatusUseCase.java new file mode 100644 index 00000000..d102e1ab --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/auth/CheckAccountLockStatusUseCase.java @@ -0,0 +1,5 @@ +package clap.server.application.port.inbound.auth; + +public interface CheckAccountLockStatusUseCase { + void checkAccountIsLocked(String clientIp); +} diff --git a/src/main/java/clap/server/application/port/outbound/log/LoggingPort.java b/src/main/java/clap/server/application/port/outbound/log/LoggingPort.java new file mode 100644 index 00000000..6392f51e --- /dev/null +++ b/src/main/java/clap/server/application/port/outbound/log/LoggingPort.java @@ -0,0 +1,10 @@ +package clap.server.application.port.outbound.log; + +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; +import jakarta.servlet.http.HttpServletRequest; + +public interface LoggingPort { + void createAnonymousLog(HttpServletRequest request, int statusCode, String customCode, LogStatus logStatus, Object responseBody, String requestBody, String nickName); + void createMemberLog(HttpServletRequest request, int statusCode, String customCode,LogStatus logStatus, Object responseBody, String requestBody, Long memberId); + void createLoginFailedLog(HttpServletRequest request, int statusCode, String customCode, LogStatus logStatus, String requestBody, String nickName); +} \ No newline at end of file diff --git a/src/main/java/clap/server/application/service/auth/LoginAttemptService.java b/src/main/java/clap/server/application/service/auth/LoginAttemptService.java index ca53f242..282cbb6d 100644 --- a/src/main/java/clap/server/application/service/auth/LoginAttemptService.java +++ b/src/main/java/clap/server/application/service/auth/LoginAttemptService.java @@ -1,21 +1,22 @@ package clap.server.application.service.auth; +import clap.server.application.port.inbound.auth.CheckAccountLockStatusUseCase; import clap.server.application.port.outbound.auth.loginLog.CommandLoginLogPort; import clap.server.application.port.outbound.auth.loginLog.LoadLoginLogPort; import clap.server.domain.model.auth.LoginLog; import clap.server.exception.AuthException; import clap.server.exception.code.AuthErrorCode; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; @RequiredArgsConstructor -@Component +@Service @Transactional -public class LoginAttemptService { +public class LoginAttemptService implements CheckAccountLockStatusUseCase { private final LoadLoginLogPort loadLoginLogPort; private final CommandLoginLogPort commandLoginLogPort; private static final int MAX_FAILED_ATTEMPTS = 5; @@ -36,6 +37,7 @@ public void recordFailedAttempt(String clientIp, String attemptNickname) { commandLoginLogPort.save(loginLog); } + @Override public void checkAccountIsLocked(String clientIp) { LoginLog loginLog = loadLoginLogPort.findByClientIp(clientIp).orElse(null); if (loginLog == null) { diff --git a/src/main/java/clap/server/application/port/inbound/domain/LogService.java b/src/main/java/clap/server/application/service/log/LogService.java similarity index 88% rename from src/main/java/clap/server/application/port/inbound/domain/LogService.java rename to src/main/java/clap/server/application/service/log/LogService.java index 8b46c6ae..5d527820 100644 --- a/src/main/java/clap/server/application/port/inbound/domain/LogService.java +++ b/src/main/java/clap/server/application/service/log/LogService.java @@ -1,15 +1,15 @@ -package clap.server.application.port.inbound.domain; +package clap.server.application.service.log; import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; +import clap.server.application.port.inbound.domain.MemberService; import clap.server.application.port.outbound.auth.loginLog.LoadLoginLogPort; import clap.server.application.port.outbound.log.CommandLogPort; +import clap.server.application.port.outbound.log.LoggingPort; import clap.server.common.utils.ClientIpParseUtil; import clap.server.domain.model.auth.LoginLog; import clap.server.domain.model.log.AnonymousLog; import clap.server.domain.model.log.MemberLog; import clap.server.domain.model.member.Member; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -18,11 +18,10 @@ @Service @RequiredArgsConstructor @Transactional -public class LogService { +public class LogService implements LoggingPort { private final CommandLogPort commandLogPort; private final MemberService memberService; private final LoadLoginLogPort loadLoginLogPort; - private final ObjectMapper objectMapper; public void createAnonymousLog(HttpServletRequest request, int statusCode, String customCode, LogStatus logStatus, Object responseBody, String requestBody, String nickName) { AnonymousLog anonymousLog = AnonymousLog.createAnonymousLog(request, statusCode,customCode, logStatus, responseBody, requestBody, nickName); @@ -35,7 +34,7 @@ public void createMemberLog(HttpServletRequest request, int statusCode, String c commandLogPort.saveMemberLog(memberLog); } - public void createLoginFailedLog(HttpServletRequest request, int statusCode, String customCode, LogStatus logStatus, String requestBody, String nickName) throws JsonProcessingException { + public void createLoginFailedLog(HttpServletRequest request, int statusCode, String customCode, LogStatus logStatus, String requestBody, String nickName) { LoginLog loginLog = loadLoginLogPort.findByClientIp(ClientIpParseUtil.getClientIp(request)).orElse(null); String responseBody = loginLog != null ? loginLog.toSummaryString() : null; AnonymousLog anonymousLog = AnonymousLog.createAnonymousLog(request, statusCode,customCode, logStatus, responseBody, requestBody, nickName); diff --git a/src/main/java/clap/server/config/aop/LoggingAspect.java b/src/main/java/clap/server/config/aop/LoggingAspect.java index e1ff3e8e..da7de978 100644 --- a/src/main/java/clap/server/config/aop/LoggingAspect.java +++ b/src/main/java/clap/server/config/aop/LoggingAspect.java @@ -3,7 +3,7 @@ import clap.server.adapter.inbound.security.service.SecurityUserDetails; import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; -import clap.server.application.port.inbound.domain.LogService; +import clap.server.application.port.outbound.log.LoggingPort; import clap.server.common.annotation.log.LogType; import clap.server.exception.BaseException; import com.fasterxml.jackson.core.JsonProcessingException; @@ -34,7 +34,7 @@ @RequiredArgsConstructor public class LoggingAspect { private final ObjectMapper objectMapper; - private final LogService logService; + private final LoggingPort loggingPort; @Pointcut("execution(* clap.server.adapter.inbound.web..*Controller.*(..))") public void controllerMethods() { @@ -77,7 +77,7 @@ public Object logApiRequests(ProceedingJoinPoint joinPoint) throws Throwable { } else { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof SecurityUserDetails userDetails) { - logService.createMemberLog(request, statusCode, customCode, logStatus, result, getRequestBody(request), userDetails.getUserId()); + loggingPort.createMemberLog(request, statusCode, customCode, logStatus, result, getRequestBody(request), userDetails.getUserId()); } } } @@ -88,9 +88,9 @@ public Object logApiRequests(ProceedingJoinPoint joinPoint) throws Throwable { private void handleLoginLog(int statusCode, HttpServletRequest request, String customCode, LogStatus logStatus, Object result) throws JsonProcessingException { if (statusCode == HttpStatus.SC_OK) { - logService.createAnonymousLog(request, statusCode, customCode, logStatus, result, getRequestBody(request), getNicknameFromRequestBody(request)); + loggingPort.createAnonymousLog(request, statusCode, customCode, logStatus, result, getRequestBody(request), getNicknameFromRequestBody(request)); } else { - logService.createLoginFailedLog(request, statusCode, customCode, logStatus, getRequestBody(request), getNicknameFromRequestBody(request)); + loggingPort.createLoginFailedLog(request, statusCode, customCode, logStatus, getRequestBody(request), getNicknameFromRequestBody(request)); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 69d999b6..402e73d8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -17,8 +17,6 @@ spring: multipart: max-file-size: 5MB - - server: port: ${APPLICATION_PORT:8080} tomcat: @@ -29,6 +27,10 @@ server: domain: local: ${TASKFLOW_LOCAL_SERVER:127.0.0.1:8080} service: ${TASKFLOW_SERVICE_SERVER:127.0.0.1:8080} + servlet.session.cookie: + http-only: true + path: / + secure: true web: domain: diff --git a/src/test/java/clap/server/TestDataFactory.java b/src/test/java/clap/server/TestDataFactory.java index 31f1ac69..689d1f0d 100644 --- a/src/test/java/clap/server/TestDataFactory.java +++ b/src/test/java/clap/server/TestDataFactory.java @@ -28,7 +28,7 @@ public static Member createAdmin() { .emailNotificationEnabled(true) .imageUrl(null) .status(MemberStatus.ACTIVE) - .password("1111") + .password("Password123!") .department(createDepartment()) .build(); } @@ -43,7 +43,7 @@ public static Member createManagerWithReviewer() { .emailNotificationEnabled(true) .imageUrl(null) .status(MemberStatus.ACTIVE) - .password("1111") + .password("Password456!") .department(createDepartment()) .build(); } @@ -58,7 +58,7 @@ public static Member createManager() { .emailNotificationEnabled(true) .imageUrl(null) .status(MemberStatus.ACTIVE) - .password("1111") + .password("Password789!") .department(createDepartment()) .build(); } @@ -73,7 +73,7 @@ public static Member createUser() { .emailNotificationEnabled(true) .imageUrl(null) .status(MemberStatus.ACTIVE) - .password("1111") + .password("Password000!") .department(createDepartment()) .build(); } @@ -90,6 +90,33 @@ public static MemberInfo createAdminInfo() { .build(); } + public static Member createNotApprovedUser() { + return Member.builder() + .memberId(4L) + .memberInfo(createNotApprovedUserInfo()) + .admin(createAdmin()) + .kakaoworkNotificationEnabled(true) + .agitNotificationEnabled(true) + .emailNotificationEnabled(true) + .imageUrl(null) + .status(MemberStatus.APPROVAL_REQUEST) + .password("Password000!") + .department(createDepartment()) + .build(); + } + + public static MemberInfo createNotApprovedUserInfo() { + return MemberInfo.builder() + .name("홍길동(등록 대기중인 사용자)") + .email("atom8426@naver.com") + .nickname("atom.user") + .isReviewer(false) + .department(null) + .role(MemberRole.ROLE_USER) + .departmentRole("인프라") + .build(); + } + public static MemberInfo createManagerWithReviewerInfo() { return MemberInfo.builder() .name("홍길동(리뷰어)") diff --git a/src/test/java/clap/server/application/service/admin/RegisterMemberCsvServiceTest.java b/src/test/java/clap/server/application/service/admin/RegisterMemberCsvServiceTest.java index e71c339e..e28b309b 100644 --- a/src/test/java/clap/server/application/service/admin/RegisterMemberCsvServiceTest.java +++ b/src/test/java/clap/server/application/service/admin/RegisterMemberCsvServiceTest.java @@ -47,7 +47,6 @@ class RegisterMemberCSVServiceTest { @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); - // 예시로 Department가 빌더를 제공한다고 가정 dummyDepartment = Department.builder().departmentId(100L).build(); } @@ -142,7 +141,6 @@ void testRegisterMembersFromCsv_duplicateThrowsException() throws Exception { Member adminMember = mock(Member.class); when(memberService.findActiveMember(adminId)).thenReturn(adminMember); - // 중복 체크: 닉네임 또는 email 중 하나라도 중복이 있으면 에러 발생 when(loadMemberPort.existsByNicknamesOrEmails(Set.of(dummyMemberInfo1.getNickname()), Set.of(dummyMemberInfo1.getEmail()))) .thenReturn(true); diff --git a/src/test/java/clap/server/application/service/auth/AuthServiceTest.java b/src/test/java/clap/server/application/service/auth/AuthServiceTest.java new file mode 100644 index 00000000..b090fbd6 --- /dev/null +++ b/src/test/java/clap/server/application/service/auth/AuthServiceTest.java @@ -0,0 +1,120 @@ +package clap.server.application.service.auth; + +import clap.server.TestDataFactory; +import clap.server.adapter.inbound.web.dto.auth.response.LoginResponse; +import clap.server.application.port.outbound.member.LoadMemberPort; +import clap.server.domain.model.auth.CustomJwts; +import clap.server.domain.model.member.Member; +import clap.server.exception.AuthException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AuthServiceTest { + + @InjectMocks + private AuthService authService; + + @Mock + private LoadMemberPort loadMemberPort; + @Mock + private ManageTokenService manageTokenService; + @Mock + private PasswordEncoder passwordEncoder; + @Mock + private LoginAttemptService loginAttemptService; + @Mock + private RefreshTokenService refreshTokenService; + + private Member user; + private Member notApprovedUser; + + @BeforeEach + void setUp() { + user = TestDataFactory.createUser(); + notApprovedUser = TestDataFactory.createNotApprovedUser(); + } + + @Test + @DisplayName("로그인 성공") + void loginSuccess() { + // Given + String nickname = "atom.user"; + String inputPassword = "Password000!"; + String clientIp = "127.0.0.1"; + Member member = user; + CustomJwts jwtTokens = new CustomJwts("accessToken", "refreshToken"); + + when(loadMemberPort.findByNickname(nickname)).thenReturn(Optional.of(member)); + when(passwordEncoder.matches(inputPassword, member.getPassword())).thenReturn(true); + when(manageTokenService.issueTokens(member)).thenReturn(jwtTokens); + + // When + LoginResponse response = authService.login(nickname, inputPassword, clientIp); + + // Then + assertNotNull(response); + assertEquals(jwtTokens.accessToken(), response.accessToken()); + assertEquals(jwtTokens.refreshToken(), response.refreshToken()); + verify(loginAttemptService).resetFailedAttempts(clientIp); + verify(refreshTokenService).saveRefreshToken(any()); + } + + @Test + @DisplayName("잘못된 비밀번호를 입력하면 로그인 실패한다.") + void loginFailureWrongPassword() { + // Given + String nickname = "atom.user"; + String inputPassword = "wrongPassword000!"; + String clientIp = "127.0.0.1"; + Member member = user; + + when(loadMemberPort.findByNickname(nickname)).thenReturn(Optional.of(member)); + when(passwordEncoder.matches(inputPassword, member.getPassword())).thenReturn(false); + + // When & Then + assertThrows(AuthException.class, () -> authService.login(nickname, inputPassword, clientIp)); + verify(loginAttemptService).recordFailedAttempt(clientIp, nickname); + } + + + @Test + @DisplayName("사용자가 초기 로그인 시 임시 토큰이 발급된다.") + void loginWithApprovalRequestStatus() { + // Given + String nickname = "atom.user"; + String inputPassword = "Password000!"; + String clientIp = "127.0.0.1"; + + Member member = notApprovedUser; + String temporaryToken = "temporaryToken"; + + when(loadMemberPort.findByNickname(nickname)).thenReturn(Optional.of(member)); + when(passwordEncoder.matches(inputPassword, member.getPassword())).thenReturn(true); + when(manageTokenService.issueTemporaryToken(notApprovedUser.getMemberId())).thenReturn(temporaryToken); + + // When + LoginResponse response = authService.login(nickname, inputPassword, clientIp); + + // Then + assertNotNull(response); + assertEquals(temporaryToken, response.accessToken()); + assertNull(response.refreshToken()); + verify(manageTokenService).issueTemporaryToken(notApprovedUser.getMemberId()); + verify(manageTokenService, never()).issueTokens(any()); + verify(refreshTokenService, never()).saveRefreshToken(any()); + verify(loginAttemptService, never()).resetFailedAttempts(any()); + } + +} diff --git a/src/test/java/clap/server/application/service/auth/LoginAttemptServiceTest.java b/src/test/java/clap/server/application/service/auth/LoginAttemptServiceTest.java new file mode 100644 index 00000000..5bfdc524 --- /dev/null +++ b/src/test/java/clap/server/application/service/auth/LoginAttemptServiceTest.java @@ -0,0 +1,135 @@ +package clap.server.application.service.auth; + +import clap.server.application.port.outbound.auth.loginLog.CommandLoginLogPort; +import clap.server.application.port.outbound.auth.loginLog.LoadLoginLogPort; +import clap.server.domain.model.auth.LoginLog; +import clap.server.exception.AuthException; +import clap.server.exception.code.AuthErrorCode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class LoginAttemptServiceTest { + + @InjectMocks + private LoginAttemptService loginAttemptService; + + @Mock + private LoadLoginLogPort loadLoginLogPort; + + @Mock + private CommandLoginLogPort commandLoginLogPort; + + private final String clientIp = "192.168.1.1"; + private LoginLog existingLoginLog; + private LoginLog lockedAccountLoginLog; + private LoginLog lockTimeExpiredLoginLog; + + public static LoginLog createLoginLog(String clientIp, int count, boolean isLocked, LocalDateTime lastAttemptAt){ + return LoginLog.builder() + .attemptNickname("testUser") + .lastAttemptAt(lastAttemptAt) + .failedCount(count) + .isLocked(isLocked) + .clientIp(clientIp) + .build(); + } + + @BeforeEach + void setUp() { + existingLoginLog = createLoginLog(clientIp, 3, false, LocalDateTime.now()); + lockedAccountLoginLog = createLoginLog(clientIp, 5, true, LocalDateTime.now()); + lockTimeExpiredLoginLog = createLoginLog(clientIp, 5, true, LocalDateTime.now().minusMinutes(31)); + } + + @Test + @DisplayName("로그인에 실패하면 IP를 통해 로그인 실패 기록이 저장된다.") + void recordFailedAttempt_NewIP() { + String nickname = "testUser"; + when(loadLoginLogPort.findByClientIp(clientIp)).thenReturn(Optional.empty()); + + loginAttemptService.recordFailedAttempt(clientIp, nickname); + + verify(commandLoginLogPort).save(any(LoginLog.class)); + } + + @Test + @DisplayName("기존 IP로 로그인에 실패하면 로그인 실패 기록이 갱신된다.") + void recordFailedAttempt_ExistingIP_BeforeLock() { + String nickname = "testUser"; + LoginLog existingLog = existingLoginLog;; + + when(loadLoginLogPort.findByClientIp(clientIp)).thenReturn(Optional.of(existingLog)); + + loginAttemptService.recordFailedAttempt(clientIp, nickname); + + verify(commandLoginLogPort).save(existingLog); + assertEquals(4, existingLog.getFailedCount()); + assertFalse(existingLog.isLocked()); + } + + @Test + @DisplayName("기존 IP로 로그인에 5회 실패하면 계정이 잠긴다.") + void recordFailedAttempt_AccountLock() { + String nickname = "testUser"; + LoginLog existingLog = existingLoginLog; + existingLog.recordFailedAttempt(); + + when(loadLoginLogPort.findByClientIp(clientIp)).thenReturn(Optional.of(existingLog)); + + assertThrows(AuthException.class, () -> loginAttemptService.recordFailedAttempt(clientIp, nickname)); + + verify(commandLoginLogPort).save(existingLog); + assertTrue(existingLog.isLocked()); + } + + @Test + @DisplayName("로그인 5회 실패가 아닐시에는 계정 잠금처리가 되지 않는다.") + void checkAccountIsLocked_NotLocked() { + LoginLog loginLog = existingLoginLog; + loginLog.setLocked(false); + + when(loadLoginLogPort.findByClientIp(clientIp)).thenReturn(Optional.of(loginLog)); + + assertDoesNotThrow(() -> loginAttemptService.checkAccountIsLocked(clientIp)); + } + + @Test + @DisplayName("잠긴 계정에 대해 로그인 시도시에 AUTH_012 예외가 발생한다.") + void checkAccountIsLocked_Locked() { + LoginLog loginLog = lockedAccountLoginLog; + + when(loadLoginLogPort.findByClientIp(clientIp)).thenReturn(Optional.of(loginLog)); + + AuthException exception = assertThrows(AuthException.class, () -> + loginAttemptService.checkAccountIsLocked(clientIp) + ); + + assertEquals(AuthErrorCode.ACCOUNT_IS_LOCKED.getMessage(), exception.getMessage(), + "잠긴 계정에 대해 로그인 시도시에 AUTH_012 예외가 발생해야 합니다."); + } + + @Test + @DisplayName("30분이 지난 후 잠김 여부를 확인하면 AUTH_012 예외를 반환하지 않는다.") + void checkAccountIsLocked_LockTimeExpired() { + LoginLog loginLog = lockTimeExpiredLoginLog; + + when(loadLoginLogPort.findByClientIp(clientIp)).thenReturn(Optional.of(loginLog)); + + loginAttemptService.checkAccountIsLocked(clientIp); + + verify(commandLoginLogPort).deleteById(clientIp); + } + +} diff --git a/src/test/java/clap/server/application/statistics/FindTaskProcessServiceTest.java b/src/test/java/clap/server/application/statistics/FindTaskProcessServiceTest.java index a5cc9c30..2b945f9e 100644 --- a/src/test/java/clap/server/application/statistics/FindTaskProcessServiceTest.java +++ b/src/test/java/clap/server/application/statistics/FindTaskProcessServiceTest.java @@ -29,7 +29,7 @@ class FindTaskProcessServiceTest { private FindTaskProcessService findTaskProcessService; @Test - @DisplayName("기간별 요청량") + @DisplayName("기간별 요청량 조회") void aggregatePeriodTaskRequest() { //given Map statistics = new TreeMap<>(); @@ -58,7 +58,7 @@ void aggregatePeriodTaskRequest() { } @Test - @DisplayName("기간별 처리량") + @DisplayName("기간별 처리량 조회") void aggregatePeriodTaskProcess() { //given Map statistics = new TreeMap<>(); @@ -87,7 +87,7 @@ void aggregatePeriodTaskProcess() { } @Test - @DisplayName("카테고리별 요청량") + @DisplayName("카테고리별 요청량 조회") void aggregateCategoryTaskRequest() { //given Map statistics = new TreeMap<>(); @@ -110,7 +110,7 @@ void aggregateCategoryTaskRequest() { } @Test - @DisplayName("담당자별 처리량") + @DisplayName("담당자별 처리량 조회") void aggregateManagerTaskProcess() { //given Map statistics = new TreeMap<>();