jwt 소스 추가

This commit is contained in:
2025-12-03 18:47:45 +09:00
parent c3c484442e
commit 7884416e75
33 changed files with 738 additions and 681 deletions

View File

@@ -64,6 +64,15 @@ dependencies {
// crypto
implementation 'org.mindrot:jbcrypt:0.4'
// security
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
// JWT (jjwt 0.12.x)
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5' // JSON (Jackson)
}
tasks.named('test') {

View File

@@ -1,151 +0,0 @@
package com.kamco.cd.kamcoback.auth;
import com.kamco.cd.kamcoback.auth.dto.AuthDto;
import com.kamco.cd.kamcoback.auth.dto.AuthDto.Basic;
import com.kamco.cd.kamcoback.auth.service.AuthService;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "관리자 관리", description = "관리자 관리 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class AuthApiController {
private final AuthService authService;
@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)
})
@PostMapping("/save")
public ApiResponseDto<Long> save(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "관리자 정보",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = AuthDto.SaveReq.class)))
@RequestBody
@Valid
AuthDto.SaveReq saveReq) {
return ApiResponseDto.createOK(authService.save(saveReq).getId());
}
@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)
})
@PutMapping("/update/{id}")
public ApiResponseDto<Long> update(@PathVariable Long id, @RequestBody AuthDto.SaveReq saveReq) {
return ApiResponseDto.createOK(authService.update(id, saveReq).getId());
}
@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)
})
@PutMapping("/withdrawal/{id}")
public ApiResponseDto<Long> withdrawal(@PathVariable Long id) {
return ApiResponseDto.deleteOk(authService.withdrawal(id).getId());
}
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = AuthDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@Operation(summary = "관리자 상세조회", description = "관리자 정보를 조회 합니다.")
@GetMapping("/detail")
public ApiResponseDto<AuthDto.Basic> getDetail(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "관리자 목록 id",
required = true)
@RequestParam
Long id) {
return ApiResponseDto.ok(authService.getFindUserById(id));
}
@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)
})
@GetMapping("/list")
public ApiResponseDto<Page<Basic>> getUserList(
@Parameter(description = "관리자 이름") @RequestParam(required = false) String userNm,
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
int page,
@Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20")
int size,
@Parameter(description = "정렬 조건 (형식: 필드명,방향)", example = "name,asc")
@RequestParam(required = false)
String sort) {
AuthDto.SearchReq searchReq = new AuthDto.SearchReq(userNm, page, size, sort);
Page<AuthDto.Basic> userList = authService.getUserList(searchReq);
return ApiResponseDto.ok(userList);
}
}

View File

@@ -0,0 +1,46 @@
package com.kamco.cd.kamcoback.auth;
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository;
import lombok.RequiredArgsConstructor;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final MembersRepository membersRepository;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String rawPassword = authentication.getCredentials().toString();
// 1. 유저 조회
MemberEntity member =
membersRepository
.findByEmployeeNo(username)
.orElseThrow(() -> new BadCredentialsException("ID 또는 비밀번호가 일치하지 않습니다."));
// 2. jBCrypt + 커스텀 salt 로 저장된 패스워드 비교
if (!BCrypt.checkpw(rawPassword, member.getPassword())) {
throw new BadCredentialsException("ID 또는 비밀번호가 일치하지 않습니다.");
}
// 3. 인증 성공 → UserDetails 생성
CustomUserDetails userDetails = new CustomUserDetails(member);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@@ -0,0 +1,56 @@
package com.kamco.cd.kamcoback.auth;
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
import java.util.Collection;
import java.util.Collections;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class CustomUserDetails implements UserDetails {
private final MemberEntity member;
public CustomUserDetails(MemberEntity member) {
this.member = member;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 권한을 Member에서 가져오는 경우 바꾸면 됩니다 — 일단 기본값
return Collections.emptyList();
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return String.valueOf(member.getUuid());
}
@Override
public boolean isAccountNonExpired() {
return true; // 추후 상태 필드에 따라 수정 가능
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return member.getStatus().equalsIgnoreCase("ACTIVE");
}
public MemberEntity getMember() {
return member;
}
}

View File

@@ -0,0 +1,21 @@
package com.kamco.cd.kamcoback.auth;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.security.crypto.password.PasswordEncoder;
public class JBCryptPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
throw new UnsupportedOperationException("custom salt 사용");
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword == null || encodedPassword.isBlank()) {
return false;
}
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
}

View File

@@ -0,0 +1,49 @@
package com.kamco.cd.kamcoback.auth;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && jwtTokenProvider.isValidToken(token)) {
String username = jwtTokenProvider.getSubject(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (bearer != null && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}

View File

@@ -0,0 +1,72 @@
package com.kamco.cd.kamcoback.auth;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-token-validity-in-ms}")
private long accessTokenValidityInMs;
@Value("${jwt.refresh-token-validity-in-ms}")
private long refreshTokenValidityInMs;
private SecretKey key;
@PostConstruct
public void init() {
// HS256용 SecretKey
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String createAccessToken(String subject) {
return createToken(subject, accessTokenValidityInMs);
}
public String createRefreshToken(String subject) {
return createToken(subject, refreshTokenValidityInMs);
}
private String createToken(String subject, long validityInMs) {
Date now = new Date();
Date expiry = new Date(now.getTime() + validityInMs);
return Jwts.builder().subject(subject).issuedAt(now).expiration(expiry).signWith(key).compact();
}
public String getSubject(String token) {
var claims = parseClaims(token).getPayload();
return claims.getSubject();
}
public boolean isValidToken(String token) {
try {
Jws<Claims> claims = parseClaims(token);
return !claims.getPayload().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
private Jws<Claims> parseClaims(String token) {
return Jwts.parser()
.verifyWith(key) // SecretKey 타입
.build()
.parseSignedClaims(token);
}
public long getRefreshTokenValidityInMs() {
return refreshTokenValidityInMs;
}
}

View File

@@ -0,0 +1,29 @@
package com.kamco.cd.kamcoback.auth;
import java.time.Duration;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class RefreshTokenService {
private final StringRedisTemplate redisTemplate;
private static final String PREFIX = "RT:";
public void save(String username, String refreshToken, long ttlMillis) {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set(PREFIX + username, refreshToken, Duration.ofMillis(ttlMillis));
}
public boolean validate(String username, String refreshToken) {
String stored = redisTemplate.opsForValue().get(PREFIX + username);
return stored != null && stored.equals(refreshToken);
}
public void delete(String username) {
redisTemplate.delete(PREFIX + username);
}
}

View File

@@ -1,178 +0,0 @@
package com.kamco.cd.kamcoback.auth.dto;
import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.time.ZonedDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@RequiredArgsConstructor
public class AuthDto {
@Getter
@Setter
public static class Basic {
private Long id;
private String userAuth;
private String userNm;
private String userId;
private String empId;
private String userEmail;
@JsonFormatDttm private ZonedDateTime createdDttm;
public Basic(
Long id,
String userAuth,
String userNm,
String userId,
String empId,
String userEmail,
ZonedDateTime createdDttm) {
this.id = id;
this.userAuth = userAuth;
this.userNm = userNm;
this.userId = userId;
this.empId = empId;
this.userEmail = userEmail;
this.createdDttm = createdDttm;
}
}
@Schema(name = "save request", description = "사용자 등록 정보")
@Getter
@Setter
public static class SaveReq {
@Schema(description = "구분", example = "관리자/라벨러/검수자 중 하나")
@NotBlank
private String userAuth;
@NotBlank
@Schema(description = "이름", example = "홍길동")
private String userNm;
@Schema(description = "ID", example = "gildong")
@NotBlank
private String userId;
@Schema(description = "PW", example = "password")
@NotBlank
private String userPw;
@Schema(description = "사번", example = "사번")
@NotBlank
private String empId;
@Schema(description = "이메일", example = "gildong@naver.com")
@NotBlank
private String userEmail;
public SaveReq(
String userAuth,
String userNm,
String userId,
String userPw,
String empId,
String userEmail) {
this.userAuth = userAuth;
this.userNm = userNm;
this.userId = userId;
this.userPw = userPw;
this.empId = empId;
this.userEmail = userEmail;
}
}
@Schema(name = "update request", description = "사용자 수정 정보")
@Getter
@Setter
public static class UpdateReq {
@Schema(description = "id", example = "1")
@NotBlank
private Long id;
@Schema(description = "구분", example = "관리자/라벨러/검수자 중 하나")
@NotBlank
private String userAuth;
@NotBlank
@Schema(description = "이름", example = "홍길동")
private String userNm;
@Schema(description = "ID", example = "gildong")
@NotBlank
private String userId;
@Schema(description = "PW", example = "password")
@NotBlank
private String userPw;
@Schema(description = "사번", example = "사번")
@NotBlank
private String empId;
@Schema(description = "이메일", example = "gildong@naver.com")
@NotBlank
private String userEmail;
public UpdateReq(
Long id,
String userAuth,
String userNm,
String userId,
String userPw,
String empId,
String userEmail) {
this.id = id;
this.userAuth = userAuth;
this.userNm = userNm;
this.userId = userId;
this.userPw = userPw;
this.empId = empId;
this.userEmail = userEmail;
}
}
@Getter
public static class User {
String userId;
String userPw;
}
@Schema(name = "UserSearchReq", description = "관리자 목록 요청 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
// 검색 조건
private String userNm;
// 페이징 파라미터
private int page = 0;
private int size = 20;
private String sort;
public Pageable toPageable() {
if (sort != null && !sort.isEmpty()) {
String[] sortParams = sort.split(",");
String property = sortParams[0];
Sort.Direction direction =
sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC;
return PageRequest.of(page, size, Sort.by(direction, property));
}
return PageRequest.of(page, size);
}
}
}

View File

@@ -1,71 +0,0 @@
package com.kamco.cd.kamcoback.auth.service;
import com.kamco.cd.kamcoback.auth.dto.AuthDto;
import com.kamco.cd.kamcoback.auth.dto.AuthDto.Basic;
import com.kamco.cd.kamcoback.postgres.core.AuthCoreService;
import com.kamco.cd.kamcoback.postgres.entity.UserEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class AuthService {
private final AuthCoreService authCoreService;
/**
* 관리자 등록
*
* @param saveReq
* @return
*/
@Transactional
public UserEntity save(AuthDto.SaveReq saveReq) {
return authCoreService.save(saveReq);
}
/**
* 관리자 정보 수정
*
* @param id
* @param saveReq
* @return
*/
public UserEntity update(Long id, AuthDto.SaveReq saveReq) {
if (saveReq.getUserPw() != null) {}
return authCoreService.update(id, saveReq);
}
/**
* 관리자 삭제
*
* @param id
* @return
*/
public UserEntity withdrawal(Long id) {
return authCoreService.withdrawal(id);
}
/**
* 시퀀스 id로 관리자 조회
*
* @param id
* @return
*/
public AuthDto.Basic getFindUserById(Long id) {
return authCoreService.findUserById(id);
}
/**
* 관리자 목록 조회
*
* @param searchReq
* @return
*/
public Page<Basic> getUserList(AuthDto.SearchReq searchReq) {
return authCoreService.getUserList(searchReq);
}
}

View File

@@ -1,4 +1,4 @@
package com.kamco.cd.kamcoback.members.dto;
package com.kamco.cd.kamcoback.common.enums;
import com.kamco.cd.kamcoback.config.enums.EnumType;
import lombok.AllArgsConstructor;

View File

@@ -0,0 +1,22 @@
package com.kamco.cd.kamcoback.common.exception;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
public class CustomApiException extends RuntimeException {
private final String codeName; // ApiResponseCode enum name과 맞추는 용도 (예: "UNPROCESSABLE_ENTITY")
private final HttpStatus status; // 응답으로 내려줄 HttpStatus
public CustomApiException(String codeName, HttpStatus status, String message) {
super(message);
this.codeName = codeName;
this.status = status;
}
public CustomApiException(String codeName, HttpStatus status) {
this.codeName = codeName;
this.status = status;
}
}

View File

@@ -1,5 +1,6 @@
package com.kamco.cd.kamcoback.config;
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
import com.kamco.cd.kamcoback.config.api.ApiLogFunction;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto.ApiResponseCode;
@@ -19,6 +20,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -366,4 +368,25 @@ public class GlobalExceptionHandler {
return errorLogRepository.save(errorLogEntity);
}
@ExceptionHandler(CustomApiException.class)
public ResponseEntity<ApiResponseDto<String>> handleCustomApiException(
CustomApiException e, HttpServletRequest request) {
log.warn("[CustomApiException] resource : {}", e.getMessage());
String codeName = e.getCodeName();
HttpStatus status = e.getStatus();
String message = e.getMessage() == null ? ApiResponseCode.getMessage(codeName) : e.getMessage();
ApiResponseCode apiCode = ApiResponseCode.getCode(codeName);
ErrorLogEntity errorLog =
saveErrerLogData(
request, apiCode, status, ErrorLogDto.LogErrorLevel.WARNING, e.getStackTrace());
ApiResponseDto<String> body =
ApiResponseDto.createException(apiCode, message, status, errorLog.getId());
return new ResponseEntity<>(body, status);
}
}

View File

@@ -0,0 +1,53 @@
package com.kamco.cd.kamcoback.config;
import com.kamco.cd.kamcoback.auth.CustomAuthenticationProvider;
import com.kamco.cd.kamcoback.auth.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomAuthenticationProvider customAuthenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable())
.logout(logout -> logout.disable())
// 🔥 여기서 우리가 만든 CustomAuthenticationProvider 하나만 등록
.authenticationProvider(customAuthenticationProvider)
.authorizeHttpRequests(
auth ->
auth.requestMatchers(
"/api/auth/signin",
"/api/auth/refresh",
"/swagger-ui/**",
"/v3/api-docs/**")
.permitAll()
.anyRequest()
.authenticated())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
throws Exception {
return configuration.getAuthenticationManager();
}
}

View File

@@ -134,7 +134,7 @@ public class ApiResponseDto<T> {
FAIL_VERIFICATION("인증에 실패하였습니다."),
INVALID_EMAIL("잘못된 형식의 이메일입니다."),
REQUIRED_EMAIL("이메일은 필수 항목입니다."),
WRONG_PASSWORD("잘못된 패스워드입니다.."),
WRONG_PASSWORD("잘못된 패스워드입니다."),
DUPLICATE_EMAIL("이미 가입된 이메일입니다."),
DUPLICATE_DATA("이미 등록되어 있습니다."),
DATA_INTEGRITY_ERROR("데이터 무결성이 위반되어 요청을 처리할수 없습니다."),

View File

@@ -0,0 +1,171 @@
package com.kamco.cd.kamcoback.members;
import com.kamco.cd.kamcoback.auth.JwtTokenProvider;
import com.kamco.cd.kamcoback.auth.RefreshTokenService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import java.time.Duration;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "인증(Auth)", description = "로그인, 토큰 재발급, 로그아웃 API")
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenService refreshTokenService;
@Value("${token.refresh-cookie-name}")
private String refreshCookieName;
@Value("${token.refresh-cookie-secure:true}")
private boolean refreshCookieSecure;
@PostMapping("/signin")
@Operation(summary = "로그인", description = "사번 또는 이메일과 비밀번호로 로그인하여 액세스/리프레시 토큰을 발급합니다.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "로그인 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse(
responseCode = "401",
description = "ID 또는 비밀번호 불일치",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
public ResponseEntity<TokenResponse> signin(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "로그인 요청 정보",
required = true)
@RequestBody
SignInRequest request,
HttpServletResponse response) {
Authentication authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.username(), request.password()));
String username = authentication.getName(); // UserDetailsService 에서 사용한 username
String accessToken = jwtTokenProvider.createAccessToken(username);
String refreshToken = jwtTokenProvider.createRefreshToken(username);
// Redis에 RefreshToken 저장 (TTL = 7일)
refreshTokenService.save(
username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
// HttpOnly + Secure 쿠키에 RefreshToken 저장
ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, refreshToken)
.httpOnly(true)
.secure(refreshCookieSecure) // 로컬 개발에서 http만 쓰면 false 로 바꿔야 할 수도 있음
.path("/")
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
.sameSite("Strict")
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ResponseEntity.ok(new TokenResponse(accessToken));
}
@PostMapping("/refresh")
@Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "재발급 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse(
responseCode = "401",
description = "만료되었거나 유효하지 않은 리프레시 토큰",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
public ResponseEntity<TokenResponse> refresh(String refreshToken, HttpServletResponse response) {
if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) {
return ResponseEntity.status(401).build();
}
String username = jwtTokenProvider.getSubject(refreshToken);
// Redis에 저장된 RefreshToken과 일치하는지 확인
if (!refreshTokenService.validate(username, refreshToken)) {
return ResponseEntity.status(401).build();
}
// 새 토큰 발급
String newAccessToken = jwtTokenProvider.createAccessToken(username);
String newRefreshToken = jwtTokenProvider.createRefreshToken(username);
// Redis 갱신
refreshTokenService.save(
username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
// 쿠키 갱신
ResponseCookie cookie =
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));
}
@PostMapping("/logout")
@Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(블랙리스트 처리 또는 리프레시 토큰 삭제)합니다.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "로그아웃 성공",
content = @Content(schema = @Schema(implementation = Void.class)))
})
public ResponseEntity<Void> logout(Authentication authentication, HttpServletResponse response) {
if (authentication != null) {
String username = authentication.getName();
// Redis에서 RefreshToken 삭제
refreshTokenService.delete(username);
}
// 쿠키 삭제 (Max-Age=0)
ResponseCookie cookie =
ResponseCookie.from(refreshCookieName, "")
.httpOnly(true)
.secure(refreshCookieSecure)
.path("/")
.maxAge(0)
.sameSite("Strict")
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return ResponseEntity.noContent().build();
}
@Schema(description = "로그인 요청 DTO")
public record SignInRequest(
@Schema(description = "사번", example = "11111") String username,
@Schema(description = "비밀번호", example = "kamco1234!") String password) {}
public record TokenResponse(String accessToken) {}
}

View File

@@ -12,7 +12,9 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.UUID;
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -40,8 +42,9 @@ public class MembersApiController {
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/list")
public ApiResponseDto<Page<Basic>> getMemberList(@RequestBody MembersDto.SearchReq searchReq) {
@GetMapping
public ApiResponseDto<Page<Basic>> getMemberList(
@ParameterObject MembersDto.SearchReq searchReq) {
return ApiResponseDto.ok(membersService.findByMembers(searchReq));
}

View File

@@ -0,0 +1,65 @@
package com.kamco.cd.kamcoback.members.dto;
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
@RequiredArgsConstructor
public class MemberDetails implements UserDetails {
private final MemberEntity member;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// TODO: tb_member_role 에서 역할 꺼내서 권한으로 변환하고 싶으면 여기 구현
// 예시 (나중에 MemberRoleEntity 보고 수정):
// return member.getTbMemberRoles().stream()
// .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getRoleName()))
// .toList();
return List.of(); // 일단 빈 권한 리스트
}
@Override
public String getPassword() {
return member.getPassword(); // 암호화된 비밀번호
}
@Override
public String getUsername() {
// 로그인 ID 로 무엇을 쓸지 선택
// 1) 이메일 로그인:
return member.getEmail();
// 2) 사번으로 로그인하고 싶으면:
// return member.getEmployeeNo();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
// status 가 ACTIVE 일 때만 로그인 허용
return "ACTIVE".equalsIgnoreCase(member.getStatus());
}
public MemberEntity getMember() {
return member;
}
}

View File

@@ -1,5 +1,6 @@
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;

View File

@@ -0,0 +1,27 @@
package com.kamco.cd.kamcoback.members.service;
import com.kamco.cd.kamcoback.auth.CustomUserDetails;
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class MemberDetailsService implements UserDetailsService {
private final MembersRepository membersRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MemberEntity member =
membersRepository
.findByEmployeeNo(username)
.orElseThrow(() -> new UsernameNotFoundException("USER NOT FOUND"));
return new CustomUserDetails(member);
}
}

View File

@@ -1,15 +1,17 @@
package com.kamco.cd.kamcoback.members.service;
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
import com.kamco.cd.kamcoback.config.BCryptSaltGenerator;
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.UUID;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.data.domain.Page;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -40,8 +42,12 @@ public class MembersService {
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 HttpMessageNotReadableException("패스워드 변경시 사번은 필수 값입니다.");
throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST);
}
// salt 생성, 사번이 salt
@@ -55,4 +61,15 @@ public class MembersService {
membersCoreService.updateMembers(uuid, updateReq);
}
/**
* 대문자 1개 이상 소문자 1개 이상 숫자 1개 이상 특수문자(!@#$) 1개 이상
*
* @param password
* @return
*/
private boolean isValidPassword(String password) {
String regex = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[!@#$]).{8,20}$";
return Pattern.matches(regex, password);
}
}

View File

@@ -1,121 +0,0 @@
package com.kamco.cd.kamcoback.postgres.core;
import com.kamco.cd.kamcoback.auth.dto.AuthDto;
import com.kamco.cd.kamcoback.auth.dto.AuthDto.SaveReq;
import com.kamco.cd.kamcoback.postgres.entity.UserEntity;
import com.kamco.cd.kamcoback.postgres.repository.auth.AuthRepository;
import jakarta.persistence.EntityNotFoundException;
import java.time.ZonedDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AuthCoreService {
private final AuthRepository authRepository;
/**
* 관리자 등록
*
* @param saveReq
* @return
*/
public UserEntity save(SaveReq saveReq) {
if (authRepository.findByUserId(saveReq.getUserId()).isPresent()) {
new EntityNotFoundException("중복된 아이디가 있습니다. " + saveReq.getUserId());
}
UserEntity userEntity =
new UserEntity(
null,
saveReq.getUserAuth(),
saveReq.getUserNm(),
saveReq.getUserId(),
saveReq.getEmpId(),
saveReq.getUserEmail(),
saveReq.getUserPw());
return authRepository.save(userEntity);
}
/**
* 관리자 정보 수정
*
* @param id
* @param saveReq
* @return
*/
public UserEntity update(Long id, AuthDto.SaveReq saveReq) {
UserEntity userEntity =
authRepository.findById(id).orElseThrow(() -> new RuntimeException("유저가 존재하지 않습니다."));
if (saveReq.getUserAuth() != null) {
userEntity.setUserAuth(saveReq.getUserAuth());
}
if (saveReq.getUserNm() != null) {
userEntity.setUserNm(saveReq.getUserNm());
}
if (saveReq.getUserId() != null) {
userEntity.setUserId(saveReq.getUserId());
}
if (saveReq.getEmpId() != null) {
userEntity.setEmpId(saveReq.getEmpId());
}
if (saveReq.getUserEmail() != null) {
userEntity.setUserEmail(saveReq.getUserEmail());
}
if (saveReq.getUserPw() != null) {
userEntity.setUserPw(saveReq.getUserPw());
}
return authRepository.save(userEntity);
}
/**
* 관리자 삭제
*
* @param id
* @return
*/
public UserEntity withdrawal(Long id) {
UserEntity userEntity =
authRepository.findById(id).orElseThrow(() -> new RuntimeException("유저가 존재하지 않습니다."));
userEntity.setId(id);
userEntity.setDateWithdrawal(ZonedDateTime.now());
userEntity.setState("WITHDRAWAL");
return authRepository.save(userEntity);
}
/**
* 시퀀스 id로 관리자 조회
*
* @param id
* @return
*/
public AuthDto.Basic findUserById(Long id) {
UserEntity entity =
authRepository
.findUserById(id)
.orElseThrow(() -> new EntityNotFoundException("관리자를 찾을 수 없습니다. " + id));
return entity.toDto();
}
/**
* 관리자 목록 조회
*
* @param searchReq
* @return
*/
public Page<AuthDto.Basic> getUserList(AuthDto.SearchReq searchReq) {
return authRepository.getUserList(searchReq);
}
}

View File

@@ -14,6 +14,7 @@ import com.kamco.cd.kamcoback.postgres.repository.members.MembersRoleRepository;
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.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
@@ -37,12 +38,12 @@ public class MembersCoreService {
* @return
*/
public Long saveMembers(MembersDto.AddReq addReq) {
if (membersRepository.findByEmployeeNo(addReq.getEmployeeNo())) {
if (membersRepository.existsByEmployeeNo(addReq.getEmployeeNo())) {
throw new MemberException.DuplicateMemberException(
MemberException.DuplicateMemberException.Field.EMPLOYEE_NO, addReq.getEmployeeNo());
}
if (membersRepository.findByEmail(addReq.getEmail())) {
if (membersRepository.existsByEmail(addReq.getEmail())) {
throw new MemberException.DuplicateMemberException(
MemberException.DuplicateMemberException.Field.EMAIL, addReq.getEmail());
}
@@ -66,16 +67,17 @@ public class MembersCoreService {
MemberEntity memberEntity =
membersRepository.findByUUID(uuid).orElseThrow(() -> new MemberNotFoundException());
if (updateReq.getEmployeeNo() != null && !memberEntity.getEmployeeNo().isEmpty()) {
if (StringUtils.isNotBlank(memberEntity.getEmployeeNo())) {
memberEntity.setEmployeeNo(updateReq.getEmployeeNo());
}
if (updateReq.getName() != null && !updateReq.getName().isEmpty()) {
if (StringUtils.isNotBlank(updateReq.getName())) {
memberEntity.setName(updateReq.getName());
}
if (updateReq.getPassword() != null && !updateReq.getPassword().isEmpty()) {
if (StringUtils.isNotBlank(updateReq.getPassword())) {
memberEntity.setPassword(updateReq.getPassword());
}
if (updateReq.getEmail() != null && !updateReq.getEmail().isEmpty()) {
if (StringUtils.isNotBlank(updateReq.getEmail())) {
memberEntity.setEmail(updateReq.getEmail());
}

View File

@@ -62,7 +62,7 @@ public class CommonCodeEntity extends CommonDateEntity {
private List<CommonCodeEntity> children = new ArrayList<>();
public CommonCodeEntity(
String code, String name, String description, Integer order, Boolean used) {
String code, String name, String description, Integer order, Boolean used) {
this.code = code;
this.name = name;
this.description = description;
@@ -71,7 +71,7 @@ public class CommonCodeEntity extends CommonDateEntity {
}
public CommonCodeEntity(
Long id, String name, String description, Integer order, Boolean used, Boolean deleted) {
Long id, String name, String description, Integer order, Boolean used, Boolean deleted) {
this.id = id;
this.name = name;
this.description = description;
@@ -82,16 +82,16 @@ public class CommonCodeEntity extends CommonDateEntity {
public CommonCodeDto.Basic toDto() {
return new CommonCodeDto.Basic(
this.id,
this.code,
this.description,
this.name,
this.order,
this.used,
this.deleted,
this.children.stream().map(CommonCodeEntity::toDto).toList(),
super.getCreatedDate(),
super.getModifiedDate());
this.id,
this.code,
this.description,
this.name,
this.order,
this.used,
this.deleted,
this.children.stream().map(CommonCodeEntity::toDto).toList(),
super.getCreatedDate(),
super.getModifiedDate());
}
public void addParent(CommonCodeEntity parent) {

View File

@@ -1,6 +1,5 @@
package com.kamco.cd.kamcoback.postgres.entity;
import com.kamco.cd.kamcoback.auth.dto.AuthDto;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
@@ -104,15 +103,4 @@ public class UserEntity {
this.userEmail = userEmail;
this.userPw = userPw;
}
public AuthDto.Basic toDto() {
return new AuthDto.Basic(
this.id,
this.userAuth,
this.userNm,
this.userId,
this.empId,
this.userEmail,
this.createdDttm);
}
}

View File

@@ -1,6 +0,0 @@
package com.kamco.cd.kamcoback.postgres.repository.auth;
import com.kamco.cd.kamcoback.postgres.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AuthRepository extends JpaRepository<UserEntity, Long>, AuthRepositoryCustom {}

View File

@@ -1,16 +0,0 @@
package com.kamco.cd.kamcoback.postgres.repository.auth;
import com.kamco.cd.kamcoback.auth.dto.AuthDto;
import com.kamco.cd.kamcoback.auth.dto.AuthDto.Basic;
import com.kamco.cd.kamcoback.postgres.entity.UserEntity;
import java.util.Optional;
import org.springframework.data.domain.Page;
public interface AuthRepositoryCustom {
Optional<UserEntity> findByUserId(String userId);
Optional<UserEntity> findUserById(Long id);
Page<Basic> getUserList(AuthDto.SearchReq searchReq);
}

View File

@@ -1,94 +0,0 @@
package com.kamco.cd.kamcoback.postgres.repository.auth;
import com.kamco.cd.kamcoback.auth.dto.AuthDto;
import com.kamco.cd.kamcoback.auth.dto.AuthDto.Basic;
import com.kamco.cd.kamcoback.postgres.entity.QUserEntity;
import com.kamco.cd.kamcoback.postgres.entity.UserEntity;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
@Repository
@RequiredArgsConstructor
public class AuthRepositoryImpl implements AuthRepositoryCustom {
private final JPAQueryFactory queryFactory;
private final QUserEntity userEntity = QUserEntity.userEntity;
/**
* 유저 아이디로 조회
*
* @param userId
* @return
*/
@Override
public Optional<UserEntity> findByUserId(String userId) {
return Optional.ofNullable(
queryFactory.selectFrom(userEntity).where(userEntity.userId.eq(userId)).fetchOne());
}
/**
* 유저 시퀀스 id로 조회
*
* @param id
* @return
*/
@Override
public Optional<UserEntity> findUserById(Long id) {
return Optional.ofNullable(
queryFactory.selectFrom(userEntity).where(userEntity.id.eq(id)).fetchOne());
}
/**
* 관리자 목록 조회
*
* @param searchReq
* @return
*/
@Override
public Page<Basic> getUserList(AuthDto.SearchReq searchReq) {
Pageable pageable = searchReq.toPageable();
BooleanBuilder builder = new BooleanBuilder();
if (searchReq.getUserNm() != null && !searchReq.getUserNm().isEmpty()) {
builder.and(likeName(userEntity, searchReq.getUserNm()));
}
List<Basic> content =
queryFactory
.select(
Projections.constructor(
AuthDto.Basic.class,
userEntity.id,
userEntity.userAuth,
userEntity.userNm,
userEntity.userId,
userEntity.empId,
userEntity.userEmail,
userEntity.createdDttm))
.from(userEntity)
.where(builder)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(userEntity.userId.asc())
.fetch();
long total = queryFactory.select(userEntity.id).from(userEntity).where(builder).fetchCount();
return new PageImpl<>(content, pageable, total);
}
private BooleanExpression likeName(QUserEntity entity, String nameStr) {
if (nameStr == null || nameStr.isEmpty()) {
return null;
}
// ex) WHERE LOWER(name) LIKE LOWER('%입력값%')
return entity.userNm.containsIgnoreCase(nameStr.trim());
}
}

View File

@@ -9,11 +9,13 @@ import org.springframework.data.domain.Page;
public interface MembersRepositoryCustom {
boolean findByEmployeeNo(String employeeNo);
boolean existsByEmployeeNo(String employeeNo);
boolean findByEmail(String email);
boolean existsByEmail(String email);
Page<Basic> findByMembers(MembersDto.SearchReq searchReq);
Optional<MemberEntity> findByUUID(UUID uuid);
Optional<MemberEntity> findByEmployeeNo(String employeeNo);
}

View File

@@ -1,8 +1,8 @@
package com.kamco.cd.kamcoback.postgres.repository.members;
import com.kamco.cd.kamcoback.common.enums.RoleType;
import com.kamco.cd.kamcoback.members.dto.MembersDto;
import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic;
import com.kamco.cd.kamcoback.members.dto.RoleType;
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMemberEntity;
import com.kamco.cd.kamcoback.postgres.entity.QMemberRoleEntity;
@@ -14,6 +14,7 @@ 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;
@@ -34,7 +35,7 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom {
* @return
*/
@Override
public boolean findByEmployeeNo(String employeeNo) {
public boolean existsByEmployeeNo(String employeeNo) {
return queryFactory
.selectOne()
.from(memberEntity)
@@ -50,7 +51,7 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom {
* @return
*/
@Override
public boolean findByEmail(String email) {
public boolean existsByEmail(String email) {
return queryFactory
.selectOne()
.from(memberEntity)
@@ -71,7 +72,7 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom {
BooleanBuilder builder = new BooleanBuilder();
BooleanBuilder leftBuilder = new BooleanBuilder();
if (searchReq.getField() != null && !searchReq.getField().isEmpty()) {
if (StringUtils.isNotBlank(searchReq.getField())) {
switch (searchReq.getField()) {
case "name" ->
builder.and(memberEntity.name.containsIgnoreCase(searchReq.getKeyword().trim()));
@@ -142,4 +143,13 @@ public class MembersRepositoryImpl implements MembersRepositoryCustom {
return Optional.ofNullable(
queryFactory.selectFrom(memberEntity).where(memberEntity.uuid.eq(uuid)).fetchOne());
}
@Override
public Optional<MemberEntity> findByEmployeeNo(String employeeNo) {
return Optional.ofNullable(
queryFactory
.selectFrom(memberEntity)
.where(memberEntity.employeeNo.eq(employeeNo))
.fetchOne());
}
}

View File

@@ -35,5 +35,15 @@ spring:
port: 6379
password: kamco
jwt:
secret: "kamco_token_9b71e778-19a3-4c1d-97bf-2d687de17d5b"
access-token-validity-in-ms: 86400000 # 1일
refresh-token-validity-in-ms: 604800000 # 7일
token:
refresh-cookie-name: kamco-dev # 개발용 쿠키 이름
refresh-cookie-secure: false # 로컬 http 테스트면 false
member:
init_password: kamco1234!

View File

@@ -25,9 +25,18 @@ spring:
data:
redis:
host: 192.168.2.109
host: localhost
port: 6379
password: kamco
password: 1234
jwt:
secret: "kamco_token_9b71e778-19a3-4c1d-97bf-2d687de17d5b"
access-token-validity-in-ms: 86400000 # 1일
refresh-token-validity-in-ms: 604800000 # 7일
token:
refresh-cookie-name: kamco-local # 개발용 쿠키 이름
refresh-cookie-secure: false # 로컬 http 테스트면 false
member:
init_password: kamco1234!

View File

@@ -21,6 +21,15 @@ spring:
minimum-idle: 10
maximum-pool-size: 20
jwt:
secret: "kamco_token_9b71e778-19a3-4c1d-97bf-2d687de17d5b"
access-token-validity-in-ms: 86400000 # 1일
refresh-token-validity-in-ms: 604800000 # 7일
token:
refresh-cookie-name: kamco # 개발용 쿠키 이름
refresh-cookie-secure: true # 로컬 http 테스트면 false
member:
init_password: kamco1234!