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 c89f793e..dd898258 100644 --- a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java +++ b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java @@ -1,14 +1,17 @@ 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; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Component; @Component @@ -16,6 +19,7 @@ import org.springframework.stereotype.Component; public class CustomAuthenticationProvider implements AuthenticationProvider { private final MembersRepository membersRepository; + private final UserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { @@ -26,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/auth/CustomUserDetails.java b/src/main/java/com/kamco/cd/kamcoback/auth/CustomUserDetails.java index b65be3f0..8efb073a 100644 --- a/src/main/java/com/kamco/cd/kamcoback/auth/CustomUserDetails.java +++ b/src/main/java/com/kamco/cd/kamcoback/auth/CustomUserDetails.java @@ -46,7 +46,7 @@ public class CustomUserDetails implements UserDetails { @Override public boolean isEnabled() { - return member.getStatus().equalsIgnoreCase("ACTIVE"); + return "ACTIVE".equals(member.getStatus()); } public MemberEntity getMember() { 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 b6ad2d51..c3ff3942 100644 --- a/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java @@ -3,10 +3,8 @@ package com.kamco.cd.kamcoback.code; import com.kamco.cd.kamcoback.code.config.CommonCodeCacheManager; import com.kamco.cd.kamcoback.code.dto.CommonCodeDto; import com.kamco.cd.kamcoback.code.service.CommonCodeService; -import com.kamco.cd.kamcoback.common.enums.DetectionClassification; +import com.kamco.cd.kamcoback.common.utils.CommonCodeUtil; import com.kamco.cd.kamcoback.config.api.ApiResponseDto; -import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; -import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Clazzes; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -14,8 +12,6 @@ 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 jakarta.validation.Valid; -import java.util.Arrays; -import java.util.Comparator; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; @@ -36,6 +32,7 @@ public class CommonCodeApiController { private final CommonCodeService commonCodeService; private final CommonCodeCacheManager commonCodeCacheManager; + private final CommonCodeUtil commonCodeUtil; @Operation(summary = "목록 조회", description = "모든 공통코드 조회") @ApiResponses( @@ -212,13 +209,22 @@ public class CommonCodeApiController { return ApiResponseDto.ok(commonCodeService.findByCode(code)); } + @Operation(summary = "변화탐지 분류 코드 목록", description = "변화탐지 분류 코드 목록(공통코드 기반)") @GetMapping("/clazz") - public ApiResponseDto> getClasses() { + public ApiResponseDto> getClasses() { - List list = - Arrays.stream(DetectionClassification.values()) - .sorted(Comparator.comparingInt(DetectionClassification::getOrder)) - .map(Clazzes::new) + // List list = + // Arrays.stream(DetectionClassification.values()) + // .sorted(Comparator.comparingInt(DetectionClassification::getOrder)) + // .map(Clazzes::new) + // .toList(); + + // 변화탐지 clazz API : enum -> 공통코드로 변경 + List list = + commonCodeUtil.getChildCodesByParentCode("0000").stream() + .map( + child -> + new CommonCodeDto.Clazzes(child.getCode(), child.getName(), child.getOrder())) .toList(); return ApiResponseDto.ok(list); diff --git a/src/main/java/com/kamco/cd/kamcoback/code/dto/CommonCodeDto.java b/src/main/java/com/kamco/cd/kamcoback/code/dto/CommonCodeDto.java index 3827603b..963d6e90 100644 --- a/src/main/java/com/kamco/cd/kamcoback/code/dto/CommonCodeDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/code/dto/CommonCodeDto.java @@ -1,5 +1,6 @@ package com.kamco.cd.kamcoback.code.dto; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.kamco.cd.kamcoback.common.utils.html.HtmlEscapeDeserializer; @@ -71,15 +72,6 @@ public class CommonCodeDto { @NotNull private Integer order; } - @Getter - @Setter - @NoArgsConstructor - @AllArgsConstructor - public static class OrderReqDetail { - @NotNull private Long id; - @NotNull private Integer order; - } - @Schema(name = "CommonCode Basic", description = "공통코드 기본 정보") @Getter public static class Basic { @@ -139,4 +131,22 @@ public class CommonCodeDto { this.deletedDttm = deletedDttm; } } + + @Getter + public static class Clazzes { + + private String code; + private String name; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private Double score; + + private Integer order; + + public Clazzes(String code, String name, Integer order) { + this.code = code; + this.name = name; + this.order = order; + } + } } 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..91956284 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java @@ -0,0 +1,24 @@ +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/CommonCodeUtil.java b/src/main/java/com/kamco/cd/kamcoback/common/utils/CommonCodeUtil.java index 1b75b4e5..a637a375 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/utils/CommonCodeUtil.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/utils/CommonCodeUtil.java @@ -1,11 +1,15 @@ package com.kamco.cd.kamcoback.common.utils; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kamco.cd.kamcoback.code.dto.CommonCodeDto; import com.kamco.cd.kamcoback.code.dto.CommonCodeDto.Basic; import com.kamco.cd.kamcoback.code.service.CommonCodeService; import com.kamco.cd.kamcoback.config.api.ApiResponseDto; +import java.util.LinkedHashMap; import java.util.List; import java.util.Optional; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** @@ -19,6 +23,8 @@ public class CommonCodeUtil { private final CommonCodeService commonCodeService; + @Autowired private ObjectMapper objectMapper; + public CommonCodeUtil(CommonCodeService commonCodeService) { this.commonCodeService = commonCodeService; } @@ -30,7 +36,21 @@ public class CommonCodeUtil { */ public List getAllCommonCodes() { try { - return commonCodeService.getFindAll(); + // 캐시에 들어간 이후 꺼낼 때, DTO를 인식하지 못하여 LinkedHashMap으로 리턴됨. + // LinkedHashMap인 경우 변환처리, 아닌 경우는 그대로 리턴함. + List allCodes = commonCodeService.getFindAll(); + return allCodes.stream() + .map( + item -> { + if (item instanceof LinkedHashMap map) { + return objectMapper.convertValue(map, CommonCodeDto.Basic.class); + } else if (item instanceof CommonCodeDto.Basic dto) { + return dto; + } else { + throw new IllegalStateException("Unsupported cache type: " + item.getClass()); + } + }) + .toList(); } catch (Exception e) { log.error("공통코드 전체 조회 중 오류 발생", e); return List.of(); @@ -111,8 +131,24 @@ public class CommonCodeUtil { } try { - List allCodes = commonCodeService.getFindAll(); - return allCodes.stream() + // 캐시에 들어간 이후 꺼낼 때, DTO를 인식하지 못하여 LinkedHashMap으로 리턴됨. + // LinkedHashMap인 경우 변환처리, 아닌 경우는 그대로 리턴함. + List allCodes = commonCodeService.getFindAll(); + List convertList = + allCodes.stream() + .map( + item -> { + if (item instanceof LinkedHashMap map) { + return objectMapper.convertValue(map, CommonCodeDto.Basic.class); + } else if (item instanceof CommonCodeDto.Basic dto) { + return dto; + } else { + throw new IllegalStateException("Unsupported cache type: " + item.getClass()); + } + }) + .toList(); + + return convertList.stream() .filter(code -> parentCode.equals(code.getCode())) .findFirst() .map(Basic::getChildren) @@ -126,12 +162,12 @@ public class CommonCodeUtil { /** * 코드 사용 가능 여부 확인 * - * @param parentId 상위 코드 ID + * @param parentId 상위 코드 ID -> 1depth 는 null 허용 (파라미터 미필수로 변경) * @param code 확인할 코드값 * @return 사용 가능 여부 (true: 사용 가능, false: 중복 또는 오류) */ public boolean isCodeAvailable(Long parentId, String code) { - if (parentId == null || parentId <= 0 || code == null || code.isEmpty()) { + if (parentId <= 0 || code == null || code.isEmpty()) { log.warn("유효하지 않은 입력: parentId={}, code={}", parentId, code); return false; } 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/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); + } +} 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 b7fdcbf2..2fecb1cd 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java @@ -48,6 +48,7 @@ public class SecurityConfig { "/api/auth/signin", "/api/auth/refresh", "/swagger-ui/**", + "/api/members/{memberId}/password", "/v3/api-docs/**") .permitAll() .anyRequest() 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..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 @@ -164,6 +164,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/AdminApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java index 6839e605..33afc12c 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java @@ -13,7 +13,6 @@ import jakarta.validation.Valid; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -84,17 +83,17 @@ public class AdminApiController { schema = @Schema(implementation = MembersDto.UpdateReq.class))) @PathVariable UUID uuid, - @RequestBody MembersDto.UpdateReq updateReq) { + @RequestBody @Valid MembersDto.UpdateReq updateReq) { adminService.updateMembers(uuid, updateReq); return ApiResponseDto.createOK(UUID.randomUUID()); } - @Operation(summary = "회원 탈퇴", description = "회원 탈퇴") + @Operation(summary = "관리자 계정 미사용 처리", description = "관리자 계정 미사용 처리") @ApiResponses( value = { @ApiResponse( responseCode = "201", - description = "회원 탈퇴", + description = "관리자 계정 미사용 처리", content = @Content( mediaType = "application/json", @@ -108,24 +107,4 @@ public class AdminApiController { adminService.deleteAccount(uuid); return ApiResponseDto.createOK(uuid); } - - @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) - }) - @PatchMapping("/{memberId}/password") - public ApiResponseDto resetPassword(@PathVariable Long memberId) { - adminService.resetPassword(memberId); - return ApiResponseDto.createOK(memberId); - } } 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 2fe356b8..a0d7ed01 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -4,8 +4,10 @@ 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.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; @@ -36,6 +38,7 @@ public class AuthController { private final AuthenticationManager authenticationManager; private final JwtTokenProvider jwtTokenProvider; private final RefreshTokenService refreshTokenService; + private final AuthService authService; @Value("${token.refresh-cookie-name}") private String refreshCookieName; @@ -52,8 +55,42 @@ public class AuthController { content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( responseCode = "401", - description = "ID 또는 비밀번호 불일치", - 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": "아이디를 잘못 입력하셨습니다." + } + """), + @ExampleObject( + name = "비밀번호 입력 오류 (4회 이하)", + description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우", + value = + """ + { + "code": "LOGIN_PASSWORD_MISMATCH", + "message": "비밀번호를 잘못 입력하셨습니다." + } + """), + @ExampleObject( + name = "비밀번호 오류 횟수 초과", + description = "비밀번호 5회 이상 오류로 계정 잠김", + value = + """ + { + "code": "LOGIN_PASSWORD_EXCEEDED", + "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다." + } + """) + })) }) public ApiResponseDto signin( @io.swagger.v3.oas.annotations.parameters.RequestBody( @@ -62,10 +99,18 @@ public class AuthController { @RequestBody SignInRequest request, HttpServletResponse response) { + Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); + String status = authService.getUserStatus(request); + + // INACTIVE 비활성 상태(새로운 패스워드 입력 해야함), ARCHIVED 탈퇴 + if (!"ACTIVE".equals(status)) { + return ApiResponseDto.ok(new TokenResponse(status, null, null)); + } + String username = authentication.getName(); // UserDetailsService 에서 사용한 username String accessToken = jwtTokenProvider.createAccessToken(username); @@ -86,7 +131,8 @@ public class AuthController { .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); - return ApiResponseDto.ok(new TokenResponse(accessToken, refreshToken)); + + return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken)); } @PostMapping("/refresh") @@ -133,7 +179,7 @@ public class AuthController { .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); - return ResponseEntity.ok(new TokenResponse(newAccessToken, newRefreshToken)); + return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken)); } @PostMapping("/logout") @@ -166,5 +212,5 @@ public class AuthController { return ApiResponseDto.createOK(ResponseEntity.noContent().build()); } - public record TokenResponse(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 08114c6f..8c7a3560 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java @@ -10,13 +10,15 @@ 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 java.util.UUID; +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; -import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -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 = "회원정보 조회") @@ -48,24 +51,28 @@ public class MembersApiController { return ApiResponseDto.ok(membersService.findByMembers(searchReq)); } - @Operation(summary = "회원정보 수정", description = "회원정보 수정") + @Operation(summary = "사용자 비밀번호 변경", description = "사용자 비밀번호 변경") @ApiResponses( value = { @ApiResponse( responseCode = "201", - description = "회원정보 수정", + description = "사용자 비밀번호 변경", content = @Content( mediaType = "application/json", - schema = @Schema(implementation = UUID.class))), + 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 updateMember( - @PathVariable UUID uuid, @RequestBody MembersDto.UpdateReq updateReq) { - // membersService.updateMember(uuid, updateReq); - return ApiResponseDto.createOK(uuid); + @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/dto/MemberDetails.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/MemberDetails.java deleted file mode 100644 index 9c1b06f4..00000000 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/MemberDetails.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.kamco.cd.kamcoback.members.dto; - -import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; -import java.util.Collection; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -@RequiredArgsConstructor -public class MemberDetails implements UserDetails { - - private final MemberEntity member; - - @Override - public Collection getAuthorities() { - // TODO: tb_member_role 에서 역할 꺼내서 권한으로 변환하고 싶으면 여기 구현 - // 예시 (나중에 MemberRoleEntity 보고 수정): - // return member.getTbMemberRoles().stream() - // .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getRoleName())) - // .toList(); - - return List.of(); // 일단 빈 권한 리스트 - } - - @Override - public String getPassword() { - return member.getPassword(); // 암호화된 비밀번호 - } - - @Override - public String getUsername() { - // 로그인 ID 로 무엇을 쓸지 선택 - // 1) 이메일 로그인: - return member.getUserId(); - - // 2) 사번으로 로그인하고 싶으면: - // return member.getEmployeeNo(); - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - // status 가 ACTIVE 일 때만 로그인 허용 - return "ACTIVE".equalsIgnoreCase(member.getStatus()); - } - - public MemberEntity getMember() { - return member; - } -} 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 b42c1d5e..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 @@ -1,8 +1,9 @@ package com.kamco.cd.kamcoback.members.dto; import com.kamco.cd.kamcoback.common.enums.RoleType; -import com.kamco.cd.kamcoback.common.utils.interfaces.EnumValid; +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; @@ -23,33 +24,58 @@ public class MembersDto { private Long id; private UUID uuid; - private String employeeNo; + private String userRole; + private String userRoleName; private String name; - private String email; + private String userId; + private String employeeNo; + private String tempPassword; private String status; - private String roleName; + private String statusName; @JsonFormatDttm private ZonedDateTime createdDttm; @JsonFormatDttm private ZonedDateTime updatedDttm; + @JsonFormatDttm private ZonedDateTime firstLoginDttm; + @JsonFormatDttm private ZonedDateTime lastLoginDttm; public Basic( Long id, UUID uuid, - String employeeNo, + String userRole, + String userRoleName, String name, - String email, + String userId, + String employeeNo, + String tempPassword, String status, - String roleName, + String statusName, ZonedDateTime createdDttm, - ZonedDateTime updatedDttm) { + ZonedDateTime updatedDttm, + ZonedDateTime firstLoginDttm, + ZonedDateTime lastLoginDttm) { this.id = id; this.uuid = uuid; - this.employeeNo = employeeNo; + this.userRole = userRole; + this.userRoleName = getUserRoleName(userRole); this.name = name; - this.email = email; + this.userId = userId; + this.employeeNo = employeeNo; + this.tempPassword = tempPassword; this.status = status; - this.roleName = roleName; + 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(); } } @@ -59,21 +85,14 @@ public class MembersDto { @AllArgsConstructor public static class SearchReq { - @Schema(description = "이름(name), 이메일(email), 사번(employeeNo)", example = "name") - private String field; + @Schema( + description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)", + example = "") + private String userRole; @Schema(description = "키워드", example = "홍길동") private String keyword; - @Schema(description = "라벨러 포함 여부", example = "true") - private boolean labeler = true; - - @Schema(description = "검수자 포함 여부", example = "true") - private boolean reviewer = true; - - @Schema(description = "운영자 포함 여부", example = "true") - private boolean admin = true; - // 페이징 파라미터 @Schema(description = "페이지 번호 (0부터 시작) ", example = "0") private int page = 0; @@ -146,29 +165,15 @@ public class MembersDto { @Getter @Setter - public static class RolesDto { + public static class InitReq { - @Schema(description = "UUID", example = "4e89e487-c828-4a34-a7fc-0d5b0e3b53b5") - private UUID uuid; - - @Schema(description = "역할 ROLE_ADMIN, ROLE_LABELER, ROLE_REVIEWER", example = "ROLE_ADMIN") - @EnumValid(enumClass = RoleType.class) - private String roleName; - - public RolesDto(UUID uuid, String roleName) { - this.uuid = uuid; - this.roleName = roleName; - } - } - - @Getter - @Setter - @NoArgsConstructor - @AllArgsConstructor - public static class StatusDto { - - @Schema(description = "변경할 상태값 ACTIVE, INACTIVE, ARCHIVED", example = "ACTIVE") + @Schema(description = "변경 패스워드", example = "") + @Size(max = 255) @NotBlank - private String status; + private String password; + + @Schema(description = "초기 패스워드", example = "") + @NotBlank + private String tempPassword; } } 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..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 @@ -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!@#") @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String password; } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/exception/MemberException.java b/src/main/java/com/kamco/cd/kamcoback/members/exception/MemberException.java index b8dce363..33d2e8da 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/exception/MemberException.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/exception/MemberException.java @@ -35,4 +35,15 @@ public class MemberException { super(message); } } + + public static class PasswordNotFoundException extends RuntimeException { + + public PasswordNotFoundException() { + super("Password not found"); + } + + public PasswordNotFoundException(String message) { + super(message); + } + } } 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 948d2454..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 @@ -1,11 +1,9 @@ package com.kamco.cd.kamcoback.members.service; -import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator; import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.postgres.core.MembersCoreService; import java.util.UUID; import lombok.RequiredArgsConstructor; -import org.mindrot.jbcrypt.BCrypt; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,69 +15,34 @@ public class AdminService { private final MembersCoreService membersCoreService; /** - * 회원가입 + * 관리자 계정 등록 * * @param addReq * @return */ @Transactional public Long saveMember(MembersDto.AddReq addReq) { - // salt 생성, 사번이 salt - String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(addReq.getUserId().trim()); - - // 패스워드 암호화, 초기 패스워드 고정 - String hashedPassword = BCrypt.hashpw(addReq.getTempPassword(), salt); - addReq.setTempPassword(hashedPassword); return membersCoreService.saveMembers(addReq); } + /** + * 관리자 계정 수정 + * + * @param uuid + * @param updateReq + */ + @Transactional public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) { membersCoreService.updateMembers(uuid, updateReq); } /** - * 역할 추가 - * - * @param rolesDto - */ - @Transactional - public void saveRoles(MembersDto.RolesDto rolesDto) { - // membersCoreService.saveRoles(rolesDto); - } - - /** - * 역할 삭제 - * - * @param rolesDto - */ - public void deleteRoles(MembersDto.RolesDto rolesDto) { - // membersCoreService.deleteRoles(rolesDto); - } - - /** - * 역할 수정 - * - * @param statusDto - */ - public void updateStatus(UUID uuid, MembersDto.StatusDto statusDto) { - // membersCoreService.updateStatus(uuid, statusDto); - } - - /** - * 회원 탈퇴 + * 관리자 계정 미사용 처리 * * @param uuid */ + @Transactional public void deleteAccount(UUID uuid) { - // membersCoreService.deleteAccount(uuid); - } - - /** - * 패스워드 초기화 - * - * @param id - */ - public void resetPassword(Long id) { - // membersCoreService.resetPassword(id); + membersCoreService.deleteAccount(uuid); } } 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 new file mode 100644 index 00000000..1dcfc578 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java @@ -0,0 +1,25 @@ +package com.kamco.cd.kamcoback.members.service; + +import com.kamco.cd.kamcoback.members.dto.SignInRequest; +import com.kamco.cd.kamcoback.postgres.core.MembersCoreService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AuthService { + + private final MembersCoreService membersCoreService; + + /** + * 사용자 상태 조회 + * + * @param request + * @return + */ + public String getUserStatus(SignInRequest request) { + return membersCoreService.getUserStatus(request); + } +} 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 fe85e06c..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 @@ -1,11 +1,13 @@ package com.kamco.cd.kamcoback.members.service; +import com.kamco.cd.kamcoback.common.exception.CustomApiException; import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; import com.kamco.cd.kamcoback.postgres.core.MembersCoreService; import java.util.regex.Pattern; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,38 +25,23 @@ public class MembersService { * @return */ public Page findByMembers(MembersDto.SearchReq searchReq) { - return null; // membersCoreService.findByMembers(searchReq); + return membersCoreService.findByMembers(searchReq); } /** - * 회원정보 수정 + * 패스워드 사용자 변경 * - * @param uuid - * @param updateReq + * @param id + * @param initReq */ - // public void updateMember(UUID uuid, MembersDto.UpdateReq updateReq) { - // - // if (StringUtils.isNotBlank(updateReq.getPassword())) { - // - // if (!this.isValidPassword(updateReq.getPassword())) { - // throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST); - // } - // - // if (StringUtils.isBlank(updateReq.getEmployeeNo())) { - // throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST); - // } - // - // // salt 생성, 사번이 salt - // String salt = - // BCryptSaltGenerator.generateSaltWithEmployeeNo(updateReq.getEmployeeNo().trim()); - // - // // 패스워드 암호화, 초기 패스워드 고정 - // String hashedPassword = BCrypt.hashpw(updateReq.getPassword(), salt); - // updateReq.setPassword(hashedPassword); - // } - // - // membersCoreService.updateMembers(uuid, updateReq); - // } + @Transactional + public void resetPassword(String id, MembersDto.InitReq initReq) { + + if (!isValidPassword(initReq.getPassword())) { + throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST); + } + membersCoreService.resetPassword(id, initReq); + } /** * 대문자 1개 이상 소문자 1개 이상 숫자 1개 이상 특수문자(!@#$) 1개 이상 @@ -63,7 +50,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 d6acd8a9..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 @@ -1,16 +1,21 @@ package com.kamco.cd.kamcoback.postgres.core; +import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator; 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; +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.postgres.entity.MemberEntity; -import com.kamco.cd.kamcoback.postgres.repository.members.MembersArchivedRepository; import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository; +import java.time.ZonedDateTime; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.mindrot.jbcrypt.BCrypt; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; @Service @@ -18,10 +23,9 @@ import org.springframework.stereotype.Service; public class MembersCoreService { private final MembersRepository membersRepository; - private final MembersArchivedRepository memberArchivedRepository; /** - * 회원가입 + * 관리자 계정 등록 * * @param addReq * @return @@ -30,11 +34,17 @@ public class MembersCoreService { if (membersRepository.existsByUserId(addReq.getUserId())) { throw new DuplicateMemberException(Field.USER_ID, addReq.getUserId()); } + + // salt 생성, 사번이 salt + String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(addReq.getEmployeeNo().trim()); + // 패스워드 암호화, 초기 패스워드 고정 + String hashedPassword = BCrypt.hashpw(addReq.getTempPassword(), salt); + MemberEntity memberEntity = new MemberEntity(); memberEntity.setUserId(addReq.getUserId()); memberEntity.setUserRole(addReq.getUserRole()); - memberEntity.setTempPassword(addReq.getTempPassword()); - memberEntity.setPassword(addReq.getTempPassword()); + memberEntity.setTempPassword(addReq.getTempPassword()); // 임시 패스워드는 암호화 하지 않음 + memberEntity.setPassword(hashedPassword); memberEntity.setName(addReq.getName()); memberEntity.setEmployeeNo(addReq.getEmployeeNo()); @@ -42,7 +52,7 @@ public class MembersCoreService { } /** - * 회원정보 수정 + * 관리자 계정 수정 * * @param uuid * @param updateReq @@ -55,9 +65,9 @@ public class MembersCoreService { memberEntity.setName(updateReq.getName()); } + // 임시 패스워드는 암호화 하지 않음 if (StringUtils.isNotBlank(updateReq.getTempPassword())) { memberEntity.setTempPassword(updateReq.getTempPassword()); - memberEntity.setPassword(updateReq.getTempPassword()); } if (StringUtils.isNotBlank(memberEntity.getEmployeeNo())) { @@ -66,123 +76,41 @@ public class MembersCoreService { membersRepository.save(memberEntity); } - // - // /** - // * 역할 추가 - // * - // * @param rolesDto - // */ - // public void saveRoles(MembersDto.RolesDto rolesDto) { - // - // MemberEntity memberEntity = - // membersRepository - // .findByUUID(rolesDto.getUuid()) - // .orElseThrow(() -> new MemberNotFoundException()); - // - // if (memberRoleRepository.findByUuidAndRoleName(rolesDto)) { - // throw new MemberException.DuplicateMemberException( - // MemberException.DuplicateMemberException.Field.DEFAULT, "중복된 역할이 있습니다."); - // } - // - // MemberRoleEntityId memberRoleEntityId = new MemberRoleEntityId(); - // memberRoleEntityId.setMemberUuid(rolesDto.getUuid()); - // memberRoleEntityId.setRoleName(rolesDto.getRoleName()); - // - // MemberRoleEntity memberRoleEntity = new MemberRoleEntity(); - // memberRoleEntity.setId(memberRoleEntityId); - // memberRoleEntity.setMemberUuid(memberEntity); - // memberRoleEntity.setCreatedDttm(ZonedDateTime.now()); - // memberRoleRepository.save(memberRoleEntity); - // } - // - // /** - // * 역할 삭제 - // * - // * @param rolesDto - // */ - // public void deleteRoles(MembersDto.RolesDto rolesDto) { - // MemberEntity memberEntity = - // membersRepository - // .findByUUID(rolesDto.getUuid()) - // .orElseThrow(() -> new MemberNotFoundException()); - // - // MemberRoleEntityId memberRoleEntityId = new MemberRoleEntityId(); - // memberRoleEntityId.setMemberUuid(rolesDto.getUuid()); - // memberRoleEntityId.setRoleName(rolesDto.getRoleName()); - // - // MemberRoleEntity memberRoleEntity = new MemberRoleEntity(); - // memberRoleEntity.setId(memberRoleEntityId); - // memberRoleEntity.setMemberUuid(memberEntity); - // - // memberRoleRepository.delete(memberRoleEntity); - // } - // - // /** - // * 상태 수정 - // * - // * @param statusDto - // */ - // public void updateStatus(UUID uuid, MembersDto.StatusDto statusDto) { - // MemberEntity memberEntity = - // membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); - // - // memberEntity.setStatus(statusDto.getStatus()); - // memberEntity.setUpdatedDttm(ZonedDateTime.now()); - // membersRepository.save(memberEntity); - // } - // - // /** - // * 회원 탈퇴 - // * - // * @param uuid - // */ - // public void deleteAccount(UUID uuid) { - // MemberEntity memberEntity = - // membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); - // - // MemberArchivedEntityId memberArchivedEntityId = new MemberArchivedEntityId(); - // memberArchivedEntityId.setUserId(memberEntity.getId()); - // memberArchivedEntityId.setUuid(memberEntity.getUuid()); - // - // MemberArchivedEntity memberArchivedEntity = new MemberArchivedEntity(); - // memberArchivedEntity.setId(memberArchivedEntityId); - // memberArchivedEntity.setEmployeeNo(memberEntity.getEmployeeNo()); - // memberArchivedEntity.setName(memberEntity.getName()); - // memberArchivedEntity.setPassword(memberEntity.getPassword()); - // memberArchivedEntity.setEmail(memberEntity.getEmail()); - // memberArchivedEntity.setStatus(memberEntity.getStatus()); - // memberArchivedEntity.setCreatedDttm(memberEntity.getCreatedDttm()); - // memberArchivedEntity.setArchivedDttm(ZonedDateTime.now()); - // memberArchivedRepository.save(memberArchivedEntity); - // - // memberEntity.setStatus("ARCHIVED"); - // memberEntity.setName("**********"); - // memberEntity.setEmployeeNo("**********"); - // memberEntity.setPassword("**********"); - // memberEntity.setEmail("**********"); - // memberEntity.setUpdatedDttm(ZonedDateTime.now()); - // membersRepository.save(memberEntity); - // } - // - // /** - // * 패스워드 초기화 - // * - // * @param id - // */ - // public void resetPassword(Long id) { - // MemberEntity memberEntity = - // membersRepository.findById(id).orElseThrow(() -> new MemberNotFoundException()); - // - // String salt = - // BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); - // // 패스워드 암호화, 초기 패스워드 고정 - // String hashedPassword = BCrypt.hashpw(password, salt); - // - // memberEntity.setPassword(hashedPassword); - // memberEntity.setStatus("INACTIVE"); - // memberEntity.setUpdatedDttm(ZonedDateTime.now()); - // membersRepository.save(memberEntity); - // } + + /** + * 관리자 계정 미사용 처리 + * + * @param uuid + */ + public void deleteAccount(UUID uuid) { + MemberEntity memberEntity = + membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); + + memberEntity.setStatus("INACTIVE"); + memberEntity.setUpdatedDttm(ZonedDateTime.now()); + membersRepository.save(memberEntity); + } + + /** + * 패스워드 변경 + * + * @param id + */ + public void resetPassword(String id, MembersDto.InitReq initReq) { + MemberEntity memberEntity = + membersRepository.findByUserId(id).orElseThrow(() -> new MemberNotFoundException()); + + String salt = + BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); + // 패스워드 암호화 + String hashedPassword = BCrypt.hashpw(initReq.getPassword(), salt); + + memberEntity.setPassword(hashedPassword); + memberEntity.setStatus("ACTIVE"); + memberEntity.setUpdatedDttm(ZonedDateTime.now()); + membersRepository.save(memberEntity); + } + // /** @@ -191,7 +119,21 @@ public class MembersCoreService { * @param searchReq * @return */ - // public Page findByMembers(MembersDto.SearchReq searchReq) { - // return membersRepository.findByMembers(searchReq); - // } + public Page findByMembers(MembersDto.SearchReq searchReq) { + return membersRepository.findByMembers(searchReq); + } + + /** + * 사용자 상태 조회 + * + * @param request + * @return + */ + public String getUserStatus(SignInRequest request) { + MemberEntity memberEntity = + membersRepository + .findByUserId(request.getUsername()) + .orElseThrow(MemberNotFoundException::new); + return memberEntity.getStatus(); + } } 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 f9cf9ef8..5a4879b0 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 @@ -73,4 +73,14 @@ public class MemberEntity { @ColumnDefault("now()") @Column(name = "updated_dttm") private ZonedDateTime updatedDttm = ZonedDateTime.now(); + + @Column(name = "first_login_dttm") + private ZonedDateTime firstLoginDttm; + + @Column(name = "last_login_dttm") + private ZonedDateTime lastLoginDttm; + + @Column(name = "login_fail_count") + @ColumnDefault("0") + private Integer loginFailCount = 0; } 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 4298eedd..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 @@ -58,11 +58,10 @@ public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom { return queryFactory .selectFrom(commonCodeEntity) .leftJoin(commonCodeEntity.children, child) - .fetchJoin() + .on(child.deleted.isFalse().or(child.deleted.isNull())) .where( commonCodeEntity.parent.isNull(), - 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()) .fetch(); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryCustom.java index 17f9ee43..bcd23223 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryCustom.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryCustom.java @@ -1,20 +1,19 @@ package com.kamco.cd.kamcoback.postgres.repository.members; +import com.kamco.cd.kamcoback.members.dto.MembersDto; +import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import java.util.Optional; import java.util.UUID; +import org.springframework.data.domain.Page; public interface MembersRepositoryCustom { boolean existsByUserId(String userId); - Optional findByUserId(String employeeNo); + Optional findByUserId(String userId); Optional findByUUID(UUID uuid); - // - // Page findByMembers(MembersDto.SearchReq searchReq); - // - // - // Optional findByEmployeeNo(String employeeNo); + Page findByMembers(MembersDto.SearchReq searchReq); } 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 c428c639..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 @@ -1,11 +1,20 @@ package com.kamco.cd.kamcoback.postgres.repository.members; +import com.kamco.cd.kamcoback.members.dto.MembersDto; +import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import com.kamco.cd.kamcoback.postgres.entity.QMemberEntity; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; import java.util.Optional; import java.util.UUID; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; @Repository @@ -31,86 +40,81 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { != null; } + /** + * 사용자 조회 user id + * + * @param userId + * @return + */ @Override public Optional findByUserId(String userId) { return Optional.ofNullable( queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne()); } - // /** - // * 회원정보 목록 조회 - // * - // * @param searchReq - // * @return - // */ - // @Override - // public Page findByMembers(MembersDto.SearchReq searchReq) { - // Pageable pageable = searchReq.toPageable(); - // BooleanBuilder builder = new BooleanBuilder(); - // BooleanBuilder leftBuilder = new BooleanBuilder(); - // - // if (StringUtils.isNotBlank(searchReq.getField())) { - // switch (searchReq.getField()) { - // case "name" -> - // builder.and(memberEntity.name.containsIgnoreCase(searchReq.getKeyword().trim())); - // } - // } - // - // List roles = new ArrayList<>(); - // // 라벨러 - // if (searchReq.isLabeler()) { - // roles.add(RoleType.ROLE_LABELER.getId()); - // } - // - // // 시스템 전체 관리자 - // if (searchReq.isAdmin()) { - // roles.add(RoleType.ROLE_ADMIN.getId()); - // } - // - // // 검수자 - // if (searchReq.isReviewer()) { - // roles.add(RoleType.ROLE_REVIEWER.getId()); - // } - // - // // 역할 in 조건 추가 - // if (!roles.isEmpty()) { - // leftBuilder.and(memberRoleEntity.id.roleName.in(roles)); - // } - // - // List content = - // queryFactory - // .select( - // Projections.constructor( - // MembersDto.Basic.class, - // memberEntity.id, - // memberEntity.uuid, - // memberEntity.employeeNo, - // memberEntity.name, - // null, - // memberEntity.status, - // memberRoleEntity.id.roleName, - // memberEntity.createdDttm, - // memberEntity.updatedDttm)) - // .from(memberEntity) - // .leftJoin(memberRoleEntity) - // .on(memberRoleEntity.memberUuid.uuid.eq(memberEntity.uuid).and(leftBuilder)) - // .where(builder) - // .offset(pageable.getOffset()) - // .limit(pageable.getPageSize()) - // .orderBy(memberEntity.createdDttm.desc()) - // .fetch(); - // - // long total = - // queryFactory - // .select(memberEntity) - // .from(memberEntity) - // .leftJoin(memberRoleEntity) - // .on(memberRoleEntity.memberUuid.uuid.eq(memberEntity.uuid).and(leftBuilder)) - // .fetchCount(); - // - // return new PageImpl<>(content, pageable, total); - // } - // + /** + * 회원정보 목록 조회 + * + * @param searchReq + * @return + */ + @Override + public Page findByMembers(MembersDto.SearchReq searchReq) { + Pageable pageable = searchReq.toPageable(); + BooleanBuilder builder = new BooleanBuilder(); + + // 검색어 + if (StringUtils.isNotBlank(searchReq.getKeyword())) { + String contains = "%" + searchReq.getKeyword() + "%"; + + builder.and( + memberEntity + .name + .likeIgnoreCase(contains) + .or(memberEntity.userId.likeIgnoreCase(contains)) + .or(memberEntity.employeeNo.likeIgnoreCase(contains))); + } + + // 권한 + if (StringUtils.isNotBlank(searchReq.getUserRole())) { + builder.and(memberEntity.userRole.eq(searchReq.getUserRole())); + } + + 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(); + + long total = queryFactory.select(memberEntity).from(memberEntity).fetchCount(); + + return new PageImpl<>(content, pageable, total); + } + + /** + * 사용자 ID 조회 UUID + * + * @param uuid + * @return + */ @Override public Optional findByUUID(UUID uuid) { return Optional.ofNullable(