Merge pull request 'feat/dev_251201' (#43) from feat/dev_251201 into develop

Reviewed-on: https://kamco.gitea.gs.dabeeo.com/dabeeo/kamco-dabeeo-backoffice/pulls/43
This commit is contained in:
2025-12-11 14:06:10 +09:00
29 changed files with 601 additions and 467 deletions

View File

@@ -1,14 +1,17 @@
package com.kamco.cd.kamcoback.auth; 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.entity.MemberEntity;
import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository; import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository;
import java.time.ZonedDateTime;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.mindrot.jbcrypt.BCrypt; import org.mindrot.jbcrypt.BCrypt;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
@@ -16,6 +19,7 @@ import org.springframework.stereotype.Component;
public class CustomAuthenticationProvider implements AuthenticationProvider { public class CustomAuthenticationProvider implements AuthenticationProvider {
private final MembersRepository membersRepository; private final MembersRepository membersRepository;
private final UserDetailsService userDetailsService;
@Override @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException { public Authentication authenticate(Authentication authentication) throws AuthenticationException {
@@ -26,14 +30,34 @@ public class CustomAuthenticationProvider implements AuthenticationProvider {
MemberEntity member = MemberEntity member =
membersRepository membersRepository
.findByUserId(username) .findByUserId(username)
.orElseThrow(() -> new BadCredentialsException("ID 또는 비밀번호가 일치하지 않습니다.")); .orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND));
// 2. jBCrypt + 커스텀 salt 로 저장된 패스워드 비교 // 2. jBCrypt + 커스텀 salt 로 저장된 패스워드 비교
if (!BCrypt.checkpw(rawPassword, member.getPassword())) { 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); CustomUserDetails userDetails = new CustomUserDetails(member);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

View File

@@ -46,7 +46,7 @@ public class CustomUserDetails implements UserDetails {
@Override @Override
public boolean isEnabled() { public boolean isEnabled() {
return member.getStatus().equalsIgnoreCase("ACTIVE"); return "ACTIVE".equals(member.getStatus());
} }
public MemberEntity getMember() { public MemberEntity getMember() {

View File

@@ -3,10 +3,8 @@ package com.kamco.cd.kamcoback.code;
import com.kamco.cd.kamcoback.code.config.CommonCodeCacheManager; import com.kamco.cd.kamcoback.code.config.CommonCodeCacheManager;
import com.kamco.cd.kamcoback.code.dto.CommonCodeDto; import com.kamco.cd.kamcoback.code.dto.CommonCodeDto;
import com.kamco.cd.kamcoback.code.service.CommonCodeService; 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.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.Operation;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; 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.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
@@ -36,6 +32,7 @@ public class CommonCodeApiController {
private final CommonCodeService commonCodeService; private final CommonCodeService commonCodeService;
private final CommonCodeCacheManager commonCodeCacheManager; private final CommonCodeCacheManager commonCodeCacheManager;
private final CommonCodeUtil commonCodeUtil;
@Operation(summary = "목록 조회", description = "모든 공통코드 조회") @Operation(summary = "목록 조회", description = "모든 공통코드 조회")
@ApiResponses( @ApiResponses(
@@ -212,13 +209,22 @@ public class CommonCodeApiController {
return ApiResponseDto.ok(commonCodeService.findByCode(code)); return ApiResponseDto.ok(commonCodeService.findByCode(code));
} }
@Operation(summary = "변화탐지 분류 코드 목록", description = "변화탐지 분류 코드 목록(공통코드 기반)")
@GetMapping("/clazz") @GetMapping("/clazz")
public ApiResponseDto<List<InferenceResultDto.Clazzes>> getClasses() { public ApiResponseDto<List<CommonCodeDto.Clazzes>> getClasses() {
List<Clazzes> list = // List<Clazzes> list =
Arrays.stream(DetectionClassification.values()) // Arrays.stream(DetectionClassification.values())
.sorted(Comparator.comparingInt(DetectionClassification::getOrder)) // .sorted(Comparator.comparingInt(DetectionClassification::getOrder))
.map(Clazzes::new) // .map(Clazzes::new)
// .toList();
// 변화탐지 clazz API : enum -> 공통코드로 변경
List<CommonCodeDto.Clazzes> list =
commonCodeUtil.getChildCodesByParentCode("0000").stream()
.map(
child ->
new CommonCodeDto.Clazzes(child.getCode(), child.getName(), child.getOrder()))
.toList(); .toList();
return ApiResponseDto.ok(list); return ApiResponseDto.ok(list);

View File

@@ -1,5 +1,6 @@
package com.kamco.cd.kamcoback.code.dto; 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.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.kamco.cd.kamcoback.common.utils.html.HtmlEscapeDeserializer; import com.kamco.cd.kamcoback.common.utils.html.HtmlEscapeDeserializer;
@@ -71,15 +72,6 @@ public class CommonCodeDto {
@NotNull private Integer order; @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 = "공통코드 기본 정보") @Schema(name = "CommonCode Basic", description = "공통코드 기본 정보")
@Getter @Getter
public static class Basic { public static class Basic {
@@ -139,4 +131,22 @@ public class CommonCodeDto {
this.deletedDttm = deletedDttm; 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;
}
}
} }

View File

@@ -7,7 +7,7 @@ import lombok.Getter;
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum RoleType implements EnumType { public enum RoleType implements EnumType {
ROLE_ADMIN("시스템 관리자"), ROLE_ADMIN("관리자"),
ROLE_LABELER("라벨러"), ROLE_LABELER("라벨러"),
ROLE_REVIEWER("검수자"); ROLE_REVIEWER("검수자");
@@ -22,4 +22,13 @@ public enum RoleType implements EnumType {
public String getText() { public String getText() {
return desc; return desc;
} }
public static RoleType from(String value) {
for (RoleType type : values()) {
if (type.name().equalsIgnoreCase(value)) {
return type;
}
}
return null;
}
} }

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -1,5 +1,6 @@
package com.kamco.cd.kamcoback.common.exception; package com.kamco.cd.kamcoback.common.exception;
import com.kamco.cd.kamcoback.common.utils.ErrorCode;
import lombok.Getter; import lombok.Getter;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@@ -19,4 +20,9 @@ public class CustomApiException extends RuntimeException {
this.codeName = codeName; this.codeName = codeName;
this.status = status; this.status = status;
} }
public CustomApiException(ErrorCode errorCode) {
this.codeName = errorCode.getCode();
this.status = errorCode.getStatus();
}
} }

View File

@@ -1,11 +1,15 @@
package com.kamco.cd.kamcoback.common.utils; 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.dto.CommonCodeDto.Basic;
import com.kamco.cd.kamcoback.code.service.CommonCodeService; import com.kamco.cd.kamcoback.code.service.CommonCodeService;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto; import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
@@ -19,6 +23,8 @@ public class CommonCodeUtil {
private final CommonCodeService commonCodeService; private final CommonCodeService commonCodeService;
@Autowired private ObjectMapper objectMapper;
public CommonCodeUtil(CommonCodeService commonCodeService) { public CommonCodeUtil(CommonCodeService commonCodeService) {
this.commonCodeService = commonCodeService; this.commonCodeService = commonCodeService;
} }
@@ -30,7 +36,21 @@ public class CommonCodeUtil {
*/ */
public List<Basic> getAllCommonCodes() { public List<Basic> getAllCommonCodes() {
try { 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) { } catch (Exception e) {
log.error("공통코드 전체 조회 중 오류 발생", e); log.error("공통코드 전체 조회 중 오류 발생", e);
return List.of(); return List.of();
@@ -111,8 +131,24 @@ public class CommonCodeUtil {
} }
try { try {
List<Basic> allCodes = commonCodeService.getFindAll(); // 캐시에 들어간 이후 꺼낼 때, DTO를 인식하지 못하여 LinkedHashMap으로 리턴됨.
return allCodes.stream() // LinkedHashMap인 경우 변환처리, 아닌 경우는 그대로 리턴함.
List<?> allCodes = commonCodeService.getFindAll();
List<Basic> 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())) .filter(code -> parentCode.equals(code.getCode()))
.findFirst() .findFirst()
.map(Basic::getChildren) .map(Basic::getChildren)
@@ -126,12 +162,12 @@ public class CommonCodeUtil {
/** /**
* 코드 사용 가능 여부 확인 * 코드 사용 가능 여부 확인
* *
* @param parentId 상위 코드 ID * @param parentId 상위 코드 ID -> 1depth 는 null 허용 (파라미터 미필수로 변경)
* @param code 확인할 코드값 * @param code 확인할 코드값
* @return 사용 가능 여부 (true: 사용 가능, false: 중복 또는 오류) * @return 사용 가능 여부 (true: 사용 가능, false: 중복 또는 오류)
*/ */
public boolean isCodeAvailable(Long parentId, String code) { 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); log.warn("유효하지 않은 입력: parentId={}, code={}", parentId, code);
return false; return false;
} }

View File

@@ -0,0 +1,10 @@
package com.kamco.cd.kamcoback.common.utils;
import org.springframework.http.HttpStatus;
public interface ErrorCode {
String getCode();
HttpStatus getStatus();
}

View File

@@ -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<MemberEntity> 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);
}
}

View File

@@ -48,6 +48,7 @@ public class SecurityConfig {
"/api/auth/signin", "/api/auth/signin",
"/api/auth/refresh", "/api/auth/refresh",
"/swagger-ui/**", "/swagger-ui/**",
"/api/members/{memberId}/password",
"/v3/api-docs/**") "/v3/api-docs/**")
.permitAll() .permitAll()
.anyRequest() .anyRequest()

View File

@@ -164,6 +164,9 @@ public class ApiResponseDto<T> {
NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."), NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."),
NOT_FOUND_USER("사용자를 찾을 수 없습니다."), NOT_FOUND_USER("사용자를 찾을 수 없습니다."),
UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."), UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."),
LOGIN_ID_NOT_FOUND("아이디를 잘못 입력하셨습니다."),
LOGIN_PASSWORD_MISMATCH("비밀번호를 잘못 입력하셨습니다."),
LOGIN_PASSWORD_EXCEEDED("비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.\n로그인 오류에 대해 관리자에게 문의하시기 바랍니다."),
INVALID_EMAIL_TOKEN( INVALID_EMAIL_TOKEN(
"You can only reset your password within 24 hours from when the email was sent.\n" "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" + "To reset your password again, please submit a new request through \"Forgot"

View File

@@ -5,4 +5,17 @@ public interface EnumType {
String getId(); String getId();
String getText(); String getText();
// code로 text
static <E extends Enum<E> & EnumType> E fromId(Class<E> enumClass, String id) {
if (id == null) {
return null;
}
for (E e : enumClass.getEnumConstants()) {
if (id.equalsIgnoreCase(e.getId())) {
return e;
}
}
return null; // 못 찾으면 null
}
} }

View File

@@ -13,7 +13,6 @@ import jakarta.validation.Valid;
import java.util.UUID; import java.util.UUID;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping; 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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.PutMapping;
@@ -84,17 +83,17 @@ public class AdminApiController {
schema = @Schema(implementation = MembersDto.UpdateReq.class))) schema = @Schema(implementation = MembersDto.UpdateReq.class)))
@PathVariable @PathVariable
UUID uuid, UUID uuid,
@RequestBody MembersDto.UpdateReq updateReq) { @RequestBody @Valid MembersDto.UpdateReq updateReq) {
adminService.updateMembers(uuid, updateReq); adminService.updateMembers(uuid, updateReq);
return ApiResponseDto.createOK(UUID.randomUUID()); return ApiResponseDto.createOK(UUID.randomUUID());
} }
@Operation(summary = "회원 탈퇴", description = "회원 탈퇴") @Operation(summary = "관리자 계정 미사용 처리", description = "관리자 계정 미사용 처리")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
responseCode = "201", responseCode = "201",
description = "회원 탈퇴", description = "관리자 계정 미사용 처리",
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
@@ -108,24 +107,4 @@ public class AdminApiController {
adminService.deleteAccount(uuid); adminService.deleteAccount(uuid);
return ApiResponseDto.createOK(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<Long> resetPassword(@PathVariable Long memberId) {
adminService.resetPassword(memberId);
return ApiResponseDto.createOK(memberId);
}
} }

View File

@@ -4,8 +4,10 @@ import com.kamco.cd.kamcoback.auth.JwtTokenProvider;
import com.kamco.cd.kamcoback.auth.RefreshTokenService; import com.kamco.cd.kamcoback.auth.RefreshTokenService;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto; import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.members.dto.SignInRequest; 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.Operation;
import io.swagger.v3.oas.annotations.media.Content; 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.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.responses.ApiResponses;
@@ -36,6 +38,7 @@ public class AuthController {
private final AuthenticationManager authenticationManager; private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider; private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenService refreshTokenService; private final RefreshTokenService refreshTokenService;
private final AuthService authService;
@Value("${token.refresh-cookie-name}") @Value("${token.refresh-cookie-name}")
private String refreshCookieName; private String refreshCookieName;
@@ -52,8 +55,42 @@ public class AuthController {
content = @Content(schema = @Schema(implementation = TokenResponse.class))), content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse( @ApiResponse(
responseCode = "401", responseCode = "401",
description = "ID 또는 비밀번호 불일치", description = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))) 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<TokenResponse> signin( public ApiResponseDto<TokenResponse> signin(
@io.swagger.v3.oas.annotations.parameters.RequestBody( @io.swagger.v3.oas.annotations.parameters.RequestBody(
@@ -62,10 +99,18 @@ public class AuthController {
@RequestBody @RequestBody
SignInRequest request, SignInRequest request,
HttpServletResponse response) { HttpServletResponse response) {
Authentication authentication = Authentication authentication =
authenticationManager.authenticate( authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); 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 username = authentication.getName(); // UserDetailsService 에서 사용한 username
String accessToken = jwtTokenProvider.createAccessToken(username); String accessToken = jwtTokenProvider.createAccessToken(username);
@@ -86,7 +131,8 @@ public class AuthController {
.build(); .build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ApiResponseDto.ok(new TokenResponse(accessToken, refreshToken));
return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken));
} }
@PostMapping("/refresh") @PostMapping("/refresh")
@@ -133,7 +179,7 @@ public class AuthController {
.build(); .build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ResponseEntity.ok(new TokenResponse(newAccessToken, newRefreshToken)); return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken));
} }
@PostMapping("/logout") @PostMapping("/logout")
@@ -166,5 +212,5 @@ public class AuthController {
return ApiResponseDto.createOK(ResponseEntity.noContent().build()); return ApiResponseDto.createOK(ResponseEntity.noContent().build());
} }
public record TokenResponse(String accessToken, String refreshToken) {} public record TokenResponse(String status, String accessToken, String refreshToken) {}
} }

View File

@@ -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.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.UUID; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject; import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Page; 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.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable; 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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -27,6 +29,7 @@ import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor @RequiredArgsConstructor
public class MembersApiController { public class MembersApiController {
private final AuthenticationManager authenticationManager;
private final MembersService membersService; private final MembersService membersService;
@Operation(summary = "회원정보 목록", description = "회원정보 조회") @Operation(summary = "회원정보 목록", description = "회원정보 조회")
@@ -48,24 +51,28 @@ public class MembersApiController {
return ApiResponseDto.ok(membersService.findByMembers(searchReq)); return ApiResponseDto.ok(membersService.findByMembers(searchReq));
} }
@Operation(summary = "회원정보 수정", description = "회원정보 수정") @Operation(summary = "사용자 비밀번호 변경", description = "사용자 비밀번호 변경")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
responseCode = "201", responseCode = "201",
description = "회원정보 수정", description = "사용자 비밀번호 변경",
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = UUID.class))), schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
}) })
@PutMapping("/{uuid}") @PatchMapping("/{memberId}/password")
public ApiResponseDto<UUID> updateMember( public ApiResponseDto<String> resetPassword(
@PathVariable UUID uuid, @RequestBody MembersDto.UpdateReq updateReq) { @PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) {
// membersService.updateMember(uuid, updateReq);
return ApiResponseDto.createOK(uuid); authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(memberId, initReq.getTempPassword()));
membersService.resetPassword(memberId, initReq);
return ApiResponseDto.createOK(memberId);
} }
} }

View File

@@ -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<? extends GrantedAuthority> 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;
}
}

View File

@@ -1,8 +1,9 @@
package com.kamco.cd.kamcoback.members.dto; package com.kamco.cd.kamcoback.members.dto;
import com.kamco.cd.kamcoback.common.enums.RoleType; 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.common.utils.interfaces.JsonFormatDttm;
import com.kamco.cd.kamcoback.config.enums.EnumType;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
@@ -23,33 +24,58 @@ public class MembersDto {
private Long id; private Long id;
private UUID uuid; private UUID uuid;
private String employeeNo; private String userRole;
private String userRoleName;
private String name; private String name;
private String email; private String userId;
private String employeeNo;
private String tempPassword;
private String status; private String status;
private String roleName; private String statusName;
@JsonFormatDttm private ZonedDateTime createdDttm; @JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm; @JsonFormatDttm private ZonedDateTime updatedDttm;
@JsonFormatDttm private ZonedDateTime firstLoginDttm;
@JsonFormatDttm private ZonedDateTime lastLoginDttm;
public Basic( public Basic(
Long id, Long id,
UUID uuid, UUID uuid,
String employeeNo, String userRole,
String userRoleName,
String name, String name,
String email, String userId,
String employeeNo,
String tempPassword,
String status, String status,
String roleName, String statusName,
ZonedDateTime createdDttm, ZonedDateTime createdDttm,
ZonedDateTime updatedDttm) { ZonedDateTime updatedDttm,
ZonedDateTime firstLoginDttm,
ZonedDateTime lastLoginDttm) {
this.id = id; this.id = id;
this.uuid = uuid; this.uuid = uuid;
this.employeeNo = employeeNo; this.userRole = userRole;
this.userRoleName = getUserRoleName(userRole);
this.name = name; this.name = name;
this.email = email; this.userId = userId;
this.employeeNo = employeeNo;
this.tempPassword = tempPassword;
this.status = status; this.status = status;
this.roleName = roleName; this.statusName = getStatusName(status);
this.createdDttm = createdDttm; this.createdDttm = createdDttm;
this.updatedDttm = updatedDttm; 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 @AllArgsConstructor
public static class SearchReq { public static class SearchReq {
@Schema(description = "이름(name), 이메일(email), 사번(employeeNo)", example = "name") @Schema(
private String field; description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)",
example = "")
private String userRole;
@Schema(description = "키워드", example = "홍길동") @Schema(description = "키워드", example = "홍길동")
private String keyword; 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") @Schema(description = "페이지 번호 (0부터 시작) ", example = "0")
private int page = 0; private int page = 0;
@@ -146,29 +165,15 @@ public class MembersDto {
@Getter @Getter
@Setter @Setter
public static class RolesDto { public static class InitReq {
@Schema(description = "UUID", example = "4e89e487-c828-4a34-a7fc-0d5b0e3b53b5") @Schema(description = "변경 패스워드", example = "")
private UUID uuid; @Size(max = 255)
@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")
@NotBlank @NotBlank
private String status; private String password;
@Schema(description = "초기 패스워드", example = "")
@NotBlank
private String tempPassword;
} }
} }

View File

@@ -11,10 +11,10 @@ import lombok.ToString;
@ToString(exclude = "password") @ToString(exclude = "password")
public class SignInRequest { public class SignInRequest {
@Schema(description = "사용자 ID", example = "admin") @Schema(description = "사용자 ID", example = "admin2")
private String username; private String username;
@Schema(description = "비밀번호", example = "kamco1234!") @Schema(description = "비밀번호", example = "Admin2!@#")
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY) @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password; private String password;
} }

View File

@@ -35,4 +35,15 @@ public class MemberException {
super(message); super(message);
} }
} }
public static class PasswordNotFoundException extends RuntimeException {
public PasswordNotFoundException() {
super("Password not found");
}
public PasswordNotFoundException(String message) {
super(message);
}
}
} }

View File

@@ -1,11 +1,9 @@
package com.kamco.cd.kamcoback.members.service; 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.members.dto.MembersDto;
import com.kamco.cd.kamcoback.postgres.core.MembersCoreService; import com.kamco.cd.kamcoback.postgres.core.MembersCoreService;
import java.util.UUID; import java.util.UUID;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -17,69 +15,34 @@ public class AdminService {
private final MembersCoreService membersCoreService; private final MembersCoreService membersCoreService;
/** /**
* 회원가입 * 관리자 계정 등록
* *
* @param addReq * @param addReq
* @return * @return
*/ */
@Transactional @Transactional
public Long saveMember(MembersDto.AddReq addReq) { 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); return membersCoreService.saveMembers(addReq);
} }
/**
* 관리자 계정 수정
*
* @param uuid
* @param updateReq
*/
@Transactional
public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) { public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) {
membersCoreService.updateMembers(uuid, 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 * @param uuid
*/ */
@Transactional
public void deleteAccount(UUID uuid) { public void deleteAccount(UUID uuid) {
// membersCoreService.deleteAccount(uuid); membersCoreService.deleteAccount(uuid);
}
/**
* 패스워드 초기화
*
* @param id
*/
public void resetPassword(Long id) {
// membersCoreService.resetPassword(id);
} }
} }

View File

@@ -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);
}
}

View File

@@ -1,11 +1,13 @@
package com.kamco.cd.kamcoback.members.service; 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;
import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic;
import com.kamco.cd.kamcoback.postgres.core.MembersCoreService; import com.kamco.cd.kamcoback.postgres.core.MembersCoreService;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -23,38 +25,23 @@ public class MembersService {
* @return * @return
*/ */
public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) { public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) {
return null; // membersCoreService.findByMembers(searchReq); return membersCoreService.findByMembers(searchReq);
} }
/** /**
* 회원정보 수정 * 패스워드 사용자 변경
* *
* @param uuid * @param id
* @param updateReq * @param initReq
*/ */
// public void updateMember(UUID uuid, MembersDto.UpdateReq updateReq) { @Transactional
// public void resetPassword(String id, MembersDto.InitReq initReq) {
// if (StringUtils.isNotBlank(updateReq.getPassword())) {
// if (!isValidPassword(initReq.getPassword())) {
// if (!this.isValidPassword(updateReq.getPassword())) { throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST);
// throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST); }
// } membersCoreService.resetPassword(id, initReq);
// }
// 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);
// }
/** /**
* 대문자 1개 이상 소문자 1개 이상 숫자 1개 이상 특수문자(!@#$) 1개 이상 * 대문자 1개 이상 소문자 1개 이상 숫자 1개 이상 특수문자(!@#$) 1개 이상
@@ -63,7 +50,8 @@ public class MembersService {
* @return * @return
*/ */
private boolean isValidPassword(String password) { private boolean isValidPassword(String password) {
String regex = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[!@#$]).{8,20}$"; String passwordPattern =
return Pattern.matches(regex, password); "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$";
return Pattern.matches(passwordPattern, password);
} }
} }

View File

@@ -1,16 +1,21 @@
package com.kamco.cd.kamcoback.postgres.core; 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;
import com.kamco.cd.kamcoback.members.dto.MembersDto.AddReq; 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;
import com.kamco.cd.kamcoback.members.exception.MemberException.DuplicateMemberException.Field; 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.MemberNotFoundException;
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; 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 com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository;
import java.time.ZonedDateTime;
import java.util.UUID; import java.util.UUID;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Service @Service
@@ -18,10 +23,9 @@ import org.springframework.stereotype.Service;
public class MembersCoreService { public class MembersCoreService {
private final MembersRepository membersRepository; private final MembersRepository membersRepository;
private final MembersArchivedRepository memberArchivedRepository;
/** /**
* 회원가입 * 관리자 계정 등록
* *
* @param addReq * @param addReq
* @return * @return
@@ -30,11 +34,17 @@ public class MembersCoreService {
if (membersRepository.existsByUserId(addReq.getUserId())) { if (membersRepository.existsByUserId(addReq.getUserId())) {
throw new DuplicateMemberException(Field.USER_ID, 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 memberEntity = new MemberEntity();
memberEntity.setUserId(addReq.getUserId()); memberEntity.setUserId(addReq.getUserId());
memberEntity.setUserRole(addReq.getUserRole()); memberEntity.setUserRole(addReq.getUserRole());
memberEntity.setTempPassword(addReq.getTempPassword()); memberEntity.setTempPassword(addReq.getTempPassword()); // 임시 패스워드는 암호화 하지 않음
memberEntity.setPassword(addReq.getTempPassword()); memberEntity.setPassword(hashedPassword);
memberEntity.setName(addReq.getName()); memberEntity.setName(addReq.getName());
memberEntity.setEmployeeNo(addReq.getEmployeeNo()); memberEntity.setEmployeeNo(addReq.getEmployeeNo());
@@ -42,7 +52,7 @@ public class MembersCoreService {
} }
/** /**
* 회원정보 수정 * 관리자 계정 수정
* *
* @param uuid * @param uuid
* @param updateReq * @param updateReq
@@ -55,9 +65,9 @@ public class MembersCoreService {
memberEntity.setName(updateReq.getName()); memberEntity.setName(updateReq.getName());
} }
// 임시 패스워드는 암호화 하지 않음
if (StringUtils.isNotBlank(updateReq.getTempPassword())) { if (StringUtils.isNotBlank(updateReq.getTempPassword())) {
memberEntity.setTempPassword(updateReq.getTempPassword()); memberEntity.setTempPassword(updateReq.getTempPassword());
memberEntity.setPassword(updateReq.getTempPassword());
} }
if (StringUtils.isNotBlank(memberEntity.getEmployeeNo())) { if (StringUtils.isNotBlank(memberEntity.getEmployeeNo())) {
@@ -66,123 +76,41 @@ public class MembersCoreService {
membersRepository.save(memberEntity); membersRepository.save(memberEntity);
} }
//
// /** /**
// * 역할 추가 * 관리자 계정 미사용 처리
// * *
// * @param rolesDto * @param uuid
// */ */
// public void saveRoles(MembersDto.RolesDto rolesDto) { public void deleteAccount(UUID uuid) {
// MemberEntity memberEntity =
// MemberEntity memberEntity = membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException());
// membersRepository
// .findByUUID(rolesDto.getUuid()) memberEntity.setStatus("INACTIVE");
// .orElseThrow(() -> new MemberNotFoundException()); memberEntity.setUpdatedDttm(ZonedDateTime.now());
// membersRepository.save(memberEntity);
// if (memberRoleRepository.findByUuidAndRoleName(rolesDto)) { }
// throw new MemberException.DuplicateMemberException(
// MemberException.DuplicateMemberException.Field.DEFAULT, "중복된 역할이 있습니다."); /**
// } * 패스워드 변경
// *
// MemberRoleEntityId memberRoleEntityId = new MemberRoleEntityId(); * @param id
// memberRoleEntityId.setMemberUuid(rolesDto.getUuid()); */
// memberRoleEntityId.setRoleName(rolesDto.getRoleName()); public void resetPassword(String id, MembersDto.InitReq initReq) {
// MemberEntity memberEntity =
// MemberRoleEntity memberRoleEntity = new MemberRoleEntity(); membersRepository.findByUserId(id).orElseThrow(() -> new MemberNotFoundException());
// memberRoleEntity.setId(memberRoleEntityId);
// memberRoleEntity.setMemberUuid(memberEntity); String salt =
// memberRoleEntity.setCreatedDttm(ZonedDateTime.now()); BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim());
// memberRoleRepository.save(memberRoleEntity); // 패스워드 암호화
// } String hashedPassword = BCrypt.hashpw(initReq.getPassword(), salt);
//
// /** memberEntity.setPassword(hashedPassword);
// * 역할 삭제 memberEntity.setStatus("ACTIVE");
// * memberEntity.setUpdatedDttm(ZonedDateTime.now());
// * @param rolesDto membersRepository.save(memberEntity);
// */ }
// 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);
// }
// //
/** /**
@@ -191,7 +119,21 @@ public class MembersCoreService {
* @param searchReq * @param searchReq
* @return * @return
*/ */
// public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) { public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) {
// return membersRepository.findByMembers(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();
}
} }

View File

@@ -73,4 +73,14 @@ public class MemberEntity {
@ColumnDefault("now()") @ColumnDefault("now()")
@Column(name = "updated_dttm") @Column(name = "updated_dttm")
private ZonedDateTime updatedDttm = ZonedDateTime.now(); 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;
} }

View File

@@ -58,11 +58,10 @@ public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom {
return queryFactory return queryFactory
.selectFrom(commonCodeEntity) .selectFrom(commonCodeEntity)
.leftJoin(commonCodeEntity.children, child) .leftJoin(commonCodeEntity.children, child)
.fetchJoin() .on(child.deleted.isFalse().or(child.deleted.isNull()))
.where( .where(
commonCodeEntity.parent.isNull(), commonCodeEntity.parent.isNull(),
commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()), commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()))
child.deleted.isFalse().or(child.deleted.isNull()))
.orderBy(commonCodeEntity.order.asc(), child.order.asc()) .orderBy(commonCodeEntity.order.asc(), child.order.asc())
.fetch(); .fetch();
} }

View File

@@ -1,20 +1,19 @@
package com.kamco.cd.kamcoback.postgres.repository.members; 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.MemberEntity;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import org.springframework.data.domain.Page;
public interface MembersRepositoryCustom { public interface MembersRepositoryCustom {
boolean existsByUserId(String userId); boolean existsByUserId(String userId);
Optional<MemberEntity> findByUserId(String employeeNo); Optional<MemberEntity> findByUserId(String userId);
Optional<MemberEntity> findByUUID(UUID uuid); Optional<MemberEntity> findByUUID(UUID uuid);
//
// Page<Basic> findByMembers(MembersDto.SearchReq searchReq);
//
// Page<Basic> findByMembers(MembersDto.SearchReq searchReq);
// Optional<MemberEntity> findByEmployeeNo(String employeeNo);
} }

View File

@@ -1,11 +1,20 @@
package com.kamco.cd.kamcoback.postgres.repository.members; 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.MemberEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMemberEntity; 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 com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import lombok.RequiredArgsConstructor; 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; import org.springframework.stereotype.Repository;
@Repository @Repository
@@ -31,86 +40,81 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom {
!= null; != null;
} }
/**
* 사용자 조회 user id
*
* @param userId
* @return
*/
@Override @Override
public Optional<MemberEntity> findByUserId(String userId) { public Optional<MemberEntity> findByUserId(String userId) {
return Optional.ofNullable( return Optional.ofNullable(
queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne()); queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne());
} }
// /** /**
// * 회원정보 목록 조회 * 회원정보 목록 조회
// * *
// * @param searchReq * @param searchReq
// * @return * @return
// */ */
// @Override @Override
// public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) { public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) {
// Pageable pageable = searchReq.toPageable(); Pageable pageable = searchReq.toPageable();
// BooleanBuilder builder = new BooleanBuilder(); BooleanBuilder builder = new BooleanBuilder();
// BooleanBuilder leftBuilder = new BooleanBuilder();
// // 검색어
// if (StringUtils.isNotBlank(searchReq.getField())) { if (StringUtils.isNotBlank(searchReq.getKeyword())) {
// switch (searchReq.getField()) { String contains = "%" + searchReq.getKeyword() + "%";
// case "name" ->
// builder.and(memberEntity.name.containsIgnoreCase(searchReq.getKeyword().trim())); builder.and(
// } memberEntity
// } .name
// .likeIgnoreCase(contains)
// List<String> roles = new ArrayList<>(); .or(memberEntity.userId.likeIgnoreCase(contains))
// // 라벨러 .or(memberEntity.employeeNo.likeIgnoreCase(contains)));
// if (searchReq.isLabeler()) { }
// roles.add(RoleType.ROLE_LABELER.getId());
// } // 권한
// if (StringUtils.isNotBlank(searchReq.getUserRole())) {
// // 시스템 전체 관리자 builder.and(memberEntity.userRole.eq(searchReq.getUserRole()));
// if (searchReq.isAdmin()) { }
// roles.add(RoleType.ROLE_ADMIN.getId());
// } List<MembersDto.Basic> content =
// queryFactory
// // 검수자 .select(
// if (searchReq.isReviewer()) { Projections.constructor(
// roles.add(RoleType.ROLE_REVIEWER.getId()); MembersDto.Basic.class,
// } memberEntity.id,
// memberEntity.uuid,
// // 역할 in 조건 추가 memberEntity.userRole,
// if (!roles.isEmpty()) { memberEntity.name,
// leftBuilder.and(memberRoleEntity.id.roleName.in(roles)); memberEntity.userId,
// } memberEntity.employeeNo,
// memberEntity.tempPassword,
// List<MembersDto.Basic> content = memberEntity.status,
// queryFactory memberEntity.createdDttm,
// .select( memberEntity.updatedDttm,
// Projections.constructor( memberEntity.firstLoginDttm,
// MembersDto.Basic.class, memberEntity.lastLoginDttm))
// memberEntity.id, .from(memberEntity)
// memberEntity.uuid, .where(builder)
// memberEntity.employeeNo, .offset(pageable.getOffset())
// memberEntity.name, .limit(pageable.getPageSize())
// null, .orderBy(memberEntity.createdDttm.desc())
// memberEntity.status, .fetch();
// memberRoleEntity.id.roleName,
// memberEntity.createdDttm, long total = queryFactory.select(memberEntity).from(memberEntity).fetchCount();
// memberEntity.updatedDttm))
// .from(memberEntity) return new PageImpl<>(content, pageable, total);
// .leftJoin(memberRoleEntity) }
// .on(memberRoleEntity.memberUuid.uuid.eq(memberEntity.uuid).and(leftBuilder))
// .where(builder) /**
// .offset(pageable.getOffset()) * 사용자 ID 조회 UUID
// .limit(pageable.getPageSize()) *
// .orderBy(memberEntity.createdDttm.desc()) * @param uuid
// .fetch(); * @return
// */
// 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);
// }
//
@Override @Override
public Optional<MemberEntity> findByUUID(UUID uuid) { public Optional<MemberEntity> findByUUID(UUID uuid) {
return Optional.ofNullable( return Optional.ofNullable(