Merge remote-tracking branch 'origin/feat/dev_251201' into feat/dev_251201

This commit is contained in:
Moon
2025-12-11 17:16:58 +09:00
30 changed files with 609 additions and 372 deletions

View File

@@ -1,31 +0,0 @@
package com.kamco.cd.kamcoback.auth;
import com.kamco.cd.kamcoback.members.service.AuthService;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationListener;
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class AuthFailureEventListener implements ApplicationListener<AbstractAuthenticationFailureEvent> {
private final AuthService authService;
@Override
public void onApplicationEvent(AbstractAuthenticationFailureEvent event) {
// 로그인 시도에 사용된 (username)
Object principal = event.getAuthentication().getPrincipal();
if (principal instanceof String username) {
// 로그인 실패 카운트 증가 로직 호출
authService.loginFail(UUID.fromString(username));
}
}
@Override
public boolean supportsAsyncExecution() {
return ApplicationListener.super.supportsAsyncExecution();
}
}

View File

@@ -1,11 +1,13 @@
package com.kamco.cd.kamcoback.auth; package com.kamco.cd.kamcoback.auth;
import com.kamco.cd.kamcoback.common.enums.StatusType;
import com.kamco.cd.kamcoback.common.enums.error.AuthErrorCode;
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository; import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository;
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;
@@ -24,18 +26,35 @@ public class CustomAuthenticationProvider implements AuthenticationProvider {
String username = authentication.getName(); String username = authentication.getName();
String rawPassword = authentication.getCredentials().toString(); String rawPassword = authentication.getCredentials().toString();
// 1. 유저 조회 // 유저 조회
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 로 저장된 패스워드 비교 // 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(StatusType.INACTIVE.getId());
}
member.setLoginFailCount(cnt);
membersRepository.save(member);
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH);
} }
// 3. 인증 성공 → UserDetails 생성 // 삭제 상태
if (member.getStatus().equals(StatusType.DELETED.getId())) {
throw new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND);
}
// 패스워드 실패 횟수 체크
if (member.getLoginFailCount() >= 5) {
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED);
}
// 인증 성공 → 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

@@ -219,7 +219,7 @@ public class CommonCodeApiController {
// .map(Clazzes::new) // .map(Clazzes::new)
// .toList(); // .toList();
//변화탐지 clazz API : enum -> 공통코드로 변경 // 변화탐지 clazz API : enum -> 공통코드로 변경
List<CommonCodeDto.Clazzes> list = List<CommonCodeDto.Clazzes> list =
commonCodeUtil.getChildCodesByParentCode("0000").stream() commonCodeUtil.getChildCodesByParentCode("0000").stream()
.map( .map(

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("비활성"),
DELETED("삭제");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -0,0 +1,22 @@
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

@@ -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,41 @@
package com.kamco.cd.kamcoback.common.utils;
import com.kamco.cd.kamcoback.auth.CustomUserDetails;
import com.kamco.cd.kamcoback.members.dto.MembersDto;
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class UserUtil {
public MembersDto.Member getCurrentUser() {
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.filter(auth -> auth.getPrincipal() instanceof CustomUserDetails)
.map(
auth -> {
CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
MemberEntity m = user.getMember();
return new MembersDto.Member(m.getId(), m.getName(), m.getEmployeeNo());
})
.orElse(null);
}
public Long getId() {
MembersDto.Member user = getCurrentUser();
return user != null ? user.getId() : null;
}
public String getName() {
MembersDto.Member user = getCurrentUser();
return user != null ? user.getName() : null;
}
public String getEmployeeNo() {
MembersDto.Member user = getCurrentUser();
return user != null ? user.getEmployeeNo() : null;
}
}

View File

@@ -13,8 +13,6 @@ public class HtmlEscapeDeserializer extends JsonDeserializer<Object> {
public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JacksonException { throws IOException, JacksonException {
String value = jsonParser.getValueAsString(); String value = jsonParser.getValueAsString();
System.out.println("🔥 HtmlEscapeDeserializer 실행됨: " + value);
System.out.println("convert : " + (value == null ? null : HtmlUtils.htmlEscape(value)));
return value == null ? null : HtmlUtils.htmlEscape(value); return value == null ? null : HtmlUtils.htmlEscape(value);
} }
} }

View File

@@ -6,12 +6,14 @@ import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@@ -28,33 +30,33 @@ public class SecurityConfig {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.cors(cors -> cors.configurationSource(corsConfigurationSource())) http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화 .csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화
.sessionManagement( .sessionManagement(
sm -> sm ->
sm.sessionCreationPolicy( sm.sessionCreationPolicy(
SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음, 요청은 JWT 인증 SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음, 요청은 JWT 인증
.formLogin(form -> form.disable()) // react에서 로그인 요청 관리 .formLogin(form -> form.disable()) // react에서 로그인 요청 관리
.httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용 .httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용
.logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음 .logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음
.authenticationProvider( .authenticationProvider(
customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용 customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용
.authorizeHttpRequests( .authorizeHttpRequests(
auth -> auth ->
auth.anyRequest().permitAll()); auth.requestMatchers(HttpMethod.OPTIONS, "/**")
// requestMatchers(HttpMethod.OPTIONS, "/**") .permitAll() // preflight 허용
// .permitAll() // preflight 허용 .requestMatchers(
// .requestMatchers( "/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()
// .authenticated()) .authenticated())
// .addFilterBefore( .addFilterBefore(
// jwtAuthenticationFilter, jwtAuthenticationFilter,
// UsernamePasswordAuthenticationFilter UsernamePasswordAuthenticationFilter
// .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장.
; ;
return http.build(); return http.build();
@@ -62,7 +64,7 @@ public class SecurityConfig {
@Bean @Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
throws Exception { throws Exception {
return configuration.getAuthenticationManager(); return configuration.getAuthenticationManager();
} }

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

@@ -28,7 +28,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@Tag(name = "추론관리 분석결과", description = "추론관리 분석결과") @Tag(name = "추론관리 분석결과", description = "추론관리 분석결과")
@RequestMapping({"/demo/inf/res", "/api/inf/res"}) @RequestMapping({"/api/inf/res"})
@RequiredArgsConstructor @RequiredArgsConstructor
@RestController @RestController
public class InferenceResultApiController { public class InferenceResultApiController {

View File

@@ -30,78 +30,78 @@ public class AdminApiController {
@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 = Long.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)
}) })
@PostMapping("/join") @PostMapping("/join")
public ApiResponseDto<Long> saveMember( public ApiResponseDto<Long> saveMember(
@io.swagger.v3.oas.annotations.parameters.RequestBody( @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "관리자 계정 등록", description = "관리자 계정 등록",
required = true, required = true,
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = MembersDto.AddReq.class))) schema = @Schema(implementation = MembersDto.AddReq.class)))
@RequestBody @RequestBody
@Valid @Valid
MembersDto.AddReq addReq) { MembersDto.AddReq addReq) {
return ApiResponseDto.createOK(adminService.saveMember(addReq)); return ApiResponseDto.createOK(adminService.saveMember(addReq));
} }
@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 = Long.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}") @PutMapping("/{uuid}")
public ApiResponseDto<UUID> updateMembers( public ApiResponseDto<UUID> updateMembers(
@io.swagger.v3.oas.annotations.parameters.RequestBody( @io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "관리자 계정 수정", description = "관리자 계정 수정",
required = true, required = true,
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = MembersDto.UpdateReq.class))) schema = @Schema(implementation = MembersDto.UpdateReq.class)))
@PathVariable @PathVariable
UUID uuid, UUID uuid,
@RequestBody @Valid 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",
schema = @Schema(implementation = UUID.class))), schema = @Schema(implementation = UUID.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)
}) })
@DeleteMapping("/delete/{uuid}") @DeleteMapping("/delete/{uuid}")
public ApiResponseDto<UUID> deleteAccount(@PathVariable UUID uuid) { public ApiResponseDto<UUID> deleteAccount(@PathVariable UUID uuid) {
adminService.deleteAccount(uuid); adminService.deleteAccount(uuid);

View File

@@ -1,12 +1,16 @@
package com.kamco.cd.kamcoback.members; package com.kamco.cd.kamcoback.members;
import com.kamco.cd.kamcoback.auth.CustomUserDetails;
import com.kamco.cd.kamcoback.auth.JwtTokenProvider; import com.kamco.cd.kamcoback.auth.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.MembersDto;
import com.kamco.cd.kamcoback.members.dto.SignInRequest; import com.kamco.cd.kamcoback.members.dto.SignInRequest;
import com.kamco.cd.kamcoback.members.dto.TokenResponse;
import com.kamco.cd.kamcoback.members.service.AuthService; import 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;
@@ -50,80 +54,118 @@ public class AuthController {
@Operation(summary = "로그인", description = "사번으로 로그인하여 액세스/리프레시 토큰을 발급.") @Operation(summary = "로그인", description = "사번으로 로그인하여 액세스/리프레시 토큰을 발급.")
@ApiResponses({ @ApiResponses({
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "로그인 성공", description = "로그인 성공",
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 =
@ApiResponse( @Content(
responseCode = "400", schema = @Schema(implementation = ErrorResponse.class),
description = "미사용 상태", examples = {
content = @Content(schema = @Schema(implementation = ErrorResponse.class))) @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(
description = "로그인 요청 정보", description = "로그인 요청 정보",
required = true) required = true)
@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); String status = authService.getUserStatus(request);
if ("INACTIVE".equals(status)) {
return ApiResponseDto.ok(new TokenResponse(status, null, null)); MembersDto.Member member = new MembersDto.Member();
// INACTIVE 비활성 상태(새로운 패스워드 입력 해야함), DELETED 탈퇴
if (!"ACTIVE".equals(status)) {
return ApiResponseDto.ok(new TokenResponse(status, null, null, member));
} }
String username = authentication.getName(); // UserDetailsService 에서 사용한 username String username = authentication.getName(); // UserDetailsService 에서 사용한 username
// 로그인 시간 저장
authService.saveLogin(UUID.fromString(username));
String accessToken = jwtTokenProvider.createAccessToken(username); String accessToken = jwtTokenProvider.createAccessToken(username);
String refreshToken = jwtTokenProvider.createRefreshToken(username); String refreshToken = jwtTokenProvider.createRefreshToken(username);
// Redis에 RefreshToken 저장 (TTL = 7일) // Redis에 RefreshToken 저장 (TTL = 7일)
refreshTokenService.save( refreshTokenService.save(
username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
// HttpOnly + Secure 쿠키에 RefreshToken 저장 // HttpOnly + Secure 쿠키에 RefreshToken 저장
ResponseCookie cookie = ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, refreshToken) ResponseCookie.from(refreshCookieName, refreshToken)
.httpOnly(true) .httpOnly(true)
.secure(refreshCookieSecure) .secure(refreshCookieSecure)
.path("/") .path("/")
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
.sameSite("Strict") .sameSite("Strict")
.build(); .build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken)); CustomUserDetails user = (CustomUserDetails) authentication.getPrincipal();
member.setId(user.getMember().getId());
member.setName(user.getMember().getName());
member.setEmployeeNo(user.getMember().getEmployeeNo());
// 인증 성공 로그인 시간 저장
authService.saveLogin(UUID.fromString(username));
return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken, member));
} }
@PostMapping("/refresh") @PostMapping("/refresh")
@Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.") @Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.")
@ApiResponses({ @ApiResponses({
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "재발급 성공", description = "재발급 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))), content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse( @ApiResponse(
responseCode = "401", responseCode = "403",
description = "만료되었거나 유효하지 않은 리프레시 토큰", description = "만료되었거나 유효하지 않은 리프레시 토큰",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))) content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
}) })
public ResponseEntity<TokenResponse> refresh(String refreshToken, HttpServletResponse response) public ResponseEntity<TokenResponse> refresh(String refreshToken, HttpServletResponse response)
throws AccessDeniedException { throws AccessDeniedException {
if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) { if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) {
throw new AccessDeniedException("만료되었거나 유효하지 않은 리프레시 토큰 입니다."); throw new AccessDeniedException("만료되었거나 유효하지 않은 리프레시 토큰 입니다.");
} }
String username = jwtTokenProvider.getSubject(refreshToken); String username = jwtTokenProvider.getSubject(refreshToken);
// Redis에 저장된 RefreshToken과 일치하는지 확인 // Redis에 저장된 RefreshToken과 일치하는지 확인
@@ -137,32 +179,33 @@ public class AuthController {
// Redis 갱신 // Redis 갱신
refreshTokenService.save( refreshTokenService.save(
username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
// 쿠키 갱신 // 쿠키 갱신
ResponseCookie cookie = ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, newRefreshToken) ResponseCookie.from(refreshCookieName, newRefreshToken)
.httpOnly(true) .httpOnly(true)
.secure(refreshCookieSecure) .secure(refreshCookieSecure)
.path("/") .path("/")
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
.sameSite("Strict") .sameSite("Strict")
.build(); .build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken)); MembersDto.Member member = new MembersDto.Member();
return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken, member));
} }
@PostMapping("/logout") @PostMapping("/logout")
@Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.") @Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.")
@ApiResponses({ @ApiResponses({
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "로그아웃 성공", description = "로그아웃 성공",
content = @Content(schema = @Schema(implementation = Void.class))) content = @Content(schema = @Schema(implementation = Void.class)))
}) })
public ApiResponseDto<ResponseEntity<Object>> logout( public ApiResponseDto<ResponseEntity<Object>> logout(
Authentication authentication, HttpServletResponse response) { Authentication authentication, HttpServletResponse response) {
if (authentication != null) { if (authentication != null) {
String username = authentication.getName(); String username = authentication.getName();
// Redis에서 RefreshToken 삭제 // Redis에서 RefreshToken 삭제
@@ -171,19 +214,15 @@ public class AuthController {
// 쿠키 삭제 (Max-Age=0) // 쿠키 삭제 (Max-Age=0)
ResponseCookie cookie = ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, "") ResponseCookie.from(refreshCookieName, "")
.httpOnly(true) .httpOnly(true)
.secure(refreshCookieSecure) .secure(refreshCookieSecure)
.path("/") .path("/")
.maxAge(0) .maxAge(0)
.sameSite("Strict") .sameSite("Strict")
.build(); .build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ApiResponseDto.createOK(ResponseEntity.noContent().build()); return ApiResponseDto.createOK(ResponseEntity.noContent().build());
} }
public record TokenResponse(String status, String accessToken, String refreshToken) {
}
} }

View File

@@ -14,6 +14,8 @@ 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.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@@ -27,44 +29,51 @@ 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 = "회원정보 조회")
@ApiResponses( @ApiResponses(
value = { value = {
@ApiResponse( @ApiResponse(
responseCode = "200", responseCode = "200",
description = "검색 성공", description = "검색 성공",
content = content =
@Content( @Content(
mediaType = "application/json", mediaType = "application/json",
schema = @Schema(implementation = Page.class))), schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
}) })
@GetMapping @GetMapping
public ApiResponseDto<Page<Basic>> getMemberList( public ApiResponseDto<Page<Basic>> getMemberList(
@ParameterObject MembersDto.SearchReq searchReq) { @ParameterObject MembersDto.SearchReq searchReq) {
return ApiResponseDto.ok(membersService.findByMembers(searchReq)); return ApiResponseDto.ok(membersService.findByMembers(searchReq));
} }
@Operation(
@Operation(summary = "사용자 비밀번호 변경", description = "사용자 비밀번호 변경") summary = "사용자 비밀번호 변경",
description = "로그인 성공후 status가 INACTIVE일때 로그인 id를 memberId로 path 생성필요")
@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 = Long.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)
}) })
@PatchMapping("/{memberId}/password") @PatchMapping("/{memberId}/password")
public ApiResponseDto<Long> resetPassword(@PathVariable Long memberId, @RequestBody @Valid MembersDto.InitReq initReq) { public ApiResponseDto<String> resetPassword(
@PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(memberId, initReq.getTempPassword()));
membersService.resetPassword(memberId, initReq); membersService.resetPassword(memberId, initReq);
return ApiResponseDto.createOK(memberId); return ApiResponseDto.createOK(memberId);
} }

View File

@@ -1,6 +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.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;
@@ -22,47 +25,56 @@ public class MembersDto {
private Long id; private Long id;
private UUID uuid; private UUID uuid;
private String userRole; private String userRole;
private String userRoleName;
private String name; private String name;
private String userId; private String userId;
private String employeeNo; private String employeeNo;
private String tempPassword; private String tempPassword;
private String status; private String status;
@JsonFormatDttm private String statusName;
private ZonedDateTime createdDttm; @JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm @JsonFormatDttm private ZonedDateTime updatedDttm;
private ZonedDateTime updatedDttm; @JsonFormatDttm private ZonedDateTime firstLoginDttm;
@JsonFormatDttm @JsonFormatDttm private ZonedDateTime lastLoginDttm;
private ZonedDateTime firstLoginDttm;
@JsonFormatDttm
private ZonedDateTime lastLoginDttm;
public Basic( public Basic(
Long id, Long id,
UUID uuid, UUID uuid,
String userRole, String userRole,
String name, String name,
String userId, String userId,
String employeeNo, String employeeNo,
String tempPassword, String tempPassword,
String status, String status,
ZonedDateTime createdDttm, ZonedDateTime createdDttm,
ZonedDateTime updatedDttm, ZonedDateTime updatedDttm,
ZonedDateTime firstLoginDttm, ZonedDateTime firstLoginDttm,
ZonedDateTime lastLoginDttm ZonedDateTime lastLoginDttm) {
) {
this.id = id; this.id = id;
this.uuid = uuid; this.uuid = uuid;
this.userRole = userRole; this.userRole = userRole;
this.userRoleName = getUserRoleName(userRole);
this.name = name; this.name = name;
this.userId = userId; this.userId = userId;
this.employeeNo = employeeNo; this.employeeNo = employeeNo;
this.tempPassword = tempPassword; this.tempPassword = tempPassword;
this.status = status; this.status = status;
this.statusName = getStatusName(status);
this.createdDttm = createdDttm; this.createdDttm = createdDttm;
this.updatedDttm = updatedDttm; this.updatedDttm = updatedDttm;
this.firstLoginDttm = firstLoginDttm; this.firstLoginDttm = firstLoginDttm;
this.lastLoginDttm = lastLoginDttm; this.lastLoginDttm = lastLoginDttm;
} }
private String getUserRoleName(String roleId) {
RoleType type = EnumType.fromId(RoleType.class, roleId);
return type.getText();
}
private String getStatusName(String status) {
StatusType type = EnumType.fromId(StatusType.class, status);
return type.getText();
}
} }
@Getter @Getter
@@ -71,7 +83,9 @@ public class MembersDto {
@AllArgsConstructor @AllArgsConstructor
public static class SearchReq { public static class SearchReq {
@Schema(description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)", example = "") @Schema(
description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)",
example = "")
private String userRole; private String userRole;
@Schema(description = "키워드", example = "홍길동") @Schema(description = "키워드", example = "홍길동")
@@ -115,7 +129,7 @@ public class MembersDto {
private String employeeNo; private String employeeNo;
public AddReq( public AddReq(
String userRole, String name, String userId, String tempPassword, String employeeNo) { String userRole, String name, String userId, String tempPassword, String employeeNo) {
this.userRole = userRole; this.userRole = userRole;
this.name = name; this.name = name;
this.userId = userId; this.userId = userId;
@@ -160,4 +174,15 @@ public class MembersDto {
@NotBlank @NotBlank
private String tempPassword; private String tempPassword;
} }
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class Member {
private Long id;
private String name;
private String employeeNo;
}
} }

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

@@ -0,0 +1,16 @@
package com.kamco.cd.kamcoback.members.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public class TokenResponse {
private String status;
private String accessToken;
private String refreshToken;
private MembersDto.Member member;
}

View File

@@ -36,7 +36,6 @@ public class AdminService {
membersCoreService.updateMembers(uuid, updateReq); membersCoreService.updateMembers(uuid, updateReq);
} }
/** /**
* 관리자 계정 미사용 처리 * 관리자 계정 미사용 처리
* *

View File

@@ -24,16 +24,6 @@ public class AuthService {
membersCoreService.saveLogin(uuid); membersCoreService.saveLogin(uuid);
} }
/**
* 로그인 실패 저장
*
* @param uuid
*/
@Transactional
public void loginFail(UUID uuid) {
membersCoreService.loginFail(uuid);
}
/** /**
* 사용자 상태 조회 * 사용자 상태 조회
* *

View File

@@ -35,7 +35,8 @@ public class MembersService {
* @param initReq * @param initReq
*/ */
@Transactional @Transactional
public void resetPassword(Long id, MembersDto.InitReq initReq) { public void resetPassword(String id, MembersDto.InitReq initReq) {
if (!isValidPassword(initReq.getPassword())) { if (!isValidPassword(initReq.getPassword())) {
throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST); throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST);
} }
@@ -43,13 +44,14 @@ public class MembersService {
} }
/** /**
* 대문자 1개 이상 소문자 1개 이상 숫자 1개 이상 특수문자(!@#$) 1개 이상 * 영문, 숫자, 특수문자를 모두 포함하여 8~20자 이내의 비밀번호
* *
* @param password * @param password
* @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,6 +1,8 @@
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.auth.BCryptSaltGenerator;
import com.kamco.cd.kamcoback.common.enums.StatusType;
import com.kamco.cd.kamcoback.common.utils.UserUtil;
import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.MembersDto;
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.MembersDto.Basic;
@@ -8,7 +10,6 @@ import com.kamco.cd.kamcoback.members.dto.SignInRequest;
import com.kamco.cd.kamcoback.members.exception.MemberException.DuplicateMemberException; import com.kamco.cd.kamcoback.members.exception.MemberException.DuplicateMemberException;
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.members.exception.MemberException.PasswordNotFoundException;
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 java.time.ZonedDateTime;
@@ -24,6 +25,7 @@ import org.springframework.stereotype.Service;
public class MembersCoreService { public class MembersCoreService {
private final MembersRepository membersRepository; private final MembersRepository membersRepository;
private final UserUtil userUtil;
/** /**
* 관리자 계정 등록 * 관리자 계정 등록
@@ -48,6 +50,7 @@ public class MembersCoreService {
memberEntity.setPassword(hashedPassword); memberEntity.setPassword(hashedPassword);
memberEntity.setName(addReq.getName()); memberEntity.setName(addReq.getName());
memberEntity.setEmployeeNo(addReq.getEmployeeNo()); memberEntity.setEmployeeNo(addReq.getEmployeeNo());
memberEntity.setRgstrUidl(userUtil.getId());
return membersRepository.save(memberEntity).getId(); return membersRepository.save(memberEntity).getId();
} }
@@ -60,7 +63,7 @@ public class MembersCoreService {
*/ */
public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) { public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) {
MemberEntity memberEntity = MemberEntity memberEntity =
membersRepository.findByUUID(uuid).orElseThrow(MemberNotFoundException::new); membersRepository.findByUUID(uuid).orElseThrow(MemberNotFoundException::new);
if (StringUtils.isNotBlank(updateReq.getName())) { if (StringUtils.isNotBlank(updateReq.getName())) {
memberEntity.setName(updateReq.getName()); memberEntity.setName(updateReq.getName());
@@ -75,48 +78,47 @@ public class MembersCoreService {
memberEntity.setEmployeeNo(updateReq.getEmployeeNo()); memberEntity.setEmployeeNo(updateReq.getEmployeeNo());
} }
memberEntity.setUpdtrUid(userUtil.getId());
membersRepository.save(memberEntity); membersRepository.save(memberEntity);
} }
/** /**
* 관리자 계정 미사용 처리 * 관리자 계정 삭제 처리
* *
* @param uuid * @param uuid
*/ */
public void deleteAccount(UUID uuid) { public void deleteAccount(UUID uuid) {
MemberEntity memberEntity = MemberEntity memberEntity =
membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException());
memberEntity.setStatus("INACTIVE"); memberEntity.setStatus(StatusType.DELETED.getId());
memberEntity.setUpdatedDttm(ZonedDateTime.now()); memberEntity.setUpdatedDttm(ZonedDateTime.now());
memberEntity.setUpdtrUid(userUtil.getId());
membersRepository.save(memberEntity); membersRepository.save(memberEntity);
} }
/** /**
* 패스워드 변경 * 패스워드 변경
* *
* @param id * @param id
*/ */
public void resetPassword(Long id, MembersDto.InitReq initReq) { public void resetPassword(String id, MembersDto.InitReq initReq) {
MemberEntity memberEntity = MemberEntity memberEntity =
membersRepository.findById(id).orElseThrow(() -> new MemberNotFoundException()); membersRepository.findByUserId(id).orElseThrow(() -> new MemberNotFoundException());
if (!memberEntity.getTempPassword().equals(initReq.getTempPassword())) {
throw new PasswordNotFoundException();
}
String salt = String salt =
BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim());
// 패스워드 암호화 // 패스워드 암호화
String hashedPassword = BCrypt.hashpw(initReq.getPassword(), salt); String hashedPassword = BCrypt.hashpw(initReq.getPassword(), salt);
memberEntity.setPassword(hashedPassword); memberEntity.setPassword(hashedPassword);
memberEntity.setStatus("ACTIVE"); memberEntity.setStatus("ACTIVE");
memberEntity.setUpdatedDttm(ZonedDateTime.now()); memberEntity.setUpdatedDttm(ZonedDateTime.now());
memberEntity.setUpdtrUid(memberEntity.getId());
membersRepository.save(memberEntity); membersRepository.save(memberEntity);
} }
// //
/** /**
@@ -129,6 +131,20 @@ public class MembersCoreService {
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();
}
/** /**
* 최초 로그인 저장 마지막 로그인 저장 * 최초 로그인 저장 마지막 로그인 저장
* *
@@ -136,7 +152,7 @@ public class MembersCoreService {
*/ */
public void saveLogin(UUID uuid) { public void saveLogin(UUID uuid) {
MemberEntity memberEntity = MemberEntity memberEntity =
membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException());
if (memberEntity.getFirstLoginDttm() == null) { if (memberEntity.getFirstLoginDttm() == null) {
memberEntity.setFirstLoginDttm(ZonedDateTime.now()); memberEntity.setFirstLoginDttm(ZonedDateTime.now());
@@ -145,33 +161,4 @@ public class MembersCoreService {
memberEntity.setLoginFailCount(0); memberEntity.setLoginFailCount(0);
membersRepository.save(memberEntity); membersRepository.save(memberEntity);
} }
/**
* 로그인 실패시 상태 저장
*
* @param uuid
*/
public void loginFail(UUID uuid) {
MemberEntity memberEntity =
membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException());
int failCnt = memberEntity.getLoginFailCount() + 1;
if (failCnt >= 5) {
memberEntity.setStatus("INACTIVE");
}
memberEntity.setLoginFailCount(failCnt);
membersRepository.save(memberEntity);
}
/**
* 사용자 상태 조회
*
* @param request
* @return
*/
public String getUserStatus(SignInRequest request) {
MemberEntity memberEntity = membersRepository.findByUserId(request.getUsername()).orElseThrow(MemberNotFoundException::new);
return memberEntity.getStatus();
}
} }

View File

@@ -2,17 +2,7 @@ package com.kamco.cd.kamcoback.postgres.entity;
import com.kamco.cd.kamcoback.code.dto.CommonCodeDto; import com.kamco.cd.kamcoback.code.dto.CommonCodeDto;
import com.kamco.cd.kamcoback.postgres.CommonDateEntity; import com.kamco.cd.kamcoback.postgres.CommonDateEntity;
import jakarta.persistence.CascadeType; import jakarta.persistence.*;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@@ -21,6 +11,7 @@ import java.util.List;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.hibernate.annotations.Where;
@Entity @Entity
@Getter @Getter
@@ -60,6 +51,7 @@ public class CommonCodeEntity extends CommonDateEntity {
private CommonCodeEntity parent; private CommonCodeEntity parent;
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL) @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@Where(clause = "deleted = false or deleted is null")
private List<CommonCodeEntity> children = new ArrayList<>(); private List<CommonCodeEntity> children = new ArrayList<>();
@Size(max = 255) @Size(max = 255)

View File

@@ -1,5 +1,6 @@
package com.kamco.cd.kamcoback.postgres.entity; package com.kamco.cd.kamcoback.postgres.entity;
import com.kamco.cd.kamcoback.common.enums.StatusType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
@@ -63,7 +64,7 @@ public class MemberEntity {
@Size(max = 20) @Size(max = 20)
@ColumnDefault("'INACTIVE'") @ColumnDefault("'INACTIVE'")
@Column(name = "status", length = 20) @Column(name = "status", length = 20)
private String status = "INACTIVE"; private String status = StatusType.INACTIVE.getId();
@NotNull @NotNull
@ColumnDefault("now()") @ColumnDefault("now()")
@@ -83,4 +84,10 @@ public class MemberEntity {
@Column(name = "login_fail_count") @Column(name = "login_fail_count")
@ColumnDefault("0") @ColumnDefault("0")
private Integer loginFailCount = 0; private Integer loginFailCount = 0;
@Column(name = "rgstr_uid")
private Long rgstrUidl;
@Column(name = "updtr_uid")
private Long updtrUid;
} }

View File

@@ -20,18 +20,8 @@ public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom {
@Override @Override
public Optional<CommonCodeEntity> findByCodeId(Long id) { public Optional<CommonCodeEntity> findByCodeId(Long id) {
QCommonCodeEntity child = new QCommonCodeEntity("child");
return Optional.ofNullable( return Optional.ofNullable(
queryFactory queryFactory.selectFrom(commonCodeEntity).where(commonCodeEntity.id.eq(id)).fetchOne());
.selectFrom(commonCodeEntity)
.leftJoin(commonCodeEntity.children, child)
.fetchJoin()
.where(
commonCodeEntity.id.eq(id),
commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()),
child.deleted.isFalse().or(child.deleted.isNull()))
.orderBy(commonCodeEntity.order.asc(), child.order.asc())
.fetchOne());
} }
@Override @Override
@@ -41,13 +31,12 @@ public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom {
queryFactory 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.code.eq(code), commonCodeEntity.code.eq(code),
commonCodeEntity.used.isTrue(), commonCodeEntity.used.isTrue(),
commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()), commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()))
child.deleted.isFalse().or(child.deleted.isNull()))
.orderBy(child.order.asc()) .orderBy(child.order.asc())
.fetchOne()); .fetchOne());
} }
@@ -61,8 +50,7 @@ public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom {
.on(child.deleted.isFalse().or(child.deleted.isNull())) .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()))
)
.orderBy(commonCodeEntity.order.asc(), child.order.asc()) .orderBy(commonCodeEntity.order.asc(), child.order.asc())
.fetch(); .fetch();
} }

View File

@@ -33,11 +33,11 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom {
@Override @Override
public boolean existsByUserId(String userId) { public boolean existsByUserId(String userId) {
return queryFactory return queryFactory
.selectOne() .selectOne()
.from(memberEntity) .from(memberEntity)
.where(memberEntity.userId.eq(userId)) .where(memberEntity.userId.eq(userId))
.fetchFirst() .fetchFirst()
!= null; != null;
} }
/** /**
@@ -49,7 +49,7 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom {
@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());
} }
/** /**
@@ -68,10 +68,11 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom {
String contains = "%" + searchReq.getKeyword() + "%"; String contains = "%" + searchReq.getKeyword() + "%";
builder.and( builder.and(
memberEntity.name.likeIgnoreCase(contains) memberEntity
.or(memberEntity.userId.likeIgnoreCase(contains)) .name
.or(memberEntity.employeeNo.likeIgnoreCase(contains)) .likeIgnoreCase(contains)
); .or(memberEntity.userId.likeIgnoreCase(contains))
.or(memberEntity.employeeNo.likeIgnoreCase(contains)));
} }
// 권한 // 권한
@@ -80,35 +81,30 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom {
} }
List<MembersDto.Basic> content = List<MembersDto.Basic> content =
queryFactory queryFactory
.select( .select(
Projections.constructor( Projections.constructor(
MembersDto.Basic.class, MembersDto.Basic.class,
memberEntity.id, memberEntity.id,
memberEntity.uuid, memberEntity.uuid,
memberEntity.userRole, memberEntity.userRole,
memberEntity.name, memberEntity.name,
memberEntity.userId, memberEntity.userId,
memberEntity.employeeNo, memberEntity.employeeNo,
memberEntity.tempPassword, memberEntity.tempPassword,
memberEntity.status, memberEntity.status,
memberEntity.createdDttm, memberEntity.createdDttm,
memberEntity.updatedDttm, memberEntity.updatedDttm,
memberEntity.firstLoginDttm, memberEntity.firstLoginDttm,
memberEntity.lastLoginDttm memberEntity.lastLoginDttm))
)) .from(memberEntity)
.from(memberEntity) .where(builder)
.where(builder) .offset(pageable.getOffset())
.offset(pageable.getOffset()) .limit(pageable.getPageSize())
.limit(pageable.getPageSize()) .orderBy(memberEntity.createdDttm.desc())
.orderBy(memberEntity.createdDttm.desc()) .fetch();
.fetch();
long total = long total = queryFactory.select(memberEntity).from(memberEntity).fetchCount();
queryFactory
.select(memberEntity)
.from(memberEntity)
.fetchCount();
return new PageImpl<>(content, pageable, total); return new PageImpl<>(content, pageable, total);
} }
@@ -122,6 +118,6 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom {
@Override @Override
public Optional<MemberEntity> findByUUID(UUID uuid) { public Optional<MemberEntity> findByUUID(UUID uuid) {
return Optional.ofNullable( return Optional.ofNullable(
queryFactory.selectFrom(memberEntity).where(memberEntity.uuid.eq(uuid)).fetchOne()); queryFactory.selectFrom(memberEntity).where(memberEntity.uuid.eq(uuid)).fetchOne());
} }
} }

View File

@@ -0,0 +1,68 @@
package com.kamco.cd.kamcoback.test;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "test api", description = "test api")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/test")
public class TestApiController {
@Operation(summary = "admin test", description = "admin test api")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "조회",
content = @Content(schema = @Schema(implementation = String.class))),
@ApiResponse(
responseCode = "403",
description = "권한 없음",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@GetMapping("/admin")
public String admin() {
return "I am administrator";
}
@Operation(summary = "label test", description = "label test api")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "조회",
content = @Content(schema = @Schema(implementation = String.class))),
@ApiResponse(
responseCode = "403",
description = "권한 없음",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@GetMapping("/label")
public String label() {
return "Labeling is available.";
}
@Operation(summary = "review test", description = "review test api")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "조회",
content = @Content(schema = @Schema(implementation = String.class))),
@ApiResponse(
responseCode = "403",
description = "권한 없음",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@GetMapping("/review")
public String review() {
return "Review is available.";
}
}

View File

@@ -38,8 +38,10 @@ spring:
jwt: jwt:
secret: "kamco_token_9b71e778-19a3-4c1d-97bf-2d687de17d5b" secret: "kamco_token_9b71e778-19a3-4c1d-97bf-2d687de17d5b"
access-token-validity-in-ms: 86400000 # 1일 #access-token-validity-in-ms: 86400000 # 1일
refresh-token-validity-in-ms: 604800000 # 7일 #refresh-token-validity-in-ms: 604800000 # 7일
access-token-validity-in-ms: 60000 # 1분
refresh-token-validity-in-ms: 300000 # 5분
token: token:
refresh-cookie-name: kamco-dev # 개발용 쿠키 이름 refresh-cookie-name: kamco-dev # 개발용 쿠키 이름