From ed0159eddac7d42599badb3ef77430861a03d901 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 12:27:20 +0900 Subject: [PATCH 01/16] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=88=98=EC=A0=95,=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EA=B3=B5=ED=86=B5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/AuthFailureEventListener.java | 31 ----------- .../auth/CustomAuthenticationProvider.java | 30 +++++++++-- .../cd/kamcoback/common/enums/RoleType.java | 11 +++- .../cd/kamcoback/common/enums/StatusType.java | 25 +++++++++ .../common/enums/error/AuthErrorCode.java | 30 +++++++++++ .../common/exception/CustomApiException.java | 6 +++ .../cd/kamcoback/common/utils/ErrorCode.java | 10 ++++ .../cd/kamcoback/config/SecurityConfig.java | 32 ++++++------ .../kamcoback/config/api/ApiResponseDto.java | 21 +++++--- .../cd/kamcoback/config/enums/EnumType.java | 13 +++++ .../cd/kamcoback/members/AuthController.java | 52 +++++++++++++++---- .../members/MembersApiController.java | 2 +- .../cd/kamcoback/members/dto/MembersDto.java | 20 +++++++ .../members/service/AuthService.java | 21 -------- .../members/service/MembersService.java | 7 +-- .../postgres/core/MembersCoreService.java | 44 ++-------------- 16 files changed, 222 insertions(+), 133 deletions(-) delete mode 100644 src/main/java/com/kamco/cd/kamcoback/auth/AuthFailureEventListener.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/common/utils/ErrorCode.java diff --git a/src/main/java/com/kamco/cd/kamcoback/auth/AuthFailureEventListener.java b/src/main/java/com/kamco/cd/kamcoback/auth/AuthFailureEventListener.java deleted file mode 100644 index 75486d9e..00000000 --- a/src/main/java/com/kamco/cd/kamcoback/auth/AuthFailureEventListener.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.kamco.cd.kamcoback.auth; - -import com.kamco.cd.kamcoback.members.service.AuthService; -import java.util.UUID; -import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationListener; -import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class AuthFailureEventListener implements ApplicationListener { - - private final AuthService authService; - - @Override - public void onApplicationEvent(AbstractAuthenticationFailureEvent event) { - // 로그인 시도에 사용된 (username) - Object principal = event.getAuthentication().getPrincipal(); - - if (principal instanceof String username) { - // 로그인 실패 카운트 증가 로직 호출 - authService.loginFail(UUID.fromString(username)); - } - } - - @Override - public boolean supportsAsyncExecution() { - return ApplicationListener.super.supportsAsyncExecution(); - } -} diff --git a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java index 8e6cb1bf..deeaf594 100644 --- a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java +++ b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java @@ -1,11 +1,13 @@ package com.kamco.cd.kamcoback.auth; +import com.kamco.cd.kamcoback.common.enums.error.AuthErrorCode; +import com.kamco.cd.kamcoback.common.exception.CustomApiException; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository; +import java.time.ZonedDateTime; import lombok.RequiredArgsConstructor; import org.mindrot.jbcrypt.BCrypt; import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -28,14 +30,34 @@ public class CustomAuthenticationProvider implements AuthenticationProvider { MemberEntity member = membersRepository .findByUserId(username) - .orElseThrow(() -> new BadCredentialsException("ID 또는 비밀번호가 일치하지 않습니다.")); + .orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND)); // 2. jBCrypt + 커스텀 salt 로 저장된 패스워드 비교 if (!BCrypt.checkpw(rawPassword, member.getPassword())) { - throw new BadCredentialsException("ID 또는 비밀번호가 일치하지 않습니다."); + // 실패 카운트 저장 + int cnt = member.getLoginFailCount() + 1; + if (cnt >= 5) { + member.setStatus("INACTIVE"); + } + member.setLoginFailCount(cnt); + membersRepository.save(member); + throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH); } - // 3. 인증 성공 → UserDetails 생성 + // 3. 패스워드 실패 횟수 체크 + if (member.getLoginFailCount() >= 5) { + throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED); + } + + // 4. 인증 성공 로그인 시간 저장 + if (member.getFirstLoginDttm() == null) { + member.setFirstLoginDttm(ZonedDateTime.now()); + } + member.setLastLoginDttm(ZonedDateTime.now()); + member.setLoginFailCount(0); + membersRepository.save(member); + + // 5. 인증 성공 → UserDetails 생성 CustomUserDetails userDetails = new CustomUserDetails(member); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/RoleType.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/RoleType.java index 3da213ea..d1945c7e 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/enums/RoleType.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/RoleType.java @@ -7,7 +7,7 @@ import lombok.Getter; @Getter @AllArgsConstructor public enum RoleType implements EnumType { - ROLE_ADMIN("시스템 관리자"), + ROLE_ADMIN("관리자"), ROLE_LABELER("라벨러"), ROLE_REVIEWER("검수자"); @@ -22,4 +22,13 @@ public enum RoleType implements EnumType { public String getText() { return desc; } + + public static RoleType from(String value) { + for (RoleType type : values()) { + if (type.name().equalsIgnoreCase(value)) { + return type; + } + } + return null; + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java new file mode 100644 index 00000000..641d3de5 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java @@ -0,0 +1,25 @@ +package com.kamco.cd.kamcoback.common.enums; + +import com.kamco.cd.kamcoback.config.enums.EnumType; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum StatusType implements EnumType { + ACTIVE("활성"), + INACTIVE("비활성"), + ARCHIVED("탈퇴"); + + private final String desc; + + @Override + public String getId() { + return name(); + } + + @Override + public String getText() { + return desc; + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java new file mode 100644 index 00000000..3eca6ef1 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java @@ -0,0 +1,30 @@ +package com.kamco.cd.kamcoback.common.enums.error; + +import com.kamco.cd.kamcoback.common.utils.ErrorCode; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum AuthErrorCode implements ErrorCode { + + // 🔐 로그인 관련 + LOGIN_ID_NOT_FOUND( + "LOGIN_ID_NOT_FOUND", + HttpStatus.UNAUTHORIZED), + + LOGIN_PASSWORD_MISMATCH( + "LOGIN_PASSWORD_MISMATCH", + HttpStatus.UNAUTHORIZED), + + LOGIN_PASSWORD_EXCEEDED( + "LOGIN_PASSWORD_EXCEEDED", + HttpStatus.UNAUTHORIZED); + + private final String code; + private final HttpStatus status; + + AuthErrorCode(String code, HttpStatus status) { + this.code = code; + this.status = status; + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/common/exception/CustomApiException.java b/src/main/java/com/kamco/cd/kamcoback/common/exception/CustomApiException.java index 482d5f6c..cd3a3a1a 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/exception/CustomApiException.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/exception/CustomApiException.java @@ -1,5 +1,6 @@ package com.kamco.cd.kamcoback.common.exception; +import com.kamco.cd.kamcoback.common.utils.ErrorCode; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -19,4 +20,9 @@ public class CustomApiException extends RuntimeException { this.codeName = codeName; this.status = status; } + + public CustomApiException(ErrorCode errorCode) { + this.codeName = errorCode.getCode(); + this.status = errorCode.getStatus(); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/common/utils/ErrorCode.java b/src/main/java/com/kamco/cd/kamcoback/common/utils/ErrorCode.java new file mode 100644 index 00000000..8ace8fe1 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/utils/ErrorCode.java @@ -0,0 +1,10 @@ +package com.kamco.cd.kamcoback.common.utils; + +import org.springframework.http.HttpStatus; + +public interface ErrorCode { + + String getCode(); + + HttpStatus getStatus(); +} diff --git a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java index a952e535..448fa31f 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java @@ -6,12 +6,14 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -40,21 +42,21 @@ public class SecurityConfig { customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용 .authorizeHttpRequests( auth -> - auth.anyRequest().permitAll()); -// requestMatchers(HttpMethod.OPTIONS, "/**") -// .permitAll() // preflight 허용 -// .requestMatchers( -// "/api/auth/signin", -// "/api/auth/refresh", -// "/swagger-ui/**", -// "/v3/api-docs/**") -// .permitAll() -// .anyRequest() -// .authenticated()) -// .addFilterBefore( -// jwtAuthenticationFilter, -// UsernamePasswordAuthenticationFilter -// .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. + auth.requestMatchers(HttpMethod.OPTIONS, "/**") + .permitAll() // preflight 허용 + .requestMatchers( + "/api/auth/signin", + "/api/auth/refresh", + "/swagger-ui/**", + "/api/members/{memberId}/password", + "/v3/api-docs/**") + .permitAll() + .anyRequest() + .authenticated()) + .addFilterBefore( + jwtAuthenticationFilter, + UsernamePasswordAuthenticationFilter + .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. ; return http.build(); diff --git a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java index 3cd2d792..de466c5d 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java @@ -20,9 +20,11 @@ public class ApiResponseDto { @JsonInclude(JsonInclude.Include.NON_NULL) private T errorData; - @JsonIgnore private HttpStatus httpStatus; + @JsonIgnore + private HttpStatus httpStatus; - @JsonIgnore private Long errorLogUid; + @JsonIgnore + private Long errorLogUid; public ApiResponseDto(T data) { this.data = data; @@ -47,7 +49,7 @@ public class ApiResponseDto { } public ApiResponseDto( - ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { + ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { this.error = new Error(code.getId(), message); this.httpStatus = httpStatus; this.errorLogUid = errorLogUid; @@ -88,17 +90,17 @@ public class ApiResponseDto { } public static ApiResponseDto createException( - ApiResponseCode code, String message, HttpStatus httpStatus) { + ApiResponseCode code, String message, HttpStatus httpStatus) { return new ApiResponseDto<>(code, message, httpStatus); } public static ApiResponseDto createException( - ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { + ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { return new ApiResponseDto<>(code, message, httpStatus, errorLogUid); } public static ApiResponseDto createException( - ApiResponseCode code, String message, T data) { + ApiResponseCode code, String message, T data) { return new ApiResponseDto<>(code, message, data); } @@ -114,7 +116,9 @@ public class ApiResponseDto { } } - /** Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object */ + /** + * Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object + */ @Getter public static class ResponseObj { @@ -164,6 +168,9 @@ public class ApiResponseDto { NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."), NOT_FOUND_USER("사용자를 찾을 수 없습니다."), UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."), + LOGIN_ID_NOT_FOUND("아이디를 잘못 입력하셨습니다."), + LOGIN_PASSWORD_MISMATCH("비밀번호를 잘못 입력하셨습니다."), + LOGIN_PASSWORD_EXCEEDED("비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.\n로그인 오류에 대해 관리자에게 문의하시기 바랍니다."), INVALID_EMAIL_TOKEN( "You can only reset your password within 24 hours from when the email was sent.\n" + "To reset your password again, please submit a new request through \"Forgot" diff --git a/src/main/java/com/kamco/cd/kamcoback/config/enums/EnumType.java b/src/main/java/com/kamco/cd/kamcoback/config/enums/EnumType.java index 274bfe72..1be49253 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/enums/EnumType.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/enums/EnumType.java @@ -5,4 +5,17 @@ public interface EnumType { String getId(); String getText(); + + // code로 text + static & EnumType> E fromId(Class enumClass, String id) { + if (id == null) { + return null; + } + for (E e : enumClass.getEnumConstants()) { + if (id.equalsIgnoreCase(e.getId())) { + return e; + } + } + return null; // 못 찾으면 null + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java index 47b76169..e948c14e 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -7,6 +7,7 @@ import com.kamco.cd.kamcoback.members.dto.SignInRequest; import com.kamco.cd.kamcoback.members.service.AuthService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -14,7 +15,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import java.nio.file.AccessDeniedException; import java.time.Duration; -import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; @@ -55,12 +55,43 @@ public class AuthController { content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( responseCode = "401", - description = "ID 또는 비밀번호 불일치", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))), - @ApiResponse( - responseCode = "400", - description = "미사용 상태", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + description = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)", + content = + @Content( + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "아이디 입력 오류", + description = "존재하지 않는 아이디", + value = """ + { + "code": "LOGIN_ID_NOT_FOUND", + "message": "아이디를 잘못 입력하셨습니다.", + "detail": null + } + """), + @ExampleObject( + name = "비밀번호 입력 오류 (4회 이하)", + description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우", + value = """ + { + "code": "LOGIN_PASSWORD_MISMATCH", + "message": "비밀번호를 잘못 입력하셨습니다.", + "detail": "비밀번호 입력 오류 3회, 2회 남았습니다." + } + """), + @ExampleObject( + name = "비밀번호 오류 횟수 초과", + description = "비밀번호 5회 이상 오류로 계정 잠김", + value = """ + { + "code": "LOGIN_PASSWORD_EXCEEDED", + "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.", + "detail": "로그인 오류에 대해 관리자에게 문의하시기 바랍니다." + } + """) + } + )) }) public ApiResponseDto signin( @io.swagger.v3.oas.annotations.parameters.RequestBody( @@ -75,15 +106,14 @@ public class AuthController { new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); String status = authService.getUserStatus(request); - if ("INACTIVE".equals(status)) { + + // INACTIVE 비활성 상태(새로운 패스워드 입력 해야함), ARCHIVED 탈퇴 + if (!"ACTIVE".equals(status)) { return ApiResponseDto.ok(new TokenResponse(status, null, null)); } String username = authentication.getName(); // UserDetailsService 에서 사용한 username - // 로그인 시간 저장 - authService.saveLogin(UUID.fromString(username)); - String accessToken = jwtTokenProvider.createAccessToken(username); String refreshToken = jwtTokenProvider.createRefreshToken(username); diff --git a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java index 6f63553c..633bf10e 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java @@ -64,7 +64,7 @@ public class MembersApiController { @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) }) @PatchMapping("/{memberId}/password") - public ApiResponseDto resetPassword(@PathVariable Long memberId, @RequestBody @Valid MembersDto.InitReq initReq) { + public ApiResponseDto resetPassword(@PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) { membersService.resetPassword(memberId, initReq); return ApiResponseDto.createOK(memberId); } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java index d28d39ca..3f5f0616 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java @@ -1,6 +1,9 @@ package com.kamco.cd.kamcoback.members.dto; +import com.kamco.cd.kamcoback.common.enums.RoleType; +import com.kamco.cd.kamcoback.common.enums.StatusType; import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm; +import com.kamco.cd.kamcoback.config.enums.EnumType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; @@ -22,11 +25,13 @@ public class MembersDto { private Long id; private UUID uuid; private String userRole; + private String userRoleName; private String name; private String userId; private String employeeNo; private String tempPassword; private String status; + private String statusName; @JsonFormatDttm private ZonedDateTime createdDttm; @JsonFormatDttm @@ -40,11 +45,13 @@ public class MembersDto { Long id, UUID uuid, String userRole, + String userRoleName, String name, String userId, String employeeNo, String tempPassword, String status, + String statusName, ZonedDateTime createdDttm, ZonedDateTime updatedDttm, ZonedDateTime firstLoginDttm, @@ -53,16 +60,29 @@ public class MembersDto { this.id = id; this.uuid = uuid; this.userRole = userRole; + this.userRoleName = getUserRoleName(userRole); this.name = name; this.userId = userId; this.employeeNo = employeeNo; this.tempPassword = tempPassword; this.status = status; + this.statusName = getStatusName(status); this.createdDttm = createdDttm; this.updatedDttm = updatedDttm; this.firstLoginDttm = firstLoginDttm; this.lastLoginDttm = lastLoginDttm; } + + private String getUserRoleName(String roleId) { + RoleType type = EnumType.fromId(RoleType.class, roleId); + return type.getText(); + } + + private String getStatusName(String status) { + StatusType type = EnumType.fromId(StatusType.class, status); + return type.getText(); + } + } @Getter diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java index 5433750b..1dcfc578 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java @@ -2,7 +2,6 @@ package com.kamco.cd.kamcoback.members.service; import com.kamco.cd.kamcoback.members.dto.SignInRequest; import com.kamco.cd.kamcoback.postgres.core.MembersCoreService; -import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,26 +13,6 @@ public class AuthService { private final MembersCoreService membersCoreService; - /** - * 로그인 일시 저장 - * - * @param uuid - */ - @Transactional - public void saveLogin(UUID uuid) { - membersCoreService.saveLogin(uuid); - } - - /** - * 로그인 실패 저장 - * - * @param uuid - */ - @Transactional - public void loginFail(UUID uuid) { - membersCoreService.loginFail(uuid); - } - /** * 사용자 상태 조회 * diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java index 945d4868..d68352a4 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java @@ -35,7 +35,7 @@ public class MembersService { * @param initReq */ @Transactional - public void resetPassword(Long id, MembersDto.InitReq initReq) { + public void resetPassword(String id, MembersDto.InitReq initReq) { if (!isValidPassword(initReq.getPassword())) { throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST); } @@ -49,7 +49,8 @@ public class MembersService { * @return */ private boolean isValidPassword(String password) { - String regex = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[!@#$]).{8,20}$"; - return Pattern.matches(regex, password); + String passwordPattern = + "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$"; + return Pattern.matches(passwordPattern, password); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java index d1a1b642..756e9f71 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java @@ -1,6 +1,8 @@ package com.kamco.cd.kamcoback.postgres.core; import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator; +import com.kamco.cd.kamcoback.common.enums.error.AuthErrorCode; +import com.kamco.cd.kamcoback.common.exception.CustomApiException; import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.MembersDto.AddReq; import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; @@ -8,7 +10,6 @@ import com.kamco.cd.kamcoback.members.dto.SignInRequest; import com.kamco.cd.kamcoback.members.exception.MemberException.DuplicateMemberException; import com.kamco.cd.kamcoback.members.exception.MemberException.DuplicateMemberException.Field; import com.kamco.cd.kamcoback.members.exception.MemberException.MemberNotFoundException; -import com.kamco.cd.kamcoback.members.exception.MemberException.PasswordNotFoundException; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository; import java.time.ZonedDateTime; @@ -99,12 +100,12 @@ public class MembersCoreService { * * @param id */ - public void resetPassword(Long id, MembersDto.InitReq initReq) { + public void resetPassword(String id, MembersDto.InitReq initReq) { MemberEntity memberEntity = - membersRepository.findById(id).orElseThrow(() -> new MemberNotFoundException()); + membersRepository.findByUserId(id).orElseThrow(() -> new MemberNotFoundException()); if (!memberEntity.getTempPassword().equals(initReq.getTempPassword())) { - throw new PasswordNotFoundException(); + throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH); } String salt = @@ -129,41 +130,6 @@ public class MembersCoreService { return membersRepository.findByMembers(searchReq); } - /** - * 최초 로그인 저장 마지막 로그인 저장 - * - * @param uuid - */ - public void saveLogin(UUID uuid) { - MemberEntity memberEntity = - membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); - - if (memberEntity.getFirstLoginDttm() == null) { - memberEntity.setFirstLoginDttm(ZonedDateTime.now()); - } - memberEntity.setLastLoginDttm(ZonedDateTime.now()); - memberEntity.setLoginFailCount(0); - membersRepository.save(memberEntity); - } - - /** - * 로그인 실패시 상태 저장 - * - * @param uuid - */ - public void loginFail(UUID uuid) { - MemberEntity memberEntity = - membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); - - int failCnt = memberEntity.getLoginFailCount() + 1; - if (failCnt >= 5) { - memberEntity.setStatus("INACTIVE"); - } - - memberEntity.setLoginFailCount(failCnt); - membersRepository.save(memberEntity); - } - /** * 사용자 상태 조회 * From ec19cf533acc84c30c5f06cdf3ebe19cac8fd9af Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 13:47:18 +0900 Subject: [PATCH 02/16] =?UTF-8?q?=ED=8C=A8=EC=8A=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/kamco/cd/kamcoback/members/AuthController.java | 9 +++------ .../kamco/cd/kamcoback/members/MembersApiController.java | 7 +++++++ .../cd/kamcoback/members/service/MembersService.java | 1 + .../cd/kamcoback/postgres/core/MembersCoreService.java | 6 ------ 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java index e948c14e..9a8d2703 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -66,8 +66,7 @@ public class AuthController { value = """ { "code": "LOGIN_ID_NOT_FOUND", - "message": "아이디를 잘못 입력하셨습니다.", - "detail": null + "message": "아이디를 잘못 입력하셨습니다." } """), @ExampleObject( @@ -76,8 +75,7 @@ public class AuthController { value = """ { "code": "LOGIN_PASSWORD_MISMATCH", - "message": "비밀번호를 잘못 입력하셨습니다.", - "detail": "비밀번호 입력 오류 3회, 2회 남았습니다." + "message": "비밀번호를 잘못 입력하셨습니다." } """), @ExampleObject( @@ -86,8 +84,7 @@ public class AuthController { value = """ { "code": "LOGIN_PASSWORD_EXCEEDED", - "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.", - "detail": "로그인 오류에 대해 관리자에게 문의하시기 바랍니다." + "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다." } """) } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java index 633bf10e..0c146f8a 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java @@ -14,6 +14,8 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -27,6 +29,7 @@ import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor public class MembersApiController { + private final AuthenticationManager authenticationManager; private final MembersService membersService; @Operation(summary = "회원정보 목록", description = "회원정보 조회") @@ -65,6 +68,10 @@ public class MembersApiController { }) @PatchMapping("/{memberId}/password") public ApiResponseDto resetPassword(@PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) { + + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(memberId, initReq.getTempPassword())); + membersService.resetPassword(memberId, initReq); return ApiResponseDto.createOK(memberId); } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java index d68352a4..3f63f430 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java @@ -36,6 +36,7 @@ public class MembersService { */ @Transactional public void resetPassword(String id, MembersDto.InitReq initReq) { + if (!isValidPassword(initReq.getPassword())) { throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java index 756e9f71..fc39f559 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java @@ -1,8 +1,6 @@ package com.kamco.cd.kamcoback.postgres.core; import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator; -import com.kamco.cd.kamcoback.common.enums.error.AuthErrorCode; -import com.kamco.cd.kamcoback.common.exception.CustomApiException; import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.MembersDto.AddReq; import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; @@ -104,10 +102,6 @@ public class MembersCoreService { MemberEntity memberEntity = membersRepository.findByUserId(id).orElseThrow(() -> new MemberNotFoundException()); - if (!memberEntity.getTempPassword().equals(initReq.getTempPassword())) { - throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH); - } - String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); // 패스워드 암호화 From d798dc16f9eed903aade91083e00646c3aef1d0a Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 13:47:57 +0900 Subject: [PATCH 03/16] =?UTF-8?q?spotlessApply=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/CustomAuthenticationProvider.java | 6 +- .../code/CommonCodeApiController.java | 2 +- .../common/enums/error/AuthErrorCode.java | 12 +- .../cd/kamcoback/config/SecurityConfig.java | 56 ++++---- .../kamcoback/config/api/ApiResponseDto.java | 18 +-- .../kamcoback/members/AdminApiController.java | 112 +++++++-------- .../cd/kamcoback/members/AuthController.java | 136 +++++++++--------- .../members/MembersApiController.java | 54 +++---- .../cd/kamcoback/members/dto/MembersDto.java | 48 +++---- .../members/service/AdminService.java | 1 - .../members/service/MembersService.java | 2 +- .../postgres/core/MembersCoreService.java | 16 ++- .../code/CommonCodeRepositoryImpl.java | 3 +- .../members/MembersRepositoryImpl.java | 74 +++++----- 14 files changed, 261 insertions(+), 279 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java index deeaf594..dd898258 100644 --- a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java +++ b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java @@ -28,9 +28,9 @@ public class CustomAuthenticationProvider implements AuthenticationProvider { // 1. 유저 조회 MemberEntity member = - membersRepository - .findByUserId(username) - .orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND)); + membersRepository + .findByUserId(username) + .orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND)); // 2. jBCrypt + 커스텀 salt 로 저장된 패스워드 비교 if (!BCrypt.checkpw(rawPassword, member.getPassword())) { diff --git a/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java b/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java index eaa7f2d6..c3ff3942 100644 --- a/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java @@ -219,7 +219,7 @@ public class CommonCodeApiController { // .map(Clazzes::new) // .toList(); - //변화탐지 clazz API : enum -> 공통코드로 변경 + // 변화탐지 clazz API : enum -> 공통코드로 변경 List list = commonCodeUtil.getChildCodesByParentCode("0000").stream() .map( diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java index 3eca6ef1..91956284 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java @@ -8,17 +8,11 @@ import org.springframework.http.HttpStatus; public enum AuthErrorCode implements ErrorCode { // 🔐 로그인 관련 - LOGIN_ID_NOT_FOUND( - "LOGIN_ID_NOT_FOUND", - HttpStatus.UNAUTHORIZED), + LOGIN_ID_NOT_FOUND("LOGIN_ID_NOT_FOUND", HttpStatus.UNAUTHORIZED), - LOGIN_PASSWORD_MISMATCH( - "LOGIN_PASSWORD_MISMATCH", - HttpStatus.UNAUTHORIZED), + LOGIN_PASSWORD_MISMATCH("LOGIN_PASSWORD_MISMATCH", HttpStatus.UNAUTHORIZED), - LOGIN_PASSWORD_EXCEEDED( - "LOGIN_PASSWORD_EXCEEDED", - HttpStatus.UNAUTHORIZED); + LOGIN_PASSWORD_EXCEEDED("LOGIN_PASSWORD_EXCEEDED", HttpStatus.UNAUTHORIZED); private final String code; private final HttpStatus status; diff --git a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java index 448fa31f..2fecb1cd 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java @@ -30,33 +30,33 @@ public class SecurityConfig { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.cors(cors -> cors.configurationSource(corsConfigurationSource())) - .csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화 - .sessionManagement( - sm -> - sm.sessionCreationPolicy( - SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음, 요청은 JWT 인증 - .formLogin(form -> form.disable()) // react에서 로그인 요청 관리 - .httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용 - .logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음 - .authenticationProvider( - customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용 - .authorizeHttpRequests( - auth -> - auth.requestMatchers(HttpMethod.OPTIONS, "/**") - .permitAll() // preflight 허용 - .requestMatchers( - "/api/auth/signin", - "/api/auth/refresh", - "/swagger-ui/**", - "/api/members/{memberId}/password", - "/v3/api-docs/**") - .permitAll() - .anyRequest() - .authenticated()) - .addFilterBefore( - jwtAuthenticationFilter, - UsernamePasswordAuthenticationFilter - .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. + .csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화 + .sessionManagement( + sm -> + sm.sessionCreationPolicy( + SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음, 요청은 JWT 인증 + .formLogin(form -> form.disable()) // react에서 로그인 요청 관리 + .httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용 + .logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음 + .authenticationProvider( + customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용 + .authorizeHttpRequests( + auth -> + auth.requestMatchers(HttpMethod.OPTIONS, "/**") + .permitAll() // preflight 허용 + .requestMatchers( + "/api/auth/signin", + "/api/auth/refresh", + "/swagger-ui/**", + "/api/members/{memberId}/password", + "/v3/api-docs/**") + .permitAll() + .anyRequest() + .authenticated()) + .addFilterBefore( + jwtAuthenticationFilter, + UsernamePasswordAuthenticationFilter + .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. ; return http.build(); @@ -64,7 +64,7 @@ public class SecurityConfig { @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) - throws Exception { + throws Exception { return configuration.getAuthenticationManager(); } diff --git a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java index de466c5d..969fde31 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java @@ -20,11 +20,9 @@ public class ApiResponseDto { @JsonInclude(JsonInclude.Include.NON_NULL) private T errorData; - @JsonIgnore - private HttpStatus httpStatus; + @JsonIgnore private HttpStatus httpStatus; - @JsonIgnore - private Long errorLogUid; + @JsonIgnore private Long errorLogUid; public ApiResponseDto(T data) { this.data = data; @@ -49,7 +47,7 @@ public class ApiResponseDto { } public ApiResponseDto( - ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { + ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { this.error = new Error(code.getId(), message); this.httpStatus = httpStatus; this.errorLogUid = errorLogUid; @@ -90,17 +88,17 @@ public class ApiResponseDto { } public static ApiResponseDto createException( - ApiResponseCode code, String message, HttpStatus httpStatus) { + ApiResponseCode code, String message, HttpStatus httpStatus) { return new ApiResponseDto<>(code, message, httpStatus); } public static ApiResponseDto createException( - ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { + ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { return new ApiResponseDto<>(code, message, httpStatus, errorLogUid); } public static ApiResponseDto createException( - ApiResponseCode code, String message, T data) { + ApiResponseCode code, String message, T data) { return new ApiResponseDto<>(code, message, data); } @@ -116,9 +114,7 @@ public class ApiResponseDto { } } - /** - * Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object - */ + /** Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object */ @Getter public static class ResponseObj { diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java index 4ed55cda..33afc12c 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java @@ -30,78 +30,78 @@ public class AdminApiController { @Operation(summary = "관리자 계정 등록", description = "관리자 계정 등록") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "등록 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "등록 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PostMapping("/join") public ApiResponseDto saveMember( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "관리자 계정 등록", - required = true, - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = MembersDto.AddReq.class))) - @RequestBody - @Valid - MembersDto.AddReq addReq) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "관리자 계정 등록", + required = true, + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = MembersDto.AddReq.class))) + @RequestBody + @Valid + MembersDto.AddReq addReq) { return ApiResponseDto.createOK(adminService.saveMember(addReq)); } @Operation(summary = "관리자 계정 수정", description = "관리자 계정 수정") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "수정 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "수정 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PutMapping("/{uuid}") public ApiResponseDto updateMembers( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "관리자 계정 수정", - required = true, - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = MembersDto.UpdateReq.class))) - @PathVariable - UUID uuid, - @RequestBody @Valid MembersDto.UpdateReq updateReq) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "관리자 계정 수정", + required = true, + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = MembersDto.UpdateReq.class))) + @PathVariable + UUID uuid, + @RequestBody @Valid MembersDto.UpdateReq updateReq) { adminService.updateMembers(uuid, updateReq); return ApiResponseDto.createOK(UUID.randomUUID()); } @Operation(summary = "관리자 계정 미사용 처리", description = "관리자 계정 미사용 처리") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "관리자 계정 미사용 처리", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = UUID.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "관리자 계정 미사용 처리", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = UUID.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @DeleteMapping("/delete/{uuid}") public ApiResponseDto deleteAccount(@PathVariable UUID uuid) { adminService.deleteAccount(uuid); diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java index 9a8d2703..a0d7ed01 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -50,57 +50,59 @@ public class AuthController { @Operation(summary = "로그인", description = "사번으로 로그인하여 액세스/리프레시 토큰을 발급.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "로그인 성공", - content = @Content(schema = @Schema(implementation = TokenResponse.class))), + responseCode = "200", + description = "로그인 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( - responseCode = "401", - description = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)", - content = - @Content( - schema = @Schema(implementation = ErrorResponse.class), - examples = { - @ExampleObject( - name = "아이디 입력 오류", - description = "존재하지 않는 아이디", - value = """ + responseCode = "401", + description = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)", + content = + @Content( + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "아이디 입력 오류", + description = "존재하지 않는 아이디", + value = + """ { "code": "LOGIN_ID_NOT_FOUND", "message": "아이디를 잘못 입력하셨습니다." } """), - @ExampleObject( - name = "비밀번호 입력 오류 (4회 이하)", - description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우", - value = """ + @ExampleObject( + name = "비밀번호 입력 오류 (4회 이하)", + description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우", + value = + """ { "code": "LOGIN_PASSWORD_MISMATCH", "message": "비밀번호를 잘못 입력하셨습니다." } """), - @ExampleObject( - name = "비밀번호 오류 횟수 초과", - description = "비밀번호 5회 이상 오류로 계정 잠김", - value = """ + @ExampleObject( + name = "비밀번호 오류 횟수 초과", + description = "비밀번호 5회 이상 오류로 계정 잠김", + value = + """ { "code": "LOGIN_PASSWORD_EXCEEDED", "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다." } """) - } - )) + })) }) public ApiResponseDto signin( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "로그인 요청 정보", - required = true) - @RequestBody - SignInRequest request, - HttpServletResponse response) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "로그인 요청 정보", + required = true) + @RequestBody + SignInRequest request, + HttpServletResponse response) { Authentication authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); String status = authService.getUserStatus(request); @@ -116,17 +118,17 @@ public class AuthController { // Redis에 RefreshToken 저장 (TTL = 7일) refreshTokenService.save( - username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); + username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); // HttpOnly + Secure 쿠키에 RefreshToken 저장 ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, refreshToken) - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, refreshToken) + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); @@ -137,16 +139,16 @@ public class AuthController { @Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "재발급 성공", - content = @Content(schema = @Schema(implementation = TokenResponse.class))), + responseCode = "200", + description = "재발급 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( - responseCode = "401", - description = "만료되었거나 유효하지 않은 리프레시 토큰", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + responseCode = "401", + description = "만료되었거나 유효하지 않은 리프레시 토큰", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) public ResponseEntity refresh(String refreshToken, HttpServletResponse response) - throws AccessDeniedException { + throws AccessDeniedException { if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) { throw new AccessDeniedException("만료되었거나 유효하지 않은 리프레시 토큰 입니다."); } @@ -164,17 +166,17 @@ public class AuthController { // Redis 갱신 refreshTokenService.save( - username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); + username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); // 쿠키 갱신 ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, newRefreshToken) - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, newRefreshToken) + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken)); @@ -184,12 +186,12 @@ public class AuthController { @Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "로그아웃 성공", - content = @Content(schema = @Schema(implementation = Void.class))) + responseCode = "200", + description = "로그아웃 성공", + content = @Content(schema = @Schema(implementation = Void.class))) }) public ApiResponseDto> logout( - Authentication authentication, HttpServletResponse response) { + Authentication authentication, HttpServletResponse response) { if (authentication != null) { String username = authentication.getName(); // Redis에서 RefreshToken 삭제 @@ -198,19 +200,17 @@ public class AuthController { // 쿠키 삭제 (Max-Age=0) ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, "") - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(0) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, "") + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(0) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); return ApiResponseDto.createOK(ResponseEntity.noContent().build()); } - public record TokenResponse(String status, String accessToken, String refreshToken) { - - } + public record TokenResponse(String status, String accessToken, String refreshToken) {} } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java index 0c146f8a..8c7a3560 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java @@ -34,43 +34,43 @@ public class MembersApiController { @Operation(summary = "회원정보 목록", description = "회원정보 조회") @ApiResponses( - value = { - @ApiResponse( - responseCode = "200", - description = "검색 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Page.class))), - @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "200", + description = "검색 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Page.class))), + @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @GetMapping public ApiResponseDto> getMemberList( - @ParameterObject MembersDto.SearchReq searchReq) { + @ParameterObject MembersDto.SearchReq searchReq) { return ApiResponseDto.ok(membersService.findByMembers(searchReq)); } - @Operation(summary = "사용자 비밀번호 변경", description = "사용자 비밀번호 변경") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "사용자 비밀번호 변경", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "사용자 비밀번호 변경", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PatchMapping("/{memberId}/password") - public ApiResponseDto resetPassword(@PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) { + public ApiResponseDto resetPassword( + @PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) { authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(memberId, initReq.getTempPassword())); + new UsernamePasswordAuthenticationToken(memberId, initReq.getTempPassword())); membersService.resetPassword(memberId, initReq); return ApiResponseDto.createOK(memberId); diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java index 3f5f0616..b8d1cf04 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java @@ -32,31 +32,26 @@ public class MembersDto { private String tempPassword; private String status; private String statusName; - @JsonFormatDttm - private ZonedDateTime createdDttm; - @JsonFormatDttm - private ZonedDateTime updatedDttm; - @JsonFormatDttm - private ZonedDateTime firstLoginDttm; - @JsonFormatDttm - private ZonedDateTime lastLoginDttm; + @JsonFormatDttm private ZonedDateTime createdDttm; + @JsonFormatDttm private ZonedDateTime updatedDttm; + @JsonFormatDttm private ZonedDateTime firstLoginDttm; + @JsonFormatDttm private ZonedDateTime lastLoginDttm; public Basic( - Long id, - UUID uuid, - String userRole, - String userRoleName, - String name, - String userId, - String employeeNo, - String tempPassword, - String status, - String statusName, - ZonedDateTime createdDttm, - ZonedDateTime updatedDttm, - ZonedDateTime firstLoginDttm, - ZonedDateTime lastLoginDttm - ) { + Long id, + UUID uuid, + String userRole, + String userRoleName, + String name, + String userId, + String employeeNo, + String tempPassword, + String status, + String statusName, + ZonedDateTime createdDttm, + ZonedDateTime updatedDttm, + ZonedDateTime firstLoginDttm, + ZonedDateTime lastLoginDttm) { this.id = id; this.uuid = uuid; this.userRole = userRole; @@ -82,7 +77,6 @@ public class MembersDto { StatusType type = EnumType.fromId(StatusType.class, status); return type.getText(); } - } @Getter @@ -91,7 +85,9 @@ public class MembersDto { @AllArgsConstructor public static class SearchReq { - @Schema(description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)", example = "") + @Schema( + description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)", + example = "") private String userRole; @Schema(description = "키워드", example = "홍길동") @@ -135,7 +131,7 @@ public class MembersDto { private String employeeNo; public AddReq( - String userRole, String name, String userId, String tempPassword, String employeeNo) { + String userRole, String name, String userId, String tempPassword, String employeeNo) { this.userRole = userRole; this.name = name; this.userId = userId; diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java index da6905d7..dd6f5621 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java @@ -36,7 +36,6 @@ public class AdminService { membersCoreService.updateMembers(uuid, updateReq); } - /** * 관리자 계정 미사용 처리 * diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java index 3f63f430..5cbd1b79 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java @@ -51,7 +51,7 @@ public class MembersService { */ private boolean isValidPassword(String password) { String passwordPattern = - "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$"; + "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$"; return Pattern.matches(passwordPattern, password); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java index fc39f559..6f1854d7 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java @@ -59,7 +59,7 @@ public class MembersCoreService { */ public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) { MemberEntity memberEntity = - membersRepository.findByUUID(uuid).orElseThrow(MemberNotFoundException::new); + membersRepository.findByUUID(uuid).orElseThrow(MemberNotFoundException::new); if (StringUtils.isNotBlank(updateReq.getName())) { memberEntity.setName(updateReq.getName()); @@ -77,7 +77,6 @@ public class MembersCoreService { membersRepository.save(memberEntity); } - /** * 관리자 계정 미사용 처리 * @@ -85,14 +84,13 @@ public class MembersCoreService { */ public void deleteAccount(UUID uuid) { MemberEntity memberEntity = - membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); + membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); memberEntity.setStatus("INACTIVE"); memberEntity.setUpdatedDttm(ZonedDateTime.now()); membersRepository.save(memberEntity); } - /** * 패스워드 변경 * @@ -100,10 +98,10 @@ public class MembersCoreService { */ public void resetPassword(String id, MembersDto.InitReq initReq) { MemberEntity memberEntity = - membersRepository.findByUserId(id).orElseThrow(() -> new MemberNotFoundException()); + membersRepository.findByUserId(id).orElseThrow(() -> new MemberNotFoundException()); String salt = - BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); + BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); // 패스워드 암호화 String hashedPassword = BCrypt.hashpw(initReq.getPassword(), salt); @@ -112,6 +110,7 @@ public class MembersCoreService { memberEntity.setUpdatedDttm(ZonedDateTime.now()); membersRepository.save(memberEntity); } + // /** @@ -131,7 +130,10 @@ public class MembersCoreService { * @return */ public String getUserStatus(SignInRequest request) { - MemberEntity memberEntity = membersRepository.findByUserId(request.getUsername()).orElseThrow(MemberNotFoundException::new); + MemberEntity memberEntity = + membersRepository + .findByUserId(request.getUsername()) + .orElseThrow(MemberNotFoundException::new); return memberEntity.getStatus(); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java index c46e73e2..286b85bc 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java @@ -61,8 +61,7 @@ public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom { .on(child.deleted.isFalse().or(child.deleted.isNull())) .where( commonCodeEntity.parent.isNull(), - commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()) - ) + commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull())) .orderBy(commonCodeEntity.order.asc(), child.order.asc()) .fetch(); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java index 7e84baec..989bdd7d 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java @@ -33,11 +33,11 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { @Override public boolean existsByUserId(String userId) { return queryFactory - .selectOne() - .from(memberEntity) - .where(memberEntity.userId.eq(userId)) - .fetchFirst() - != null; + .selectOne() + .from(memberEntity) + .where(memberEntity.userId.eq(userId)) + .fetchFirst() + != null; } /** @@ -49,7 +49,7 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { @Override public Optional findByUserId(String userId) { return Optional.ofNullable( - queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne()); + queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne()); } /** @@ -68,10 +68,11 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { String contains = "%" + searchReq.getKeyword() + "%"; builder.and( - memberEntity.name.likeIgnoreCase(contains) - .or(memberEntity.userId.likeIgnoreCase(contains)) - .or(memberEntity.employeeNo.likeIgnoreCase(contains)) - ); + memberEntity + .name + .likeIgnoreCase(contains) + .or(memberEntity.userId.likeIgnoreCase(contains)) + .or(memberEntity.employeeNo.likeIgnoreCase(contains))); } // 권한 @@ -80,35 +81,30 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { } List content = - queryFactory - .select( - Projections.constructor( - MembersDto.Basic.class, - memberEntity.id, - memberEntity.uuid, - memberEntity.userRole, - memberEntity.name, - memberEntity.userId, - memberEntity.employeeNo, - memberEntity.tempPassword, - memberEntity.status, - memberEntity.createdDttm, - memberEntity.updatedDttm, - memberEntity.firstLoginDttm, - memberEntity.lastLoginDttm - )) - .from(memberEntity) - .where(builder) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .orderBy(memberEntity.createdDttm.desc()) - .fetch(); + queryFactory + .select( + Projections.constructor( + MembersDto.Basic.class, + memberEntity.id, + memberEntity.uuid, + memberEntity.userRole, + memberEntity.name, + memberEntity.userId, + memberEntity.employeeNo, + memberEntity.tempPassword, + memberEntity.status, + memberEntity.createdDttm, + memberEntity.updatedDttm, + memberEntity.firstLoginDttm, + memberEntity.lastLoginDttm)) + .from(memberEntity) + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(memberEntity.createdDttm.desc()) + .fetch(); - long total = - queryFactory - .select(memberEntity) - .from(memberEntity) - .fetchCount(); + long total = queryFactory.select(memberEntity).from(memberEntity).fetchCount(); return new PageImpl<>(content, pageable, total); } @@ -122,6 +118,6 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { @Override public Optional findByUUID(UUID uuid) { return Optional.ofNullable( - queryFactory.selectFrom(memberEntity).where(memberEntity.uuid.eq(uuid)).fetchOne()); + queryFactory.selectFrom(memberEntity).where(memberEntity.uuid.eq(uuid)).fetchOne()); } } From 0b5976cb94ed722ab3b1ec5bad9e12b7d12778b6 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 13:51:24 +0900 Subject: [PATCH 04/16] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=98=88?= =?UTF-8?q?=EC=A0=9C=20=EC=A0=95=EB=B3=B4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/kamco/cd/kamcoback/members/dto/SignInRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java index a13992b3..b720f9dd 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java @@ -11,10 +11,10 @@ import lombok.ToString; @ToString(exclude = "password") public class SignInRequest { - @Schema(description = "사용자 ID", example = "admin") + @Schema(description = "사용자 ID", example = "admin2") private String username; - @Schema(description = "비밀번호", example = "kamco1234!") + @Schema(description = "비밀번호", example = "Admin2!@1#") @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String password; } From 8ff0ae376da6ac5632bb2b7aade349edee177ac3 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 13:58:07 +0900 Subject: [PATCH 05/16] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=98=88?= =?UTF-8?q?=EC=A0=9C=20=EC=A0=95=EB=B3=B4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java index b720f9dd..1ad47961 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java @@ -14,7 +14,7 @@ public class SignInRequest { @Schema(description = "사용자 ID", example = "admin2") private String username; - @Schema(description = "비밀번호", example = "Admin2!@1#") + @Schema(description = "비밀번호", example = "Admin2!@#") @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String password; } From 507819b2e67eaf2eb22af8db543269cea8b36612 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 14:05:26 +0900 Subject: [PATCH 06/16] =?UTF-8?q?spotlessApply=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cd/kamcoback/common/utils/UserUtil.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java diff --git a/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java b/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java new file mode 100644 index 00000000..a96da1a3 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java @@ -0,0 +1,54 @@ +package com.kamco.cd.kamcoback.common.utils; + +import com.kamco.cd.kamcoback.auth.CustomUserDetails; +import com.kamco.cd.kamcoback.auth.JwtTokenProvider; +import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; +import java.util.Optional; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class UserUtil { + + private final JwtTokenProvider jwtTokenProvider; + + /** + * 현재 SecurityContext의 Authentication 에서 사용자 ID(Long) 가져오기 - 로그인 된 상태(AccessToken 인증 이후)에서만 사용 가능 + * - 인증 정보 없으면 null 리턴 + */ + public Long getCurrentUserId() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .filter(auth -> auth.getPrincipal() instanceof CustomUserDetails) + .map(auth -> ((CustomUserDetails) auth.getPrincipal()).getMember().getId()) + .orElse(null); + } + + /** 현재 SecurityContext의 Authentication 에서 사용자 UUID 가져오기 - 인증 정보 없으면 null 리턴 */ + public UUID getCurrentUserUuid() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .filter(auth -> auth.getPrincipal() instanceof CustomUserDetails) + .map(auth -> ((CustomUserDetails) auth.getPrincipal()).getMember().getUuid()) + .orElse(null); + } + + /** 현재 로그인한 사용자의 MemberEntity 통째로 가져오기 (Optional) */ + public Optional getCurrentMember() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .filter(auth -> auth.getPrincipal() instanceof CustomUserDetails) + .map(auth -> ((CustomUserDetails) auth.getPrincipal()).getMember()); + } + + /** 현재 로그인한 사용자의 MemberEntity 가져오기 (없으면 예외) */ + public MemberEntity getCurrentMemberOrThrow() { + return getCurrentMember().orElseThrow(() -> new IllegalStateException("인증된 사용자를 찾을 수 없습니다.")); + // 필요하면 여기서 CustomApiException 으로 바꿔도 됨 + } + + /** AccessToken / RefreshToken 에서 sub(subject, uuid) 추출 - 토큰 문자열만 있을 때 uuid 가져올 때 사용 */ + public String getSubjectFromToken(String token) { + return jwtTokenProvider.getSubject(token); + } +} From 90d6ef9c05e587a14c3eac25e4fd1325a4f0e8b9 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 14:24:14 +0900 Subject: [PATCH 07/16] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../members/MembersApiController.java | 54 +++++++++---------- .../cd/kamcoback/members/dto/MembersDto.java | 44 +++++++-------- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java index 8c7a3560..f73a366e 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java @@ -34,43 +34,43 @@ public class MembersApiController { @Operation(summary = "회원정보 목록", description = "회원정보 조회") @ApiResponses( - value = { - @ApiResponse( - responseCode = "200", - description = "검색 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Page.class))), - @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "200", + description = "검색 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Page.class))), + @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @GetMapping public ApiResponseDto> getMemberList( - @ParameterObject MembersDto.SearchReq searchReq) { + @ParameterObject MembersDto.SearchReq searchReq) { return ApiResponseDto.ok(membersService.findByMembers(searchReq)); } - @Operation(summary = "사용자 비밀번호 변경", description = "사용자 비밀번호 변경") + @Operation(summary = "사용자 비밀번호 변경", description = "로그인 성공후 status가 INACTIVE일때 로그인 id를 memberId로 path 생성필요") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "사용자 비밀번호 변경", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "사용자 비밀번호 변경", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PatchMapping("/{memberId}/password") public ApiResponseDto resetPassword( - @PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) { + @PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) { authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(memberId, initReq.getTempPassword())); + new UsernamePasswordAuthenticationToken(memberId, initReq.getTempPassword())); membersService.resetPassword(memberId, initReq); return ApiResponseDto.createOK(memberId); diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java index b8d1cf04..bc2da653 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java @@ -32,26 +32,28 @@ public class MembersDto { private String tempPassword; private String status; private String statusName; - @JsonFormatDttm private ZonedDateTime createdDttm; - @JsonFormatDttm private ZonedDateTime updatedDttm; - @JsonFormatDttm private ZonedDateTime firstLoginDttm; - @JsonFormatDttm private ZonedDateTime lastLoginDttm; + @JsonFormatDttm + private ZonedDateTime createdDttm; + @JsonFormatDttm + private ZonedDateTime updatedDttm; + @JsonFormatDttm + private ZonedDateTime firstLoginDttm; + @JsonFormatDttm + private ZonedDateTime lastLoginDttm; public Basic( - Long id, - UUID uuid, - String userRole, - String userRoleName, - String name, - String userId, - String employeeNo, - String tempPassword, - String status, - String statusName, - ZonedDateTime createdDttm, - ZonedDateTime updatedDttm, - ZonedDateTime firstLoginDttm, - ZonedDateTime lastLoginDttm) { + Long id, + UUID uuid, + String userRole, + String name, + String userId, + String employeeNo, + String tempPassword, + String status, + ZonedDateTime createdDttm, + ZonedDateTime updatedDttm, + ZonedDateTime firstLoginDttm, + ZonedDateTime lastLoginDttm) { this.id = id; this.uuid = uuid; this.userRole = userRole; @@ -86,8 +88,8 @@ public class MembersDto { public static class SearchReq { @Schema( - description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)", - example = "") + description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)", + example = "") private String userRole; @Schema(description = "키워드", example = "홍길동") @@ -131,7 +133,7 @@ public class MembersDto { private String employeeNo; public AddReq( - String userRole, String name, String userId, String tempPassword, String employeeNo) { + String userRole, String name, String userId, String tempPassword, String employeeNo) { this.userRole = userRole; this.name = name; this.userId = userId; From ec87bddee866574c5e49fa73a0987c7af60aef9b Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 14:24:22 +0900 Subject: [PATCH 08/16] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../members/MembersApiController.java | 56 ++++++++++--------- .../cd/kamcoback/members/dto/MembersDto.java | 42 +++++++------- 2 files changed, 48 insertions(+), 50 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java index f73a366e..2d6a850b 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java @@ -34,43 +34,45 @@ public class MembersApiController { @Operation(summary = "회원정보 목록", description = "회원정보 조회") @ApiResponses( - value = { - @ApiResponse( - responseCode = "200", - description = "검색 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Page.class))), - @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "200", + description = "검색 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Page.class))), + @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @GetMapping public ApiResponseDto> getMemberList( - @ParameterObject MembersDto.SearchReq searchReq) { + @ParameterObject MembersDto.SearchReq searchReq) { return ApiResponseDto.ok(membersService.findByMembers(searchReq)); } - @Operation(summary = "사용자 비밀번호 변경", description = "로그인 성공후 status가 INACTIVE일때 로그인 id를 memberId로 path 생성필요") + @Operation( + summary = "사용자 비밀번호 변경", + description = "로그인 성공후 status가 INACTIVE일때 로그인 id를 memberId로 path 생성필요") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "사용자 비밀번호 변경", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "사용자 비밀번호 변경", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PatchMapping("/{memberId}/password") public ApiResponseDto resetPassword( - @PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) { + @PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) { authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(memberId, initReq.getTempPassword())); + new UsernamePasswordAuthenticationToken(memberId, initReq.getTempPassword())); membersService.resetPassword(memberId, initReq); return ApiResponseDto.createOK(memberId); diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java index bc2da653..fd11e504 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java @@ -32,28 +32,24 @@ public class MembersDto { private String tempPassword; private String status; private String statusName; - @JsonFormatDttm - private ZonedDateTime createdDttm; - @JsonFormatDttm - private ZonedDateTime updatedDttm; - @JsonFormatDttm - private ZonedDateTime firstLoginDttm; - @JsonFormatDttm - private ZonedDateTime lastLoginDttm; + @JsonFormatDttm private ZonedDateTime createdDttm; + @JsonFormatDttm private ZonedDateTime updatedDttm; + @JsonFormatDttm private ZonedDateTime firstLoginDttm; + @JsonFormatDttm private ZonedDateTime lastLoginDttm; public Basic( - Long id, - UUID uuid, - String userRole, - String name, - String userId, - String employeeNo, - String tempPassword, - String status, - ZonedDateTime createdDttm, - ZonedDateTime updatedDttm, - ZonedDateTime firstLoginDttm, - ZonedDateTime lastLoginDttm) { + Long id, + UUID uuid, + String userRole, + String name, + String userId, + String employeeNo, + String tempPassword, + String status, + ZonedDateTime createdDttm, + ZonedDateTime updatedDttm, + ZonedDateTime firstLoginDttm, + ZonedDateTime lastLoginDttm) { this.id = id; this.uuid = uuid; this.userRole = userRole; @@ -88,8 +84,8 @@ public class MembersDto { public static class SearchReq { @Schema( - description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)", - example = "") + description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)", + example = "") private String userRole; @Schema(description = "키워드", example = "홍길동") @@ -133,7 +129,7 @@ public class MembersDto { private String employeeNo; public AddReq( - String userRole, String name, String userId, String tempPassword, String employeeNo) { + String userRole, String name, String userId, String tempPassword, String employeeNo) { this.userRole = userRole; this.name = name; this.userId = userId; From ec18d9d25b015df7fed3c8e6bb9ea6c1e1a6ffc9 Mon Sep 17 00:00:00 2001 From: "gayoun.park" Date: Thu, 11 Dec 2025 14:39:31 +0900 Subject: [PATCH 09/16] =?UTF-8?q?=EC=B6=94=EB=A1=A0=EA=B2=B0=EA=B3=BC=20de?= =?UTF-8?q?mo=20URL=20=EC=A0=9C=EA=B1=B0,=20=EA=B3=B5=ED=86=B5=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20fetchJoin=20=EB=B9=BC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inference/InferenceResultApiController.java | 2 +- .../repository/code/CommonCodeRepositoryImpl.java | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java b/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java index 9ee43a3b..12bd6454 100644 --- a/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java @@ -28,7 +28,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Tag(name = "추론관리 분석결과", description = "추론관리 분석결과") -@RequestMapping({"/demo/inf/res", "/api/inf/res"}) +@RequestMapping({"/api/inf/res"}) @RequiredArgsConstructor @RestController public class InferenceResultApiController { diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java index 286b85bc..4ba7304b 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java @@ -25,11 +25,10 @@ public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom { queryFactory .selectFrom(commonCodeEntity) .leftJoin(commonCodeEntity.children, child) - .fetchJoin() + .on(child.deleted.isFalse().or(child.deleted.isNull())) .where( commonCodeEntity.id.eq(id), - commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()), - child.deleted.isFalse().or(child.deleted.isNull())) + commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull())) .orderBy(commonCodeEntity.order.asc(), child.order.asc()) .fetchOne()); } @@ -41,13 +40,12 @@ public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom { queryFactory .selectFrom(commonCodeEntity) .leftJoin(commonCodeEntity.children, child) - .fetchJoin() + .on(child.deleted.isFalse().or(child.deleted.isNull())) .where( commonCodeEntity.parent.isNull(), commonCodeEntity.code.eq(code), commonCodeEntity.used.isTrue(), - commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()), - child.deleted.isFalse().or(child.deleted.isNull())) + commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull())) .orderBy(child.order.asc()) .fetchOne()); } From ddd9af53dad155873eea0233e148844b3452aacd Mon Sep 17 00:00:00 2001 From: "gayoun.park" Date: Thu, 11 Dec 2025 14:52:11 +0900 Subject: [PATCH 10/16] =?UTF-8?q?=EA=B3=B5=ED=86=B5=EC=BD=94=EB=93=9C=20id?= =?UTF-8?q?=EA=B0=92=20=EC=A1=B0=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/code/CommonCodeRepositoryImpl.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java index 4ba7304b..2180f2dc 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java @@ -20,17 +20,8 @@ public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom { @Override public Optional findByCodeId(Long id) { - QCommonCodeEntity child = new QCommonCodeEntity("child"); return Optional.ofNullable( - queryFactory - .selectFrom(commonCodeEntity) - .leftJoin(commonCodeEntity.children, child) - .on(child.deleted.isFalse().or(child.deleted.isNull())) - .where( - commonCodeEntity.id.eq(id), - commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull())) - .orderBy(commonCodeEntity.order.asc(), child.order.asc()) - .fetchOne()); + queryFactory.selectFrom(commonCodeEntity).where(commonCodeEntity.id.eq(id)).fetchOne()); } @Override From 9d6cfc9d1775f5a7e82dc5302fe4ef215ca39ed6 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 15:38:05 +0900 Subject: [PATCH 11/16] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/CustomAuthenticationProvider.java | 25 ++++----- .../cd/kamcoback/common/enums/StatusType.java | 2 +- .../cd/kamcoback/common/utils/UserUtil.java | 47 ++++++---------- .../cd/kamcoback/members/AuthController.java | 55 ++++++++++++------- .../cd/kamcoback/members/dto/MembersDto.java | 11 ++++ .../kamcoback/members/dto/TokenResponse.java | 16 ++++++ .../members/service/AuthService.java | 11 ++++ .../members/service/MembersService.java | 2 +- .../postgres/core/MembersCoreService.java | 29 +++++++++- .../postgres/entity/MemberEntity.java | 9 ++- 10 files changed, 137 insertions(+), 70 deletions(-) create mode 100644 src/main/java/com/kamco/cd/kamcoback/members/dto/TokenResponse.java diff --git a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java index dd898258..0de417b1 100644 --- a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java +++ b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java @@ -1,10 +1,10 @@ package com.kamco.cd.kamcoback.auth; +import com.kamco.cd.kamcoback.common.enums.StatusType; import com.kamco.cd.kamcoback.common.enums.error.AuthErrorCode; import com.kamco.cd.kamcoback.common.exception.CustomApiException; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository; -import java.time.ZonedDateTime; import lombok.RequiredArgsConstructor; import org.mindrot.jbcrypt.BCrypt; import org.springframework.security.authentication.AuthenticationProvider; @@ -26,38 +26,35 @@ public class CustomAuthenticationProvider implements AuthenticationProvider { String username = authentication.getName(); String rawPassword = authentication.getCredentials().toString(); - // 1. 유저 조회 + // 유저 조회 MemberEntity member = membersRepository .findByUserId(username) .orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND)); - // 2. jBCrypt + 커스텀 salt 로 저장된 패스워드 비교 + // jBCrypt + 커스텀 salt 로 저장된 패스워드 비교 if (!BCrypt.checkpw(rawPassword, member.getPassword())) { // 실패 카운트 저장 int cnt = member.getLoginFailCount() + 1; if (cnt >= 5) { - member.setStatus("INACTIVE"); + member.setStatus(StatusType.INACTIVE.getId()); } member.setLoginFailCount(cnt); membersRepository.save(member); throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH); } - // 3. 패스워드 실패 횟수 체크 + // 삭제 상태 + if (member.getStatus().equals(StatusType.DELETED.getId())) { + throw new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND); + } + + // 패스워드 실패 횟수 체크 if (member.getLoginFailCount() >= 5) { throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED); } - // 4. 인증 성공 로그인 시간 저장 - if (member.getFirstLoginDttm() == null) { - member.setFirstLoginDttm(ZonedDateTime.now()); - } - member.setLastLoginDttm(ZonedDateTime.now()); - member.setLoginFailCount(0); - membersRepository.save(member); - - // 5. 인증 성공 → UserDetails 생성 + // 인증 성공 → UserDetails 생성 CustomUserDetails userDetails = new CustomUserDetails(member); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java index 641d3de5..0da73338 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java @@ -9,7 +9,7 @@ import lombok.Getter; public enum StatusType implements EnumType { ACTIVE("활성"), INACTIVE("비활성"), - ARCHIVED("탈퇴"); + DELETED("삭제"); private final String desc; diff --git a/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java b/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java index a96da1a3..b404cbf8 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java @@ -1,10 +1,9 @@ package com.kamco.cd.kamcoback.common.utils; import com.kamco.cd.kamcoback.auth.CustomUserDetails; -import com.kamco.cd.kamcoback.auth.JwtTokenProvider; +import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import java.util.Optional; -import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; @@ -13,42 +12,30 @@ import org.springframework.stereotype.Component; @RequiredArgsConstructor public class UserUtil { - private final JwtTokenProvider jwtTokenProvider; - - /** - * 현재 SecurityContext의 Authentication 에서 사용자 ID(Long) 가져오기 - 로그인 된 상태(AccessToken 인증 이후)에서만 사용 가능 - * - 인증 정보 없으면 null 리턴 - */ - public Long getCurrentUserId() { + public MembersDto.Member getCurrentUser() { return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) .filter(auth -> auth.getPrincipal() instanceof CustomUserDetails) - .map(auth -> ((CustomUserDetails) auth.getPrincipal()).getMember().getId()) + .map( + auth -> { + CustomUserDetails user = (CustomUserDetails) auth.getPrincipal(); + MemberEntity m = user.getMember(); + return new MembersDto.Member(m.getId(), m.getName(), m.getEmployeeNo()); + }) .orElse(null); } - /** 현재 SecurityContext의 Authentication 에서 사용자 UUID 가져오기 - 인증 정보 없으면 null 리턴 */ - public UUID getCurrentUserUuid() { - return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) - .filter(auth -> auth.getPrincipal() instanceof CustomUserDetails) - .map(auth -> ((CustomUserDetails) auth.getPrincipal()).getMember().getUuid()) - .orElse(null); + public Long getId() { + MembersDto.Member user = getCurrentUser(); + return user != null ? user.getId() : null; } - /** 현재 로그인한 사용자의 MemberEntity 통째로 가져오기 (Optional) */ - public Optional getCurrentMember() { - return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) - .filter(auth -> auth.getPrincipal() instanceof CustomUserDetails) - .map(auth -> ((CustomUserDetails) auth.getPrincipal()).getMember()); + public String getName() { + MembersDto.Member user = getCurrentUser(); + return user != null ? user.getName() : null; } - /** 현재 로그인한 사용자의 MemberEntity 가져오기 (없으면 예외) */ - public MemberEntity getCurrentMemberOrThrow() { - return getCurrentMember().orElseThrow(() -> new IllegalStateException("인증된 사용자를 찾을 수 없습니다.")); - // 필요하면 여기서 CustomApiException 으로 바꿔도 됨 - } - - /** AccessToken / RefreshToken 에서 sub(subject, uuid) 추출 - 토큰 문자열만 있을 때 uuid 가져올 때 사용 */ - public String getSubjectFromToken(String token) { - return jwtTokenProvider.getSubject(token); + public String getEmployeeNo() { + MembersDto.Member user = getCurrentUser(); + return user != null ? user.getEmployeeNo() : null; } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java index a0d7ed01..d80dbfdf 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -1,9 +1,12 @@ package com.kamco.cd.kamcoback.members; +import com.kamco.cd.kamcoback.auth.CustomUserDetails; import com.kamco.cd.kamcoback.auth.JwtTokenProvider; import com.kamco.cd.kamcoback.auth.RefreshTokenService; import com.kamco.cd.kamcoback.config.api.ApiResponseDto; +import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.SignInRequest; +import com.kamco.cd.kamcoback.members.dto.TokenResponse; import com.kamco.cd.kamcoback.members.service.AuthService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -15,6 +18,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import java.nio.file.AccessDeniedException; import java.time.Duration; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; @@ -65,31 +69,31 @@ public class AuthController { description = "존재하지 않는 아이디", value = """ - { - "code": "LOGIN_ID_NOT_FOUND", - "message": "아이디를 잘못 입력하셨습니다." - } - """), + { + "code": "LOGIN_ID_NOT_FOUND", + "message": "아이디를 잘못 입력하셨습니다." + } + """), @ExampleObject( name = "비밀번호 입력 오류 (4회 이하)", description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우", value = """ - { - "code": "LOGIN_PASSWORD_MISMATCH", - "message": "비밀번호를 잘못 입력하셨습니다." - } - """), + { + "code": "LOGIN_PASSWORD_MISMATCH", + "message": "비밀번호를 잘못 입력하셨습니다." + } + """), @ExampleObject( name = "비밀번호 오류 횟수 초과", description = "비밀번호 5회 이상 오류로 계정 잠김", value = """ - { - "code": "LOGIN_PASSWORD_EXCEEDED", - "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다." - } - """) + { + "code": "LOGIN_PASSWORD_EXCEEDED", + "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다." + } + """) })) }) public ApiResponseDto signin( @@ -106,9 +110,11 @@ public class AuthController { String status = authService.getUserStatus(request); - // INACTIVE 비활성 상태(새로운 패스워드 입력 해야함), ARCHIVED 탈퇴 + MembersDto.Member member = new MembersDto.Member(); + + // INACTIVE 비활성 상태(새로운 패스워드 입력 해야함), DELETED 탈퇴 if (!"ACTIVE".equals(status)) { - return ApiResponseDto.ok(new TokenResponse(status, null, null)); + return ApiResponseDto.ok(new TokenResponse(status, null, null, member)); } String username = authentication.getName(); // UserDetailsService 에서 사용한 username @@ -132,7 +138,15 @@ public class AuthController { response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); - return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken)); + CustomUserDetails user = (CustomUserDetails) authentication.getPrincipal(); + member.setId(user.getMember().getId()); + member.setName(user.getMember().getName()); + member.setEmployeeNo(user.getMember().getEmployeeNo()); + + // 인증 성공 로그인 시간 저장 + authService.saveLogin(UUID.fromString(username)); + + return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken, member)); } @PostMapping("/refresh") @@ -179,7 +193,8 @@ public class AuthController { .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); - return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken)); + MembersDto.Member member = new MembersDto.Member(); + return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken, member)); } @PostMapping("/logout") @@ -211,6 +226,4 @@ public class AuthController { return ApiResponseDto.createOK(ResponseEntity.noContent().build()); } - - public record TokenResponse(String status, String accessToken, String refreshToken) {} } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java index fd11e504..7f6fe46c 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java @@ -174,4 +174,15 @@ public class MembersDto { @NotBlank private String tempPassword; } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + public static class Member { + + private Long id; + private String name; + private String employeeNo; + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/TokenResponse.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/TokenResponse.java new file mode 100644 index 00000000..d410279e --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/TokenResponse.java @@ -0,0 +1,16 @@ +package com.kamco.cd.kamcoback.members.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class TokenResponse { + + private String status; + private String accessToken; + private String refreshToken; + private MembersDto.Member member; +} diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java index 1dcfc578..287bc99b 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java @@ -2,6 +2,7 @@ package com.kamco.cd.kamcoback.members.service; import com.kamco.cd.kamcoback.members.dto.SignInRequest; import com.kamco.cd.kamcoback.postgres.core.MembersCoreService; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -13,6 +14,16 @@ public class AuthService { private final MembersCoreService membersCoreService; + /** + * 로그인 일시 저장 + * + * @param uuid + */ + @Transactional + public void saveLogin(UUID uuid) { + membersCoreService.saveLogin(uuid); + } + /** * 사용자 상태 조회 * diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java index 5cbd1b79..c9611d6d 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java @@ -44,7 +44,7 @@ public class MembersService { } /** - * 대문자 1개 이상 소문자 1개 이상 숫자 1개 이상 특수문자(!@#$) 1개 이상 + * 영문, 숫자, 특수문자를 모두 포함하여 8~20자 이내의 비밀번호 * * @param password * @return diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java index 6f1854d7..77039d5a 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java @@ -1,6 +1,8 @@ package com.kamco.cd.kamcoback.postgres.core; import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator; +import com.kamco.cd.kamcoback.common.enums.StatusType; +import com.kamco.cd.kamcoback.common.utils.UserUtil; import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.MembersDto.AddReq; import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; @@ -23,6 +25,7 @@ import org.springframework.stereotype.Service; public class MembersCoreService { private final MembersRepository membersRepository; + private final UserUtil userUtil; /** * 관리자 계정 등록 @@ -47,6 +50,7 @@ public class MembersCoreService { memberEntity.setPassword(hashedPassword); memberEntity.setName(addReq.getName()); memberEntity.setEmployeeNo(addReq.getEmployeeNo()); + memberEntity.setRgstrUidl(userUtil.getId()); return membersRepository.save(memberEntity).getId(); } @@ -74,11 +78,13 @@ public class MembersCoreService { memberEntity.setEmployeeNo(updateReq.getEmployeeNo()); } + memberEntity.setUpdtrUid(userUtil.getId()); + membersRepository.save(memberEntity); } /** - * 관리자 계정 미사용 처리 + * 관리자 계정 삭제 처리 * * @param uuid */ @@ -86,8 +92,9 @@ public class MembersCoreService { MemberEntity memberEntity = membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); - memberEntity.setStatus("INACTIVE"); + memberEntity.setStatus(StatusType.DELETED.getId()); memberEntity.setUpdatedDttm(ZonedDateTime.now()); + memberEntity.setUpdtrUid(userUtil.getId()); membersRepository.save(memberEntity); } @@ -108,6 +115,7 @@ public class MembersCoreService { memberEntity.setPassword(hashedPassword); memberEntity.setStatus("ACTIVE"); memberEntity.setUpdatedDttm(ZonedDateTime.now()); + memberEntity.setUpdtrUid(memberEntity.getId()); membersRepository.save(memberEntity); } @@ -136,4 +144,21 @@ public class MembersCoreService { .orElseThrow(MemberNotFoundException::new); return memberEntity.getStatus(); } + + /** + * 최초 로그인 저장 마지막 로그인 저장 + * + * @param uuid + */ + public void saveLogin(UUID uuid) { + MemberEntity memberEntity = + membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); + + if (memberEntity.getFirstLoginDttm() == null) { + memberEntity.setFirstLoginDttm(ZonedDateTime.now()); + } + memberEntity.setLastLoginDttm(ZonedDateTime.now()); + memberEntity.setLoginFailCount(0); + membersRepository.save(memberEntity); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java index 5a4879b0..19f7e4d3 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java @@ -1,5 +1,6 @@ package com.kamco.cd.kamcoback.postgres.entity; +import com.kamco.cd.kamcoback.common.enums.StatusType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -63,7 +64,7 @@ public class MemberEntity { @Size(max = 20) @ColumnDefault("'INACTIVE'") @Column(name = "status", length = 20) - private String status = "INACTIVE"; + private String status = StatusType.INACTIVE.getId(); @NotNull @ColumnDefault("now()") @@ -83,4 +84,10 @@ public class MemberEntity { @Column(name = "login_fail_count") @ColumnDefault("0") private Integer loginFailCount = 0; + + @Column(name = "rgstr_uid") + private Long rgstrUidl; + + @Column(name = "updtr_uid") + private Long updtrUid; } From 508565d2ecc8dcfffb4781f753a07f6597fbfcdf Mon Sep 17 00:00:00 2001 From: "gayoun.park" Date: Thu, 11 Dec 2025 15:38:37 +0900 Subject: [PATCH 12/16] =?UTF-8?q?=EA=B3=B5=ED=86=B5=EC=BD=94=EB=93=9C=20ch?= =?UTF-8?q?ild=20where=20=EC=A1=B0=EA=B1=B4=20=EB=AA=85=EC=8B=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../postgres/entity/CommonCodeEntity.java | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/CommonCodeEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/CommonCodeEntity.java index 6fe00c0c..f2d829eb 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/CommonCodeEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/CommonCodeEntity.java @@ -2,17 +2,7 @@ package com.kamco.cd.kamcoback.postgres.entity; import com.kamco.cd.kamcoback.code.dto.CommonCodeDto; import com.kamco.cd.kamcoback.postgres.CommonDateEntity; -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Table; +import jakarta.persistence.*; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import java.time.ZonedDateTime; @@ -21,6 +11,7 @@ import java.util.List; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.Where; @Entity @Getter @@ -60,6 +51,7 @@ public class CommonCodeEntity extends CommonDateEntity { private CommonCodeEntity parent; @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @Where(clause = "deleted = false or deleted is null") private List children = new ArrayList<>(); @Size(max = 255) From c9a450cec92389465f4dbb9ed4b641787ae75ba7 Mon Sep 17 00:00:00 2001 From: "gayoun.park" Date: Thu, 11 Dec 2025 15:54:39 +0900 Subject: [PATCH 13/16] =?UTF-8?q?sysout=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cd/kamcoback/common/utils/html/HtmlEscapeDeserializer.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/common/utils/html/HtmlEscapeDeserializer.java b/src/main/java/com/kamco/cd/kamcoback/common/utils/html/HtmlEscapeDeserializer.java index 7496a2d8..df1c8a32 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/utils/html/HtmlEscapeDeserializer.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/utils/html/HtmlEscapeDeserializer.java @@ -13,8 +13,6 @@ public class HtmlEscapeDeserializer extends JsonDeserializer { public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException { String value = jsonParser.getValueAsString(); - System.out.println("🔥 HtmlEscapeDeserializer 실행됨: " + value); - System.out.println("convert : " + (value == null ? null : HtmlUtils.htmlEscape(value))); return value == null ? null : HtmlUtils.htmlEscape(value); } } From 7cc8e46056b893f62311eaeae46cf5b2e121217e Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 15:58:38 +0900 Subject: [PATCH 14/16] =?UTF-8?q?=EA=B6=8C=ED=95=9C=20enum=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java index 91956284..f54478ff 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java @@ -7,7 +7,6 @@ import org.springframework.http.HttpStatus; @Getter public enum AuthErrorCode implements ErrorCode { - // 🔐 로그인 관련 LOGIN_ID_NOT_FOUND("LOGIN_ID_NOT_FOUND", HttpStatus.UNAUTHORIZED), LOGIN_PASSWORD_MISMATCH("LOGIN_PASSWORD_MISMATCH", HttpStatus.UNAUTHORIZED), From d18e4db0600d83de388a5aca3cb2dfa9d142ebfd Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 17:06:13 +0900 Subject: [PATCH 15/16] =?UTF-8?q?test=20api=20=EC=B6=94=EA=B0=80,=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=EC=84=9C=EB=B2=84=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=EC=8B=9C=EA=B0=84=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cd/kamcoback/members/AuthController.java | 135 ++++---- .../InferenceResultRepositoryImpl.java | 287 +++++++++--------- .../cd/kamcoback/test/TestApiController.java | 68 +++++ src/main/resources/application-dev.yml | 6 +- 4 files changed, 284 insertions(+), 212 deletions(-) create mode 100644 src/main/java/com/kamco/cd/kamcoback/test/TestApiController.java diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java index d80dbfdf..a2e21d56 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -54,59 +54,59 @@ public class AuthController { @Operation(summary = "로그인", description = "사번으로 로그인하여 액세스/리프레시 토큰을 발급.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "로그인 성공", - content = @Content(schema = @Schema(implementation = TokenResponse.class))), + responseCode = "200", + description = "로그인 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( - responseCode = "401", - description = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)", - content = - @Content( - schema = @Schema(implementation = ErrorResponse.class), - examples = { - @ExampleObject( - name = "아이디 입력 오류", - description = "존재하지 않는 아이디", - value = - """ + responseCode = "401", + description = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)", + content = + @Content( + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "아이디 입력 오류", + description = "존재하지 않는 아이디", + value = + """ { "code": "LOGIN_ID_NOT_FOUND", "message": "아이디를 잘못 입력하셨습니다." } """), - @ExampleObject( - name = "비밀번호 입력 오류 (4회 이하)", - description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우", - value = - """ + @ExampleObject( + name = "비밀번호 입력 오류 (4회 이하)", + description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우", + value = + """ { "code": "LOGIN_PASSWORD_MISMATCH", "message": "비밀번호를 잘못 입력하셨습니다." } """), - @ExampleObject( - name = "비밀번호 오류 횟수 초과", - description = "비밀번호 5회 이상 오류로 계정 잠김", - value = - """ + @ExampleObject( + name = "비밀번호 오류 횟수 초과", + description = "비밀번호 5회 이상 오류로 계정 잠김", + value = + """ { "code": "LOGIN_PASSWORD_EXCEEDED", "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다." } """) - })) + })) }) public ApiResponseDto signin( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "로그인 요청 정보", - required = true) - @RequestBody - SignInRequest request, - HttpServletResponse response) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "로그인 요청 정보", + required = true) + @RequestBody + SignInRequest request, + HttpServletResponse response) { Authentication authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); String status = authService.getUserStatus(request); @@ -124,17 +124,17 @@ public class AuthController { // Redis에 RefreshToken 저장 (TTL = 7일) refreshTokenService.save( - username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); + username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); // HttpOnly + Secure 쿠키에 RefreshToken 저장 ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, refreshToken) - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, refreshToken) + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); @@ -153,20 +153,19 @@ public class AuthController { @Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "재발급 성공", - content = @Content(schema = @Schema(implementation = TokenResponse.class))), + responseCode = "200", + description = "재발급 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( - responseCode = "401", - description = "만료되었거나 유효하지 않은 리프레시 토큰", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + responseCode = "403", + description = "만료되었거나 유효하지 않은 리프레시 토큰", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) public ResponseEntity refresh(String refreshToken, HttpServletResponse response) - throws AccessDeniedException { + throws AccessDeniedException { if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) { throw new AccessDeniedException("만료되었거나 유효하지 않은 리프레시 토큰 입니다."); } - String username = jwtTokenProvider.getSubject(refreshToken); // Redis에 저장된 RefreshToken과 일치하는지 확인 @@ -180,17 +179,17 @@ public class AuthController { // Redis 갱신 refreshTokenService.save( - username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); + username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); // 쿠키 갱신 ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, newRefreshToken) - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, newRefreshToken) + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); MembersDto.Member member = new MembersDto.Member(); @@ -201,12 +200,12 @@ public class AuthController { @Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "로그아웃 성공", - content = @Content(schema = @Schema(implementation = Void.class))) + responseCode = "200", + description = "로그아웃 성공", + content = @Content(schema = @Schema(implementation = Void.class))) }) public ApiResponseDto> logout( - Authentication authentication, HttpServletResponse response) { + Authentication authentication, HttpServletResponse response) { if (authentication != null) { String username = authentication.getName(); // Redis에서 RefreshToken 삭제 @@ -215,13 +214,13 @@ public class AuthController { // 쿠키 삭제 (Max-Age=0) ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, "") - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(0) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, "") + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(0) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); return ApiResponseDto.createOK(ResponseEntity.noContent().build()); diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java index dfcab8d6..8421fb08 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java @@ -40,11 +40,11 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC private final QModelVerEntity tmv = QModelVerEntity.modelVerEntity; private final QMapSheetAnalEntity mapSheetAnalEntity = QMapSheetAnalEntity.mapSheetAnalEntity; private final QMapSheetAnalDataEntity mapSheetAnalDataEntity = - QMapSheetAnalDataEntity.mapSheetAnalDataEntity; + QMapSheetAnalDataEntity.mapSheetAnalDataEntity; private final QMapSheetAnalDataGeomEntity mapSheetAnalDataGeomEntity = - QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity; + QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity; private final QMapSheetAnalSttcEntity mapSheetAnalSttcEntity = - QMapSheetAnalSttcEntity.mapSheetAnalSttcEntity; + QMapSheetAnalSttcEntity.mapSheetAnalSttcEntity; /** * 분석결과 목록 조회 @@ -54,7 +54,7 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC */ @Override public Page getInferenceResultList( - InferenceResultDto.SearchReq searchReq) { + InferenceResultDto.SearchReq searchReq) { Pageable pageable = searchReq.toPageable(); // "0000" 전체조회 BooleanBuilder builder = new BooleanBuilder(); @@ -68,35 +68,35 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC } List content = - queryFactory - .select( - Projections.constructor( - InferenceResultDto.AnalResList.class, - mapSheetAnalEntity.id, - mapSheetAnalEntity.analTitle, - mapSheetAnalEntity.analMapSheet, - mapSheetAnalEntity.detectingCnt, - mapSheetAnalEntity.analStrtDttm, - mapSheetAnalEntity.analEndDttm, - mapSheetAnalEntity.analSec, - mapSheetAnalEntity.analPredSec, - mapSheetAnalEntity.analState, - Expressions.stringTemplate( - "fn_code_name({0}, {1})", "0002", mapSheetAnalEntity.analState), - mapSheetAnalEntity.gukyuinUsed)) - .from(mapSheetAnalEntity) - .where(builder) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .orderBy(mapSheetAnalEntity.id.desc()) - .fetch(); + queryFactory + .select( + Projections.constructor( + InferenceResultDto.AnalResList.class, + mapSheetAnalEntity.id, + mapSheetAnalEntity.analTitle, + mapSheetAnalEntity.analMapSheet, + mapSheetAnalEntity.detectingCnt, + mapSheetAnalEntity.analStrtDttm, + mapSheetAnalEntity.analEndDttm, + mapSheetAnalEntity.analSec, + mapSheetAnalEntity.analPredSec, + mapSheetAnalEntity.analState, + Expressions.stringTemplate( + "fn_code_name({0}, {1})", "0002", mapSheetAnalEntity.analState), + mapSheetAnalEntity.gukyuinUsed)) + .from(mapSheetAnalEntity) + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(mapSheetAnalEntity.id.desc()) + .fetch(); long total = - queryFactory - .select(mapSheetAnalEntity.id) - .from(mapSheetAnalEntity) - .where(builder) - .fetchCount(); + queryFactory + .select(mapSheetAnalEntity.id) + .from(mapSheetAnalEntity) + .where(builder) + .fetchCount(); return new PageImpl<>(content, pageable, total); } @@ -112,40 +112,41 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC // 1. 최신 버전 UID를 가져오는 서브쿼리 JPQLQuery latestVerUidSub = - JPAExpressions.select(tmv.id.max()).from(tmv).where(tmv.modelUid.eq(tmm.id)); + JPAExpressions.select(tmv.id.max()).from(tmv).where(tmv.modelUid.eq(tmm.id)); Optional content = - Optional.ofNullable( - queryFactory - .select( - Projections.constructor( - InferenceResultDto.AnalResSummary.class, - mapSheetAnalEntity.id, - mapSheetAnalEntity.analTitle, - tmm.modelNm.concat(" ").concat(tmv.modelVer).as("modelInfo"), - mapSheetAnalEntity.targetYyyy, - mapSheetAnalEntity.compareYyyy, - mapSheetAnalEntity.analMapSheet, - mapSheetAnalEntity.analStrtDttm, - mapSheetAnalEntity.analEndDttm, - mapSheetAnalEntity.analSec, - mapSheetAnalEntity.analPredSec, - mapSheetAnalEntity.resultUrl, - mapSheetAnalEntity.detectingCnt, - mapSheetAnalEntity.accuracy, - mapSheetAnalEntity.analState, - Expressions.stringTemplate( - "fn_code_name({0}, {1})", "0002", mapSheetAnalEntity.analState))) - .from(mapSheetAnalEntity) - .leftJoin(tmm) - .on(mapSheetAnalEntity.modelUid.eq(tmm.id)) - .leftJoin(tmv) - .on(tmv.modelUid.eq(tmm.id).and(tmv.id.eq(latestVerUidSub))) - .where(mapSheetAnalEntity.id.eq(id)) - .fetchOne()); + Optional.ofNullable( + queryFactory + .select( + Projections.constructor( + InferenceResultDto.AnalResSummary.class, + mapSheetAnalEntity.id, + mapSheetAnalEntity.analTitle, + tmm.modelNm.concat(" ").concat(tmv.modelVer).as("modelInfo"), + mapSheetAnalEntity.targetYyyy, + mapSheetAnalEntity.compareYyyy, + mapSheetAnalEntity.analMapSheet, + mapSheetAnalEntity.analStrtDttm, + mapSheetAnalEntity.analEndDttm, + mapSheetAnalEntity.analSec, + mapSheetAnalEntity.analPredSec, + mapSheetAnalEntity.resultUrl, + mapSheetAnalEntity.detectingCnt, + mapSheetAnalEntity.accuracy, + mapSheetAnalEntity.analState, + Expressions.stringTemplate( + "fn_code_name({0}, {1})", "0002", mapSheetAnalEntity.analState))) + .from(mapSheetAnalEntity) + .leftJoin(tmm) + .on(mapSheetAnalEntity.modelUid.eq(tmm.id)) + .leftJoin(tmv) + .on(tmv.modelUid.eq(tmm.id).and(tmv.id.eq(latestVerUidSub))) + .where(mapSheetAnalEntity.id.eq(id)) + .fetchOne()); return content; } + /** * 분석결과 상세 class name별 탐지 개수 * @@ -155,16 +156,16 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC @Override public List getDashboard(Long id) { return queryFactory - .select( - Projections.constructor( - Dashboard.class, - mapSheetAnalSttcEntity.id.classAfterCd, - mapSheetAnalSttcEntity.classAfterCnt.sum())) - .from(mapSheetAnalSttcEntity) - .where(mapSheetAnalSttcEntity.id.analUid.eq(id)) - .groupBy(mapSheetAnalSttcEntity.id.classAfterCd) - .orderBy(mapSheetAnalSttcEntity.id.classAfterCd.asc()) - .fetch(); + .select( + Projections.constructor( + Dashboard.class, + mapSheetAnalSttcEntity.id.classAfterCd, + mapSheetAnalSttcEntity.classAfterCnt.sum())) + .from(mapSheetAnalSttcEntity) + .where(mapSheetAnalSttcEntity.id.analUid.eq(id)) + .groupBy(mapSheetAnalSttcEntity.id.classAfterCd) + .orderBy(mapSheetAnalSttcEntity.id.classAfterCd.asc()) + .fetch(); } @Override @@ -181,22 +182,22 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC */ @Override public Page listInferenceResultWithGeom( - List ids, SearchGeoReq searchReq) { + List ids, SearchGeoReq searchReq) { // 분석 차수 QMapSheetAnalDataGeomEntity detectedEntity = - QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity; + QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity; Pageable pageable = searchReq.toPageable(); // 검색조건 JPAQuery query = - queryFactory - .selectFrom(detectedEntity) - .where( - detectedEntity.dataUid.in(ids), - eqTargetClass(detectedEntity, searchReq.getTargetClass()), - eqCompareClass(detectedEntity, searchReq.getCompareClass()), - containsMapSheetNum(detectedEntity, searchReq.getMapSheetNum())); + queryFactory + .selectFrom(detectedEntity) + .where( + detectedEntity.dataUid.in(ids), + eqTargetClass(detectedEntity, searchReq.getTargetClass()), + eqCompareClass(detectedEntity, searchReq.getCompareClass()), + containsMapSheetNum(detectedEntity, searchReq.getMapSheetNum())); // count long total = query.fetchCount(); @@ -208,11 +209,11 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC } List content = - query - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .orderBy(orders.toArray(new OrderSpecifier[0])) - .fetch(); + query + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(orders.toArray(new OrderSpecifier[0])) + .fetch(); return new PageImpl<>(content, pageable, total); } @@ -234,19 +235,19 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC // 기준년도 분류 if (searchGeoReq.getTargetClass() != null && !searchGeoReq.getTargetClass().equals("")) { builder.and( - mapSheetAnalDataGeomEntity - .classAfterCd - .toLowerCase() - .eq(searchGeoReq.getTargetClass().toLowerCase())); + mapSheetAnalDataGeomEntity + .classAfterCd + .toLowerCase() + .eq(searchGeoReq.getTargetClass().toLowerCase())); } // 비교년도 분류 if (searchGeoReq.getCompareClass() != null && !searchGeoReq.getCompareClass().equals("")) { builder.and( - mapSheetAnalDataGeomEntity - .classBeforeCd - .toLowerCase() - .eq(searchGeoReq.getCompareClass().toLowerCase())); + mapSheetAnalDataGeomEntity + .classBeforeCd + .toLowerCase() + .eq(searchGeoReq.getCompareClass().toLowerCase())); } // 분석도엽 @@ -256,39 +257,39 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC } List content = - queryFactory - .select( - Projections.constructor( - InferenceResultDto.Geom.class, - mapSheetAnalDataGeomEntity.compareYyyy, - mapSheetAnalDataGeomEntity.targetYyyy, - mapSheetAnalDataGeomEntity.classBeforeCd, - mapSheetAnalDataGeomEntity.classBeforeProb, - mapSheetAnalDataGeomEntity.classAfterCd, - mapSheetAnalDataGeomEntity.classAfterProb, - mapSheetAnalDataGeomEntity.mapSheetNum, - mapSheetAnalDataGeomEntity.geom, - mapSheetAnalDataGeomEntity.geomCenter)) - .from(mapSheetAnalEntity) - .join(mapSheetAnalDataEntity) - .on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id)) - .join(mapSheetAnalDataGeomEntity) - .on(mapSheetAnalDataGeomEntity.dataUid.eq(mapSheetAnalDataEntity.id)) - .where(builder) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); + queryFactory + .select( + Projections.constructor( + InferenceResultDto.Geom.class, + mapSheetAnalDataGeomEntity.compareYyyy, + mapSheetAnalDataGeomEntity.targetYyyy, + mapSheetAnalDataGeomEntity.classBeforeCd, + mapSheetAnalDataGeomEntity.classBeforeProb, + mapSheetAnalDataGeomEntity.classAfterCd, + mapSheetAnalDataGeomEntity.classAfterProb, + mapSheetAnalDataGeomEntity.mapSheetNum, + mapSheetAnalDataGeomEntity.geom, + mapSheetAnalDataGeomEntity.geomCenter)) + .from(mapSheetAnalEntity) + .join(mapSheetAnalDataEntity) + .on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id)) + .join(mapSheetAnalDataGeomEntity) + .on(mapSheetAnalDataGeomEntity.dataUid.eq(mapSheetAnalDataEntity.id)) + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); long total = - queryFactory - .select(mapSheetAnalDataGeomEntity.id) - .from(mapSheetAnalEntity) - .join(mapSheetAnalDataEntity) - .on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id)) - .join(mapSheetAnalDataGeomEntity) - .on(mapSheetAnalDataGeomEntity.dataUid.eq(mapSheetAnalDataEntity.id)) - .where(builder) - .fetchCount(); + queryFactory + .select(mapSheetAnalDataGeomEntity.id) + .from(mapSheetAnalEntity) + .join(mapSheetAnalDataEntity) + .on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id)) + .join(mapSheetAnalDataGeomEntity) + .on(mapSheetAnalDataGeomEntity.dataUid.eq(mapSheetAnalDataEntity.id)) + .where(builder) + .fetchCount(); return new PageImpl<>(content, pageable, total); } @@ -302,16 +303,18 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC @Override public List getSheets(Long id) { return queryFactory - .select(mapSheetAnalDataEntity.mapSheetNum) - .from(mapSheetAnalEntity) - .join(mapSheetAnalDataEntity) - .on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id)) - .where(mapSheetAnalEntity.id.eq(id)) - .groupBy(mapSheetAnalDataEntity.mapSheetNum) - .fetch(); + .select(mapSheetAnalDataEntity.mapSheetNum) + .from(mapSheetAnalEntity) + .join(mapSheetAnalDataEntity) + .on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id)) + .where(mapSheetAnalEntity.id.eq(id)) + .groupBy(mapSheetAnalDataEntity.mapSheetNum) + .fetch(); } - /** Pageable의 Sort를 QueryDSL OrderSpecifier로 변환 */ + /** + * Pageable의 Sort를 QueryDSL OrderSpecifier로 변환 + */ @SuppressWarnings({"unchecked", "rawtypes"}) private List> getOrderSpecifiers(Sort sort) { List> orders = new ArrayList<>(); @@ -326,8 +329,7 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC // 유효한 필드만 처리 switch (property) { case "classBeforeCd" -> orders.add(new OrderSpecifier(direction, entity.classBeforeCd)); - case "classBeforeProb" -> - orders.add(new OrderSpecifier(direction, entity.classBeforeProb)); + case "classBeforeProb" -> orders.add(new OrderSpecifier(direction, entity.classBeforeProb)); case "classAfterCd" -> orders.add(new OrderSpecifier(direction, entity.classAfterCd)); case "classAfterProb" -> orders.add(new OrderSpecifier(direction, entity.classAfterProb)); case "mapSheetNum" -> orders.add(new OrderSpecifier(direction, entity.mapSheetNum)); @@ -336,8 +338,9 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC case "area" -> orders.add(new OrderSpecifier(direction, entity.area)); case "createdDttm" -> orders.add(new OrderSpecifier(direction, entity.createdDttm)); case "updatedDttm" -> orders.add(new OrderSpecifier(direction, entity.updatedDttm)); - // 유효하지 않은 필드는 무시 - default -> {} + // 유효하지 않은 필드는 무시 + default -> { + } } } } @@ -346,21 +349,21 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC } private BooleanExpression eqTargetClass( - QMapSheetAnalDataGeomEntity detectedEntity, String targetClass) { + QMapSheetAnalDataGeomEntity detectedEntity, String targetClass) { return targetClass != null && !targetClass.isEmpty() - ? detectedEntity.classAfterCd.toLowerCase().eq(targetClass.toLowerCase()) - : null; + ? detectedEntity.classAfterCd.toLowerCase().eq(targetClass.toLowerCase()) + : null; } private BooleanExpression eqCompareClass( - QMapSheetAnalDataGeomEntity detectedEntity, String compareClass) { + QMapSheetAnalDataGeomEntity detectedEntity, String compareClass) { return compareClass != null && !compareClass.isEmpty() - ? detectedEntity.classBeforeCd.toLowerCase().eq(compareClass.toLowerCase()) - : null; + ? detectedEntity.classBeforeCd.toLowerCase().eq(compareClass.toLowerCase()) + : null; } private BooleanExpression containsMapSheetNum( - QMapSheetAnalDataGeomEntity detectedEntity, List mapSheet) { + QMapSheetAnalDataGeomEntity detectedEntity, List mapSheet) { if (mapSheet == null || mapSheet.isEmpty()) { return null; } diff --git a/src/main/java/com/kamco/cd/kamcoback/test/TestApiController.java b/src/main/java/com/kamco/cd/kamcoback/test/TestApiController.java new file mode 100644 index 00000000..b0430975 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/test/TestApiController.java @@ -0,0 +1,68 @@ +package com.kamco.cd.kamcoback.test; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.ErrorResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "test api", description = "test api") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/test") +public class TestApiController { + + @Operation(summary = "admin test", description = "admin test api") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "조회", + content = @Content(schema = @Schema(implementation = String.class))), + @ApiResponse( + responseCode = "403", + description = "권한 없음", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/admin") + public String admin() { + return "I am administrator"; + } + + @Operation(summary = "label test", description = "label test api") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "조회", + content = @Content(schema = @Schema(implementation = String.class))), + @ApiResponse( + responseCode = "403", + description = "권한 없음", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/label") + public String label() { + return "Labeling is available."; + } + + @Operation(summary = "review test", description = "review test api") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "조회", + content = @Content(schema = @Schema(implementation = String.class))), + @ApiResponse( + responseCode = "403", + description = "권한 없음", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @GetMapping("/review") + public String review() { + return "Review is available."; + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 0b2d19f6..2013e5fb 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -38,8 +38,10 @@ spring: jwt: secret: "kamco_token_9b71e778-19a3-4c1d-97bf-2d687de17d5b" - access-token-validity-in-ms: 86400000 # 1일 - refresh-token-validity-in-ms: 604800000 # 7일 + #access-token-validity-in-ms: 86400000 # 1일 + #refresh-token-validity-in-ms: 604800000 # 7일 + access-token-validity-in-ms: 60000 # 1분 + refresh-token-validity-in-ms: 300000 # 5분 token: refresh-cookie-name: kamco-dev # 개발용 쿠키 이름 From ce8534cf0f14503303fc6db7d007c84cf67fba6b Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 17:06:36 +0900 Subject: [PATCH 16/16] =?UTF-8?q?test=20api=20=EC=B6=94=EA=B0=80,=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=EC=84=9C=EB=B2=84=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=A7=8C=EB=A3=8C=EC=8B=9C=EA=B0=84=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/enums/error/AuthErrorCode.java | 1 - .../cd/kamcoback/members/AuthController.java | 134 ++++---- .../InferenceResultRepositoryImpl.java | 287 +++++++++--------- .../cd/kamcoback/test/TestApiController.java | 36 +-- 4 files changed, 227 insertions(+), 231 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java index f54478ff..4f5f45dd 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java @@ -6,7 +6,6 @@ import org.springframework.http.HttpStatus; @Getter public enum AuthErrorCode implements ErrorCode { - LOGIN_ID_NOT_FOUND("LOGIN_ID_NOT_FOUND", HttpStatus.UNAUTHORIZED), LOGIN_PASSWORD_MISMATCH("LOGIN_PASSWORD_MISMATCH", HttpStatus.UNAUTHORIZED), diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java index a2e21d56..8596df04 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -54,59 +54,59 @@ public class AuthController { @Operation(summary = "로그인", description = "사번으로 로그인하여 액세스/리프레시 토큰을 발급.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "로그인 성공", - content = @Content(schema = @Schema(implementation = TokenResponse.class))), + responseCode = "200", + description = "로그인 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( - responseCode = "401", - description = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)", - content = - @Content( - schema = @Schema(implementation = ErrorResponse.class), - examples = { - @ExampleObject( - name = "아이디 입력 오류", - description = "존재하지 않는 아이디", - value = - """ + responseCode = "401", + description = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)", + content = + @Content( + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "아이디 입력 오류", + description = "존재하지 않는 아이디", + value = + """ { "code": "LOGIN_ID_NOT_FOUND", "message": "아이디를 잘못 입력하셨습니다." } """), - @ExampleObject( - name = "비밀번호 입력 오류 (4회 이하)", - description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우", - value = - """ + @ExampleObject( + name = "비밀번호 입력 오류 (4회 이하)", + description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우", + value = + """ { "code": "LOGIN_PASSWORD_MISMATCH", "message": "비밀번호를 잘못 입력하셨습니다." } """), - @ExampleObject( - name = "비밀번호 오류 횟수 초과", - description = "비밀번호 5회 이상 오류로 계정 잠김", - value = - """ + @ExampleObject( + name = "비밀번호 오류 횟수 초과", + description = "비밀번호 5회 이상 오류로 계정 잠김", + value = + """ { "code": "LOGIN_PASSWORD_EXCEEDED", "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다." } """) - })) + })) }) public ApiResponseDto signin( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "로그인 요청 정보", - required = true) - @RequestBody - SignInRequest request, - HttpServletResponse response) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "로그인 요청 정보", + required = true) + @RequestBody + SignInRequest request, + HttpServletResponse response) { Authentication authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); String status = authService.getUserStatus(request); @@ -124,17 +124,17 @@ public class AuthController { // Redis에 RefreshToken 저장 (TTL = 7일) refreshTokenService.save( - username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); + username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); // HttpOnly + Secure 쿠키에 RefreshToken 저장 ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, refreshToken) - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, refreshToken) + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); @@ -153,16 +153,16 @@ public class AuthController { @Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "재발급 성공", - content = @Content(schema = @Schema(implementation = TokenResponse.class))), + responseCode = "200", + description = "재발급 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( - responseCode = "403", - description = "만료되었거나 유효하지 않은 리프레시 토큰", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + responseCode = "403", + description = "만료되었거나 유효하지 않은 리프레시 토큰", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) public ResponseEntity refresh(String refreshToken, HttpServletResponse response) - throws AccessDeniedException { + throws AccessDeniedException { if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) { throw new AccessDeniedException("만료되었거나 유효하지 않은 리프레시 토큰 입니다."); } @@ -179,17 +179,17 @@ public class AuthController { // Redis 갱신 refreshTokenService.save( - username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); + username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); // 쿠키 갱신 ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, newRefreshToken) - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, newRefreshToken) + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); MembersDto.Member member = new MembersDto.Member(); @@ -200,12 +200,12 @@ public class AuthController { @Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "로그아웃 성공", - content = @Content(schema = @Schema(implementation = Void.class))) + responseCode = "200", + description = "로그아웃 성공", + content = @Content(schema = @Schema(implementation = Void.class))) }) public ApiResponseDto> logout( - Authentication authentication, HttpServletResponse response) { + Authentication authentication, HttpServletResponse response) { if (authentication != null) { String username = authentication.getName(); // Redis에서 RefreshToken 삭제 @@ -214,13 +214,13 @@ public class AuthController { // 쿠키 삭제 (Max-Age=0) ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, "") - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(0) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, "") + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(0) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); return ApiResponseDto.createOK(ResponseEntity.noContent().build()); diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java index 8421fb08..dfcab8d6 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/Inference/InferenceResultRepositoryImpl.java @@ -40,11 +40,11 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC private final QModelVerEntity tmv = QModelVerEntity.modelVerEntity; private final QMapSheetAnalEntity mapSheetAnalEntity = QMapSheetAnalEntity.mapSheetAnalEntity; private final QMapSheetAnalDataEntity mapSheetAnalDataEntity = - QMapSheetAnalDataEntity.mapSheetAnalDataEntity; + QMapSheetAnalDataEntity.mapSheetAnalDataEntity; private final QMapSheetAnalDataGeomEntity mapSheetAnalDataGeomEntity = - QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity; + QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity; private final QMapSheetAnalSttcEntity mapSheetAnalSttcEntity = - QMapSheetAnalSttcEntity.mapSheetAnalSttcEntity; + QMapSheetAnalSttcEntity.mapSheetAnalSttcEntity; /** * 분석결과 목록 조회 @@ -54,7 +54,7 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC */ @Override public Page getInferenceResultList( - InferenceResultDto.SearchReq searchReq) { + InferenceResultDto.SearchReq searchReq) { Pageable pageable = searchReq.toPageable(); // "0000" 전체조회 BooleanBuilder builder = new BooleanBuilder(); @@ -68,35 +68,35 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC } List content = - queryFactory - .select( - Projections.constructor( - InferenceResultDto.AnalResList.class, - mapSheetAnalEntity.id, - mapSheetAnalEntity.analTitle, - mapSheetAnalEntity.analMapSheet, - mapSheetAnalEntity.detectingCnt, - mapSheetAnalEntity.analStrtDttm, - mapSheetAnalEntity.analEndDttm, - mapSheetAnalEntity.analSec, - mapSheetAnalEntity.analPredSec, - mapSheetAnalEntity.analState, - Expressions.stringTemplate( - "fn_code_name({0}, {1})", "0002", mapSheetAnalEntity.analState), - mapSheetAnalEntity.gukyuinUsed)) - .from(mapSheetAnalEntity) - .where(builder) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .orderBy(mapSheetAnalEntity.id.desc()) - .fetch(); + queryFactory + .select( + Projections.constructor( + InferenceResultDto.AnalResList.class, + mapSheetAnalEntity.id, + mapSheetAnalEntity.analTitle, + mapSheetAnalEntity.analMapSheet, + mapSheetAnalEntity.detectingCnt, + mapSheetAnalEntity.analStrtDttm, + mapSheetAnalEntity.analEndDttm, + mapSheetAnalEntity.analSec, + mapSheetAnalEntity.analPredSec, + mapSheetAnalEntity.analState, + Expressions.stringTemplate( + "fn_code_name({0}, {1})", "0002", mapSheetAnalEntity.analState), + mapSheetAnalEntity.gukyuinUsed)) + .from(mapSheetAnalEntity) + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(mapSheetAnalEntity.id.desc()) + .fetch(); long total = - queryFactory - .select(mapSheetAnalEntity.id) - .from(mapSheetAnalEntity) - .where(builder) - .fetchCount(); + queryFactory + .select(mapSheetAnalEntity.id) + .from(mapSheetAnalEntity) + .where(builder) + .fetchCount(); return new PageImpl<>(content, pageable, total); } @@ -112,41 +112,40 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC // 1. 최신 버전 UID를 가져오는 서브쿼리 JPQLQuery latestVerUidSub = - JPAExpressions.select(tmv.id.max()).from(tmv).where(tmv.modelUid.eq(tmm.id)); + JPAExpressions.select(tmv.id.max()).from(tmv).where(tmv.modelUid.eq(tmm.id)); Optional content = - Optional.ofNullable( - queryFactory - .select( - Projections.constructor( - InferenceResultDto.AnalResSummary.class, - mapSheetAnalEntity.id, - mapSheetAnalEntity.analTitle, - tmm.modelNm.concat(" ").concat(tmv.modelVer).as("modelInfo"), - mapSheetAnalEntity.targetYyyy, - mapSheetAnalEntity.compareYyyy, - mapSheetAnalEntity.analMapSheet, - mapSheetAnalEntity.analStrtDttm, - mapSheetAnalEntity.analEndDttm, - mapSheetAnalEntity.analSec, - mapSheetAnalEntity.analPredSec, - mapSheetAnalEntity.resultUrl, - mapSheetAnalEntity.detectingCnt, - mapSheetAnalEntity.accuracy, - mapSheetAnalEntity.analState, - Expressions.stringTemplate( - "fn_code_name({0}, {1})", "0002", mapSheetAnalEntity.analState))) - .from(mapSheetAnalEntity) - .leftJoin(tmm) - .on(mapSheetAnalEntity.modelUid.eq(tmm.id)) - .leftJoin(tmv) - .on(tmv.modelUid.eq(tmm.id).and(tmv.id.eq(latestVerUidSub))) - .where(mapSheetAnalEntity.id.eq(id)) - .fetchOne()); + Optional.ofNullable( + queryFactory + .select( + Projections.constructor( + InferenceResultDto.AnalResSummary.class, + mapSheetAnalEntity.id, + mapSheetAnalEntity.analTitle, + tmm.modelNm.concat(" ").concat(tmv.modelVer).as("modelInfo"), + mapSheetAnalEntity.targetYyyy, + mapSheetAnalEntity.compareYyyy, + mapSheetAnalEntity.analMapSheet, + mapSheetAnalEntity.analStrtDttm, + mapSheetAnalEntity.analEndDttm, + mapSheetAnalEntity.analSec, + mapSheetAnalEntity.analPredSec, + mapSheetAnalEntity.resultUrl, + mapSheetAnalEntity.detectingCnt, + mapSheetAnalEntity.accuracy, + mapSheetAnalEntity.analState, + Expressions.stringTemplate( + "fn_code_name({0}, {1})", "0002", mapSheetAnalEntity.analState))) + .from(mapSheetAnalEntity) + .leftJoin(tmm) + .on(mapSheetAnalEntity.modelUid.eq(tmm.id)) + .leftJoin(tmv) + .on(tmv.modelUid.eq(tmm.id).and(tmv.id.eq(latestVerUidSub))) + .where(mapSheetAnalEntity.id.eq(id)) + .fetchOne()); return content; } - /** * 분석결과 상세 class name별 탐지 개수 * @@ -156,16 +155,16 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC @Override public List getDashboard(Long id) { return queryFactory - .select( - Projections.constructor( - Dashboard.class, - mapSheetAnalSttcEntity.id.classAfterCd, - mapSheetAnalSttcEntity.classAfterCnt.sum())) - .from(mapSheetAnalSttcEntity) - .where(mapSheetAnalSttcEntity.id.analUid.eq(id)) - .groupBy(mapSheetAnalSttcEntity.id.classAfterCd) - .orderBy(mapSheetAnalSttcEntity.id.classAfterCd.asc()) - .fetch(); + .select( + Projections.constructor( + Dashboard.class, + mapSheetAnalSttcEntity.id.classAfterCd, + mapSheetAnalSttcEntity.classAfterCnt.sum())) + .from(mapSheetAnalSttcEntity) + .where(mapSheetAnalSttcEntity.id.analUid.eq(id)) + .groupBy(mapSheetAnalSttcEntity.id.classAfterCd) + .orderBy(mapSheetAnalSttcEntity.id.classAfterCd.asc()) + .fetch(); } @Override @@ -182,22 +181,22 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC */ @Override public Page listInferenceResultWithGeom( - List ids, SearchGeoReq searchReq) { + List ids, SearchGeoReq searchReq) { // 분석 차수 QMapSheetAnalDataGeomEntity detectedEntity = - QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity; + QMapSheetAnalDataGeomEntity.mapSheetAnalDataGeomEntity; Pageable pageable = searchReq.toPageable(); // 검색조건 JPAQuery query = - queryFactory - .selectFrom(detectedEntity) - .where( - detectedEntity.dataUid.in(ids), - eqTargetClass(detectedEntity, searchReq.getTargetClass()), - eqCompareClass(detectedEntity, searchReq.getCompareClass()), - containsMapSheetNum(detectedEntity, searchReq.getMapSheetNum())); + queryFactory + .selectFrom(detectedEntity) + .where( + detectedEntity.dataUid.in(ids), + eqTargetClass(detectedEntity, searchReq.getTargetClass()), + eqCompareClass(detectedEntity, searchReq.getCompareClass()), + containsMapSheetNum(detectedEntity, searchReq.getMapSheetNum())); // count long total = query.fetchCount(); @@ -209,11 +208,11 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC } List content = - query - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .orderBy(orders.toArray(new OrderSpecifier[0])) - .fetch(); + query + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(orders.toArray(new OrderSpecifier[0])) + .fetch(); return new PageImpl<>(content, pageable, total); } @@ -235,19 +234,19 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC // 기준년도 분류 if (searchGeoReq.getTargetClass() != null && !searchGeoReq.getTargetClass().equals("")) { builder.and( - mapSheetAnalDataGeomEntity - .classAfterCd - .toLowerCase() - .eq(searchGeoReq.getTargetClass().toLowerCase())); + mapSheetAnalDataGeomEntity + .classAfterCd + .toLowerCase() + .eq(searchGeoReq.getTargetClass().toLowerCase())); } // 비교년도 분류 if (searchGeoReq.getCompareClass() != null && !searchGeoReq.getCompareClass().equals("")) { builder.and( - mapSheetAnalDataGeomEntity - .classBeforeCd - .toLowerCase() - .eq(searchGeoReq.getCompareClass().toLowerCase())); + mapSheetAnalDataGeomEntity + .classBeforeCd + .toLowerCase() + .eq(searchGeoReq.getCompareClass().toLowerCase())); } // 분석도엽 @@ -257,39 +256,39 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC } List content = - queryFactory - .select( - Projections.constructor( - InferenceResultDto.Geom.class, - mapSheetAnalDataGeomEntity.compareYyyy, - mapSheetAnalDataGeomEntity.targetYyyy, - mapSheetAnalDataGeomEntity.classBeforeCd, - mapSheetAnalDataGeomEntity.classBeforeProb, - mapSheetAnalDataGeomEntity.classAfterCd, - mapSheetAnalDataGeomEntity.classAfterProb, - mapSheetAnalDataGeomEntity.mapSheetNum, - mapSheetAnalDataGeomEntity.geom, - mapSheetAnalDataGeomEntity.geomCenter)) - .from(mapSheetAnalEntity) - .join(mapSheetAnalDataEntity) - .on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id)) - .join(mapSheetAnalDataGeomEntity) - .on(mapSheetAnalDataGeomEntity.dataUid.eq(mapSheetAnalDataEntity.id)) - .where(builder) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .fetch(); + queryFactory + .select( + Projections.constructor( + InferenceResultDto.Geom.class, + mapSheetAnalDataGeomEntity.compareYyyy, + mapSheetAnalDataGeomEntity.targetYyyy, + mapSheetAnalDataGeomEntity.classBeforeCd, + mapSheetAnalDataGeomEntity.classBeforeProb, + mapSheetAnalDataGeomEntity.classAfterCd, + mapSheetAnalDataGeomEntity.classAfterProb, + mapSheetAnalDataGeomEntity.mapSheetNum, + mapSheetAnalDataGeomEntity.geom, + mapSheetAnalDataGeomEntity.geomCenter)) + .from(mapSheetAnalEntity) + .join(mapSheetAnalDataEntity) + .on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id)) + .join(mapSheetAnalDataGeomEntity) + .on(mapSheetAnalDataGeomEntity.dataUid.eq(mapSheetAnalDataEntity.id)) + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); long total = - queryFactory - .select(mapSheetAnalDataGeomEntity.id) - .from(mapSheetAnalEntity) - .join(mapSheetAnalDataEntity) - .on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id)) - .join(mapSheetAnalDataGeomEntity) - .on(mapSheetAnalDataGeomEntity.dataUid.eq(mapSheetAnalDataEntity.id)) - .where(builder) - .fetchCount(); + queryFactory + .select(mapSheetAnalDataGeomEntity.id) + .from(mapSheetAnalEntity) + .join(mapSheetAnalDataEntity) + .on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id)) + .join(mapSheetAnalDataGeomEntity) + .on(mapSheetAnalDataGeomEntity.dataUid.eq(mapSheetAnalDataEntity.id)) + .where(builder) + .fetchCount(); return new PageImpl<>(content, pageable, total); } @@ -303,18 +302,16 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC @Override public List getSheets(Long id) { return queryFactory - .select(mapSheetAnalDataEntity.mapSheetNum) - .from(mapSheetAnalEntity) - .join(mapSheetAnalDataEntity) - .on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id)) - .where(mapSheetAnalEntity.id.eq(id)) - .groupBy(mapSheetAnalDataEntity.mapSheetNum) - .fetch(); + .select(mapSheetAnalDataEntity.mapSheetNum) + .from(mapSheetAnalEntity) + .join(mapSheetAnalDataEntity) + .on(mapSheetAnalDataEntity.analUid.eq(mapSheetAnalEntity.id)) + .where(mapSheetAnalEntity.id.eq(id)) + .groupBy(mapSheetAnalDataEntity.mapSheetNum) + .fetch(); } - /** - * Pageable의 Sort를 QueryDSL OrderSpecifier로 변환 - */ + /** Pageable의 Sort를 QueryDSL OrderSpecifier로 변환 */ @SuppressWarnings({"unchecked", "rawtypes"}) private List> getOrderSpecifiers(Sort sort) { List> orders = new ArrayList<>(); @@ -329,7 +326,8 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC // 유효한 필드만 처리 switch (property) { case "classBeforeCd" -> orders.add(new OrderSpecifier(direction, entity.classBeforeCd)); - case "classBeforeProb" -> orders.add(new OrderSpecifier(direction, entity.classBeforeProb)); + case "classBeforeProb" -> + orders.add(new OrderSpecifier(direction, entity.classBeforeProb)); case "classAfterCd" -> orders.add(new OrderSpecifier(direction, entity.classAfterCd)); case "classAfterProb" -> orders.add(new OrderSpecifier(direction, entity.classAfterProb)); case "mapSheetNum" -> orders.add(new OrderSpecifier(direction, entity.mapSheetNum)); @@ -338,9 +336,8 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC case "area" -> orders.add(new OrderSpecifier(direction, entity.area)); case "createdDttm" -> orders.add(new OrderSpecifier(direction, entity.createdDttm)); case "updatedDttm" -> orders.add(new OrderSpecifier(direction, entity.updatedDttm)); - // 유효하지 않은 필드는 무시 - default -> { - } + // 유효하지 않은 필드는 무시 + default -> {} } } } @@ -349,21 +346,21 @@ public class InferenceResultRepositoryImpl implements InferenceResultRepositoryC } private BooleanExpression eqTargetClass( - QMapSheetAnalDataGeomEntity detectedEntity, String targetClass) { + QMapSheetAnalDataGeomEntity detectedEntity, String targetClass) { return targetClass != null && !targetClass.isEmpty() - ? detectedEntity.classAfterCd.toLowerCase().eq(targetClass.toLowerCase()) - : null; + ? detectedEntity.classAfterCd.toLowerCase().eq(targetClass.toLowerCase()) + : null; } private BooleanExpression eqCompareClass( - QMapSheetAnalDataGeomEntity detectedEntity, String compareClass) { + QMapSheetAnalDataGeomEntity detectedEntity, String compareClass) { return compareClass != null && !compareClass.isEmpty() - ? detectedEntity.classBeforeCd.toLowerCase().eq(compareClass.toLowerCase()) - : null; + ? detectedEntity.classBeforeCd.toLowerCase().eq(compareClass.toLowerCase()) + : null; } private BooleanExpression containsMapSheetNum( - QMapSheetAnalDataGeomEntity detectedEntity, List mapSheet) { + QMapSheetAnalDataGeomEntity detectedEntity, List mapSheet) { if (mapSheet == null || mapSheet.isEmpty()) { return null; } diff --git a/src/main/java/com/kamco/cd/kamcoback/test/TestApiController.java b/src/main/java/com/kamco/cd/kamcoback/test/TestApiController.java index b0430975..ca2dfe53 100644 --- a/src/main/java/com/kamco/cd/kamcoback/test/TestApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/test/TestApiController.java @@ -21,13 +21,13 @@ public class TestApiController { @Operation(summary = "admin test", description = "admin test api") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "조회", - content = @Content(schema = @Schema(implementation = String.class))), + responseCode = "200", + description = "조회", + content = @Content(schema = @Schema(implementation = String.class))), @ApiResponse( - responseCode = "403", - description = "권한 없음", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + responseCode = "403", + description = "권한 없음", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) @GetMapping("/admin") public String admin() { @@ -37,13 +37,13 @@ public class TestApiController { @Operation(summary = "label test", description = "label test api") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "조회", - content = @Content(schema = @Schema(implementation = String.class))), + responseCode = "200", + description = "조회", + content = @Content(schema = @Schema(implementation = String.class))), @ApiResponse( - responseCode = "403", - description = "권한 없음", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + responseCode = "403", + description = "권한 없음", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) @GetMapping("/label") public String label() { @@ -53,13 +53,13 @@ public class TestApiController { @Operation(summary = "review test", description = "review test api") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "조회", - content = @Content(schema = @Schema(implementation = String.class))), + responseCode = "200", + description = "조회", + content = @Content(schema = @Schema(implementation = String.class))), @ApiResponse( - responseCode = "403", - description = "권한 없음", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + responseCode = "403", + description = "권한 없음", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) @GetMapping("/review") public String review() {