From bdb5ba7011ca5d369023c70a1e6b7012e99bcbaa Mon Sep 17 00:00:00 2001 From: teddy Date: Wed, 10 Dec 2025 17:30:54 +0900 Subject: [PATCH 01/11] =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/AuthFailureEventListener.java | 31 +++ .../kamcoback/members/AdminApiController.java | 135 +++++------ .../cd/kamcoback/members/AuthController.java | 107 +++++---- .../members/MembersApiController.java | 64 ++--- .../cd/kamcoback/members/dto/MembersDto.java | 92 +++----- .../members/service/AdminService.java | 58 +---- .../members/service/AuthService.java | 35 +++ .../members/service/MembersService.java | 40 ++-- .../postgres/core/MembersCoreService.java | 219 ++++++++---------- .../postgres/entity/MemberEntity.java | 10 + .../members/MembersRepositoryCustom.java | 11 +- .../members/MembersRepositoryImpl.java | 170 +++++++------- 12 files changed, 470 insertions(+), 502 deletions(-) create mode 100644 src/main/java/com/kamco/cd/kamcoback/auth/AuthFailureEventListener.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java diff --git a/src/main/java/com/kamco/cd/kamcoback/auth/AuthFailureEventListener.java b/src/main/java/com/kamco/cd/kamcoback/auth/AuthFailureEventListener.java new file mode 100644 index 00000000..75486d9e --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/auth/AuthFailureEventListener.java @@ -0,0 +1,31 @@ +package com.kamco.cd.kamcoback.auth; + +import com.kamco.cd.kamcoback.members.service.AuthService; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationListener; +import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AuthFailureEventListener implements ApplicationListener { + + private final AuthService authService; + + @Override + public void onApplicationEvent(AbstractAuthenticationFailureEvent event) { + // 로그인 시도에 사용된 (username) + Object principal = event.getAuthentication().getPrincipal(); + + if (principal instanceof String username) { + // 로그인 실패 카운트 증가 로직 호출 + authService.loginFail(UUID.fromString(username)); + } + } + + @Override + public boolean supportsAsyncExecution() { + return ApplicationListener.super.supportsAsyncExecution(); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java index 6839e605..4ed55cda 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java @@ -13,7 +13,6 @@ import jakarta.validation.Valid; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -31,101 +30,81 @@ public class AdminApiController { @Operation(summary = "관리자 계정 등록", description = "관리자 계정 등록") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "등록 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "등록 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PostMapping("/join") public ApiResponseDto saveMember( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "관리자 계정 등록", - required = true, - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = MembersDto.AddReq.class))) - @RequestBody - @Valid - MembersDto.AddReq addReq) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "관리자 계정 등록", + required = true, + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = MembersDto.AddReq.class))) + @RequestBody + @Valid + MembersDto.AddReq addReq) { return ApiResponseDto.createOK(adminService.saveMember(addReq)); } @Operation(summary = "관리자 계정 수정", description = "관리자 계정 수정") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "수정 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "수정 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PutMapping("/{uuid}") public ApiResponseDto updateMembers( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "관리자 계정 수정", - required = true, - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = MembersDto.UpdateReq.class))) - @PathVariable - UUID uuid, - @RequestBody MembersDto.UpdateReq updateReq) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "관리자 계정 수정", + required = true, + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = MembersDto.UpdateReq.class))) + @PathVariable + UUID uuid, + @RequestBody @Valid MembersDto.UpdateReq updateReq) { adminService.updateMembers(uuid, updateReq); return ApiResponseDto.createOK(UUID.randomUUID()); } - @Operation(summary = "회원 탈퇴", description = "회원 탈퇴") + @Operation(summary = "관리자 계정 미사용 처리", description = "관리자 계정 미사용 처리") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "회원 탈퇴", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = UUID.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "관리자 계정 미사용 처리", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = UUID.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @DeleteMapping("/delete/{uuid}") public ApiResponseDto deleteAccount(@PathVariable UUID uuid) { adminService.deleteAccount(uuid); return ApiResponseDto.createOK(uuid); } - - @Operation(summary = "비밀번호 초기화", description = "비밀번호 초기화") - @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "비밀번호 초기화", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) - @PatchMapping("/{memberId}/password") - public ApiResponseDto resetPassword(@PathVariable Long memberId) { - adminService.resetPassword(memberId); - return ApiResponseDto.createOK(memberId); - } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java index 2fe356b8..d14a9a4a 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -4,6 +4,7 @@ import com.kamco.cd.kamcoback.auth.JwtTokenProvider; import com.kamco.cd.kamcoback.auth.RefreshTokenService; import com.kamco.cd.kamcoback.config.api.ApiResponseDto; import com.kamco.cd.kamcoback.members.dto.SignInRequest; +import com.kamco.cd.kamcoback.members.service.AuthService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -13,6 +14,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import java.nio.file.AccessDeniedException; import java.time.Duration; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; @@ -36,6 +38,7 @@ public class AuthController { private final AuthenticationManager authenticationManager; private final JwtTokenProvider jwtTokenProvider; private final RefreshTokenService refreshTokenService; + private final AuthService authService; @Value("${token.refresh-cookie-name}") private String refreshCookieName; @@ -47,45 +50,49 @@ public class AuthController { @Operation(summary = "로그인", description = "사번으로 로그인하여 액세스/리프레시 토큰을 발급.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "로그인 성공", - content = @Content(schema = @Schema(implementation = TokenResponse.class))), + responseCode = "200", + description = "로그인 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( - responseCode = "401", - description = "ID 또는 비밀번호 불일치", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + responseCode = "401", + description = "ID 또는 비밀번호 불일치", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) public ApiResponseDto signin( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "로그인 요청 정보", - required = true) - @RequestBody - SignInRequest request, - HttpServletResponse response) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "로그인 요청 정보", + required = true) + @RequestBody + SignInRequest request, + HttpServletResponse response) { Authentication authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); String username = authentication.getName(); // UserDetailsService 에서 사용한 username + // 로그인 시간 저장 + authService.saveLogin(UUID.fromString(username)); + String accessToken = jwtTokenProvider.createAccessToken(username); String refreshToken = jwtTokenProvider.createRefreshToken(username); // Redis에 RefreshToken 저장 (TTL = 7일) refreshTokenService.save( - username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); + username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); // HttpOnly + Secure 쿠키에 RefreshToken 저장 ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, refreshToken) - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, refreshToken) + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + return ApiResponseDto.ok(new TokenResponse(accessToken, refreshToken)); } @@ -93,16 +100,16 @@ public class AuthController { @Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "재발급 성공", - content = @Content(schema = @Schema(implementation = TokenResponse.class))), + responseCode = "200", + description = "재발급 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( - responseCode = "401", - description = "만료되었거나 유효하지 않은 리프레시 토큰", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + responseCode = "401", + description = "만료되었거나 유효하지 않은 리프레시 토큰", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) public ResponseEntity refresh(String refreshToken, HttpServletResponse response) - throws AccessDeniedException { + throws AccessDeniedException { if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) { throw new AccessDeniedException("만료되었거나 유효하지 않은 리프레시 토큰 입니다."); } @@ -120,17 +127,17 @@ public class AuthController { // Redis 갱신 refreshTokenService.save( - username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); + username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); // 쿠키 갱신 ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, newRefreshToken) - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, newRefreshToken) + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); return ResponseEntity.ok(new TokenResponse(newAccessToken, newRefreshToken)); @@ -140,12 +147,12 @@ public class AuthController { @Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "로그아웃 성공", - content = @Content(schema = @Schema(implementation = Void.class))) + responseCode = "200", + description = "로그아웃 성공", + content = @Content(schema = @Schema(implementation = Void.class))) }) public ApiResponseDto> logout( - Authentication authentication, HttpServletResponse response) { + Authentication authentication, HttpServletResponse response) { if (authentication != null) { String username = authentication.getName(); // Redis에서 RefreshToken 삭제 @@ -154,17 +161,19 @@ public class AuthController { // 쿠키 삭제 (Max-Age=0) ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, "") - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(0) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, "") + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(0) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); return ApiResponseDto.createOK(ResponseEntity.noContent().build()); } - public record TokenResponse(String accessToken, String refreshToken) {} + public record TokenResponse(String accessToken, String refreshToken) { + + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java index 08114c6f..bfcdc221 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java @@ -10,13 +10,13 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -import java.util.UUID; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -31,41 +31,41 @@ public class MembersApiController { @Operation(summary = "회원정보 목록", description = "회원정보 조회") @ApiResponses( - value = { - @ApiResponse( - responseCode = "200", - description = "검색 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Page.class))), - @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "200", + description = "검색 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Page.class))), + @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @GetMapping public ApiResponseDto> getMemberList( - @ParameterObject MembersDto.SearchReq searchReq) { + @ParameterObject MembersDto.SearchReq searchReq) { return ApiResponseDto.ok(membersService.findByMembers(searchReq)); } - @Operation(summary = "회원정보 수정", description = "회원정보 수정") + + @Operation(summary = "비밀번호 초기화", description = "비밀번호 초기화") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "회원정보 수정", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = UUID.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) - @PutMapping("/{uuid}") - public ApiResponseDto updateMember( - @PathVariable UUID uuid, @RequestBody MembersDto.UpdateReq updateReq) { - // membersService.updateMember(uuid, updateReq); - return ApiResponseDto.createOK(uuid); + value = { + @ApiResponse( + responseCode = "201", + description = "비밀번호 초기화", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + @PatchMapping("/{memberId}/password") + public ApiResponseDto resetPassword(@PathVariable Long memberId, @RequestBody @Valid MembersDto.InitReq initReq) { + membersService.resetPassword(memberId, initReq); + return ApiResponseDto.createOK(memberId); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java index b42c1d5e..af052366 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java @@ -1,7 +1,5 @@ package com.kamco.cd.kamcoback.members.dto; -import com.kamco.cd.kamcoback.common.enums.RoleType; -import com.kamco.cd.kamcoback.common.utils.interfaces.EnumValid; import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; @@ -23,33 +21,47 @@ public class MembersDto { private Long id; private UUID uuid; - private String employeeNo; + private String userRole; private String name; - private String email; + private String userId; + private String employeeNo; + private String tempPassword; private String status; - private String roleName; - @JsonFormatDttm private ZonedDateTime createdDttm; - @JsonFormatDttm private ZonedDateTime updatedDttm; + @JsonFormatDttm + private ZonedDateTime createdDttm; + @JsonFormatDttm + private ZonedDateTime updatedDttm; + @JsonFormatDttm + private ZonedDateTime firstLoginDttm; + @JsonFormatDttm + private ZonedDateTime lastLoginDttm; public Basic( - Long id, - UUID uuid, - String employeeNo, - String name, - String email, - String status, - String roleName, - ZonedDateTime createdDttm, - ZonedDateTime updatedDttm) { + Long id, + UUID uuid, + String userRole, + String name, + String userId, + String employeeNo, + String tempPassword, + String status, + ZonedDateTime createdDttm, + ZonedDateTime updatedDttm, + ZonedDateTime firstLoginDttm, + ZonedDateTime lastLoginDttm + ) { this.id = id; this.uuid = uuid; - this.employeeNo = employeeNo; + this.userRole = userRole; this.name = name; - this.email = email; + this.userId = userId; + this.employeeNo = employeeNo; + this.tempPassword = tempPassword; this.status = status; - this.roleName = roleName; this.createdDttm = createdDttm; this.updatedDttm = updatedDttm; + this.firstLoginDttm = firstLoginDttm; + this.lastLoginDttm = lastLoginDttm; } } @@ -59,21 +71,12 @@ public class MembersDto { @AllArgsConstructor public static class SearchReq { - @Schema(description = "이름(name), 이메일(email), 사번(employeeNo)", example = "name") - private String field; + @Schema(description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)", example = "") + private String userRole; @Schema(description = "키워드", example = "홍길동") private String keyword; - @Schema(description = "라벨러 포함 여부", example = "true") - private boolean labeler = true; - - @Schema(description = "검수자 포함 여부", example = "true") - private boolean reviewer = true; - - @Schema(description = "운영자 포함 여부", example = "true") - private boolean admin = true; - // 페이징 파라미터 @Schema(description = "페이지 번호 (0부터 시작) ", example = "0") private int page = 0; @@ -112,7 +115,7 @@ public class MembersDto { private String employeeNo; public AddReq( - String userRole, String name, String userId, String tempPassword, String employeeNo) { + String userRole, String name, String userId, String tempPassword, String employeeNo) { this.userRole = userRole; this.name = name; this.userId = userId; @@ -146,29 +149,10 @@ public class MembersDto { @Getter @Setter - public static class RolesDto { + public static class InitReq { - @Schema(description = "UUID", example = "4e89e487-c828-4a34-a7fc-0d5b0e3b53b5") - private UUID uuid; - - @Schema(description = "역할 ROLE_ADMIN, ROLE_LABELER, ROLE_REVIEWER", example = "ROLE_ADMIN") - @EnumValid(enumClass = RoleType.class) - private String roleName; - - public RolesDto(UUID uuid, String roleName) { - this.uuid = uuid; - this.roleName = roleName; - } - } - - @Getter - @Setter - @NoArgsConstructor - @AllArgsConstructor - public static class StatusDto { - - @Schema(description = "변경할 상태값 ACTIVE, INACTIVE, ARCHIVED", example = "ACTIVE") - @NotBlank - private String status; + @Schema(description = "패스워드", example = "") + @Size(max = 255) + private String password; } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java index 948d2454..da6905d7 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java @@ -1,11 +1,9 @@ package com.kamco.cd.kamcoback.members.service; -import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator; import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.postgres.core.MembersCoreService; import java.util.UUID; import lombok.RequiredArgsConstructor; -import org.mindrot.jbcrypt.BCrypt; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -17,69 +15,35 @@ public class AdminService { private final MembersCoreService membersCoreService; /** - * 회원가입 + * 관리자 계정 등록 * * @param addReq * @return */ @Transactional public Long saveMember(MembersDto.AddReq addReq) { - // salt 생성, 사번이 salt - String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(addReq.getUserId().trim()); - - // 패스워드 암호화, 초기 패스워드 고정 - String hashedPassword = BCrypt.hashpw(addReq.getTempPassword(), salt); - addReq.setTempPassword(hashedPassword); return membersCoreService.saveMembers(addReq); } + /** + * 관리자 계정 수정 + * + * @param uuid + * @param updateReq + */ + @Transactional public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) { membersCoreService.updateMembers(uuid, updateReq); } - /** - * 역할 추가 - * - * @param rolesDto - */ - @Transactional - public void saveRoles(MembersDto.RolesDto rolesDto) { - // membersCoreService.saveRoles(rolesDto); - } /** - * 역할 삭제 - * - * @param rolesDto - */ - public void deleteRoles(MembersDto.RolesDto rolesDto) { - // membersCoreService.deleteRoles(rolesDto); - } - - /** - * 역할 수정 - * - * @param statusDto - */ - public void updateStatus(UUID uuid, MembersDto.StatusDto statusDto) { - // membersCoreService.updateStatus(uuid, statusDto); - } - - /** - * 회원 탈퇴 + * 관리자 계정 미사용 처리 * * @param uuid */ + @Transactional public void deleteAccount(UUID uuid) { - // membersCoreService.deleteAccount(uuid); - } - - /** - * 패스워드 초기화 - * - * @param id - */ - public void resetPassword(Long id) { - // membersCoreService.resetPassword(id); + membersCoreService.deleteAccount(uuid); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java new file mode 100644 index 00000000..49ce1bce --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java @@ -0,0 +1,35 @@ +package com.kamco.cd.kamcoback.members.service; + +import com.kamco.cd.kamcoback.postgres.core.MembersCoreService; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AuthService { + + private final MembersCoreService membersCoreService; + + /** + * 로그인 일시 저장 + * + * @param uuid + */ + @Transactional + public void saveLogin(UUID uuid) { + membersCoreService.saveLogin(uuid); + } + + /** + * 로그인 실패 저장 + * + * @param uuid + */ + @Transactional + public void loginFail(UUID uuid) { + membersCoreService.loginFail(uuid); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java index fe85e06c..945d4868 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java @@ -1,11 +1,13 @@ package com.kamco.cd.kamcoback.members.service; +import com.kamco.cd.kamcoback.common.exception.CustomApiException; import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; import com.kamco.cd.kamcoback.postgres.core.MembersCoreService; import java.util.regex.Pattern; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,38 +25,22 @@ public class MembersService { * @return */ public Page findByMembers(MembersDto.SearchReq searchReq) { - return null; // membersCoreService.findByMembers(searchReq); + return membersCoreService.findByMembers(searchReq); } /** - * 회원정보 수정 + * 패스워드 사용자 변경 * - * @param uuid - * @param updateReq + * @param id + * @param initReq */ - // public void updateMember(UUID uuid, MembersDto.UpdateReq updateReq) { - // - // if (StringUtils.isNotBlank(updateReq.getPassword())) { - // - // if (!this.isValidPassword(updateReq.getPassword())) { - // throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST); - // } - // - // if (StringUtils.isBlank(updateReq.getEmployeeNo())) { - // throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST); - // } - // - // // salt 생성, 사번이 salt - // String salt = - // BCryptSaltGenerator.generateSaltWithEmployeeNo(updateReq.getEmployeeNo().trim()); - // - // // 패스워드 암호화, 초기 패스워드 고정 - // String hashedPassword = BCrypt.hashpw(updateReq.getPassword(), salt); - // updateReq.setPassword(hashedPassword); - // } - // - // membersCoreService.updateMembers(uuid, updateReq); - // } + @Transactional + public void resetPassword(Long id, MembersDto.InitReq initReq) { + if (!isValidPassword(initReq.getPassword())) { + throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST); + } + membersCoreService.resetPassword(id, initReq); + } /** * 대문자 1개 이상 소문자 1개 이상 숫자 1개 이상 특수문자(!@#$) 1개 이상 diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java index d6acd8a9..ca2102f1 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java @@ -1,16 +1,20 @@ package com.kamco.cd.kamcoback.postgres.core; +import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator; import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.MembersDto.AddReq; +import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; import com.kamco.cd.kamcoback.members.exception.MemberException.DuplicateMemberException; import com.kamco.cd.kamcoback.members.exception.MemberException.DuplicateMemberException.Field; import com.kamco.cd.kamcoback.members.exception.MemberException.MemberNotFoundException; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; -import com.kamco.cd.kamcoback.postgres.repository.members.MembersArchivedRepository; import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository; +import java.time.ZonedDateTime; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; +import org.mindrot.jbcrypt.BCrypt; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; @Service @@ -18,10 +22,9 @@ import org.springframework.stereotype.Service; public class MembersCoreService { private final MembersRepository membersRepository; - private final MembersArchivedRepository memberArchivedRepository; /** - * 회원가입 + * 관리자 계정 등록 * * @param addReq * @return @@ -30,11 +33,17 @@ public class MembersCoreService { if (membersRepository.existsByUserId(addReq.getUserId())) { throw new DuplicateMemberException(Field.USER_ID, addReq.getUserId()); } + + // salt 생성, 사번이 salt + String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(addReq.getUserId().trim()); + // 패스워드 암호화, 초기 패스워드 고정 + String hashedPassword = BCrypt.hashpw(addReq.getTempPassword(), salt); + MemberEntity memberEntity = new MemberEntity(); memberEntity.setUserId(addReq.getUserId()); memberEntity.setUserRole(addReq.getUserRole()); - memberEntity.setTempPassword(addReq.getTempPassword()); - memberEntity.setPassword(addReq.getTempPassword()); + memberEntity.setTempPassword(addReq.getTempPassword()); // 임시 패스워드는 암호화 하지 않음 + memberEntity.setPassword(hashedPassword); memberEntity.setName(addReq.getName()); memberEntity.setEmployeeNo(addReq.getEmployeeNo()); @@ -42,22 +51,22 @@ public class MembersCoreService { } /** - * 회원정보 수정 + * 관리자 계정 수정 * * @param uuid * @param updateReq */ public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) { MemberEntity memberEntity = - membersRepository.findByUUID(uuid).orElseThrow(MemberNotFoundException::new); + membersRepository.findByUUID(uuid).orElseThrow(MemberNotFoundException::new); if (StringUtils.isNotBlank(updateReq.getName())) { memberEntity.setName(updateReq.getName()); } + // 임시 패스워드는 암호화 하지 않음 if (StringUtils.isNotBlank(updateReq.getTempPassword())) { memberEntity.setTempPassword(updateReq.getTempPassword()); - memberEntity.setPassword(updateReq.getTempPassword()); } if (StringUtils.isNotBlank(memberEntity.getEmployeeNo())) { @@ -66,123 +75,42 @@ public class MembersCoreService { membersRepository.save(memberEntity); } - // - // /** - // * 역할 추가 - // * - // * @param rolesDto - // */ - // public void saveRoles(MembersDto.RolesDto rolesDto) { - // - // MemberEntity memberEntity = - // membersRepository - // .findByUUID(rolesDto.getUuid()) - // .orElseThrow(() -> new MemberNotFoundException()); - // - // if (memberRoleRepository.findByUuidAndRoleName(rolesDto)) { - // throw new MemberException.DuplicateMemberException( - // MemberException.DuplicateMemberException.Field.DEFAULT, "중복된 역할이 있습니다."); - // } - // - // MemberRoleEntityId memberRoleEntityId = new MemberRoleEntityId(); - // memberRoleEntityId.setMemberUuid(rolesDto.getUuid()); - // memberRoleEntityId.setRoleName(rolesDto.getRoleName()); - // - // MemberRoleEntity memberRoleEntity = new MemberRoleEntity(); - // memberRoleEntity.setId(memberRoleEntityId); - // memberRoleEntity.setMemberUuid(memberEntity); - // memberRoleEntity.setCreatedDttm(ZonedDateTime.now()); - // memberRoleRepository.save(memberRoleEntity); - // } - // - // /** - // * 역할 삭제 - // * - // * @param rolesDto - // */ - // public void deleteRoles(MembersDto.RolesDto rolesDto) { - // MemberEntity memberEntity = - // membersRepository - // .findByUUID(rolesDto.getUuid()) - // .orElseThrow(() -> new MemberNotFoundException()); - // - // MemberRoleEntityId memberRoleEntityId = new MemberRoleEntityId(); - // memberRoleEntityId.setMemberUuid(rolesDto.getUuid()); - // memberRoleEntityId.setRoleName(rolesDto.getRoleName()); - // - // MemberRoleEntity memberRoleEntity = new MemberRoleEntity(); - // memberRoleEntity.setId(memberRoleEntityId); - // memberRoleEntity.setMemberUuid(memberEntity); - // - // memberRoleRepository.delete(memberRoleEntity); - // } - // - // /** - // * 상태 수정 - // * - // * @param statusDto - // */ - // public void updateStatus(UUID uuid, MembersDto.StatusDto statusDto) { - // MemberEntity memberEntity = - // membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); - // - // memberEntity.setStatus(statusDto.getStatus()); - // memberEntity.setUpdatedDttm(ZonedDateTime.now()); - // membersRepository.save(memberEntity); - // } - // - // /** - // * 회원 탈퇴 - // * - // * @param uuid - // */ - // public void deleteAccount(UUID uuid) { - // MemberEntity memberEntity = - // membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); - // - // MemberArchivedEntityId memberArchivedEntityId = new MemberArchivedEntityId(); - // memberArchivedEntityId.setUserId(memberEntity.getId()); - // memberArchivedEntityId.setUuid(memberEntity.getUuid()); - // - // MemberArchivedEntity memberArchivedEntity = new MemberArchivedEntity(); - // memberArchivedEntity.setId(memberArchivedEntityId); - // memberArchivedEntity.setEmployeeNo(memberEntity.getEmployeeNo()); - // memberArchivedEntity.setName(memberEntity.getName()); - // memberArchivedEntity.setPassword(memberEntity.getPassword()); - // memberArchivedEntity.setEmail(memberEntity.getEmail()); - // memberArchivedEntity.setStatus(memberEntity.getStatus()); - // memberArchivedEntity.setCreatedDttm(memberEntity.getCreatedDttm()); - // memberArchivedEntity.setArchivedDttm(ZonedDateTime.now()); - // memberArchivedRepository.save(memberArchivedEntity); - // - // memberEntity.setStatus("ARCHIVED"); - // memberEntity.setName("**********"); - // memberEntity.setEmployeeNo("**********"); - // memberEntity.setPassword("**********"); - // memberEntity.setEmail("**********"); - // memberEntity.setUpdatedDttm(ZonedDateTime.now()); - // membersRepository.save(memberEntity); - // } - // - // /** - // * 패스워드 초기화 - // * - // * @param id - // */ - // public void resetPassword(Long id) { - // MemberEntity memberEntity = - // membersRepository.findById(id).orElseThrow(() -> new MemberNotFoundException()); - // - // String salt = - // BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); - // // 패스워드 암호화, 초기 패스워드 고정 - // String hashedPassword = BCrypt.hashpw(password, salt); - // - // memberEntity.setPassword(hashedPassword); - // memberEntity.setStatus("INACTIVE"); - // memberEntity.setUpdatedDttm(ZonedDateTime.now()); - // membersRepository.save(memberEntity); - // } + + + /** + * 관리자 계정 미사용 처리 + * + * @param uuid + */ + public void deleteAccount(UUID uuid) { + MemberEntity memberEntity = + membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); + + memberEntity.setStatus("INACTIVE"); + memberEntity.setUpdatedDttm(ZonedDateTime.now()); + membersRepository.save(memberEntity); + } + + + /** + * 패스워드 변경 + * + * @param id + */ + public void resetPassword(Long id, MembersDto.InitReq initReq) { + MemberEntity memberEntity = + membersRepository.findById(id).orElseThrow(() -> new MemberNotFoundException()); + + String salt = + BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); + // 패스워드 암호화 + String hashedPassword = BCrypt.hashpw(initReq.getPassword(), salt); + + memberEntity.setPassword(hashedPassword); + memberEntity.setStatus("ACTIVE"); + memberEntity.setUpdatedDttm(ZonedDateTime.now()); + membersRepository.save(memberEntity); + } // /** @@ -191,7 +119,42 @@ public class MembersCoreService { * @param searchReq * @return */ - // public Page findByMembers(MembersDto.SearchReq searchReq) { - // return membersRepository.findByMembers(searchReq); - // } + public Page findByMembers(MembersDto.SearchReq searchReq) { + return membersRepository.findByMembers(searchReq); + } + + /** + * 최초 로그인 저장 마지막 로그인 저장 + * + * @param uuid + */ + public void saveLogin(UUID uuid) { + MemberEntity memberEntity = + membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); + + if (memberEntity.getFirstLoginDttm() == null) { + memberEntity.setFirstLoginDttm(ZonedDateTime.now()); + } + memberEntity.setLastLoginDttm(ZonedDateTime.now()); + memberEntity.setLoginFailCount(0); + membersRepository.save(memberEntity); + } + + /** + * 로그인 실패시 상태 저장 + * + * @param uuid + */ + public void loginFail(UUID uuid) { + MemberEntity memberEntity = + membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); + + int failCnt = memberEntity.getLoginFailCount() + 1; + if (failCnt >= 5) { + memberEntity.setStatus("INACTIVE"); + } + + memberEntity.setLoginFailCount(failCnt); + membersRepository.save(memberEntity); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java index f9cf9ef8..5a4879b0 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MemberEntity.java @@ -73,4 +73,14 @@ public class MemberEntity { @ColumnDefault("now()") @Column(name = "updated_dttm") private ZonedDateTime updatedDttm = ZonedDateTime.now(); + + @Column(name = "first_login_dttm") + private ZonedDateTime firstLoginDttm; + + @Column(name = "last_login_dttm") + private ZonedDateTime lastLoginDttm; + + @Column(name = "login_fail_count") + @ColumnDefault("0") + private Integer loginFailCount = 0; } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryCustom.java index 17f9ee43..bcd23223 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryCustom.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryCustom.java @@ -1,20 +1,19 @@ package com.kamco.cd.kamcoback.postgres.repository.members; +import com.kamco.cd.kamcoback.members.dto.MembersDto; +import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import java.util.Optional; import java.util.UUID; +import org.springframework.data.domain.Page; public interface MembersRepositoryCustom { boolean existsByUserId(String userId); - Optional findByUserId(String employeeNo); + Optional findByUserId(String userId); Optional findByUUID(UUID uuid); - // - // Page findByMembers(MembersDto.SearchReq searchReq); - // - // - // Optional findByEmployeeNo(String employeeNo); + Page findByMembers(MembersDto.SearchReq searchReq); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java index c428c639..7e84baec 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java @@ -1,11 +1,20 @@ package com.kamco.cd.kamcoback.postgres.repository.members; +import com.kamco.cd.kamcoback.members.dto.MembersDto; +import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import com.kamco.cd.kamcoback.postgres.entity.QMemberEntity; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.util.List; import java.util.Optional; import java.util.UUID; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; @Repository @@ -24,96 +33,95 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { @Override public boolean existsByUserId(String userId) { return queryFactory - .selectOne() - .from(memberEntity) - .where(memberEntity.userId.eq(userId)) - .fetchFirst() - != null; + .selectOne() + .from(memberEntity) + .where(memberEntity.userId.eq(userId)) + .fetchFirst() + != null; } + /** + * 사용자 조회 user id + * + * @param userId + * @return + */ @Override public Optional findByUserId(String userId) { return Optional.ofNullable( - queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne()); + queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne()); } - // /** - // * 회원정보 목록 조회 - // * - // * @param searchReq - // * @return - // */ - // @Override - // public Page findByMembers(MembersDto.SearchReq searchReq) { - // Pageable pageable = searchReq.toPageable(); - // BooleanBuilder builder = new BooleanBuilder(); - // BooleanBuilder leftBuilder = new BooleanBuilder(); - // - // if (StringUtils.isNotBlank(searchReq.getField())) { - // switch (searchReq.getField()) { - // case "name" -> - // builder.and(memberEntity.name.containsIgnoreCase(searchReq.getKeyword().trim())); - // } - // } - // - // List roles = new ArrayList<>(); - // // 라벨러 - // if (searchReq.isLabeler()) { - // roles.add(RoleType.ROLE_LABELER.getId()); - // } - // - // // 시스템 전체 관리자 - // if (searchReq.isAdmin()) { - // roles.add(RoleType.ROLE_ADMIN.getId()); - // } - // - // // 검수자 - // if (searchReq.isReviewer()) { - // roles.add(RoleType.ROLE_REVIEWER.getId()); - // } - // - // // 역할 in 조건 추가 - // if (!roles.isEmpty()) { - // leftBuilder.and(memberRoleEntity.id.roleName.in(roles)); - // } - // - // List content = - // queryFactory - // .select( - // Projections.constructor( - // MembersDto.Basic.class, - // memberEntity.id, - // memberEntity.uuid, - // memberEntity.employeeNo, - // memberEntity.name, - // null, - // memberEntity.status, - // memberRoleEntity.id.roleName, - // memberEntity.createdDttm, - // memberEntity.updatedDttm)) - // .from(memberEntity) - // .leftJoin(memberRoleEntity) - // .on(memberRoleEntity.memberUuid.uuid.eq(memberEntity.uuid).and(leftBuilder)) - // .where(builder) - // .offset(pageable.getOffset()) - // .limit(pageable.getPageSize()) - // .orderBy(memberEntity.createdDttm.desc()) - // .fetch(); - // - // long total = - // queryFactory - // .select(memberEntity) - // .from(memberEntity) - // .leftJoin(memberRoleEntity) - // .on(memberRoleEntity.memberUuid.uuid.eq(memberEntity.uuid).and(leftBuilder)) - // .fetchCount(); - // - // return new PageImpl<>(content, pageable, total); - // } - // + /** + * 회원정보 목록 조회 + * + * @param searchReq + * @return + */ + @Override + public Page findByMembers(MembersDto.SearchReq searchReq) { + Pageable pageable = searchReq.toPageable(); + BooleanBuilder builder = new BooleanBuilder(); + + // 검색어 + if (StringUtils.isNotBlank(searchReq.getKeyword())) { + String contains = "%" + searchReq.getKeyword() + "%"; + + builder.and( + memberEntity.name.likeIgnoreCase(contains) + .or(memberEntity.userId.likeIgnoreCase(contains)) + .or(memberEntity.employeeNo.likeIgnoreCase(contains)) + ); + } + + // 권한 + if (StringUtils.isNotBlank(searchReq.getUserRole())) { + builder.and(memberEntity.userRole.eq(searchReq.getUserRole())); + } + + List content = + queryFactory + .select( + Projections.constructor( + MembersDto.Basic.class, + memberEntity.id, + memberEntity.uuid, + memberEntity.userRole, + memberEntity.name, + memberEntity.userId, + memberEntity.employeeNo, + memberEntity.tempPassword, + memberEntity.status, + memberEntity.createdDttm, + memberEntity.updatedDttm, + memberEntity.firstLoginDttm, + memberEntity.lastLoginDttm + )) + .from(memberEntity) + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(memberEntity.createdDttm.desc()) + .fetch(); + + long total = + queryFactory + .select(memberEntity) + .from(memberEntity) + .fetchCount(); + + return new PageImpl<>(content, pageable, total); + } + + /** + * 사용자 ID 조회 UUID + * + * @param uuid + * @return + */ @Override public Optional findByUUID(UUID uuid) { return Optional.ofNullable( - queryFactory.selectFrom(memberEntity).where(memberEntity.uuid.eq(uuid)).fetchOne()); + queryFactory.selectFrom(memberEntity).where(memberEntity.uuid.eq(uuid)).fetchOne()); } } From fc2edf7c6d765dcda7780a7ef6bb0da603f9a40d Mon Sep 17 00:00:00 2001 From: teddy Date: Wed, 10 Dec 2025 18:30:44 +0900 Subject: [PATCH 02/11] =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/CustomAuthenticationProvider.java | 14 +++- .../cd/kamcoback/auth/CustomUserDetails.java | 2 +- .../cd/kamcoback/members/AuthController.java | 10 +++ .../members/MembersApiController.java | 4 +- .../kamcoback/members/dto/MemberDetails.java | 65 ------------------- .../cd/kamcoback/members/dto/MembersDto.java | 7 +- .../members/exception/MemberException.java | 11 ++++ .../members/service/AuthService.java | 5 ++ .../postgres/core/MembersCoreService.java | 22 +++++++ 9 files changed, 68 insertions(+), 72 deletions(-) delete mode 100644 src/main/java/com/kamco/cd/kamcoback/members/dto/MemberDetails.java diff --git a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java index c89f793e..36260107 100644 --- a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java +++ b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java @@ -6,9 +6,11 @@ import lombok.RequiredArgsConstructor; import org.mindrot.jbcrypt.BCrypt; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Component; @Component @@ -16,6 +18,7 @@ import org.springframework.stereotype.Component; public class CustomAuthenticationProvider implements AuthenticationProvider { private final MembersRepository membersRepository; + private final UserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { @@ -24,9 +27,9 @@ public class CustomAuthenticationProvider implements AuthenticationProvider { // 1. 유저 조회 MemberEntity member = - membersRepository - .findByUserId(username) - .orElseThrow(() -> new BadCredentialsException("ID 또는 비밀번호가 일치하지 않습니다.")); + membersRepository + .findByUserId(username) + .orElseThrow(() -> new BadCredentialsException("ID 또는 비밀번호가 일치하지 않습니다.")); // 2. jBCrypt + 커스텀 salt 로 저장된 패스워드 비교 if (!BCrypt.checkpw(rawPassword, member.getPassword())) { @@ -36,6 +39,11 @@ public class CustomAuthenticationProvider implements AuthenticationProvider { // 3. 인증 성공 → UserDetails 생성 CustomUserDetails userDetails = new CustomUserDetails(member); + // 4. 상태값 확인 + if (!userDetails.isEnabled()) { + throw new DisabledException("비활성화된 계정입니다."); + } + return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); } diff --git a/src/main/java/com/kamco/cd/kamcoback/auth/CustomUserDetails.java b/src/main/java/com/kamco/cd/kamcoback/auth/CustomUserDetails.java index b65be3f0..8efb073a 100644 --- a/src/main/java/com/kamco/cd/kamcoback/auth/CustomUserDetails.java +++ b/src/main/java/com/kamco/cd/kamcoback/auth/CustomUserDetails.java @@ -46,7 +46,7 @@ public class CustomUserDetails implements UserDetails { @Override public boolean isEnabled() { - return member.getStatus().equalsIgnoreCase("ACTIVE"); + return "ACTIVE".equals(member.getStatus()); } public MemberEntity getMember() { diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java index d14a9a4a..28877c5e 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -56,6 +56,10 @@ public class AuthController { @ApiResponse( responseCode = "401", description = "ID 또는 비밀번호 불일치", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))), + @ApiResponse( + responseCode = "400", + description = "미사용 상태", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) public ApiResponseDto signin( @@ -65,6 +69,12 @@ public class AuthController { @RequestBody SignInRequest request, HttpServletResponse response) { + + // + if (authService.existsUsername(request)) { +// return + } + Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); diff --git a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java index bfcdc221..6f63553c 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java @@ -49,12 +49,12 @@ public class MembersApiController { } - @Operation(summary = "비밀번호 초기화", description = "비밀번호 초기화") + @Operation(summary = "사용자 비밀번호 변경", description = "사용자 비밀번호 변경") @ApiResponses( value = { @ApiResponse( responseCode = "201", - description = "비밀번호 초기화", + description = "사용자 비밀번호 변경", content = @Content( mediaType = "application/json", diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/MemberDetails.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/MemberDetails.java deleted file mode 100644 index 9c1b06f4..00000000 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/MemberDetails.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.kamco.cd.kamcoback.members.dto; - -import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; -import java.util.Collection; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -@RequiredArgsConstructor -public class MemberDetails implements UserDetails { - - private final MemberEntity member; - - @Override - public Collection getAuthorities() { - // TODO: tb_member_role 에서 역할 꺼내서 권한으로 변환하고 싶으면 여기 구현 - // 예시 (나중에 MemberRoleEntity 보고 수정): - // return member.getTbMemberRoles().stream() - // .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getRoleName())) - // .toList(); - - return List.of(); // 일단 빈 권한 리스트 - } - - @Override - public String getPassword() { - return member.getPassword(); // 암호화된 비밀번호 - } - - @Override - public String getUsername() { - // 로그인 ID 로 무엇을 쓸지 선택 - // 1) 이메일 로그인: - return member.getUserId(); - - // 2) 사번으로 로그인하고 싶으면: - // return member.getEmployeeNo(); - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - // status 가 ACTIVE 일 때만 로그인 허용 - return "ACTIVE".equalsIgnoreCase(member.getStatus()); - } - - public MemberEntity getMember() { - return member; - } -} diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java index af052366..d28d39ca 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java @@ -151,8 +151,13 @@ public class MembersDto { @Setter public static class InitReq { - @Schema(description = "패스워드", example = "") + @Schema(description = "변경 패스워드", example = "") @Size(max = 255) + @NotBlank private String password; + + @Schema(description = "초기 패스워드", example = "") + @NotBlank + private String tempPassword; } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/exception/MemberException.java b/src/main/java/com/kamco/cd/kamcoback/members/exception/MemberException.java index b8dce363..33d2e8da 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/exception/MemberException.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/exception/MemberException.java @@ -35,4 +35,15 @@ public class MemberException { super(message); } } + + public static class PasswordNotFoundException extends RuntimeException { + + public PasswordNotFoundException() { + super("Password not found"); + } + + public PasswordNotFoundException(String message) { + super(message); + } + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java index 49ce1bce..065c7aaa 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java @@ -1,5 +1,6 @@ package com.kamco.cd.kamcoback.members.service; +import com.kamco.cd.kamcoback.members.dto.SignInRequest; import com.kamco.cd.kamcoback.postgres.core.MembersCoreService; import java.util.UUID; import lombok.RequiredArgsConstructor; @@ -32,4 +33,8 @@ public class AuthService { public void loginFail(UUID uuid) { membersCoreService.loginFail(uuid); } + + public boolean existsUsername(SignInRequest request) { + return membersCoreService.existsUsername(request); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java index ca2102f1..e1613003 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java @@ -4,9 +4,11 @@ import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator; import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.MembersDto.AddReq; import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; +import com.kamco.cd.kamcoback.members.dto.SignInRequest; import com.kamco.cd.kamcoback.members.exception.MemberException.DuplicateMemberException; import com.kamco.cd.kamcoback.members.exception.MemberException.DuplicateMemberException.Field; import com.kamco.cd.kamcoback.members.exception.MemberException.MemberNotFoundException; +import com.kamco.cd.kamcoback.members.exception.MemberException.PasswordNotFoundException; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository; import java.time.ZonedDateTime; @@ -101,6 +103,10 @@ public class MembersCoreService { MemberEntity memberEntity = membersRepository.findById(id).orElseThrow(() -> new MemberNotFoundException()); + if (!memberEntity.getTempPassword().equals(initReq.getTempPassword())) { + throw new PasswordNotFoundException(); + } + String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); // 패스워드 암호화 @@ -157,4 +163,20 @@ public class MembersCoreService { memberEntity.setLoginFailCount(failCnt); membersRepository.save(memberEntity); } + + /** + * 등록, 패스워드가 초기 상태일때 + * + * @param request + * @return + */ + public boolean existsUsername(SignInRequest request) { + MemberEntity memberEntity = membersRepository.findByUserId(request.getUsername()).orElseThrow(MemberNotFoundException::new); + + if (memberEntity.getStatus().equals("INACTIVE") && memberEntity.getTempPassword().equals(request.getPassword())) { + return true; + } + + return false; + } } From 83bfdc272848c3323d6a8c6502ae024adccd4e23 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 10:18:03 +0900 Subject: [PATCH 03/11] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EA=B0=92=20=EA=B2=80=EC=A6=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/CustomAuthenticationProvider.java | 6 -- .../cd/kamcoback/config/SecurityConfig.java | 57 +++++++++---------- .../cd/kamcoback/members/AuthController.java | 16 +++--- .../members/service/AuthService.java | 10 +++- .../postgres/core/MembersCoreService.java | 13 ++--- 5 files changed, 48 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java index 36260107..8e6cb1bf 100644 --- a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java +++ b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java @@ -6,7 +6,6 @@ import lombok.RequiredArgsConstructor; import org.mindrot.jbcrypt.BCrypt; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -39,11 +38,6 @@ public class CustomAuthenticationProvider implements AuthenticationProvider { // 3. 인증 성공 → UserDetails 생성 CustomUserDetails userDetails = new CustomUserDetails(member); - // 4. 상태값 확인 - if (!userDetails.isEnabled()) { - throw new DisabledException("비활성화된 계정입니다."); - } - return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); } diff --git a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java index b7fdcbf2..a952e535 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java @@ -6,14 +6,12 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -30,32 +28,33 @@ public class SecurityConfig { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.cors(cors -> cors.configurationSource(corsConfigurationSource())) - .csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화 - .sessionManagement( - sm -> - sm.sessionCreationPolicy( - SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음, 요청은 JWT 인증 - .formLogin(form -> form.disable()) // react에서 로그인 요청 관리 - .httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용 - .logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음 - .authenticationProvider( - customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용 - .authorizeHttpRequests( - auth -> - auth.requestMatchers(HttpMethod.OPTIONS, "/**") - .permitAll() // preflight 허용 - .requestMatchers( - "/api/auth/signin", - "/api/auth/refresh", - "/swagger-ui/**", - "/v3/api-docs/**") - .permitAll() - .anyRequest() - .authenticated()) - .addFilterBefore( - jwtAuthenticationFilter, - UsernamePasswordAuthenticationFilter - .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. + .csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화 + .sessionManagement( + sm -> + sm.sessionCreationPolicy( + SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음, 요청은 JWT 인증 + .formLogin(form -> form.disable()) // react에서 로그인 요청 관리 + .httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용 + .logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음 + .authenticationProvider( + customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용 + .authorizeHttpRequests( + auth -> + auth.anyRequest().permitAll()); +// requestMatchers(HttpMethod.OPTIONS, "/**") +// .permitAll() // preflight 허용 +// .requestMatchers( +// "/api/auth/signin", +// "/api/auth/refresh", +// "/swagger-ui/**", +// "/v3/api-docs/**") +// .permitAll() +// .anyRequest() +// .authenticated()) +// .addFilterBefore( +// jwtAuthenticationFilter, +// UsernamePasswordAuthenticationFilter +// .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. ; return http.build(); @@ -63,7 +62,7 @@ public class SecurityConfig { @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) - throws Exception { + throws Exception { return configuration.getAuthenticationManager(); } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java index 28877c5e..47b76169 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -70,15 +70,15 @@ public class AuthController { SignInRequest request, HttpServletResponse response) { - // - if (authService.existsUsername(request)) { -// return - } - Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); + String status = authService.getUserStatus(request); + if ("INACTIVE".equals(status)) { + return ApiResponseDto.ok(new TokenResponse(status, null, null)); + } + String username = authentication.getName(); // UserDetailsService 에서 사용한 username // 로그인 시간 저장 @@ -103,7 +103,7 @@ public class AuthController { response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); - return ApiResponseDto.ok(new TokenResponse(accessToken, refreshToken)); + return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken)); } @PostMapping("/refresh") @@ -150,7 +150,7 @@ public class AuthController { .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); - return ResponseEntity.ok(new TokenResponse(newAccessToken, newRefreshToken)); + return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken)); } @PostMapping("/logout") @@ -183,7 +183,7 @@ public class AuthController { return ApiResponseDto.createOK(ResponseEntity.noContent().build()); } - public record TokenResponse(String accessToken, String refreshToken) { + public record TokenResponse(String status, String accessToken, String refreshToken) { } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java index 065c7aaa..5433750b 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java @@ -34,7 +34,13 @@ public class AuthService { membersCoreService.loginFail(uuid); } - public boolean existsUsername(SignInRequest request) { - return membersCoreService.existsUsername(request); + /** + * 사용자 상태 조회 + * + * @param request + * @return + */ + public String getUserStatus(SignInRequest request) { + return membersCoreService.getUserStatus(request); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java index e1613003..d1a1b642 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java @@ -37,7 +37,7 @@ public class MembersCoreService { } // salt 생성, 사번이 salt - String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(addReq.getUserId().trim()); + String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(addReq.getEmployeeNo().trim()); // 패스워드 암호화, 초기 패스워드 고정 String hashedPassword = BCrypt.hashpw(addReq.getTempPassword(), salt); @@ -165,18 +165,13 @@ public class MembersCoreService { } /** - * 등록, 패스워드가 초기 상태일때 + * 사용자 상태 조회 * * @param request * @return */ - public boolean existsUsername(SignInRequest request) { + public String getUserStatus(SignInRequest request) { MemberEntity memberEntity = membersRepository.findByUserId(request.getUsername()).orElseThrow(MemberNotFoundException::new); - - if (memberEntity.getStatus().equals("INACTIVE") && memberEntity.getTempPassword().equals(request.getPassword())) { - return true; - } - - return false; + return memberEntity.getStatus(); } } From 979aab24f4a736715e44fde35e141277b48bbc6f Mon Sep 17 00:00:00 2001 From: "gayoun.park" Date: Thu, 11 Dec 2025 10:31:31 +0900 Subject: [PATCH 04/11] =?UTF-8?q?=EB=B3=80=ED=99=94=ED=83=90=EC=A7=80=20cl?= =?UTF-8?q?azz=20API=20:=20enum=20->=20=EA=B3=B5=ED=86=B5=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code/CommonCodeApiController.java | 25 ++++++---- .../cd/kamcoback/code/dto/CommonCodeDto.java | 28 +++++++---- .../common/utils/CommonCodeUtil.java | 46 +++++++++++++++++-- 3 files changed, 75 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java b/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java index b6ad2d51..20324db6 100644 --- a/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java @@ -3,10 +3,8 @@ package com.kamco.cd.kamcoback.code; import com.kamco.cd.kamcoback.code.config.CommonCodeCacheManager; import com.kamco.cd.kamcoback.code.dto.CommonCodeDto; import com.kamco.cd.kamcoback.code.service.CommonCodeService; -import com.kamco.cd.kamcoback.common.enums.DetectionClassification; +import com.kamco.cd.kamcoback.common.utils.CommonCodeUtil; import com.kamco.cd.kamcoback.config.api.ApiResponseDto; -import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; -import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.Clazzes; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; @@ -14,8 +12,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; -import java.util.Arrays; -import java.util.Comparator; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; @@ -36,6 +32,7 @@ public class CommonCodeApiController { private final CommonCodeService commonCodeService; private final CommonCodeCacheManager commonCodeCacheManager; + private final CommonCodeUtil commonCodeUtil; @Operation(summary = "목록 조회", description = "모든 공통코드 조회") @ApiResponses( @@ -213,12 +210,20 @@ public class CommonCodeApiController { } @GetMapping("/clazz") - public ApiResponseDto> getClasses() { + public ApiResponseDto> getClasses() { - List list = - Arrays.stream(DetectionClassification.values()) - .sorted(Comparator.comparingInt(DetectionClassification::getOrder)) - .map(Clazzes::new) + // List list = + // Arrays.stream(DetectionClassification.values()) + // .sorted(Comparator.comparingInt(DetectionClassification::getOrder)) + // .map(Clazzes::new) + // .toList(); + + //변화탐지 clazz API : enum -> 공통코드로 변경 + List list = + commonCodeUtil.getChildCodesByParentCode("0000").stream() + .map( + child -> + new CommonCodeDto.Clazzes(child.getCode(), child.getName(), child.getOrder())) .toList(); return ApiResponseDto.ok(list); diff --git a/src/main/java/com/kamco/cd/kamcoback/code/dto/CommonCodeDto.java b/src/main/java/com/kamco/cd/kamcoback/code/dto/CommonCodeDto.java index 3827603b..963d6e90 100644 --- a/src/main/java/com/kamco/cd/kamcoback/code/dto/CommonCodeDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/code/dto/CommonCodeDto.java @@ -1,5 +1,6 @@ package com.kamco.cd.kamcoback.code.dto; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.kamco.cd.kamcoback.common.utils.html.HtmlEscapeDeserializer; @@ -71,15 +72,6 @@ public class CommonCodeDto { @NotNull private Integer order; } - @Getter - @Setter - @NoArgsConstructor - @AllArgsConstructor - public static class OrderReqDetail { - @NotNull private Long id; - @NotNull private Integer order; - } - @Schema(name = "CommonCode Basic", description = "공통코드 기본 정보") @Getter public static class Basic { @@ -139,4 +131,22 @@ public class CommonCodeDto { this.deletedDttm = deletedDttm; } } + + @Getter + public static class Clazzes { + + private String code; + private String name; + + @JsonInclude(JsonInclude.Include.NON_NULL) + private Double score; + + private Integer order; + + public Clazzes(String code, String name, Integer order) { + this.code = code; + this.name = name; + this.order = order; + } + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/common/utils/CommonCodeUtil.java b/src/main/java/com/kamco/cd/kamcoback/common/utils/CommonCodeUtil.java index 1b75b4e5..a637a375 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/utils/CommonCodeUtil.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/utils/CommonCodeUtil.java @@ -1,11 +1,15 @@ package com.kamco.cd.kamcoback.common.utils; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kamco.cd.kamcoback.code.dto.CommonCodeDto; import com.kamco.cd.kamcoback.code.dto.CommonCodeDto.Basic; import com.kamco.cd.kamcoback.code.service.CommonCodeService; import com.kamco.cd.kamcoback.config.api.ApiResponseDto; +import java.util.LinkedHashMap; import java.util.List; import java.util.Optional; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** @@ -19,6 +23,8 @@ public class CommonCodeUtil { private final CommonCodeService commonCodeService; + @Autowired private ObjectMapper objectMapper; + public CommonCodeUtil(CommonCodeService commonCodeService) { this.commonCodeService = commonCodeService; } @@ -30,7 +36,21 @@ public class CommonCodeUtil { */ public List getAllCommonCodes() { try { - return commonCodeService.getFindAll(); + // 캐시에 들어간 이후 꺼낼 때, DTO를 인식하지 못하여 LinkedHashMap으로 리턴됨. + // LinkedHashMap인 경우 변환처리, 아닌 경우는 그대로 리턴함. + List allCodes = commonCodeService.getFindAll(); + return allCodes.stream() + .map( + item -> { + if (item instanceof LinkedHashMap map) { + return objectMapper.convertValue(map, CommonCodeDto.Basic.class); + } else if (item instanceof CommonCodeDto.Basic dto) { + return dto; + } else { + throw new IllegalStateException("Unsupported cache type: " + item.getClass()); + } + }) + .toList(); } catch (Exception e) { log.error("공통코드 전체 조회 중 오류 발생", e); return List.of(); @@ -111,8 +131,24 @@ public class CommonCodeUtil { } try { - List allCodes = commonCodeService.getFindAll(); - return allCodes.stream() + // 캐시에 들어간 이후 꺼낼 때, DTO를 인식하지 못하여 LinkedHashMap으로 리턴됨. + // LinkedHashMap인 경우 변환처리, 아닌 경우는 그대로 리턴함. + List allCodes = commonCodeService.getFindAll(); + List convertList = + allCodes.stream() + .map( + item -> { + if (item instanceof LinkedHashMap map) { + return objectMapper.convertValue(map, CommonCodeDto.Basic.class); + } else if (item instanceof CommonCodeDto.Basic dto) { + return dto; + } else { + throw new IllegalStateException("Unsupported cache type: " + item.getClass()); + } + }) + .toList(); + + return convertList.stream() .filter(code -> parentCode.equals(code.getCode())) .findFirst() .map(Basic::getChildren) @@ -126,12 +162,12 @@ public class CommonCodeUtil { /** * 코드 사용 가능 여부 확인 * - * @param parentId 상위 코드 ID + * @param parentId 상위 코드 ID -> 1depth 는 null 허용 (파라미터 미필수로 변경) * @param code 확인할 코드값 * @return 사용 가능 여부 (true: 사용 가능, false: 중복 또는 오류) */ public boolean isCodeAvailable(Long parentId, String code) { - if (parentId == null || parentId <= 0 || code == null || code.isEmpty()) { + if (parentId <= 0 || code == null || code.isEmpty()) { log.warn("유효하지 않은 입력: parentId={}, code={}", parentId, code); return false; } From 2b38a317ba9c30121c4660a789c8ae589aa69b87 Mon Sep 17 00:00:00 2001 From: "gayoun.park" Date: Thu, 11 Dec 2025 11:29:41 +0900 Subject: [PATCH 05/11] =?UTF-8?q?=EA=B3=B5=ED=86=B5=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=AA=A8=EB=93=A0=20=EC=A1=B0=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?fetchJoin=20->=20leftJoin=EC=9C=BC=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kamco/cd/kamcoback/code/CommonCodeApiController.java | 1 + .../postgres/repository/code/CommonCodeRepositoryImpl.java | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java b/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java index 20324db6..eaa7f2d6 100644 --- a/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java @@ -209,6 +209,7 @@ public class CommonCodeApiController { return ApiResponseDto.ok(commonCodeService.findByCode(code)); } + @Operation(summary = "변화탐지 분류 코드 목록", description = "변화탐지 분류 코드 목록(공통코드 기반)") @GetMapping("/clazz") public ApiResponseDto> getClasses() { diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java index 4298eedd..c46e73e2 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java @@ -58,11 +58,11 @@ public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom { return queryFactory .selectFrom(commonCodeEntity) .leftJoin(commonCodeEntity.children, child) - .fetchJoin() + .on(child.deleted.isFalse().or(child.deleted.isNull())) .where( commonCodeEntity.parent.isNull(), - commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()), - child.deleted.isFalse().or(child.deleted.isNull())) + commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()) + ) .orderBy(commonCodeEntity.order.asc(), child.order.asc()) .fetch(); } From ed0159eddac7d42599badb3ef77430861a03d901 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 12:27:20 +0900 Subject: [PATCH 06/11] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=88=98=EC=A0=95,=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EA=B3=B5=ED=86=B5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/AuthFailureEventListener.java | 31 ----------- .../auth/CustomAuthenticationProvider.java | 30 +++++++++-- .../cd/kamcoback/common/enums/RoleType.java | 11 +++- .../cd/kamcoback/common/enums/StatusType.java | 25 +++++++++ .../common/enums/error/AuthErrorCode.java | 30 +++++++++++ .../common/exception/CustomApiException.java | 6 +++ .../cd/kamcoback/common/utils/ErrorCode.java | 10 ++++ .../cd/kamcoback/config/SecurityConfig.java | 32 ++++++------ .../kamcoback/config/api/ApiResponseDto.java | 21 +++++--- .../cd/kamcoback/config/enums/EnumType.java | 13 +++++ .../cd/kamcoback/members/AuthController.java | 52 +++++++++++++++---- .../members/MembersApiController.java | 2 +- .../cd/kamcoback/members/dto/MembersDto.java | 20 +++++++ .../members/service/AuthService.java | 21 -------- .../members/service/MembersService.java | 7 +-- .../postgres/core/MembersCoreService.java | 44 ++-------------- 16 files changed, 222 insertions(+), 133 deletions(-) delete mode 100644 src/main/java/com/kamco/cd/kamcoback/auth/AuthFailureEventListener.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/common/utils/ErrorCode.java diff --git a/src/main/java/com/kamco/cd/kamcoback/auth/AuthFailureEventListener.java b/src/main/java/com/kamco/cd/kamcoback/auth/AuthFailureEventListener.java deleted file mode 100644 index 75486d9e..00000000 --- a/src/main/java/com/kamco/cd/kamcoback/auth/AuthFailureEventListener.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.kamco.cd.kamcoback.auth; - -import com.kamco.cd.kamcoback.members.service.AuthService; -import java.util.UUID; -import lombok.RequiredArgsConstructor; -import org.springframework.context.ApplicationListener; -import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class AuthFailureEventListener implements ApplicationListener { - - private final AuthService authService; - - @Override - public void onApplicationEvent(AbstractAuthenticationFailureEvent event) { - // 로그인 시도에 사용된 (username) - Object principal = event.getAuthentication().getPrincipal(); - - if (principal instanceof String username) { - // 로그인 실패 카운트 증가 로직 호출 - authService.loginFail(UUID.fromString(username)); - } - } - - @Override - public boolean supportsAsyncExecution() { - return ApplicationListener.super.supportsAsyncExecution(); - } -} diff --git a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java index 8e6cb1bf..deeaf594 100644 --- a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java +++ b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java @@ -1,11 +1,13 @@ package com.kamco.cd.kamcoback.auth; +import com.kamco.cd.kamcoback.common.enums.error.AuthErrorCode; +import com.kamco.cd.kamcoback.common.exception.CustomApiException; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository; +import java.time.ZonedDateTime; import lombok.RequiredArgsConstructor; import org.mindrot.jbcrypt.BCrypt; import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -28,14 +30,34 @@ public class CustomAuthenticationProvider implements AuthenticationProvider { MemberEntity member = membersRepository .findByUserId(username) - .orElseThrow(() -> new BadCredentialsException("ID 또는 비밀번호가 일치하지 않습니다.")); + .orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND)); // 2. jBCrypt + 커스텀 salt 로 저장된 패스워드 비교 if (!BCrypt.checkpw(rawPassword, member.getPassword())) { - throw new BadCredentialsException("ID 또는 비밀번호가 일치하지 않습니다."); + // 실패 카운트 저장 + int cnt = member.getLoginFailCount() + 1; + if (cnt >= 5) { + member.setStatus("INACTIVE"); + } + member.setLoginFailCount(cnt); + membersRepository.save(member); + throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH); } - // 3. 인증 성공 → UserDetails 생성 + // 3. 패스워드 실패 횟수 체크 + if (member.getLoginFailCount() >= 5) { + throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED); + } + + // 4. 인증 성공 로그인 시간 저장 + if (member.getFirstLoginDttm() == null) { + member.setFirstLoginDttm(ZonedDateTime.now()); + } + member.setLastLoginDttm(ZonedDateTime.now()); + member.setLoginFailCount(0); + membersRepository.save(member); + + // 5. 인증 성공 → UserDetails 생성 CustomUserDetails userDetails = new CustomUserDetails(member); return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/RoleType.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/RoleType.java index 3da213ea..d1945c7e 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/enums/RoleType.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/RoleType.java @@ -7,7 +7,7 @@ import lombok.Getter; @Getter @AllArgsConstructor public enum RoleType implements EnumType { - ROLE_ADMIN("시스템 관리자"), + ROLE_ADMIN("관리자"), ROLE_LABELER("라벨러"), ROLE_REVIEWER("검수자"); @@ -22,4 +22,13 @@ public enum RoleType implements EnumType { public String getText() { return desc; } + + public static RoleType from(String value) { + for (RoleType type : values()) { + if (type.name().equalsIgnoreCase(value)) { + return type; + } + } + return null; + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java new file mode 100644 index 00000000..641d3de5 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/StatusType.java @@ -0,0 +1,25 @@ +package com.kamco.cd.kamcoback.common.enums; + +import com.kamco.cd.kamcoback.config.enums.EnumType; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum StatusType implements EnumType { + ACTIVE("활성"), + INACTIVE("비활성"), + ARCHIVED("탈퇴"); + + private final String desc; + + @Override + public String getId() { + return name(); + } + + @Override + public String getText() { + return desc; + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java new file mode 100644 index 00000000..3eca6ef1 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java @@ -0,0 +1,30 @@ +package com.kamco.cd.kamcoback.common.enums.error; + +import com.kamco.cd.kamcoback.common.utils.ErrorCode; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum AuthErrorCode implements ErrorCode { + + // 🔐 로그인 관련 + LOGIN_ID_NOT_FOUND( + "LOGIN_ID_NOT_FOUND", + HttpStatus.UNAUTHORIZED), + + LOGIN_PASSWORD_MISMATCH( + "LOGIN_PASSWORD_MISMATCH", + HttpStatus.UNAUTHORIZED), + + LOGIN_PASSWORD_EXCEEDED( + "LOGIN_PASSWORD_EXCEEDED", + HttpStatus.UNAUTHORIZED); + + private final String code; + private final HttpStatus status; + + AuthErrorCode(String code, HttpStatus status) { + this.code = code; + this.status = status; + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/common/exception/CustomApiException.java b/src/main/java/com/kamco/cd/kamcoback/common/exception/CustomApiException.java index 482d5f6c..cd3a3a1a 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/exception/CustomApiException.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/exception/CustomApiException.java @@ -1,5 +1,6 @@ package com.kamco.cd.kamcoback.common.exception; +import com.kamco.cd.kamcoback.common.utils.ErrorCode; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -19,4 +20,9 @@ public class CustomApiException extends RuntimeException { this.codeName = codeName; this.status = status; } + + public CustomApiException(ErrorCode errorCode) { + this.codeName = errorCode.getCode(); + this.status = errorCode.getStatus(); + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/common/utils/ErrorCode.java b/src/main/java/com/kamco/cd/kamcoback/common/utils/ErrorCode.java new file mode 100644 index 00000000..8ace8fe1 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/utils/ErrorCode.java @@ -0,0 +1,10 @@ +package com.kamco.cd.kamcoback.common.utils; + +import org.springframework.http.HttpStatus; + +public interface ErrorCode { + + String getCode(); + + HttpStatus getStatus(); +} diff --git a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java index a952e535..448fa31f 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java @@ -6,12 +6,14 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -40,21 +42,21 @@ public class SecurityConfig { customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용 .authorizeHttpRequests( auth -> - auth.anyRequest().permitAll()); -// requestMatchers(HttpMethod.OPTIONS, "/**") -// .permitAll() // preflight 허용 -// .requestMatchers( -// "/api/auth/signin", -// "/api/auth/refresh", -// "/swagger-ui/**", -// "/v3/api-docs/**") -// .permitAll() -// .anyRequest() -// .authenticated()) -// .addFilterBefore( -// jwtAuthenticationFilter, -// UsernamePasswordAuthenticationFilter -// .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. + auth.requestMatchers(HttpMethod.OPTIONS, "/**") + .permitAll() // preflight 허용 + .requestMatchers( + "/api/auth/signin", + "/api/auth/refresh", + "/swagger-ui/**", + "/api/members/{memberId}/password", + "/v3/api-docs/**") + .permitAll() + .anyRequest() + .authenticated()) + .addFilterBefore( + jwtAuthenticationFilter, + UsernamePasswordAuthenticationFilter + .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. ; return http.build(); diff --git a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java index 3cd2d792..de466c5d 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java @@ -20,9 +20,11 @@ public class ApiResponseDto { @JsonInclude(JsonInclude.Include.NON_NULL) private T errorData; - @JsonIgnore private HttpStatus httpStatus; + @JsonIgnore + private HttpStatus httpStatus; - @JsonIgnore private Long errorLogUid; + @JsonIgnore + private Long errorLogUid; public ApiResponseDto(T data) { this.data = data; @@ -47,7 +49,7 @@ public class ApiResponseDto { } public ApiResponseDto( - ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { + ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { this.error = new Error(code.getId(), message); this.httpStatus = httpStatus; this.errorLogUid = errorLogUid; @@ -88,17 +90,17 @@ public class ApiResponseDto { } public static ApiResponseDto createException( - ApiResponseCode code, String message, HttpStatus httpStatus) { + ApiResponseCode code, String message, HttpStatus httpStatus) { return new ApiResponseDto<>(code, message, httpStatus); } public static ApiResponseDto createException( - ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { + ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { return new ApiResponseDto<>(code, message, httpStatus, errorLogUid); } public static ApiResponseDto createException( - ApiResponseCode code, String message, T data) { + ApiResponseCode code, String message, T data) { return new ApiResponseDto<>(code, message, data); } @@ -114,7 +116,9 @@ public class ApiResponseDto { } } - /** Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object */ + /** + * Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object + */ @Getter public static class ResponseObj { @@ -164,6 +168,9 @@ public class ApiResponseDto { NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."), NOT_FOUND_USER("사용자를 찾을 수 없습니다."), UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."), + LOGIN_ID_NOT_FOUND("아이디를 잘못 입력하셨습니다."), + LOGIN_PASSWORD_MISMATCH("비밀번호를 잘못 입력하셨습니다."), + LOGIN_PASSWORD_EXCEEDED("비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.\n로그인 오류에 대해 관리자에게 문의하시기 바랍니다."), INVALID_EMAIL_TOKEN( "You can only reset your password within 24 hours from when the email was sent.\n" + "To reset your password again, please submit a new request through \"Forgot" diff --git a/src/main/java/com/kamco/cd/kamcoback/config/enums/EnumType.java b/src/main/java/com/kamco/cd/kamcoback/config/enums/EnumType.java index 274bfe72..1be49253 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/enums/EnumType.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/enums/EnumType.java @@ -5,4 +5,17 @@ public interface EnumType { String getId(); String getText(); + + // code로 text + static & EnumType> E fromId(Class enumClass, String id) { + if (id == null) { + return null; + } + for (E e : enumClass.getEnumConstants()) { + if (id.equalsIgnoreCase(e.getId())) { + return e; + } + } + return null; // 못 찾으면 null + } } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java index 47b76169..e948c14e 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -7,6 +7,7 @@ import com.kamco.cd.kamcoback.members.dto.SignInRequest; import com.kamco.cd.kamcoback.members.service.AuthService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -14,7 +15,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import java.nio.file.AccessDeniedException; import java.time.Duration; -import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; @@ -55,12 +55,43 @@ public class AuthController { content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( responseCode = "401", - description = "ID 또는 비밀번호 불일치", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))), - @ApiResponse( - responseCode = "400", - description = "미사용 상태", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + description = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)", + content = + @Content( + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "아이디 입력 오류", + description = "존재하지 않는 아이디", + value = """ + { + "code": "LOGIN_ID_NOT_FOUND", + "message": "아이디를 잘못 입력하셨습니다.", + "detail": null + } + """), + @ExampleObject( + name = "비밀번호 입력 오류 (4회 이하)", + description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우", + value = """ + { + "code": "LOGIN_PASSWORD_MISMATCH", + "message": "비밀번호를 잘못 입력하셨습니다.", + "detail": "비밀번호 입력 오류 3회, 2회 남았습니다." + } + """), + @ExampleObject( + name = "비밀번호 오류 횟수 초과", + description = "비밀번호 5회 이상 오류로 계정 잠김", + value = """ + { + "code": "LOGIN_PASSWORD_EXCEEDED", + "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.", + "detail": "로그인 오류에 대해 관리자에게 문의하시기 바랍니다." + } + """) + } + )) }) public ApiResponseDto signin( @io.swagger.v3.oas.annotations.parameters.RequestBody( @@ -75,15 +106,14 @@ public class AuthController { new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); String status = authService.getUserStatus(request); - if ("INACTIVE".equals(status)) { + + // INACTIVE 비활성 상태(새로운 패스워드 입력 해야함), ARCHIVED 탈퇴 + if (!"ACTIVE".equals(status)) { return ApiResponseDto.ok(new TokenResponse(status, null, null)); } String username = authentication.getName(); // UserDetailsService 에서 사용한 username - // 로그인 시간 저장 - authService.saveLogin(UUID.fromString(username)); - String accessToken = jwtTokenProvider.createAccessToken(username); String refreshToken = jwtTokenProvider.createRefreshToken(username); diff --git a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java index 6f63553c..633bf10e 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java @@ -64,7 +64,7 @@ public class MembersApiController { @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) }) @PatchMapping("/{memberId}/password") - public ApiResponseDto resetPassword(@PathVariable Long memberId, @RequestBody @Valid MembersDto.InitReq initReq) { + public ApiResponseDto resetPassword(@PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) { membersService.resetPassword(memberId, initReq); return ApiResponseDto.createOK(memberId); } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java index d28d39ca..3f5f0616 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java @@ -1,6 +1,9 @@ package com.kamco.cd.kamcoback.members.dto; +import com.kamco.cd.kamcoback.common.enums.RoleType; +import com.kamco.cd.kamcoback.common.enums.StatusType; import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm; +import com.kamco.cd.kamcoback.config.enums.EnumType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; @@ -22,11 +25,13 @@ public class MembersDto { private Long id; private UUID uuid; private String userRole; + private String userRoleName; private String name; private String userId; private String employeeNo; private String tempPassword; private String status; + private String statusName; @JsonFormatDttm private ZonedDateTime createdDttm; @JsonFormatDttm @@ -40,11 +45,13 @@ public class MembersDto { Long id, UUID uuid, String userRole, + String userRoleName, String name, String userId, String employeeNo, String tempPassword, String status, + String statusName, ZonedDateTime createdDttm, ZonedDateTime updatedDttm, ZonedDateTime firstLoginDttm, @@ -53,16 +60,29 @@ public class MembersDto { this.id = id; this.uuid = uuid; this.userRole = userRole; + this.userRoleName = getUserRoleName(userRole); this.name = name; this.userId = userId; this.employeeNo = employeeNo; this.tempPassword = tempPassword; this.status = status; + this.statusName = getStatusName(status); this.createdDttm = createdDttm; this.updatedDttm = updatedDttm; this.firstLoginDttm = firstLoginDttm; this.lastLoginDttm = lastLoginDttm; } + + private String getUserRoleName(String roleId) { + RoleType type = EnumType.fromId(RoleType.class, roleId); + return type.getText(); + } + + private String getStatusName(String status) { + StatusType type = EnumType.fromId(StatusType.class, status); + return type.getText(); + } + } @Getter diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java index 5433750b..1dcfc578 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/AuthService.java @@ -2,7 +2,6 @@ package com.kamco.cd.kamcoback.members.service; import com.kamco.cd.kamcoback.members.dto.SignInRequest; import com.kamco.cd.kamcoback.postgres.core.MembersCoreService; -import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,26 +13,6 @@ public class AuthService { private final MembersCoreService membersCoreService; - /** - * 로그인 일시 저장 - * - * @param uuid - */ - @Transactional - public void saveLogin(UUID uuid) { - membersCoreService.saveLogin(uuid); - } - - /** - * 로그인 실패 저장 - * - * @param uuid - */ - @Transactional - public void loginFail(UUID uuid) { - membersCoreService.loginFail(uuid); - } - /** * 사용자 상태 조회 * diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java index 945d4868..d68352a4 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java @@ -35,7 +35,7 @@ public class MembersService { * @param initReq */ @Transactional - public void resetPassword(Long id, MembersDto.InitReq initReq) { + public void resetPassword(String id, MembersDto.InitReq initReq) { if (!isValidPassword(initReq.getPassword())) { throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST); } @@ -49,7 +49,8 @@ public class MembersService { * @return */ private boolean isValidPassword(String password) { - String regex = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[!@#$]).{8,20}$"; - return Pattern.matches(regex, password); + String passwordPattern = + "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$"; + return Pattern.matches(passwordPattern, password); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java index d1a1b642..756e9f71 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java @@ -1,6 +1,8 @@ package com.kamco.cd.kamcoback.postgres.core; import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator; +import com.kamco.cd.kamcoback.common.enums.error.AuthErrorCode; +import com.kamco.cd.kamcoback.common.exception.CustomApiException; import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.MembersDto.AddReq; import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; @@ -8,7 +10,6 @@ import com.kamco.cd.kamcoback.members.dto.SignInRequest; import com.kamco.cd.kamcoback.members.exception.MemberException.DuplicateMemberException; import com.kamco.cd.kamcoback.members.exception.MemberException.DuplicateMemberException.Field; import com.kamco.cd.kamcoback.members.exception.MemberException.MemberNotFoundException; -import com.kamco.cd.kamcoback.members.exception.MemberException.PasswordNotFoundException; import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository; import java.time.ZonedDateTime; @@ -99,12 +100,12 @@ public class MembersCoreService { * * @param id */ - public void resetPassword(Long id, MembersDto.InitReq initReq) { + public void resetPassword(String id, MembersDto.InitReq initReq) { MemberEntity memberEntity = - membersRepository.findById(id).orElseThrow(() -> new MemberNotFoundException()); + membersRepository.findByUserId(id).orElseThrow(() -> new MemberNotFoundException()); if (!memberEntity.getTempPassword().equals(initReq.getTempPassword())) { - throw new PasswordNotFoundException(); + throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH); } String salt = @@ -129,41 +130,6 @@ public class MembersCoreService { return membersRepository.findByMembers(searchReq); } - /** - * 최초 로그인 저장 마지막 로그인 저장 - * - * @param uuid - */ - public void saveLogin(UUID uuid) { - MemberEntity memberEntity = - membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); - - if (memberEntity.getFirstLoginDttm() == null) { - memberEntity.setFirstLoginDttm(ZonedDateTime.now()); - } - memberEntity.setLastLoginDttm(ZonedDateTime.now()); - memberEntity.setLoginFailCount(0); - membersRepository.save(memberEntity); - } - - /** - * 로그인 실패시 상태 저장 - * - * @param uuid - */ - public void loginFail(UUID uuid) { - MemberEntity memberEntity = - membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); - - int failCnt = memberEntity.getLoginFailCount() + 1; - if (failCnt >= 5) { - memberEntity.setStatus("INACTIVE"); - } - - memberEntity.setLoginFailCount(failCnt); - membersRepository.save(memberEntity); - } - /** * 사용자 상태 조회 * From ec19cf533acc84c30c5f06cdf3ebe19cac8fd9af Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 13:47:18 +0900 Subject: [PATCH 07/11] =?UTF-8?q?=ED=8C=A8=EC=8A=A4=EC=9B=8C=EB=93=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/kamco/cd/kamcoback/members/AuthController.java | 9 +++------ .../kamco/cd/kamcoback/members/MembersApiController.java | 7 +++++++ .../cd/kamcoback/members/service/MembersService.java | 1 + .../cd/kamcoback/postgres/core/MembersCoreService.java | 6 ------ 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java index e948c14e..9a8d2703 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -66,8 +66,7 @@ public class AuthController { value = """ { "code": "LOGIN_ID_NOT_FOUND", - "message": "아이디를 잘못 입력하셨습니다.", - "detail": null + "message": "아이디를 잘못 입력하셨습니다." } """), @ExampleObject( @@ -76,8 +75,7 @@ public class AuthController { value = """ { "code": "LOGIN_PASSWORD_MISMATCH", - "message": "비밀번호를 잘못 입력하셨습니다.", - "detail": "비밀번호 입력 오류 3회, 2회 남았습니다." + "message": "비밀번호를 잘못 입력하셨습니다." } """), @ExampleObject( @@ -86,8 +84,7 @@ public class AuthController { value = """ { "code": "LOGIN_PASSWORD_EXCEEDED", - "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.", - "detail": "로그인 오류에 대해 관리자에게 문의하시기 바랍니다." + "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다." } """) } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java index 633bf10e..0c146f8a 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java @@ -14,6 +14,8 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Page; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -27,6 +29,7 @@ import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor public class MembersApiController { + private final AuthenticationManager authenticationManager; private final MembersService membersService; @Operation(summary = "회원정보 목록", description = "회원정보 조회") @@ -65,6 +68,10 @@ public class MembersApiController { }) @PatchMapping("/{memberId}/password") public ApiResponseDto resetPassword(@PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) { + + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(memberId, initReq.getTempPassword())); + membersService.resetPassword(memberId, initReq); return ApiResponseDto.createOK(memberId); } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java index d68352a4..3f63f430 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java @@ -36,6 +36,7 @@ public class MembersService { */ @Transactional public void resetPassword(String id, MembersDto.InitReq initReq) { + if (!isValidPassword(initReq.getPassword())) { throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java index 756e9f71..fc39f559 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java @@ -1,8 +1,6 @@ package com.kamco.cd.kamcoback.postgres.core; import com.kamco.cd.kamcoback.auth.BCryptSaltGenerator; -import com.kamco.cd.kamcoback.common.enums.error.AuthErrorCode; -import com.kamco.cd.kamcoback.common.exception.CustomApiException; import com.kamco.cd.kamcoback.members.dto.MembersDto; import com.kamco.cd.kamcoback.members.dto.MembersDto.AddReq; import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic; @@ -104,10 +102,6 @@ public class MembersCoreService { MemberEntity memberEntity = membersRepository.findByUserId(id).orElseThrow(() -> new MemberNotFoundException()); - if (!memberEntity.getTempPassword().equals(initReq.getTempPassword())) { - throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH); - } - String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); // 패스워드 암호화 From d798dc16f9eed903aade91083e00646c3aef1d0a Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 13:47:57 +0900 Subject: [PATCH 08/11] =?UTF-8?q?spotlessApply=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/CustomAuthenticationProvider.java | 6 +- .../code/CommonCodeApiController.java | 2 +- .../common/enums/error/AuthErrorCode.java | 12 +- .../cd/kamcoback/config/SecurityConfig.java | 56 ++++---- .../kamcoback/config/api/ApiResponseDto.java | 18 +-- .../kamcoback/members/AdminApiController.java | 112 +++++++-------- .../cd/kamcoback/members/AuthController.java | 136 +++++++++--------- .../members/MembersApiController.java | 54 +++---- .../cd/kamcoback/members/dto/MembersDto.java | 48 +++---- .../members/service/AdminService.java | 1 - .../members/service/MembersService.java | 2 +- .../postgres/core/MembersCoreService.java | 16 ++- .../code/CommonCodeRepositoryImpl.java | 3 +- .../members/MembersRepositoryImpl.java | 74 +++++----- 14 files changed, 261 insertions(+), 279 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java index deeaf594..dd898258 100644 --- a/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java +++ b/src/main/java/com/kamco/cd/kamcoback/auth/CustomAuthenticationProvider.java @@ -28,9 +28,9 @@ public class CustomAuthenticationProvider implements AuthenticationProvider { // 1. 유저 조회 MemberEntity member = - membersRepository - .findByUserId(username) - .orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND)); + membersRepository + .findByUserId(username) + .orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND)); // 2. jBCrypt + 커스텀 salt 로 저장된 패스워드 비교 if (!BCrypt.checkpw(rawPassword, member.getPassword())) { diff --git a/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java b/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java index eaa7f2d6..c3ff3942 100644 --- a/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/code/CommonCodeApiController.java @@ -219,7 +219,7 @@ public class CommonCodeApiController { // .map(Clazzes::new) // .toList(); - //변화탐지 clazz API : enum -> 공통코드로 변경 + // 변화탐지 clazz API : enum -> 공통코드로 변경 List list = commonCodeUtil.getChildCodesByParentCode("0000").stream() .map( diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java index 3eca6ef1..91956284 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/error/AuthErrorCode.java @@ -8,17 +8,11 @@ import org.springframework.http.HttpStatus; public enum AuthErrorCode implements ErrorCode { // 🔐 로그인 관련 - LOGIN_ID_NOT_FOUND( - "LOGIN_ID_NOT_FOUND", - HttpStatus.UNAUTHORIZED), + LOGIN_ID_NOT_FOUND("LOGIN_ID_NOT_FOUND", HttpStatus.UNAUTHORIZED), - LOGIN_PASSWORD_MISMATCH( - "LOGIN_PASSWORD_MISMATCH", - HttpStatus.UNAUTHORIZED), + LOGIN_PASSWORD_MISMATCH("LOGIN_PASSWORD_MISMATCH", HttpStatus.UNAUTHORIZED), - LOGIN_PASSWORD_EXCEEDED( - "LOGIN_PASSWORD_EXCEEDED", - HttpStatus.UNAUTHORIZED); + LOGIN_PASSWORD_EXCEEDED("LOGIN_PASSWORD_EXCEEDED", HttpStatus.UNAUTHORIZED); private final String code; private final HttpStatus status; diff --git a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java index 448fa31f..2fecb1cd 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/SecurityConfig.java @@ -30,33 +30,33 @@ public class SecurityConfig { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.cors(cors -> cors.configurationSource(corsConfigurationSource())) - .csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화 - .sessionManagement( - sm -> - sm.sessionCreationPolicy( - SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음, 요청은 JWT 인증 - .formLogin(form -> form.disable()) // react에서 로그인 요청 관리 - .httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용 - .logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음 - .authenticationProvider( - customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용 - .authorizeHttpRequests( - auth -> - auth.requestMatchers(HttpMethod.OPTIONS, "/**") - .permitAll() // preflight 허용 - .requestMatchers( - "/api/auth/signin", - "/api/auth/refresh", - "/swagger-ui/**", - "/api/members/{memberId}/password", - "/v3/api-docs/**") - .permitAll() - .anyRequest() - .authenticated()) - .addFilterBefore( - jwtAuthenticationFilter, - UsernamePasswordAuthenticationFilter - .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. + .csrf(csrf -> csrf.disable()) // CSRF 보안 기능 비활성화 + .sessionManagement( + sm -> + sm.sessionCreationPolicy( + SessionCreationPolicy.STATELESS)) // 서버 세션 만들지 않음, 요청은 JWT 인증 + .formLogin(form -> form.disable()) // react에서 로그인 요청 관리 + .httpBasic(basic -> basic.disable()) // 기본 basic 인증 비활성화 JWT 인증사용 + .logout(logout -> logout.disable()) // 기본 로그아웃 비활성화 JWT는 서버 상태가 없으므로 로그아웃 처리 필요 없음 + .authenticationProvider( + customAuthenticationProvider) // 로그인 패스워드 비교방식 스프링 기본 Provider 사용안함 커스텀 사용 + .authorizeHttpRequests( + auth -> + auth.requestMatchers(HttpMethod.OPTIONS, "/**") + .permitAll() // preflight 허용 + .requestMatchers( + "/api/auth/signin", + "/api/auth/refresh", + "/swagger-ui/**", + "/api/members/{memberId}/password", + "/v3/api-docs/**") + .permitAll() + .anyRequest() + .authenticated()) + .addFilterBefore( + jwtAuthenticationFilter, + UsernamePasswordAuthenticationFilter + .class) // 요청 들어오면 먼저 JWT 토큰 검사 후 security context 에 사용자 정보 저장. ; return http.build(); @@ -64,7 +64,7 @@ public class SecurityConfig { @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) - throws Exception { + throws Exception { return configuration.getAuthenticationManager(); } diff --git a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java index de466c5d..969fde31 100644 --- a/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/config/api/ApiResponseDto.java @@ -20,11 +20,9 @@ public class ApiResponseDto { @JsonInclude(JsonInclude.Include.NON_NULL) private T errorData; - @JsonIgnore - private HttpStatus httpStatus; + @JsonIgnore private HttpStatus httpStatus; - @JsonIgnore - private Long errorLogUid; + @JsonIgnore private Long errorLogUid; public ApiResponseDto(T data) { this.data = data; @@ -49,7 +47,7 @@ public class ApiResponseDto { } public ApiResponseDto( - ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { + ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { this.error = new Error(code.getId(), message); this.httpStatus = httpStatus; this.errorLogUid = errorLogUid; @@ -90,17 +88,17 @@ public class ApiResponseDto { } public static ApiResponseDto createException( - ApiResponseCode code, String message, HttpStatus httpStatus) { + ApiResponseCode code, String message, HttpStatus httpStatus) { return new ApiResponseDto<>(code, message, httpStatus); } public static ApiResponseDto createException( - ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { + ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) { return new ApiResponseDto<>(code, message, httpStatus, errorLogUid); } public static ApiResponseDto createException( - ApiResponseCode code, String message, T data) { + ApiResponseCode code, String message, T data) { return new ApiResponseDto<>(code, message, data); } @@ -116,9 +114,7 @@ public class ApiResponseDto { } } - /** - * Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object - */ + /** Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object */ @Getter public static class ResponseObj { diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java index 4ed55cda..33afc12c 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AdminApiController.java @@ -30,78 +30,78 @@ public class AdminApiController { @Operation(summary = "관리자 계정 등록", description = "관리자 계정 등록") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "등록 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "등록 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PostMapping("/join") public ApiResponseDto saveMember( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "관리자 계정 등록", - required = true, - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = MembersDto.AddReq.class))) - @RequestBody - @Valid - MembersDto.AddReq addReq) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "관리자 계정 등록", + required = true, + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = MembersDto.AddReq.class))) + @RequestBody + @Valid + MembersDto.AddReq addReq) { return ApiResponseDto.createOK(adminService.saveMember(addReq)); } @Operation(summary = "관리자 계정 수정", description = "관리자 계정 수정") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "수정 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "수정 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PutMapping("/{uuid}") public ApiResponseDto updateMembers( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "관리자 계정 수정", - required = true, - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = MembersDto.UpdateReq.class))) - @PathVariable - UUID uuid, - @RequestBody @Valid MembersDto.UpdateReq updateReq) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "관리자 계정 수정", + required = true, + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = MembersDto.UpdateReq.class))) + @PathVariable + UUID uuid, + @RequestBody @Valid MembersDto.UpdateReq updateReq) { adminService.updateMembers(uuid, updateReq); return ApiResponseDto.createOK(UUID.randomUUID()); } @Operation(summary = "관리자 계정 미사용 처리", description = "관리자 계정 미사용 처리") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "관리자 계정 미사용 처리", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = UUID.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "관리자 계정 미사용 처리", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = UUID.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @DeleteMapping("/delete/{uuid}") public ApiResponseDto deleteAccount(@PathVariable UUID uuid) { adminService.deleteAccount(uuid); diff --git a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java index 9a8d2703..a0d7ed01 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/AuthController.java @@ -50,57 +50,59 @@ public class AuthController { @Operation(summary = "로그인", description = "사번으로 로그인하여 액세스/리프레시 토큰을 발급.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "로그인 성공", - content = @Content(schema = @Schema(implementation = TokenResponse.class))), + responseCode = "200", + description = "로그인 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( - responseCode = "401", - description = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)", - content = - @Content( - schema = @Schema(implementation = ErrorResponse.class), - examples = { - @ExampleObject( - name = "아이디 입력 오류", - description = "존재하지 않는 아이디", - value = """ + responseCode = "401", + description = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)", + content = + @Content( + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "아이디 입력 오류", + description = "존재하지 않는 아이디", + value = + """ { "code": "LOGIN_ID_NOT_FOUND", "message": "아이디를 잘못 입력하셨습니다." } """), - @ExampleObject( - name = "비밀번호 입력 오류 (4회 이하)", - description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우", - value = """ + @ExampleObject( + name = "비밀번호 입력 오류 (4회 이하)", + description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우", + value = + """ { "code": "LOGIN_PASSWORD_MISMATCH", "message": "비밀번호를 잘못 입력하셨습니다." } """), - @ExampleObject( - name = "비밀번호 오류 횟수 초과", - description = "비밀번호 5회 이상 오류로 계정 잠김", - value = """ + @ExampleObject( + name = "비밀번호 오류 횟수 초과", + description = "비밀번호 5회 이상 오류로 계정 잠김", + value = + """ { "code": "LOGIN_PASSWORD_EXCEEDED", "message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다." } """) - } - )) + })) }) public ApiResponseDto signin( - @io.swagger.v3.oas.annotations.parameters.RequestBody( - description = "로그인 요청 정보", - required = true) - @RequestBody - SignInRequest request, - HttpServletResponse response) { + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "로그인 요청 정보", + required = true) + @RequestBody + SignInRequest request, + HttpServletResponse response) { Authentication authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())); String status = authService.getUserStatus(request); @@ -116,17 +118,17 @@ public class AuthController { // Redis에 RefreshToken 저장 (TTL = 7일) refreshTokenService.save( - username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); + username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); // HttpOnly + Secure 쿠키에 RefreshToken 저장 ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, refreshToken) - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, refreshToken) + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); @@ -137,16 +139,16 @@ public class AuthController { @Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "재발급 성공", - content = @Content(schema = @Schema(implementation = TokenResponse.class))), + responseCode = "200", + description = "재발급 성공", + content = @Content(schema = @Schema(implementation = TokenResponse.class))), @ApiResponse( - responseCode = "401", - description = "만료되었거나 유효하지 않은 리프레시 토큰", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + responseCode = "401", + description = "만료되었거나 유효하지 않은 리프레시 토큰", + content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) public ResponseEntity refresh(String refreshToken, HttpServletResponse response) - throws AccessDeniedException { + throws AccessDeniedException { if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) { throw new AccessDeniedException("만료되었거나 유효하지 않은 리프레시 토큰 입니다."); } @@ -164,17 +166,17 @@ public class AuthController { // Redis 갱신 refreshTokenService.save( - username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); + username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs()); // 쿠키 갱신 ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, newRefreshToken) - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, newRefreshToken) + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs())) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken)); @@ -184,12 +186,12 @@ public class AuthController { @Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.") @ApiResponses({ @ApiResponse( - responseCode = "200", - description = "로그아웃 성공", - content = @Content(schema = @Schema(implementation = Void.class))) + responseCode = "200", + description = "로그아웃 성공", + content = @Content(schema = @Schema(implementation = Void.class))) }) public ApiResponseDto> logout( - Authentication authentication, HttpServletResponse response) { + Authentication authentication, HttpServletResponse response) { if (authentication != null) { String username = authentication.getName(); // Redis에서 RefreshToken 삭제 @@ -198,19 +200,17 @@ public class AuthController { // 쿠키 삭제 (Max-Age=0) ResponseCookie cookie = - ResponseCookie.from(refreshCookieName, "") - .httpOnly(true) - .secure(refreshCookieSecure) - .path("/") - .maxAge(0) - .sameSite("Strict") - .build(); + ResponseCookie.from(refreshCookieName, "") + .httpOnly(true) + .secure(refreshCookieSecure) + .path("/") + .maxAge(0) + .sameSite("Strict") + .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); return ApiResponseDto.createOK(ResponseEntity.noContent().build()); } - public record TokenResponse(String status, String accessToken, String refreshToken) { - - } + public record TokenResponse(String status, String accessToken, String refreshToken) {} } diff --git a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java index 0c146f8a..8c7a3560 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/MembersApiController.java @@ -34,43 +34,43 @@ public class MembersApiController { @Operation(summary = "회원정보 목록", description = "회원정보 조회") @ApiResponses( - value = { - @ApiResponse( - responseCode = "200", - description = "검색 성공", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Page.class))), - @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "200", + description = "검색 성공", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Page.class))), + @ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @GetMapping public ApiResponseDto> getMemberList( - @ParameterObject MembersDto.SearchReq searchReq) { + @ParameterObject MembersDto.SearchReq searchReq) { return ApiResponseDto.ok(membersService.findByMembers(searchReq)); } - @Operation(summary = "사용자 비밀번호 변경", description = "사용자 비밀번호 변경") @ApiResponses( - value = { - @ApiResponse( - responseCode = "201", - description = "사용자 비밀번호 변경", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = Long.class))), - @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), - @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), - @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) - }) + value = { + @ApiResponse( + responseCode = "201", + description = "사용자 비밀번호 변경", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = Long.class))), + @ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content), + @ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) @PatchMapping("/{memberId}/password") - public ApiResponseDto resetPassword(@PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) { + public ApiResponseDto resetPassword( + @PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) { authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(memberId, initReq.getTempPassword())); + new UsernamePasswordAuthenticationToken(memberId, initReq.getTempPassword())); membersService.resetPassword(memberId, initReq); return ApiResponseDto.createOK(memberId); diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java index 3f5f0616..b8d1cf04 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/MembersDto.java @@ -32,31 +32,26 @@ public class MembersDto { private String tempPassword; private String status; private String statusName; - @JsonFormatDttm - private ZonedDateTime createdDttm; - @JsonFormatDttm - private ZonedDateTime updatedDttm; - @JsonFormatDttm - private ZonedDateTime firstLoginDttm; - @JsonFormatDttm - private ZonedDateTime lastLoginDttm; + @JsonFormatDttm private ZonedDateTime createdDttm; + @JsonFormatDttm private ZonedDateTime updatedDttm; + @JsonFormatDttm private ZonedDateTime firstLoginDttm; + @JsonFormatDttm private ZonedDateTime lastLoginDttm; public Basic( - Long id, - UUID uuid, - String userRole, - String userRoleName, - String name, - String userId, - String employeeNo, - String tempPassword, - String status, - String statusName, - ZonedDateTime createdDttm, - ZonedDateTime updatedDttm, - ZonedDateTime firstLoginDttm, - ZonedDateTime lastLoginDttm - ) { + Long id, + UUID uuid, + String userRole, + String userRoleName, + String name, + String userId, + String employeeNo, + String tempPassword, + String status, + String statusName, + ZonedDateTime createdDttm, + ZonedDateTime updatedDttm, + ZonedDateTime firstLoginDttm, + ZonedDateTime lastLoginDttm) { this.id = id; this.uuid = uuid; this.userRole = userRole; @@ -82,7 +77,6 @@ public class MembersDto { StatusType type = EnumType.fromId(StatusType.class, status); return type.getText(); } - } @Getter @@ -91,7 +85,9 @@ public class MembersDto { @AllArgsConstructor public static class SearchReq { - @Schema(description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)", example = "") + @Schema( + description = "전체, 관리자(ROLE_ADMIN), 라벨러(ROLE_LABELER), 검수자(ROLE_REVIEWER)", + example = "") private String userRole; @Schema(description = "키워드", example = "홍길동") @@ -135,7 +131,7 @@ public class MembersDto { private String employeeNo; public AddReq( - String userRole, String name, String userId, String tempPassword, String employeeNo) { + String userRole, String name, String userId, String tempPassword, String employeeNo) { this.userRole = userRole; this.name = name; this.userId = userId; diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java index da6905d7..dd6f5621 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/AdminService.java @@ -36,7 +36,6 @@ public class AdminService { membersCoreService.updateMembers(uuid, updateReq); } - /** * 관리자 계정 미사용 처리 * diff --git a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java index 3f63f430..5cbd1b79 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/service/MembersService.java @@ -51,7 +51,7 @@ public class MembersService { */ private boolean isValidPassword(String password) { String passwordPattern = - "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$"; + "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$"; return Pattern.matches(passwordPattern, password); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java index fc39f559..6f1854d7 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/MembersCoreService.java @@ -59,7 +59,7 @@ public class MembersCoreService { */ public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) { MemberEntity memberEntity = - membersRepository.findByUUID(uuid).orElseThrow(MemberNotFoundException::new); + membersRepository.findByUUID(uuid).orElseThrow(MemberNotFoundException::new); if (StringUtils.isNotBlank(updateReq.getName())) { memberEntity.setName(updateReq.getName()); @@ -77,7 +77,6 @@ public class MembersCoreService { membersRepository.save(memberEntity); } - /** * 관리자 계정 미사용 처리 * @@ -85,14 +84,13 @@ public class MembersCoreService { */ public void deleteAccount(UUID uuid) { MemberEntity memberEntity = - membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); + membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException()); memberEntity.setStatus("INACTIVE"); memberEntity.setUpdatedDttm(ZonedDateTime.now()); membersRepository.save(memberEntity); } - /** * 패스워드 변경 * @@ -100,10 +98,10 @@ public class MembersCoreService { */ public void resetPassword(String id, MembersDto.InitReq initReq) { MemberEntity memberEntity = - membersRepository.findByUserId(id).orElseThrow(() -> new MemberNotFoundException()); + membersRepository.findByUserId(id).orElseThrow(() -> new MemberNotFoundException()); String salt = - BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); + BCryptSaltGenerator.generateSaltWithEmployeeNo(memberEntity.getEmployeeNo().trim()); // 패스워드 암호화 String hashedPassword = BCrypt.hashpw(initReq.getPassword(), salt); @@ -112,6 +110,7 @@ public class MembersCoreService { memberEntity.setUpdatedDttm(ZonedDateTime.now()); membersRepository.save(memberEntity); } + // /** @@ -131,7 +130,10 @@ public class MembersCoreService { * @return */ public String getUserStatus(SignInRequest request) { - MemberEntity memberEntity = membersRepository.findByUserId(request.getUsername()).orElseThrow(MemberNotFoundException::new); + MemberEntity memberEntity = + membersRepository + .findByUserId(request.getUsername()) + .orElseThrow(MemberNotFoundException::new); return memberEntity.getStatus(); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java index c46e73e2..286b85bc 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/code/CommonCodeRepositoryImpl.java @@ -61,8 +61,7 @@ public class CommonCodeRepositoryImpl implements CommonCodeRepositoryCustom { .on(child.deleted.isFalse().or(child.deleted.isNull())) .where( commonCodeEntity.parent.isNull(), - commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull()) - ) + commonCodeEntity.deleted.isFalse().or(commonCodeEntity.deleted.isNull())) .orderBy(commonCodeEntity.order.asc(), child.order.asc()) .fetch(); } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java index 7e84baec..989bdd7d 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/members/MembersRepositoryImpl.java @@ -33,11 +33,11 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { @Override public boolean existsByUserId(String userId) { return queryFactory - .selectOne() - .from(memberEntity) - .where(memberEntity.userId.eq(userId)) - .fetchFirst() - != null; + .selectOne() + .from(memberEntity) + .where(memberEntity.userId.eq(userId)) + .fetchFirst() + != null; } /** @@ -49,7 +49,7 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { @Override public Optional findByUserId(String userId) { return Optional.ofNullable( - queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne()); + queryFactory.selectFrom(memberEntity).where(memberEntity.userId.eq(userId)).fetchOne()); } /** @@ -68,10 +68,11 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { String contains = "%" + searchReq.getKeyword() + "%"; builder.and( - memberEntity.name.likeIgnoreCase(contains) - .or(memberEntity.userId.likeIgnoreCase(contains)) - .or(memberEntity.employeeNo.likeIgnoreCase(contains)) - ); + memberEntity + .name + .likeIgnoreCase(contains) + .or(memberEntity.userId.likeIgnoreCase(contains)) + .or(memberEntity.employeeNo.likeIgnoreCase(contains))); } // 권한 @@ -80,35 +81,30 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { } List content = - queryFactory - .select( - Projections.constructor( - MembersDto.Basic.class, - memberEntity.id, - memberEntity.uuid, - memberEntity.userRole, - memberEntity.name, - memberEntity.userId, - memberEntity.employeeNo, - memberEntity.tempPassword, - memberEntity.status, - memberEntity.createdDttm, - memberEntity.updatedDttm, - memberEntity.firstLoginDttm, - memberEntity.lastLoginDttm - )) - .from(memberEntity) - .where(builder) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .orderBy(memberEntity.createdDttm.desc()) - .fetch(); + queryFactory + .select( + Projections.constructor( + MembersDto.Basic.class, + memberEntity.id, + memberEntity.uuid, + memberEntity.userRole, + memberEntity.name, + memberEntity.userId, + memberEntity.employeeNo, + memberEntity.tempPassword, + memberEntity.status, + memberEntity.createdDttm, + memberEntity.updatedDttm, + memberEntity.firstLoginDttm, + memberEntity.lastLoginDttm)) + .from(memberEntity) + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(memberEntity.createdDttm.desc()) + .fetch(); - long total = - queryFactory - .select(memberEntity) - .from(memberEntity) - .fetchCount(); + long total = queryFactory.select(memberEntity).from(memberEntity).fetchCount(); return new PageImpl<>(content, pageable, total); } @@ -122,6 +118,6 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom { @Override public Optional findByUUID(UUID uuid) { return Optional.ofNullable( - queryFactory.selectFrom(memberEntity).where(memberEntity.uuid.eq(uuid)).fetchOne()); + queryFactory.selectFrom(memberEntity).where(memberEntity.uuid.eq(uuid)).fetchOne()); } } From 0b5976cb94ed722ab3b1ec5bad9e12b7d12778b6 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 13:51:24 +0900 Subject: [PATCH 09/11] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=98=88?= =?UTF-8?q?=EC=A0=9C=20=EC=A0=95=EB=B3=B4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/kamco/cd/kamcoback/members/dto/SignInRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java index a13992b3..b720f9dd 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java @@ -11,10 +11,10 @@ import lombok.ToString; @ToString(exclude = "password") public class SignInRequest { - @Schema(description = "사용자 ID", example = "admin") + @Schema(description = "사용자 ID", example = "admin2") private String username; - @Schema(description = "비밀번호", example = "kamco1234!") + @Schema(description = "비밀번호", example = "Admin2!@1#") @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String password; } From 8ff0ae376da6ac5632bb2b7aade349edee177ac3 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 13:58:07 +0900 Subject: [PATCH 10/11] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=98=88?= =?UTF-8?q?=EC=A0=9C=20=EC=A0=95=EB=B3=B4=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java b/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java index b720f9dd..1ad47961 100644 --- a/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java +++ b/src/main/java/com/kamco/cd/kamcoback/members/dto/SignInRequest.java @@ -14,7 +14,7 @@ public class SignInRequest { @Schema(description = "사용자 ID", example = "admin2") private String username; - @Schema(description = "비밀번호", example = "Admin2!@1#") + @Schema(description = "비밀번호", example = "Admin2!@#") @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String password; } From 507819b2e67eaf2eb22af8db543269cea8b36612 Mon Sep 17 00:00:00 2001 From: teddy Date: Thu, 11 Dec 2025 14:05:26 +0900 Subject: [PATCH 11/11] =?UTF-8?q?spotlessApply=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cd/kamcoback/common/utils/UserUtil.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java diff --git a/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java b/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java new file mode 100644 index 00000000..a96da1a3 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/utils/UserUtil.java @@ -0,0 +1,54 @@ +package com.kamco.cd.kamcoback.common.utils; + +import com.kamco.cd.kamcoback.auth.CustomUserDetails; +import com.kamco.cd.kamcoback.auth.JwtTokenProvider; +import com.kamco.cd.kamcoback.postgres.entity.MemberEntity; +import java.util.Optional; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class UserUtil { + + private final JwtTokenProvider jwtTokenProvider; + + /** + * 현재 SecurityContext의 Authentication 에서 사용자 ID(Long) 가져오기 - 로그인 된 상태(AccessToken 인증 이후)에서만 사용 가능 + * - 인증 정보 없으면 null 리턴 + */ + public Long getCurrentUserId() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .filter(auth -> auth.getPrincipal() instanceof CustomUserDetails) + .map(auth -> ((CustomUserDetails) auth.getPrincipal()).getMember().getId()) + .orElse(null); + } + + /** 현재 SecurityContext의 Authentication 에서 사용자 UUID 가져오기 - 인증 정보 없으면 null 리턴 */ + public UUID getCurrentUserUuid() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .filter(auth -> auth.getPrincipal() instanceof CustomUserDetails) + .map(auth -> ((CustomUserDetails) auth.getPrincipal()).getMember().getUuid()) + .orElse(null); + } + + /** 현재 로그인한 사용자의 MemberEntity 통째로 가져오기 (Optional) */ + public Optional getCurrentMember() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()) + .filter(auth -> auth.getPrincipal() instanceof CustomUserDetails) + .map(auth -> ((CustomUserDetails) auth.getPrincipal()).getMember()); + } + + /** 현재 로그인한 사용자의 MemberEntity 가져오기 (없으면 예외) */ + public MemberEntity getCurrentMemberOrThrow() { + return getCurrentMember().orElseThrow(() -> new IllegalStateException("인증된 사용자를 찾을 수 없습니다.")); + // 필요하면 여기서 CustomApiException 으로 바꿔도 됨 + } + + /** AccessToken / RefreshToken 에서 sub(subject, uuid) 추출 - 토큰 문자열만 있을 때 uuid 가져올 때 사용 */ + public String getSubjectFromToken(String token) { + return jwtTokenProvider.getSubject(token); + } +}