init spotless 적용

This commit is contained in:
2026-02-02 15:48:23 +09:00
parent 495ef7d86c
commit a1ffad1c4e
153 changed files with 12870 additions and 12931 deletions

View File

@@ -1,14 +1,14 @@
package com.kamco.cd.training;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class KamcoTrainingApplication {
public static void main(String[] args) {
SpringApplication.run(KamcoTrainingApplication.class, args);
}
}
package com.kamco.cd.training;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class KamcoTrainingApplication {
public static void main(String[] args) {
SpringApplication.run(KamcoTrainingApplication.class, args);
}
}

View File

@@ -1,22 +1,22 @@
package com.kamco.cd.training.auth;
import java.security.SecureRandom;
import java.util.Base64;
public class BCryptSaltGenerator {
public static String generateSaltWithEmployeeNo(String employeeNo) {
// bcrypt salt는 16바이트(128비트) 필요
byte[] randomBytes = new byte[16];
new SecureRandom().nextBytes(randomBytes);
String base64 = Base64.getEncoder().encodeToString(randomBytes);
// 사번을 포함 (22자 제한 → 잘라내기)
String mixedSalt = (employeeNo + base64).substring(0, 22);
// bcrypt 포맷에 맞게 구성
return "$2a$10$" + mixedSalt;
}
}
package com.kamco.cd.training.auth;
import java.security.SecureRandom;
import java.util.Base64;
public class BCryptSaltGenerator {
public static String generateSaltWithEmployeeNo(String employeeNo) {
// bcrypt salt는 16바이트(128비트) 필요
byte[] randomBytes = new byte[16];
new SecureRandom().nextBytes(randomBytes);
String base64 = Base64.getEncoder().encodeToString(randomBytes);
// 사번을 포함 (22자 제한 → 잘라내기)
String mixedSalt = (employeeNo + base64).substring(0, 22);
// bcrypt 포맷에 맞게 구성
return "$2a$10$" + mixedSalt;
}
}

View File

@@ -1,62 +1,62 @@
package com.kamco.cd.training.auth;
import com.kamco.cd.training.common.enums.StatusType;
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.postgres.entity.MemberEntity;
import com.kamco.cd.training.postgres.repository.members.MembersRepository;
import lombok.RequiredArgsConstructor;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.security.authentication.AuthenticationProvider;
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();
// 유저 조회
MemberEntity member =
membersRepository
.findByEmployeeNo(username)
.orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND));
// 미사용 상태
if (member.getStatus().equals(StatusType.INACTIVE.getId())) {
throw new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND);
}
// jBCrypt + 커스텀 salt 로 저장된 패스워드 비교
if (!BCrypt.checkpw(rawPassword, member.getPassword())) {
// 실패 카운트 저장
int cnt = member.getLoginFailCount() + 1;
member.setLoginFailCount(cnt);
membersRepository.save(member);
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH);
}
// 로그인 실패 체크
if (member.getLoginFailCount() >= 5) {
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED);
}
// 인증 성공 → UserDetails 생성
CustomUserDetails userDetails = new CustomUserDetails(member);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
package com.kamco.cd.training.auth;
import com.kamco.cd.training.common.enums.StatusType;
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.postgres.entity.MemberEntity;
import com.kamco.cd.training.postgres.repository.members.MembersRepository;
import lombok.RequiredArgsConstructor;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.security.authentication.AuthenticationProvider;
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();
// 유저 조회
MemberEntity member =
membersRepository
.findByEmployeeNo(username)
.orElseThrow(() -> new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND));
// 미사용 상태
if (member.getStatus().equals(StatusType.INACTIVE.getId())) {
throw new CustomApiException(AuthErrorCode.LOGIN_ID_NOT_FOUND);
}
// jBCrypt + 커스텀 salt 로 저장된 패스워드 비교
if (!BCrypt.checkpw(rawPassword, member.getPassword())) {
// 실패 카운트 저장
int cnt = member.getLoginFailCount() + 1;
member.setLoginFailCount(cnt);
membersRepository.save(member);
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH);
}
// 로그인 실패 체크
if (member.getLoginFailCount() >= 5) {
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_EXCEEDED);
}
// 인증 성공 → 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

@@ -1,56 +1,56 @@
package com.kamco.cd.training.auth;
import com.kamco.cd.training.postgres.entity.MemberEntity;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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() {
return List.of(new SimpleGrantedAuthority("ROLE_" + member.getUserRole()));
}
@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 "ACTIVE".equals(member.getStatus());
}
public MemberEntity getMember() {
return member;
}
}
package com.kamco.cd.training.auth;
import com.kamco.cd.training.postgres.entity.MemberEntity;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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() {
return List.of(new SimpleGrantedAuthority("ROLE_" + member.getUserRole()));
}
@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 "ACTIVE".equals(member.getStatus());
}
public MemberEntity getMember() {
return member;
}
}

View File

@@ -1,70 +1,70 @@
package com.kamco.cd.training.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.lang.NonNull;
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.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final UserDetailsService userDetailsService;
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
private static final String[] EXCLUDE_PATHS = {
"/api/auth/signin", "/api/auth/refresh", "/api/auth/logout", "/api/members/*/password"
};
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull 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);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getServletPath();
// JWT 필터를 타지 않게 할 URL 패턴들
for (String pattern : EXCLUDE_PATHS) {
if (PATH_MATCHER.match(pattern, path)) {
return true;
}
}
return false;
}
private String resolveToken(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (bearer != null && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}
package com.kamco.cd.training.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.lang.NonNull;
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.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final UserDetailsService userDetailsService;
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
private static final String[] EXCLUDE_PATHS = {
"/api/auth/signin", "/api/auth/refresh", "/api/auth/logout", "/api/members/*/password"
};
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull 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);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getServletPath();
// JWT 필터를 타지 않게 할 URL 패턴들
for (String pattern : EXCLUDE_PATHS) {
if (PATH_MATCHER.match(pattern, path)) {
return true;
}
}
return false;
}
private String resolveToken(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (bearer != null && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}

View File

@@ -1,72 +1,72 @@
package com.kamco.cd.training.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;
}
}
package com.kamco.cd.training.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

@@ -1,299 +1,299 @@
package com.kamco.cd.training.code;
import com.kamco.cd.training.code.dto.CommonCodeDto;
import com.kamco.cd.training.code.service.CommonCodeService;
import com.kamco.cd.training.common.utils.CommonCodeUtil;
import com.kamco.cd.training.common.utils.enums.CodeDto;
import com.kamco.cd.training.config.api.ApiResponseDto;
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.validation.Valid;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "공통코드 관리", description = "공통코드 관리 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/code")
public class CommonCodeApiController {
private final CommonCodeService commonCodeService;
private final CommonCodeUtil commonCodeUtil;
@Operation(summary = "목록 조회", description = "모든 공통코드 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping
public ApiResponseDto<List<CommonCodeDto.Basic>> getFindAll() {
return ApiResponseDto.createOK(commonCodeService.getFindAll());
}
@Operation(summary = "단건 조회", description = "단건 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/{id}")
public ApiResponseDto<CommonCodeDto.Basic> getOneById(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "단건 조회", required = true)
@PathVariable
Long id) {
return ApiResponseDto.ok(commonCodeService.getOneById(id));
}
@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
public ApiResponseDto<ApiResponseDto.ResponseObj> save(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 생성 요청 정보",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.AddReq.class)))
@RequestBody
@Valid
CommonCodeDto.AddReq req) {
return ApiResponseDto.okObject(commonCodeService.save(req));
}
@Operation(summary = "수정", description = "공통코드를 수정 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "204",
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("/{id}")
public ApiResponseDto<ApiResponseDto.ResponseObj> update(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 수정 요청 정보",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.ModifyReq.class)))
@PathVariable
Long id,
@RequestBody @Valid CommonCodeDto.ModifyReq req) {
return ApiResponseDto.okObject(commonCodeService.update(id, req));
}
@Operation(summary = "삭제", description = "공통코드를 삭제 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "204",
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)
})
@DeleteMapping("/{id}")
public ApiResponseDto<ApiResponseDto.ResponseObj> remove(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 삭제 요청 정보",
required = true)
@PathVariable
Long id) {
return ApiResponseDto.okObject(commonCodeService.removeCode(id));
}
@Operation(summary = "순서 변경", description = "공통코드 순서를 변경 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "204",
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("/order")
public ApiResponseDto<ApiResponseDto.ResponseObj> updateOrder(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 순서변경 요청 정보",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.OrderReq.class)))
@RequestBody
@Valid
CommonCodeDto.OrderReq req) {
return ApiResponseDto.okObject(commonCodeService.updateOrder(req));
}
@Operation(summary = "code 기반 조회", description = "code 기반 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "code 기반 조회 성공",
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)
})
@GetMapping("/used")
public ApiResponseDto<List<CommonCodeDto.Basic>> getByCode(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 순서변경 요청 정보",
required = true)
@RequestParam
String code) {
return ApiResponseDto.ok(commonCodeService.findByCode(code));
}
@Operation(summary = "변화탐지 분류 코드 목록", description = "변화탐지 분류 코드 목록(공통코드 기반)")
@GetMapping("/clazz")
public ApiResponseDto<List<CommonCodeDto.Clazzes>> getClasses() {
// List<Clazzes> list =
// Arrays.stream(DetectionClassification.values())
// .sorted(Comparator.comparingInt(DetectionClassification::getOrder))
// .map(Clazzes::new)
// .toList();
// 변화탐지 clazz API : enum -> 공통코드로 변경
List<CommonCodeDto.Clazzes> list =
commonCodeUtil.getChildCodesByParentCode("0000").stream()
.map(
child ->
new CommonCodeDto.Clazzes(
child.getCode(), child.getName(), child.getOrder(), child.getProps2()))
.toList();
return ApiResponseDto.ok(list);
}
@Operation(summary = "공통코드 중복여부 체크", description = "공통코드 중복여부 체크")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/check-duplicate")
public ApiResponseDto<ApiResponseDto.ResponseObj> getCodeCheckDuplicate(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "단건 조회", required = true)
@RequestParam(required = false)
Long parentId,
@RequestParam String code) {
return ApiResponseDto.okObject(commonCodeService.getCodeCheckDuplicate(parentId, code));
}
@Operation(summary = "코드 조회", description = "코드 리스트 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "코드 조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CodeDto.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/type/codes")
public ApiResponseDto<Map<String, List<CodeDto>>> getTypeCodes() {
return ApiResponseDto.ok(commonCodeService.getTypeCodes());
}
@Operation(summary = "코드 단건 조회", description = "코드 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "코드 조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CodeDto.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/type/{type}")
public ApiResponseDto<List<CodeDto>> getTypeCode(@PathVariable String type) {
return ApiResponseDto.ok(commonCodeService.getTypeCode(type));
}
@Operation(summary = "캐시 초기화", description = "공통코드 캐시를 초기화합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "캐시 초기화 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/cache/refresh")
public ApiResponseDto<String> refreshCommonCodeCache() {
commonCodeService.refresh();
return ApiResponseDto.ok("공통코드 캐시가 초기화 되었습니다.");
}
}
package com.kamco.cd.training.code;
import com.kamco.cd.training.code.dto.CommonCodeDto;
import com.kamco.cd.training.code.service.CommonCodeService;
import com.kamco.cd.training.common.utils.CommonCodeUtil;
import com.kamco.cd.training.common.utils.enums.CodeDto;
import com.kamco.cd.training.config.api.ApiResponseDto;
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.validation.Valid;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Tag(name = "공통코드 관리", description = "공통코드 관리 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/code")
public class CommonCodeApiController {
private final CommonCodeService commonCodeService;
private final CommonCodeUtil commonCodeUtil;
@Operation(summary = "목록 조회", description = "모든 공통코드 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping
public ApiResponseDto<List<CommonCodeDto.Basic>> getFindAll() {
return ApiResponseDto.createOK(commonCodeService.getFindAll());
}
@Operation(summary = "단건 조회", description = "단건 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/{id}")
public ApiResponseDto<CommonCodeDto.Basic> getOneById(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "단건 조회", required = true)
@PathVariable
Long id) {
return ApiResponseDto.ok(commonCodeService.getOneById(id));
}
@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
public ApiResponseDto<ApiResponseDto.ResponseObj> save(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 생성 요청 정보",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.AddReq.class)))
@RequestBody
@Valid
CommonCodeDto.AddReq req) {
return ApiResponseDto.okObject(commonCodeService.save(req));
}
@Operation(summary = "수정", description = "공통코드를 수정 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "204",
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("/{id}")
public ApiResponseDto<ApiResponseDto.ResponseObj> update(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 수정 요청 정보",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.ModifyReq.class)))
@PathVariable
Long id,
@RequestBody @Valid CommonCodeDto.ModifyReq req) {
return ApiResponseDto.okObject(commonCodeService.update(id, req));
}
@Operation(summary = "삭제", description = "공통코드를 삭제 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "204",
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)
})
@DeleteMapping("/{id}")
public ApiResponseDto<ApiResponseDto.ResponseObj> remove(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 삭제 요청 정보",
required = true)
@PathVariable
Long id) {
return ApiResponseDto.okObject(commonCodeService.removeCode(id));
}
@Operation(summary = "순서 변경", description = "공통코드 순서를 변경 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "204",
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("/order")
public ApiResponseDto<ApiResponseDto.ResponseObj> updateOrder(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 순서변경 요청 정보",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.OrderReq.class)))
@RequestBody
@Valid
CommonCodeDto.OrderReq req) {
return ApiResponseDto.okObject(commonCodeService.updateOrder(req));
}
@Operation(summary = "code 기반 조회", description = "code 기반 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "code 기반 조회 성공",
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)
})
@GetMapping("/used")
public ApiResponseDto<List<CommonCodeDto.Basic>> getByCode(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "공통코드 순서변경 요청 정보",
required = true)
@RequestParam
String code) {
return ApiResponseDto.ok(commonCodeService.findByCode(code));
}
@Operation(summary = "변화탐지 분류 코드 목록", description = "변화탐지 분류 코드 목록(공통코드 기반)")
@GetMapping("/clazz")
public ApiResponseDto<List<CommonCodeDto.Clazzes>> getClasses() {
// List<Clazzes> list =
// Arrays.stream(DetectionClassification.values())
// .sorted(Comparator.comparingInt(DetectionClassification::getOrder))
// .map(Clazzes::new)
// .toList();
// 변화탐지 clazz API : enum -> 공통코드로 변경
List<CommonCodeDto.Clazzes> list =
commonCodeUtil.getChildCodesByParentCode("0000").stream()
.map(
child ->
new CommonCodeDto.Clazzes(
child.getCode(), child.getName(), child.getOrder(), child.getProps2()))
.toList();
return ApiResponseDto.ok(list);
}
@Operation(summary = "공통코드 중복여부 체크", description = "공통코드 중복여부 체크")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CommonCodeDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/check-duplicate")
public ApiResponseDto<ApiResponseDto.ResponseObj> getCodeCheckDuplicate(
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "단건 조회", required = true)
@RequestParam(required = false)
Long parentId,
@RequestParam String code) {
return ApiResponseDto.okObject(commonCodeService.getCodeCheckDuplicate(parentId, code));
}
@Operation(summary = "코드 조회", description = "코드 리스트 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "코드 조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CodeDto.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/type/codes")
public ApiResponseDto<Map<String, List<CodeDto>>> getTypeCodes() {
return ApiResponseDto.ok(commonCodeService.getTypeCodes());
}
@Operation(summary = "코드 단건 조회", description = "코드 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "코드 조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = CodeDto.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/type/{type}")
public ApiResponseDto<List<CodeDto>> getTypeCode(@PathVariable String type) {
return ApiResponseDto.ok(commonCodeService.getTypeCode(type));
}
@Operation(summary = "캐시 초기화", description = "공통코드 캐시를 초기화합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "캐시 초기화 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/cache/refresh")
public ApiResponseDto<String> refreshCommonCodeCache() {
commonCodeService.refresh();
return ApiResponseDto.ok("공통코드 캐시가 초기화 되었습니다.");
}
}

View File

@@ -1,179 +1,179 @@
package com.kamco.cd.training.code.dto;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.kamco.cd.training.common.utils.html.HtmlEscapeDeserializer;
import com.kamco.cd.training.common.utils.html.HtmlUnescapeSerializer;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class CommonCodeDto {
@Schema(name = "CodeAddReq", description = "공통코드 저장 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class AddReq {
@NotEmpty private String code;
@NotEmpty private String name;
private String description;
private int order;
private boolean used;
private Long parentId;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props1;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props2;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props3;
}
@Schema(name = "CodeModifyReq", description = "공통코드 수정 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class ModifyReq {
@NotEmpty private String name;
private String description;
private boolean used;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props1;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props2;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props3;
}
@Schema(name = "CodeOrderReq", description = "공통코드 순서 변경 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class OrderReq {
@NotNull private Long id;
@NotNull private Integer order;
}
@Schema(name = "CommonCode Basic", description = "공통코드 기본 정보")
@Getter
public static class Basic {
private Long id;
private String code;
private String description;
private String name;
private Integer order;
private Boolean used;
private Boolean deleted;
private List<CommonCodeDto.Basic> children;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm;
@JsonSerialize(using = HtmlUnescapeSerializer.class)
private String props1;
@JsonSerialize(using = HtmlUnescapeSerializer.class)
private String props2;
@JsonSerialize(using = HtmlUnescapeSerializer.class)
private String props3;
@JsonFormatDttm private ZonedDateTime deletedDttm;
public Basic(
Long id,
String code,
String description,
String name,
Integer order,
Boolean used,
Boolean deleted,
List<CommonCodeDto.Basic> children,
ZonedDateTime createdDttm,
ZonedDateTime updatedDttm,
String props1,
String props2,
String props3,
ZonedDateTime deletedDttm) {
this.id = id;
this.code = code;
this.description = description;
this.name = name;
this.order = order;
this.used = used;
this.deleted = deleted;
this.children = children;
this.createdDttm = createdDttm;
this.updatedDttm = updatedDttm;
this.props1 = props1;
this.props2 = props2;
this.props3 = props3;
this.deletedDttm = deletedDttm;
}
}
@Schema(name = "SearchReq", description = "검색 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
// 검색 조건
private String name;
// 페이징 파라미터
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);
}
}
@Getter
public static class Clazzes {
private String code;
private String name;
private Integer order;
private String color;
public Clazzes(String code, String name, Integer order, String color) {
this.code = code;
this.name = name;
this.order = order;
this.color = color;
}
}
}
package com.kamco.cd.training.code.dto;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.kamco.cd.training.common.utils.html.HtmlEscapeDeserializer;
import com.kamco.cd.training.common.utils.html.HtmlUnescapeSerializer;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class CommonCodeDto {
@Schema(name = "CodeAddReq", description = "공통코드 저장 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class AddReq {
@NotEmpty private String code;
@NotEmpty private String name;
private String description;
private int order;
private boolean used;
private Long parentId;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props1;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props2;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props3;
}
@Schema(name = "CodeModifyReq", description = "공통코드 수정 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class ModifyReq {
@NotEmpty private String name;
private String description;
private boolean used;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props1;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props2;
@JsonDeserialize(using = HtmlEscapeDeserializer.class)
private String props3;
}
@Schema(name = "CodeOrderReq", description = "공통코드 순서 변경 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class OrderReq {
@NotNull private Long id;
@NotNull private Integer order;
}
@Schema(name = "CommonCode Basic", description = "공통코드 기본 정보")
@Getter
public static class Basic {
private Long id;
private String code;
private String description;
private String name;
private Integer order;
private Boolean used;
private Boolean deleted;
private List<CommonCodeDto.Basic> children;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm;
@JsonSerialize(using = HtmlUnescapeSerializer.class)
private String props1;
@JsonSerialize(using = HtmlUnescapeSerializer.class)
private String props2;
@JsonSerialize(using = HtmlUnescapeSerializer.class)
private String props3;
@JsonFormatDttm private ZonedDateTime deletedDttm;
public Basic(
Long id,
String code,
String description,
String name,
Integer order,
Boolean used,
Boolean deleted,
List<CommonCodeDto.Basic> children,
ZonedDateTime createdDttm,
ZonedDateTime updatedDttm,
String props1,
String props2,
String props3,
ZonedDateTime deletedDttm) {
this.id = id;
this.code = code;
this.description = description;
this.name = name;
this.order = order;
this.used = used;
this.deleted = deleted;
this.children = children;
this.createdDttm = createdDttm;
this.updatedDttm = updatedDttm;
this.props1 = props1;
this.props2 = props2;
this.props3 = props3;
this.deletedDttm = deletedDttm;
}
}
@Schema(name = "SearchReq", description = "검색 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
// 검색 조건
private String name;
// 페이징 파라미터
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);
}
}
@Getter
public static class Clazzes {
private String code;
private String name;
private Integer order;
private String color;
public Clazzes(String code, String name, Integer order, String color) {
this.code = code;
this.name = name;
this.order = order;
this.color = color;
}
}
}

View File

@@ -1,155 +1,155 @@
package com.kamco.cd.training.code.service;
import com.kamco.cd.training.code.dto.CommonCodeDto.AddReq;
import com.kamco.cd.training.code.dto.CommonCodeDto.Basic;
import com.kamco.cd.training.code.dto.CommonCodeDto.ModifyReq;
import com.kamco.cd.training.code.dto.CommonCodeDto.OrderReq;
import com.kamco.cd.training.common.utils.enums.CodeDto;
import com.kamco.cd.training.common.utils.enums.Enums;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.postgres.core.CommonCodeCoreService;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
// => org.springframework.cache.annotation.Cacheable
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommonCodeService {
private final CommonCodeCoreService commonCodeCoreService;
/**
* 공통코드 목록 조회
*
* @return 모튼 코드 정보
*/
@Cacheable("trainCommonCodes")
public List<Basic> getFindAll() {
return commonCodeCoreService.findAll();
}
/**
* 공통코드 단건 조회
*
* @param id
* @return 코드 아이디로 조회한 코드 정보
*/
public Basic getOneById(Long id) {
return commonCodeCoreService.getOneById(id);
}
/**
* 공통코드 생성 요청
*
* @param req 생성요청 정보
* @return 생성된 코드 id
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj save(AddReq req) {
return commonCodeCoreService.save(req);
}
/**
* 공통코드 수정 요청
*
* @param id 코드 아이디
* @param req 수정요청 정보
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj update(Long id, ModifyReq req) {
return commonCodeCoreService.update(id, req);
}
/**
* 공통코드 삭제 처리
*
* @param id 코드 아이디
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj removeCode(Long id) {
return commonCodeCoreService.removeCode(id);
}
/**
* 공통코드 순서 변경
*
* @param req id, order 정보를 가진 List
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj updateOrder(OrderReq req) {
return commonCodeCoreService.updateOrder(req);
}
/**
* 코드기반 조회
*
* @param code 코드
* @return 코드로 조회한 공통코드 정보
*/
public List<Basic> findByCode(String code) {
return commonCodeCoreService.findByCode(code);
}
/**
* 중복 체크
*
* @param parentId
* @param code
* @return
*/
public ApiResponseDto.ResponseObj getCodeCheckDuplicate(Long parentId, String code) {
return commonCodeCoreService.getCodeCheckDuplicate(parentId, code);
}
/**
* 공통코드 이름 조회
*
* @param parentCodeCd 상위 코드
* @param childCodeCd 하위 코드
* @return 공통코드명
*/
public Optional<String> getCode(String parentCodeCd, String childCodeCd) {
return commonCodeCoreService.getCode(parentCodeCd, childCodeCd);
}
/**
* 공통코드 이름 조회
*
* @param parentCodeCd 상위 코드
* @param childCodeCd 하위 코드
* @return 공통코드명
*/
public Optional<String> getTypeCode(String parentCodeCd, String childCodeCd) {
return commonCodeCoreService.getCode(parentCodeCd, childCodeCd);
}
public List<CodeDto> getTypeCode(String type) {
return Enums.getCodes(type);
}
/**
* 공통코드 리스트 조회
*
* @return
*/
public Map<String, List<CodeDto>> getTypeCodes() {
return Enums.getAllCodes();
}
/** 메모리 캐시 초기화 */
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public void refresh() {}
}
package com.kamco.cd.training.code.service;
import com.kamco.cd.training.code.dto.CommonCodeDto.AddReq;
import com.kamco.cd.training.code.dto.CommonCodeDto.Basic;
import com.kamco.cd.training.code.dto.CommonCodeDto.ModifyReq;
import com.kamco.cd.training.code.dto.CommonCodeDto.OrderReq;
import com.kamco.cd.training.common.utils.enums.CodeDto;
import com.kamco.cd.training.common.utils.enums.Enums;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.postgres.core.CommonCodeCoreService;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
// => org.springframework.cache.annotation.Cacheable
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommonCodeService {
private final CommonCodeCoreService commonCodeCoreService;
/**
* 공통코드 목록 조회
*
* @return 모튼 코드 정보
*/
@Cacheable("trainCommonCodes")
public List<Basic> getFindAll() {
return commonCodeCoreService.findAll();
}
/**
* 공통코드 단건 조회
*
* @param id
* @return 코드 아이디로 조회한 코드 정보
*/
public Basic getOneById(Long id) {
return commonCodeCoreService.getOneById(id);
}
/**
* 공통코드 생성 요청
*
* @param req 생성요청 정보
* @return 생성된 코드 id
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj save(AddReq req) {
return commonCodeCoreService.save(req);
}
/**
* 공통코드 수정 요청
*
* @param id 코드 아이디
* @param req 수정요청 정보
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj update(Long id, ModifyReq req) {
return commonCodeCoreService.update(id, req);
}
/**
* 공통코드 삭제 처리
*
* @param id 코드 아이디
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj removeCode(Long id) {
return commonCodeCoreService.removeCode(id);
}
/**
* 공통코드 순서 변경
*
* @param req id, order 정보를 가진 List
*/
@Transactional
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public ApiResponseDto.ResponseObj updateOrder(OrderReq req) {
return commonCodeCoreService.updateOrder(req);
}
/**
* 코드기반 조회
*
* @param code 코드
* @return 코드로 조회한 공통코드 정보
*/
public List<Basic> findByCode(String code) {
return commonCodeCoreService.findByCode(code);
}
/**
* 중복 체크
*
* @param parentId
* @param code
* @return
*/
public ApiResponseDto.ResponseObj getCodeCheckDuplicate(Long parentId, String code) {
return commonCodeCoreService.getCodeCheckDuplicate(parentId, code);
}
/**
* 공통코드 이름 조회
*
* @param parentCodeCd 상위 코드
* @param childCodeCd 하위 코드
* @return 공통코드명
*/
public Optional<String> getCode(String parentCodeCd, String childCodeCd) {
return commonCodeCoreService.getCode(parentCodeCd, childCodeCd);
}
/**
* 공통코드 이름 조회
*
* @param parentCodeCd 상위 코드
* @param childCodeCd 하위 코드
* @return 공통코드명
*/
public Optional<String> getTypeCode(String parentCodeCd, String childCodeCd) {
return commonCodeCoreService.getCode(parentCodeCd, childCodeCd);
}
public List<CodeDto> getTypeCode(String type) {
return Enums.getCodes(type);
}
/**
* 공통코드 리스트 조회
*
* @return
*/
public Map<String, List<CodeDto>> getTypeCodes() {
return Enums.getAllCodes();
}
/** 메모리 캐시 초기화 */
@CacheEvict(value = "trainCommonCodes", allEntries = true)
public void refresh() {}
}

View File

@@ -1,19 +1,19 @@
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@CodeExpose
@Getter
@AllArgsConstructor
public enum DeployTargetType implements EnumType {
// @formatter:off
GUKU("GUKU", "국토교통부"),
PROD("PROD", "운영계");
// @formatter:on
private final String id;
private final String text;
}
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@CodeExpose
@Getter
@AllArgsConstructor
public enum DeployTargetType implements EnumType {
// @formatter:off
GUKU("GUKU", "국토교통부"),
PROD("PROD", "운영계");
// @formatter:on
private final String id;
private final String text;
}

View File

@@ -1,55 +1,55 @@
package com.kamco.cd.training.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum DetectionClassification {
BUILDING("building", "건물", 10),
CONTAINER("container", "컨테이너", 20),
FIELD("field", "경작지", 30),
FOREST("forest", "", 40),
GRASS("grass", "초지", 50),
GREENHOUSE("greenhouse", "비닐하우스", 60),
LAND("land", "일반토지", 70),
ORCHARD("orchard", "과수원", 80),
ROAD("road", "도로", 90),
STONE("stone", "모래/자갈", 100),
TANK("tank", "물탱크", 110),
TUMULUS("tumulus", "토분(무덤)", 120),
WASTE("waste", "폐기물", 130),
WATER("water", "", 140),
ETC("ETC", "기타", 200); // For 'etc' (miscellaneous/other)
private final String id;
private final String desc;
private final int order;
/**
* Optional: Helper method to get the enum from a String, case-insensitive, or return ETC if not
* found.
*/
public static DetectionClassification fromString(String text) {
if (text == null || text.trim().isEmpty()) {
return ETC;
}
try {
return DetectionClassification.valueOf(text.toUpperCase());
} catch (IllegalArgumentException e) {
// If the string doesn't match any enum constant name, return ETC
return ETC;
}
}
/**
* Desc 한글명 get 하기
*
* @return
*/
public static String fromStrDesc(String text) {
DetectionClassification dtf = fromString(text);
return dtf.getDesc();
}
}
package com.kamco.cd.training.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum DetectionClassification {
BUILDING("building", "건물", 10),
CONTAINER("container", "컨테이너", 20),
FIELD("field", "경작지", 30),
FOREST("forest", "", 40),
GRASS("grass", "초지", 50),
GREENHOUSE("greenhouse", "비닐하우스", 60),
LAND("land", "일반토지", 70),
ORCHARD("orchard", "과수원", 80),
ROAD("road", "도로", 90),
STONE("stone", "모래/자갈", 100),
TANK("tank", "물탱크", 110),
TUMULUS("tumulus", "토분(무덤)", 120),
WASTE("waste", "폐기물", 130),
WATER("water", "", 140),
ETC("ETC", "기타", 200); // For 'etc' (miscellaneous/other)
private final String id;
private final String desc;
private final int order;
/**
* Optional: Helper method to get the enum from a String, case-insensitive, or return ETC if not
* found.
*/
public static DetectionClassification fromString(String text) {
if (text == null || text.trim().isEmpty()) {
return ETC;
}
try {
return DetectionClassification.valueOf(text.toUpperCase());
} catch (IllegalArgumentException e) {
// If the string doesn't match any enum constant name, return ETC
return ETC;
}
}
/**
* Desc 한글명 get 하기
*
* @return
*/
public static String fromStrDesc(String text) {
DetectionClassification dtf = fromString(text);
return dtf.getDesc();
}
}

View File

@@ -1,26 +1,26 @@
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum LearnDataRegister implements EnumType {
READY("준비"),
UPLOADING("업로드중"),
UPLOAD_FAILED("업로드 실패"),
COMPLETED("완료");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum LearnDataRegister implements EnumType {
READY("준비"),
UPLOADING("업로드중"),
UPLOAD_FAILED("업로드 실패"),
COMPLETED("완료");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -1,26 +1,26 @@
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@CodeExpose
@Getter
@AllArgsConstructor
public enum LearnDataType implements EnumType {
DELIVER("납품"),
PRODUCTION("제작");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@CodeExpose
@Getter
@AllArgsConstructor
public enum LearnDataType implements EnumType {
DELIVER("납품"),
PRODUCTION("제작");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -1,27 +1,27 @@
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@CodeExpose
@Getter
@AllArgsConstructor
public enum ModelMngStatusType implements EnumType {
READY("준비"),
IN_PROGRESS("진행중"),
COMPLETED("완료");
private String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@CodeExpose
@Getter
@AllArgsConstructor
public enum ModelMngStatusType implements EnumType {
READY("준비"),
IN_PROGRESS("진행중"),
COMPLETED("완료");
private String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -1,20 +1,20 @@
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@CodeExpose
@Getter
@AllArgsConstructor
public enum ProcessStepType implements EnumType {
// @formatter:off
STEP1("STEP1", "학습 중"),
STEP2("STEP2", "테스트 중"),
STEP3("STEP3", "완료");
// @formatter:on
private final String id;
private final String text;
}
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@CodeExpose
@Getter
@AllArgsConstructor
public enum ProcessStepType implements EnumType {
// @formatter:off
STEP1("STEP1", "학습 중"),
STEP2("STEP2", "테스트 중"),
STEP3("STEP3", "완료");
// @formatter:on
private final String id;
private final String text;
}

View File

@@ -1,25 +1,25 @@
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum RoleType implements EnumType {
ROLE_ADMIN("시스템 관리자"),
ROLE_LABELER("라벨러"),
ROLE_REVIEWER("검수자");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum RoleType implements EnumType {
ROLE_ADMIN("시스템 관리자"),
ROLE_LABELER("라벨러"),
ROLE_REVIEWER("검수자");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -1,25 +1,25 @@
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum StatusType implements EnumType {
ACTIVE("사용"),
INACTIVE("미사용"),
PENDING("계정등록");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum StatusType implements EnumType {
ACTIVE("사용"),
INACTIVE("미사용"),
PENDING("계정등록");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -1,22 +1,22 @@
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@CodeExpose
@Getter
@AllArgsConstructor
public enum TrainStatusType implements EnumType {
// @formatter:off
READY("READY", "대기"),
ING("ING", "진행중"),
COMPLETED("COMPLETED", "완료"),
STOPPED("STOPPED", "중단됨"),
ERROR("ERROR", "오류");
// @formatter:on
private final String id;
private final String text;
}
package com.kamco.cd.training.common.enums;
import com.kamco.cd.training.common.utils.enums.CodeExpose;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@CodeExpose
@Getter
@AllArgsConstructor
public enum TrainStatusType implements EnumType {
// @formatter:off
READY("READY", "대기"),
ING("ING", "진행중"),
COMPLETED("COMPLETED", "완료"),
STOPPED("STOPPED", "중단됨"),
ERROR("ERROR", "오류");
// @formatter:on
private final String id;
private final String text;
}

View File

@@ -1,26 +1,26 @@
package com.kamco.cd.training.common.enums.error;
import com.kamco.cd.training.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),
REFRESH_TOKEN_EXPIRED_OR_REVOKED("REFRESH_TOKEN_EXPIRED_OR_REVOKED", HttpStatus.UNAUTHORIZED),
REFRESH_TOKEN_MISMATCH("REFRESH_TOKEN_MISMATCH", HttpStatus.UNAUTHORIZED);
private final String code;
private final HttpStatus status;
AuthErrorCode(String code, HttpStatus status) {
this.code = code;
this.status = status;
}
}
package com.kamco.cd.training.common.enums.error;
import com.kamco.cd.training.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),
REFRESH_TOKEN_EXPIRED_OR_REVOKED("REFRESH_TOKEN_EXPIRED_OR_REVOKED", HttpStatus.UNAUTHORIZED),
REFRESH_TOKEN_MISMATCH("REFRESH_TOKEN_MISMATCH", HttpStatus.UNAUTHORIZED);
private final String code;
private final HttpStatus status;
AuthErrorCode(String code, HttpStatus status) {
this.code = code;
this.status = status;
}
}

View File

@@ -1,10 +1,10 @@
package com.kamco.cd.training.common.exception;
import org.springframework.http.HttpStatus;
public class BadRequestException extends CustomApiException {
public BadRequestException(String message) {
super("BAD_REQUEST", HttpStatus.BAD_REQUEST, message);
}
}
package com.kamco.cd.training.common.exception;
import org.springframework.http.HttpStatus;
public class BadRequestException extends CustomApiException {
public BadRequestException(String message) {
super("BAD_REQUEST", HttpStatus.BAD_REQUEST, message);
}
}

View File

@@ -1,28 +1,28 @@
package com.kamco.cd.training.common.exception;
import com.kamco.cd.training.common.utils.ErrorCode;
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;
}
public CustomApiException(ErrorCode errorCode) {
this.codeName = errorCode.getCode();
this.status = errorCode.getStatus();
}
}
package com.kamco.cd.training.common.exception;
import com.kamco.cd.training.common.utils.ErrorCode;
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;
}
public CustomApiException(ErrorCode errorCode) {
this.codeName = errorCode.getCode();
this.status = errorCode.getStatus();
}
}

View File

@@ -1,7 +1,7 @@
package com.kamco.cd.training.common.exception;
public class DuplicateFileException extends RuntimeException {
public DuplicateFileException(String message) {
super(message);
}
}
package com.kamco.cd.training.common.exception;
public class DuplicateFileException extends RuntimeException {
public DuplicateFileException(String message) {
super(message);
}
}

View File

@@ -1,10 +1,10 @@
package com.kamco.cd.training.common.exception;
import org.springframework.http.HttpStatus;
public class NotFoundException extends CustomApiException {
public NotFoundException(String message) {
super("NOT_FOUND", HttpStatus.NOT_FOUND, message);
}
}
package com.kamco.cd.training.common.exception;
import org.springframework.http.HttpStatus;
public class NotFoundException extends CustomApiException {
public NotFoundException(String message) {
super("NOT_FOUND", HttpStatus.NOT_FOUND, message);
}
}

View File

@@ -1,7 +1,7 @@
package com.kamco.cd.training.common.exception;
public class ValidationException extends RuntimeException {
public ValidationException(String message) {
super(message);
}
}
package com.kamco.cd.training.common.exception;
public class ValidationException extends RuntimeException {
public ValidationException(String message) {
super(message);
}
}

View File

@@ -1,38 +1,38 @@
package com.kamco.cd.training.common.service;
import org.springframework.data.domain.Page;
/**
* Base Core Service Interface
*
* <p>CRUD operations를 정의하는 기본 서비스 인터페이스
*
* @param <T> Entity 타입
* @param <ID> Entity의 ID 타입
* @param <S> Search Request 타입
*/
public interface BaseCoreService<T, ID, S> {
/**
* ID로 엔티티를 삭제합니다.
*
* @param id 삭제할 엔티티의 ID
*/
void remove(ID id);
/**
* ID로 단건 조회합니다.
*
* @param id 조회할 엔티티의 ID
* @return 조회된 엔티티
*/
T getOneById(ID id);
/**
* 검색 조건과 페이징으로 조회합니다.
*
* @param searchReq 검색 조건
* @return 페이징 처리된 검색 결과
*/
Page<T> search(S searchReq);
}
package com.kamco.cd.training.common.service;
import org.springframework.data.domain.Page;
/**
* Base Core Service Interface
*
* <p>CRUD operations를 정의하는 기본 서비스 인터페이스
*
* @param <T> Entity 타입
* @param <ID> Entity의 ID 타입
* @param <S> Search Request 타입
*/
public interface BaseCoreService<T, ID, S> {
/**
* ID로 엔티티를 삭제합니다.
*
* @param id 삭제할 엔티티의 ID
*/
void remove(ID id);
/**
* ID로 단건 조회합니다.
*
* @param id 조회할 엔티티의 ID
* @return 조회된 엔티티
*/
T getOneById(ID id);
/**
* 검색 조건과 페이징으로 조회합니다.
*
* @param searchReq 검색 조건
* @return 페이징 처리된 검색 결과
*/
Page<T> search(S searchReq);
}

View File

@@ -1,152 +1,152 @@
package com.kamco.cd.training.common.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.training.code.dto.CommonCodeDto.Basic;
import com.kamco.cd.training.code.service.CommonCodeService;
import com.kamco.cd.training.config.api.ApiResponseDto;
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;
/**
* 공통코드 조회 유틸리티 클래스 애플리케이션 전역에서 공통코드를 조회하기 위한 유틸리티입니다. training 서버는 Redis 사용하고 Spring 내장 메모리 캐시
* 사용합니다.
*/
//
@Slf4j
@Component
public class CommonCodeUtil {
private final CommonCodeService commonCodeService;
@Autowired private ObjectMapper objectMapper;
public CommonCodeUtil(CommonCodeService commonCodeService) {
this.commonCodeService = commonCodeService;
}
/**
* 모든 공통코드 조회
*
* @return 캐시된 모든 공통코드 목록
*/
public List<Basic> getAllCommonCodes() {
try {
return commonCodeService.getFindAll();
} catch (Exception e) {
log.error("공통코드 전체 조회 중 오류 발생", e);
return List.of();
}
}
/**
* 특정 코드로 공통코드 조회
*
* @param code 코드값
* @return 해당 코드의 공통코드 목록
*/
public List<Basic> getCommonCodesByCode(String code) {
if (code == null || code.isEmpty()) {
log.warn("유효하지 않은 코드: {}", code);
return List.of();
}
try {
return commonCodeService.findByCode(code);
} catch (Exception e) {
log.error("코드 기반 공통코드 조회 중 오류 발생: {}", code, e);
return List.of();
}
}
/**
* 특정 ID로 공통코드 단건 조회
*
* @param id 공통코드 ID
* @return 조회된 공통코드
*/
public Optional<Basic> getCommonCodeById(Long id) {
if (id == null || id <= 0) {
log.warn("유효하지 않은 ID: {}", id);
return Optional.empty();
}
try {
return Optional.of(commonCodeService.getOneById(id));
} catch (Exception e) {
log.error("ID 기반 공통코드 조회 중 오류 발생: {}", id, e);
return Optional.empty();
}
}
/**
* 상위 코드와 하위 코드로 공통코드명 조회
*
* @param parentCode 상위 코드
* @param childCode 하위 코드
* @return 공통코드명
*/
public Optional<String> getCodeName(String parentCode, String childCode) {
if (parentCode == null || parentCode.isEmpty() || childCode == null || childCode.isEmpty()) {
log.warn("유효하지 않은 코드: parentCode={}, childCode={}", parentCode, childCode);
return Optional.empty();
}
try {
return commonCodeService.getCode(parentCode, childCode);
} catch (Exception e) {
log.error("코드명 조회 중 오류 발생: parentCode={}, childCode={}", parentCode, childCode, e);
return Optional.empty();
}
}
/**
* 상위 코드를 기반으로 하위 코드 조회
*
* @param parentCode 상위 코드
* @return 해당 상위 코드의 하위 공통코드 목록
*/
public List<Basic> getChildCodesByParentCode(String parentCode) {
if (parentCode == null || parentCode.isEmpty()) {
log.warn("유효하지 않은 상위 코드: {}", parentCode);
return List.of();
}
try {
return commonCodeService.getFindAll().stream()
.filter(code -> parentCode.equals(code.getCode()))
.findFirst()
.map(Basic::getChildren)
.orElse(List.of());
} catch (Exception e) {
log.error("상위 코드 기반 하위 코드 조회 중 오류 발생: {}", parentCode, e);
return List.of();
}
}
/**
* 코드 사용 가능 여부 확인
*
* @param parentId 상위 코드 ID -> 1depth 는 null 허용 (파라미터 미필수로 변경)
* @param code 확인할 코드값
* @return 사용 가능 여부 (true: 사용 가능, false: 중복 또는 오류)
*/
public boolean isCodeAvailable(Long parentId, String code) {
if (parentId <= 0 || code == null || code.isEmpty()) {
log.warn("유효하지 않은 입력: parentId={}, code={}", parentId, code);
return false;
}
try {
ApiResponseDto.ResponseObj response = commonCodeService.getCodeCheckDuplicate(parentId, code);
// ResponseObj의 code 필드 : OK이면 성공, 아니면 실패
return response.getCode() != null
&& response.getCode().equals(ApiResponseDto.ApiResponseCode.OK);
} catch (Exception e) {
log.error("코드 중복 확인 중 오류 발생: parentId={}, code={}", parentId, code, e);
return false;
}
}
}
package com.kamco.cd.training.common.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.training.code.dto.CommonCodeDto.Basic;
import com.kamco.cd.training.code.service.CommonCodeService;
import com.kamco.cd.training.config.api.ApiResponseDto;
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;
/**
* 공통코드 조회 유틸리티 클래스 애플리케이션 전역에서 공통코드를 조회하기 위한 유틸리티입니다. training 서버는 Redis 사용하고 Spring 내장 메모리 캐시
* 사용합니다.
*/
//
@Slf4j
@Component
public class CommonCodeUtil {
private final CommonCodeService commonCodeService;
@Autowired private ObjectMapper objectMapper;
public CommonCodeUtil(CommonCodeService commonCodeService) {
this.commonCodeService = commonCodeService;
}
/**
* 모든 공통코드 조회
*
* @return 캐시된 모든 공통코드 목록
*/
public List<Basic> getAllCommonCodes() {
try {
return commonCodeService.getFindAll();
} catch (Exception e) {
log.error("공통코드 전체 조회 중 오류 발생", e);
return List.of();
}
}
/**
* 특정 코드로 공통코드 조회
*
* @param code 코드값
* @return 해당 코드의 공통코드 목록
*/
public List<Basic> getCommonCodesByCode(String code) {
if (code == null || code.isEmpty()) {
log.warn("유효하지 않은 코드: {}", code);
return List.of();
}
try {
return commonCodeService.findByCode(code);
} catch (Exception e) {
log.error("코드 기반 공통코드 조회 중 오류 발생: {}", code, e);
return List.of();
}
}
/**
* 특정 ID로 공통코드 단건 조회
*
* @param id 공통코드 ID
* @return 조회된 공통코드
*/
public Optional<Basic> getCommonCodeById(Long id) {
if (id == null || id <= 0) {
log.warn("유효하지 않은 ID: {}", id);
return Optional.empty();
}
try {
return Optional.of(commonCodeService.getOneById(id));
} catch (Exception e) {
log.error("ID 기반 공통코드 조회 중 오류 발생: {}", id, e);
return Optional.empty();
}
}
/**
* 상위 코드와 하위 코드로 공통코드명 조회
*
* @param parentCode 상위 코드
* @param childCode 하위 코드
* @return 공통코드명
*/
public Optional<String> getCodeName(String parentCode, String childCode) {
if (parentCode == null || parentCode.isEmpty() || childCode == null || childCode.isEmpty()) {
log.warn("유효하지 않은 코드: parentCode={}, childCode={}", parentCode, childCode);
return Optional.empty();
}
try {
return commonCodeService.getCode(parentCode, childCode);
} catch (Exception e) {
log.error("코드명 조회 중 오류 발생: parentCode={}, childCode={}", parentCode, childCode, e);
return Optional.empty();
}
}
/**
* 상위 코드를 기반으로 하위 코드 조회
*
* @param parentCode 상위 코드
* @return 해당 상위 코드의 하위 공통코드 목록
*/
public List<Basic> getChildCodesByParentCode(String parentCode) {
if (parentCode == null || parentCode.isEmpty()) {
log.warn("유효하지 않은 상위 코드: {}", parentCode);
return List.of();
}
try {
return commonCodeService.getFindAll().stream()
.filter(code -> parentCode.equals(code.getCode()))
.findFirst()
.map(Basic::getChildren)
.orElse(List.of());
} catch (Exception e) {
log.error("상위 코드 기반 하위 코드 조회 중 오류 발생: {}", parentCode, e);
return List.of();
}
}
/**
* 코드 사용 가능 여부 확인
*
* @param parentId 상위 코드 ID -> 1depth 는 null 허용 (파라미터 미필수로 변경)
* @param code 확인할 코드값
* @return 사용 가능 여부 (true: 사용 가능, false: 중복 또는 오류)
*/
public boolean isCodeAvailable(Long parentId, String code) {
if (parentId <= 0 || code == null || code.isEmpty()) {
log.warn("유효하지 않은 입력: parentId={}, code={}", parentId, code);
return false;
}
try {
ApiResponseDto.ResponseObj response = commonCodeService.getCodeCheckDuplicate(parentId, code);
// ResponseObj의 code 필드 : OK이면 성공, 아니면 실패
return response.getCode() != null
&& response.getCode().equals(ApiResponseDto.ApiResponseCode.OK);
} catch (Exception e) {
log.error("코드 중복 확인 중 오류 발생: parentId={}, code={}", parentId, code, e);
return false;
}
}
}

View File

@@ -1,32 +1,32 @@
package com.kamco.cd.training.common.utils;
import com.kamco.cd.training.auth.BCryptSaltGenerator;
import java.util.regex.Pattern;
import org.mindrot.jbcrypt.BCrypt;
public class CommonStringUtils {
/**
* 영문, 숫자, 특수문자를 모두 포함하여 8~20자 이내의 비밀번호
*
* @param password 벨리데이션 필요한 패스워드
* @return
*/
public static boolean isValidPassword(String password) {
String passwordPattern =
"^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$";
return Pattern.matches(passwordPattern, password);
}
/**
* 패스워드 암호화
*
* @param password 암호화 필요한 패스워드
* @param employeeNo salt 생성에 필요한 사원번호
* @return
*/
public static String hashPassword(String password, String employeeNo) {
String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(employeeNo.trim());
return BCrypt.hashpw(password.trim(), salt);
}
}
package com.kamco.cd.training.common.utils;
import com.kamco.cd.training.auth.BCryptSaltGenerator;
import java.util.regex.Pattern;
import org.mindrot.jbcrypt.BCrypt;
public class CommonStringUtils {
/**
* 영문, 숫자, 특수문자를 모두 포함하여 8~20자 이내의 비밀번호
*
* @param password 벨리데이션 필요한 패스워드
* @return
*/
public static boolean isValidPassword(String password) {
String passwordPattern =
"^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-\\[\\]{};':\"\\\\|,.<>/?]).{8,20}$";
return Pattern.matches(passwordPattern, password);
}
/**
* 패스워드 암호화
*
* @param password 암호화 필요한 패스워드
* @param employeeNo salt 생성에 필요한 사원번호
* @return
*/
public static String hashPassword(String password, String employeeNo) {
String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(employeeNo.trim());
return BCrypt.hashpw(password.trim(), salt);
}
}

View File

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

View File

@@ -1,43 +1,43 @@
package com.kamco.cd.training.common.utils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class NameValidator {
private static final String HANGUL_REGEX = ".*\\p{IsHangul}.*";
private static final Pattern HANGUL_PATTERN = Pattern.compile(HANGUL_REGEX);
private static final String WHITESPACE_REGEX = ".*\\s.*";
private static final Pattern WHITESPACE_PATTERN = Pattern.compile(WHITESPACE_REGEX);
public static boolean containsKorean(String str) {
if (str == null || str.isEmpty()) {
return false;
}
Matcher matcher = HANGUL_PATTERN.matcher(str);
return matcher.matches();
}
public static boolean containsWhitespaceRegex(String str) {
if (str == null || str.isEmpty()) {
return false;
}
Matcher matcher = WHITESPACE_PATTERN.matcher(str);
// find()를 사용하여 문자열 내에서 패턴이 일치하는 부분이 있는지 확인
return matcher.find();
}
public static boolean isNullOrEmpty(String str) {
if (str == null) {
return true;
}
if (str.isEmpty()) {
return true;
}
return false;
}
}
package com.kamco.cd.training.common.utils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class NameValidator {
private static final String HANGUL_REGEX = ".*\\p{IsHangul}.*";
private static final Pattern HANGUL_PATTERN = Pattern.compile(HANGUL_REGEX);
private static final String WHITESPACE_REGEX = ".*\\s.*";
private static final Pattern WHITESPACE_PATTERN = Pattern.compile(WHITESPACE_REGEX);
public static boolean containsKorean(String str) {
if (str == null || str.isEmpty()) {
return false;
}
Matcher matcher = HANGUL_PATTERN.matcher(str);
return matcher.matches();
}
public static boolean containsWhitespaceRegex(String str) {
if (str == null || str.isEmpty()) {
return false;
}
Matcher matcher = WHITESPACE_PATTERN.matcher(str);
// find()를 사용하여 문자열 내에서 패턴이 일치하는 부분이 있는지 확인
return matcher.find();
}
public static boolean isNullOrEmpty(String str) {
if (str == null) {
return true;
}
if (str.isEmpty()) {
return true;
}
return false;
}
}

View File

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

View File

@@ -1,20 +1,20 @@
package com.kamco.cd.training.common.utils.enums;
public class CodeDto {
private String code;
private String name;
public CodeDto(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
}
package com.kamco.cd.training.common.utils.enums;
public class CodeDto {
private String code;
private String name;
public CodeDto(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
}

View File

@@ -1,10 +1,10 @@
package com.kamco.cd.training.common.utils.enums;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CodeExpose {}
package com.kamco.cd.training.common.utils.enums;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CodeExpose {}

View File

@@ -1,8 +1,8 @@
package com.kamco.cd.training.common.utils.enums;
public interface EnumType {
String getId();
String getText();
}
package com.kamco.cd.training.common.utils.enums;
public interface EnumType {
String getId();
String getText();
}

View File

@@ -1,26 +1,26 @@
package com.kamco.cd.training.common.utils.enums;
import com.kamco.cd.training.common.utils.interfaces.EnumValid;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
public class EnumValidator implements ConstraintValidator<EnumValid, String> {
private Set<String> acceptedValues;
@Override
public void initialize(EnumValid constraintAnnotation) {
acceptedValues =
Arrays.stream(constraintAnnotation.enumClass().getEnumConstants())
.map(Enum::name)
.collect(Collectors.toSet());
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && acceptedValues.contains(value);
}
}
package com.kamco.cd.training.common.utils.enums;
import com.kamco.cd.training.common.utils.interfaces.EnumValid;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
public class EnumValidator implements ConstraintValidator<EnumValid, String> {
private Set<String> acceptedValues;
@Override
public void initialize(EnumValid constraintAnnotation) {
acceptedValues =
Arrays.stream(constraintAnnotation.enumClass().getEnumConstants())
.map(Enum::name)
.collect(Collectors.toSet());
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && acceptedValues.contains(value);
}
}

View File

@@ -1,76 +1,76 @@
package com.kamco.cd.training.common.utils.enums;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.reflections.Reflections;
public class Enums {
private static final String BASE_PACKAGE = "com.kamco.cd.training";
/** 노출 가능한 enum만 모아둔 맵 key: enum simpleName (예: RoleType) value: enum Class */
private static final Map<String, Class<? extends Enum<?>>> exposedEnumMap = scanExposedEnumMap();
// code로 enum 찾기
public static <E extends Enum<E> & EnumType> E fromId(Class<E> enumClass, String id) {
if (id == null) {
return null;
}
for (E e : enumClass.getEnumConstants()) {
if (id.equalsIgnoreCase(e.getId())) {
return e;
}
}
return null;
}
// enum -> CodeDto list
public static List<CodeDto> toList(Class<? extends Enum<?>> enumClass) {
Object[] enums = enumClass.getEnumConstants();
return Arrays.stream(enums)
.map(e -> (EnumType) e)
.map(e -> new CodeDto(e.getId(), e.getText()))
.toList();
}
/** 특정 타입(enum)만 조회 /codes/{type} -> type = RoleType 같은 값 */
public static List<CodeDto> getCodes(String type) {
Class<? extends Enum<?>> enumClass = exposedEnumMap.get(type);
if (enumClass == null) {
throw new IllegalArgumentException("지원하지 않는 코드 타입: " + type);
}
return toList(enumClass);
}
/** 전체 enum 코드 조회 */
public static Map<String, List<CodeDto>> getAllCodes() {
Map<String, List<CodeDto>> result = new HashMap<>();
for (Map.Entry<String, Class<? extends Enum<?>>> e : exposedEnumMap.entrySet()) {
result.put(e.getKey(), toList(e.getValue()));
}
return result;
}
/**
* @CodeExpose + EnumType 인 enum만 스캔해서 Map 구성
*/
private static Map<String, Class<? extends Enum<?>>> scanExposedEnumMap() {
Reflections reflections = new Reflections(BASE_PACKAGE);
Set<Class<?>> types = reflections.getTypesAnnotatedWith(CodeExpose.class);
Map<String, Class<? extends Enum<?>>> result = new HashMap<>();
for (Class<?> clazz : types) {
if (clazz.isEnum() && EnumType.class.isAssignableFrom(clazz)) {
result.put(clazz.getSimpleName(), (Class<? extends Enum<?>>) clazz);
}
}
return result;
}
}
package com.kamco.cd.training.common.utils.enums;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.reflections.Reflections;
public class Enums {
private static final String BASE_PACKAGE = "com.kamco.cd.training";
/** 노출 가능한 enum만 모아둔 맵 key: enum simpleName (예: RoleType) value: enum Class */
private static final Map<String, Class<? extends Enum<?>>> exposedEnumMap = scanExposedEnumMap();
// code로 enum 찾기
public static <E extends Enum<E> & EnumType> E fromId(Class<E> enumClass, String id) {
if (id == null) {
return null;
}
for (E e : enumClass.getEnumConstants()) {
if (id.equalsIgnoreCase(e.getId())) {
return e;
}
}
return null;
}
// enum -> CodeDto list
public static List<CodeDto> toList(Class<? extends Enum<?>> enumClass) {
Object[] enums = enumClass.getEnumConstants();
return Arrays.stream(enums)
.map(e -> (EnumType) e)
.map(e -> new CodeDto(e.getId(), e.getText()))
.toList();
}
/** 특정 타입(enum)만 조회 /codes/{type} -> type = RoleType 같은 값 */
public static List<CodeDto> getCodes(String type) {
Class<? extends Enum<?>> enumClass = exposedEnumMap.get(type);
if (enumClass == null) {
throw new IllegalArgumentException("지원하지 않는 코드 타입: " + type);
}
return toList(enumClass);
}
/** 전체 enum 코드 조회 */
public static Map<String, List<CodeDto>> getAllCodes() {
Map<String, List<CodeDto>> result = new HashMap<>();
for (Map.Entry<String, Class<? extends Enum<?>>> e : exposedEnumMap.entrySet()) {
result.put(e.getKey(), toList(e.getValue()));
}
return result;
}
/**
* @CodeExpose + EnumType 인 enum만 스캔해서 Map 구성
*/
private static Map<String, Class<? extends Enum<?>>> scanExposedEnumMap() {
Reflections reflections = new Reflections(BASE_PACKAGE);
Set<Class<?>> types = reflections.getTypesAnnotatedWith(CodeExpose.class);
Map<String, Class<? extends Enum<?>>> result = new HashMap<>();
for (Class<?> clazz : types) {
if (clazz.isEnum() && EnumType.class.isAssignableFrom(clazz)) {
result.put(clazz.getSimpleName(), (Class<? extends Enum<?>>) clazz);
}
}
return result;
}
}

View File

@@ -1,36 +1,36 @@
package com.kamco.cd.training.common.utils.geometry;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import java.io.IOException;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.geojson.GeoJsonReader;
import org.springframework.util.StringUtils;
public class GeometryDeserializer<T extends Geometry> extends StdDeserializer<T> {
public GeometryDeserializer(Class<T> targetType) {
super(targetType);
}
// TODO: test code
@SuppressWarnings("unchecked")
@Override
public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JacksonException {
String json = jsonParser.readValueAsTree().toString();
if (!StringUtils.hasText(json)) {
return null;
}
try {
GeoJsonReader reader = new GeoJsonReader();
return (T) reader.read(json);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to deserialize GeoJSON into Geometry", e);
}
}
}
package com.kamco.cd.training.common.utils.geometry;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import java.io.IOException;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.geojson.GeoJsonReader;
import org.springframework.util.StringUtils;
public class GeometryDeserializer<T extends Geometry> extends StdDeserializer<T> {
public GeometryDeserializer(Class<T> targetType) {
super(targetType);
}
// TODO: test code
@SuppressWarnings("unchecked")
@Override
public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JacksonException {
String json = jsonParser.readValueAsTree().toString();
if (!StringUtils.hasText(json)) {
return null;
}
try {
GeoJsonReader reader = new GeoJsonReader();
return (T) reader.read(json);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to deserialize GeoJSON into Geometry", e);
}
}
}

View File

@@ -1,31 +1,31 @@
package com.kamco.cd.training.common.utils.geometry;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
import java.util.Objects;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.geojson.GeoJsonWriter;
public class GeometrySerializer<T extends Geometry> extends StdSerializer<T> {
// TODO: test code
public GeometrySerializer(Class<T> targetType) {
super(targetType);
}
@Override
public void serialize(
T geometry, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
if (Objects.nonNull(geometry)) {
// default: 8자리 강제로 반올림시킴. 16자리로 늘려줌
GeoJsonWriter writer = new GeoJsonWriter(16);
String json = writer.write(geometry);
jsonGenerator.writeRawValue(json);
} else {
jsonGenerator.writeNull();
}
}
}
package com.kamco.cd.training.common.utils.geometry;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
import java.util.Objects;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.geojson.GeoJsonWriter;
public class GeometrySerializer<T extends Geometry> extends StdSerializer<T> {
// TODO: test code
public GeometrySerializer(Class<T> targetType) {
super(targetType);
}
@Override
public void serialize(
T geometry, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
if (Objects.nonNull(geometry)) {
// default: 8자리 강제로 반올림시킴. 16자리로 늘려줌
GeoJsonWriter writer = new GeoJsonWriter(16);
String json = writer.write(geometry);
jsonGenerator.writeRawValue(json);
} else {
jsonGenerator.writeNull();
}
}
}

View File

@@ -1,18 +1,18 @@
package com.kamco.cd.training.common.utils.html;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import org.springframework.web.util.HtmlUtils;
public class HtmlEscapeDeserializer extends JsonDeserializer<Object> {
@Override
public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JacksonException {
String value = jsonParser.getValueAsString();
return value == null ? null : HtmlUtils.htmlEscape(value);
}
}
package com.kamco.cd.training.common.utils.html;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import org.springframework.web.util.HtmlUtils;
public class HtmlEscapeDeserializer extends JsonDeserializer<Object> {
@Override
public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JacksonException {
String value = jsonParser.getValueAsString();
return value == null ? null : HtmlUtils.htmlEscape(value);
}
}

View File

@@ -1,20 +1,20 @@
package com.kamco.cd.training.common.utils.html;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import org.springframework.web.util.HtmlUtils;
public class HtmlUnescapeSerializer extends JsonSerializer<String> {
@Override
public void serialize(
String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
if (value == null) {
jsonGenerator.writeNull();
} else {
jsonGenerator.writeString(HtmlUtils.htmlUnescape(value));
}
}
}
package com.kamco.cd.training.common.utils.html;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import org.springframework.web.util.HtmlUtils;
public class HtmlUnescapeSerializer extends JsonSerializer<String> {
@Override
public void serialize(
String value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
if (value == null) {
jsonGenerator.writeNull();
} else {
jsonGenerator.writeString(HtmlUtils.htmlUnescape(value));
}
}
}

View File

@@ -1,23 +1,23 @@
package com.kamco.cd.training.common.utils.interfaces;
import com.kamco.cd.training.common.utils.enums.EnumValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValidator.class)
public @interface EnumValid {
String message() default "올바르지 않은 값입니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Enum<?>> enumClass();
}
package com.kamco.cd.training.common.utils.interfaces;
import com.kamco.cd.training.common.utils.enums.EnumValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValidator.class)
public @interface EnumValid {
String message() default "올바르지 않은 값입니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends Enum<?>> enumClass();
}

View File

@@ -1,15 +1,15 @@
package com.kamco.cd.training.common.utils.interfaces;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@JacksonAnnotationsInside
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd'T'HH:mm:ssXXX",
timezone = "Asia/Seoul")
public @interface JsonFormatDttm {}
package com.kamco.cd.training.common.utils.interfaces;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@JacksonAnnotationsInside
@JsonFormat(
shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd'T'HH:mm:ssXXX",
timezone = "Asia/Seoul")
public @interface JsonFormatDttm {}

View File

@@ -1,11 +1,11 @@
package com.kamco.cd.training.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
@EnableCaching
@Configuration
public class CacheConfig {
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
// => org.springframework.cache.annotation.Cacheable
}
package com.kamco.cd.training.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
@EnableCaching
@Configuration
public class CacheConfig {
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
// => org.springframework.cache.annotation.Cacheable
}

View File

@@ -1,27 +1,27 @@
package com.kamco.cd.training.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/** GeoJSON 파일 모니터링 설정 */
@Component
@ConfigurationProperties(prefix = "file.config")
@Getter
@Setter
public class FileConfig {
// private String rootDir = "D:\\app/";
private String rootDir = "/app/";
private String rootSyncDir = rootDir + "original-images/";
private String tmpSyncDir = rootSyncDir + "tmp/";
private String dataSetDir = rootDir + "dataset/";
private String tmpDataSetDir = dataSetDir + "tmp/";
// private String rootSyncDir = "/app/original-images/";
// private String tmpSyncDir = rootSyncDir + "tmp/";
private String syncFileExt = "tfw,tif";
}
package com.kamco.cd.training.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/** GeoJSON 파일 모니터링 설정 */
@Component
@ConfigurationProperties(prefix = "file.config")
@Getter
@Setter
public class FileConfig {
// private String rootDir = "D:\\app/";
private String rootDir = "/app/";
private String rootSyncDir = rootDir + "original-images/";
private String tmpSyncDir = rootSyncDir + "tmp/";
private String dataSetDir = rootDir + "dataset/";
private String tmpDataSetDir = dataSetDir + "tmp/";
// private String rootSyncDir = "/app/original-images/";
// private String tmpSyncDir = rootSyncDir + "tmp/";
private String syncFileExt = "tfw,tif";
}

View File

@@ -1,77 +1,77 @@
package com.kamco.cd.training.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
@Value("${swagger.local-port}")
private String localPort;
@Value("${spring.profiles.active:local}")
private String profile;
@Value("${swagger.dev-url:https://kamco.training-dev-api.gs.dabeeo.com}")
private String devUrl;
@Value("${swagger.prod-url:https://api.training-kamco.com}")
private String prodUrl;
@Bean
public OpenAPI kamcoOpenAPI() {
// 1) SecurityScheme 정의 (Bearer JWT)
SecurityScheme bearerAuth =
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.name("Authorization");
// 2) SecurityRequirement (기본으로 BearerAuth 사용)
SecurityRequirement securityRequirement = new SecurityRequirement().addList("BearerAuth");
// 3) Components 에 SecurityScheme 등록
Components components = new Components().addSecuritySchemes("BearerAuth", bearerAuth);
// profile 별 server url 분기
List<Server> servers = new ArrayList<>();
if ("dev".equals(profile)) {
servers.add(new Server().url(devUrl).description("개발 서버"));
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
// servers.add(new Server().url(prodUrl).description("운영 서버"));
} else if ("prod".equals(profile)) {
// servers.add(new Server().url(prodUrl).description("운영 서버"));
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
servers.add(new Server().url(devUrl).description("개발 서버"));
} else {
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
servers.add(new Server().url(devUrl).description("개발 서버"));
// servers.add(new Server().url(prodUrl).description("운영 서버"));
}
return new OpenAPI()
.info(
new Info()
.title("KAMCO Change Detection API")
.description(
"KAMCO 변화 탐지 시스템 API 문서\n\n"
+ "이 API는 지리공간 데이터를 활용한 변화 탐지 시스템을 제공합니다.\n"
+ "GeoJSON 형식의 공간 데이터를 처리하며, PostgreSQL/PostGIS 기반으로 동작합니다.")
.version("v1.0.0"))
.servers(servers)
// 만들어둔 components를 넣어야 함
.components(components)
.addSecurityItem(securityRequirement);
}
}
package com.kamco.cd.training.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
@Value("${swagger.local-port}")
private String localPort;
@Value("${spring.profiles.active:local}")
private String profile;
@Value("${swagger.dev-url:https://kamco.training-dev-api.gs.dabeeo.com}")
private String devUrl;
@Value("${swagger.prod-url:https://api.training-kamco.com}")
private String prodUrl;
@Bean
public OpenAPI kamcoOpenAPI() {
// 1) SecurityScheme 정의 (Bearer JWT)
SecurityScheme bearerAuth =
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.name("Authorization");
// 2) SecurityRequirement (기본으로 BearerAuth 사용)
SecurityRequirement securityRequirement = new SecurityRequirement().addList("BearerAuth");
// 3) Components 에 SecurityScheme 등록
Components components = new Components().addSecuritySchemes("BearerAuth", bearerAuth);
// profile 별 server url 분기
List<Server> servers = new ArrayList<>();
if ("dev".equals(profile)) {
servers.add(new Server().url(devUrl).description("개발 서버"));
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
// servers.add(new Server().url(prodUrl).description("운영 서버"));
} else if ("prod".equals(profile)) {
// servers.add(new Server().url(prodUrl).description("운영 서버"));
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
servers.add(new Server().url(devUrl).description("개발 서버"));
} else {
servers.add(new Server().url("http://localhost:" + localPort).description("로컬 서버"));
servers.add(new Server().url(devUrl).description("개발 서버"));
// servers.add(new Server().url(prodUrl).description("운영 서버"));
}
return new OpenAPI()
.info(
new Info()
.title("KAMCO Change Detection API")
.description(
"KAMCO 변화 탐지 시스템 API 문서\n\n"
+ "이 API는 지리공간 데이터를 활용한 변화 탐지 시스템을 제공합니다.\n"
+ "GeoJSON 형식의 공간 데이터를 처리하며, PostgreSQL/PostGIS 기반으로 동작합니다.")
.version("v1.0.0"))
.servers(servers)
// 만들어둔 components를 넣어야 함
.components(components)
.addSecurityItem(securityRequirement);
}
}

View File

@@ -1,136 +1,136 @@
package com.kamco.cd.training.config;
import com.kamco.cd.training.auth.CustomAuthenticationProvider;
import com.kamco.cd.training.auth.JwtAuthenticationFilter;
import java.util.List;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(
org.springframework.security.config.annotation.web.builders.HttpSecurity http,
JwtAuthenticationFilter jwtAuthenticationFilter,
CustomAuthenticationProvider customAuthenticationProvider)
throws Exception {
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(form -> form.disable())
// /monitor 에서 Basic 인증을 쓰려면 disable 하면 안됨
.httpBasic(basic -> {})
.logout(logout -> logout.disable())
.authenticationProvider(customAuthenticationProvider)
.authorizeHttpRequests(
auth ->
auth
// monitor
.requestMatchers("/monitor/health", "/monitor/health/**")
.permitAll()
.requestMatchers("/monitor/**")
.authenticated() // Basic으로 인증되게끔
// mapsheet
.requestMatchers("/api/mapsheet/**")
.permitAll()
.requestMatchers(HttpMethod.POST, "/api/mapsheet/upload")
.permitAll()
// test role
.requestMatchers("/api/test/admin")
.hasRole("ADMIN")
.requestMatchers("/api/test/label")
.hasAnyRole("ADMIN", "LABELER")
.requestMatchers("/api/test/review")
.hasAnyRole("ADMIN", "REVIEWER")
// common permit
.requestMatchers("/error")
.permitAll()
.requestMatchers(HttpMethod.OPTIONS, "/**")
.permitAll()
.requestMatchers(
"/api/auth/signin",
"/api/auth/refresh",
"/api/auth/logout",
"/swagger-ui/**",
"/v3/api-docs/**",
"/api/members/*/password",
"/api/upload/chunk-upload-dataset",
"/api/upload/chunk-upload-complete")
.permitAll()
// default
.anyRequest()
.authenticated())
// JWT 필터는 앞단에
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/** CORS 설정 */
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration(); // CORS 객체 생성
config.setAllowedOriginPatterns(List.of("*")); // 도메인 허용
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*")); // 헤더요청 Authorization, Content-Type, X-Custom-Header
config.setAllowCredentials(true); // 쿠키, Authorization 헤더, Bearer Token 등 자격증명 포함 요청을 허용할지 설정
config.setExposedHeaders(List.of("Content-Disposition"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
/** "/**" → 모든 API 경로에 대해 이 CORS 규칙을 적용 /api/** 같이 특정 경로만 지정 가능. */
source.registerCorsConfiguration("/**", config); // CORS 정책을 등록
return source;
}
@Bean
public HttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedSlash(true);
firewall.setAllowUrlEncodedDoubleSlash(true);
firewall.setAllowUrlEncodedPercent(true);
firewall.setAllowSemicolon(true);
return firewall;
}
/** 완전 제외(필터 자체를 안 탐) */
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/api/mapsheet/**");
}
}
package com.kamco.cd.training.config;
import com.kamco.cd.training.auth.CustomAuthenticationProvider;
import com.kamco.cd.training.auth.JwtAuthenticationFilter;
import java.util.List;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(
org.springframework.security.config.annotation.web.builders.HttpSecurity http,
JwtAuthenticationFilter jwtAuthenticationFilter,
CustomAuthenticationProvider customAuthenticationProvider)
throws Exception {
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin(form -> form.disable())
// /monitor 에서 Basic 인증을 쓰려면 disable 하면 안됨
.httpBasic(basic -> {})
.logout(logout -> logout.disable())
.authenticationProvider(customAuthenticationProvider)
.authorizeHttpRequests(
auth ->
auth
// monitor
.requestMatchers("/monitor/health", "/monitor/health/**")
.permitAll()
.requestMatchers("/monitor/**")
.authenticated() // Basic으로 인증되게끔
// mapsheet
.requestMatchers("/api/mapsheet/**")
.permitAll()
.requestMatchers(HttpMethod.POST, "/api/mapsheet/upload")
.permitAll()
// test role
.requestMatchers("/api/test/admin")
.hasRole("ADMIN")
.requestMatchers("/api/test/label")
.hasAnyRole("ADMIN", "LABELER")
.requestMatchers("/api/test/review")
.hasAnyRole("ADMIN", "REVIEWER")
// common permit
.requestMatchers("/error")
.permitAll()
.requestMatchers(HttpMethod.OPTIONS, "/**")
.permitAll()
.requestMatchers(
"/api/auth/signin",
"/api/auth/refresh",
"/api/auth/logout",
"/swagger-ui/**",
"/v3/api-docs/**",
"/api/members/*/password",
"/api/upload/chunk-upload-dataset",
"/api/upload/chunk-upload-complete")
.permitAll()
// default
.anyRequest()
.authenticated())
// JWT 필터는 앞단에
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/** CORS 설정 */
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration(); // CORS 객체 생성
config.setAllowedOriginPatterns(List.of("*")); // 도메인 허용
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*")); // 헤더요청 Authorization, Content-Type, X-Custom-Header
config.setAllowCredentials(true); // 쿠키, Authorization 헤더, Bearer Token 등 자격증명 포함 요청을 허용할지 설정
config.setExposedHeaders(List.of("Content-Disposition"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
/** "/**" → 모든 API 경로에 대해 이 CORS 규칙을 적용 /api/** 같이 특정 경로만 지정 가능. */
source.registerCorsConfiguration("/**", config); // CORS 정책을 등록
return source;
}
@Bean
public HttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedSlash(true);
firewall.setAllowUrlEncodedDoubleSlash(true);
firewall.setAllowUrlEncodedPercent(true);
firewall.setAllowSemicolon(true);
return firewall;
}
/** 완전 제외(필터 자체를 안 탐) */
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/api/mapsheet/**");
}
}

View File

@@ -1,96 +1,96 @@
package com.kamco.cd.training.config;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class StartupLogger {
private final Environment environment;
private final DataSource dataSource;
@EventListener(ApplicationReadyEvent.class)
public void logStartupInfo() {
String[] activeProfiles = environment.getActiveProfiles();
String profileInfo = activeProfiles.length > 0 ? String.join(", ", activeProfiles) : "default";
// Database connection information
String dbUrl = environment.getProperty("spring.datasource.url");
String dbUsername = environment.getProperty("spring.datasource.username");
String dbDriver = environment.getProperty("spring.datasource.driver-class-name");
// HikariCP pool settings
String poolInfo = "";
if (dataSource instanceof HikariDataSource hikariDs) {
poolInfo =
String.format(
"""
│ Pool Size : min=%d, max=%d
│ Connection Timeout: %dms
│ Idle Timeout : %dms
│ Max Lifetime : %dms""",
hikariDs.getMinimumIdle(),
hikariDs.getMaximumPoolSize(),
hikariDs.getConnectionTimeout(),
hikariDs.getIdleTimeout(),
hikariDs.getMaxLifetime());
}
// JPA/Hibernate settings
String showSql = environment.getProperty("spring.jpa.show-sql", "false");
String ddlAuto = environment.getProperty("spring.jpa.hibernate.ddl-auto", "none");
String batchSize =
environment.getProperty("spring.jpa.properties.hibernate.jdbc.batch_size", "N/A");
String batchFetchSize =
environment.getProperty("spring.jpa.properties.hibernate.default_batch_fetch_size", "N/A");
String startupMessage =
String.format(
"""
╔════════════════════════════════════════════════════════════════════════════════╗
║ 🚀 APPLICATION STARTUP INFORMATION ║
╠════════════════════════════════════════════════════════════════════════════════╣
║ PROFILE CONFIGURATION ║
╠────────────────────────────────────────────────────────────────────────────────╣
│ Active Profile(s): %s
╠════════════════════════════════════════════════════════════════════════════════╣
║ DATABASE CONFIGURATION ║
╠────────────────────────────────────────────────────────────────────────────────╣
│ Database URL : %s
│ Username : %s
│ Driver : %s
╠════════════════════════════════════════════════════════════════════════════════╣
║ HIKARICP CONNECTION POOL ║
╠────────────────────────────────────────────────────────────────────────────────╣
%s
╠════════════════════════════════════════════════════════════════════════════════╣
║ JPA/HIBERNATE CONFIGURATION ║
╠────────────────────────────────────────────────────────────────────────────────╣
│ Show SQL : %s
│ DDL Auto : %s
│ JDBC Batch Size : %s
│ Fetch Batch Size : %s
╚════════════════════════════════════════════════════════════════════════════════╝
""",
profileInfo,
dbUrl != null ? dbUrl : "N/A",
dbUsername != null ? dbUsername : "N/A",
dbDriver != null ? dbDriver : "PostgreSQL JDBC Driver (auto-detected)",
poolInfo,
showSql,
ddlAuto,
batchSize,
batchFetchSize);
log.info(startupMessage);
}
}
package com.kamco.cd.training.config;
import com.zaxxer.hikari.HikariDataSource;
import javax.sql.DataSource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class StartupLogger {
private final Environment environment;
private final DataSource dataSource;
@EventListener(ApplicationReadyEvent.class)
public void logStartupInfo() {
String[] activeProfiles = environment.getActiveProfiles();
String profileInfo = activeProfiles.length > 0 ? String.join(", ", activeProfiles) : "default";
// Database connection information
String dbUrl = environment.getProperty("spring.datasource.url");
String dbUsername = environment.getProperty("spring.datasource.username");
String dbDriver = environment.getProperty("spring.datasource.driver-class-name");
// HikariCP pool settings
String poolInfo = "";
if (dataSource instanceof HikariDataSource hikariDs) {
poolInfo =
String.format(
"""
│ Pool Size : min=%d, max=%d
│ Connection Timeout: %dms
│ Idle Timeout : %dms
│ Max Lifetime : %dms""",
hikariDs.getMinimumIdle(),
hikariDs.getMaximumPoolSize(),
hikariDs.getConnectionTimeout(),
hikariDs.getIdleTimeout(),
hikariDs.getMaxLifetime());
}
// JPA/Hibernate settings
String showSql = environment.getProperty("spring.jpa.show-sql", "false");
String ddlAuto = environment.getProperty("spring.jpa.hibernate.ddl-auto", "none");
String batchSize =
environment.getProperty("spring.jpa.properties.hibernate.jdbc.batch_size", "N/A");
String batchFetchSize =
environment.getProperty("spring.jpa.properties.hibernate.default_batch_fetch_size", "N/A");
String startupMessage =
String.format(
"""
╔════════════════════════════════════════════════════════════════════════════════╗
║ 🚀 APPLICATION STARTUP INFORMATION ║
╠════════════════════════════════════════════════════════════════════════════════╣
║ PROFILE CONFIGURATION ║
╠────────────────────────────────────────────────────────────────────────────────╣
│ Active Profile(s): %s
╠════════════════════════════════════════════════════════════════════════════════╣
║ DATABASE CONFIGURATION ║
╠────────────────────────────────────────────────────────────────────────────────╣
│ Database URL : %s
│ Username : %s
│ Driver : %s
╠════════════════════════════════════════════════════════════════════════════════╣
║ HIKARICP CONNECTION POOL ║
╠────────────────────────────────────────────────────────────────────────────────╣
%s
╠════════════════════════════════════════════════════════════════════════════════╣
║ JPA/HIBERNATE CONFIGURATION ║
╠────────────────────────────────────────────────────────────────────────────────╣
│ Show SQL : %s
│ DDL Auto : %s
│ JDBC Batch Size : %s
│ Fetch Batch Size : %s
╚════════════════════════════════════════════════════════════════════════════════╝
""",
profileInfo,
dbUrl != null ? dbUrl : "N/A",
dbUsername != null ? dbUsername : "N/A",
dbDriver != null ? dbDriver : "PostgreSQL JDBC Driver (auto-detected)",
poolInfo,
showSql,
ddlAuto,
batchSize,
batchFetchSize);
log.info(startupMessage);
}
}

View File

@@ -1,13 +1,13 @@
package com.kamco.cd.training.config;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import org.springframework.context.annotation.Configuration;
@Configuration
@SecurityScheme(
name = "BearerAuth",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT")
public class SwaggerConfig {}
package com.kamco.cd.training.config;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import org.springframework.context.annotation.Configuration;
@Configuration
@SecurityScheme(
name = "BearerAuth",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT")
public class SwaggerConfig {}

View File

@@ -1,32 +1,32 @@
package com.kamco.cd.training.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.kamco.cd.training.common.utils.geometry.GeometryDeserializer;
import com.kamco.cd.training.common.utils.geometry.GeometrySerializer;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public ObjectMapper objectMapper() {
SimpleModule module = new SimpleModule();
module.addSerializer(Geometry.class, new GeometrySerializer<>(Geometry.class));
module.addDeserializer(Geometry.class, new GeometryDeserializer<>(Geometry.class));
module.addSerializer(Polygon.class, new GeometrySerializer<>(Polygon.class));
module.addDeserializer(Polygon.class, new GeometryDeserializer<>(Polygon.class));
module.addSerializer(Point.class, new GeometrySerializer<>(Point.class));
module.addDeserializer(Point.class, new GeometryDeserializer<>(Point.class));
return Jackson2ObjectMapperBuilder.json().modulesToInstall(module).build();
}
}
package com.kamco.cd.training.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.kamco.cd.training.common.utils.geometry.GeometryDeserializer;
import com.kamco.cd.training.common.utils.geometry.GeometrySerializer;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public ObjectMapper objectMapper() {
SimpleModule module = new SimpleModule();
module.addSerializer(Geometry.class, new GeometrySerializer<>(Geometry.class));
module.addDeserializer(Geometry.class, new GeometryDeserializer<>(Geometry.class));
module.addSerializer(Polygon.class, new GeometrySerializer<>(Polygon.class));
module.addDeserializer(Polygon.class, new GeometryDeserializer<>(Polygon.class));
module.addSerializer(Point.class, new GeometrySerializer<>(Point.class));
module.addDeserializer(Point.class, new GeometryDeserializer<>(Point.class));
return Jackson2ObjectMapperBuilder.json().modulesToInstall(module).build();
}
}

View File

@@ -1,28 +1,28 @@
package com.kamco.cd.training.config.api;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
@Component
public class ApiLogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
// 반드시 response body copy
wrappedResponse.copyBodyToResponse();
}
}
package com.kamco.cd.training.config.api;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
@Component
public class ApiLogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
// 반드시 response body copy
wrappedResponse.copyBodyToResponse();
}
}

View File

@@ -1,132 +1,132 @@
package com.kamco.cd.training.config.api;
import com.kamco.cd.training.log.dto.EventStatus;
import com.kamco.cd.training.log.dto.EventType;
import com.kamco.cd.training.menu.dto.MenuDto;
import jakarta.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.web.util.ContentCachingRequestWrapper;
public class ApiLogFunction {
// 클라이언트 IP 추출
public static String getClientIp(HttpServletRequest request) {
String[] headers = {
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_CLIENT_IP",
"HTTP_X_FORWARDED_FOR"
};
for (String header : headers) {
String ip = request.getHeader(header);
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
return ip.split(",")[0];
}
}
String ip = request.getRemoteAddr();
if ("0:0:0:0:0:0:0:1".equals(ip)) { // local 일 때
ip = "127.0.0.1";
}
return ip;
}
// 사용자 ID 추출 예시 (Spring Security 기준)
public static String getUserId(HttpServletRequest request) {
try {
return request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : null;
} catch (Exception e) {
return null;
}
}
public static EventType getEventType(HttpServletRequest request) {
String method = request.getMethod().toUpperCase();
String uri = request.getRequestURI().toLowerCase();
// URL 기반 DOWNLOAD/PRINT 분류
if (uri.contains("/download") || uri.contains("/export")) {
return EventType.DOWNLOAD;
}
if (uri.contains("/print")) {
return EventType.PRINT;
}
// 일반 CRUD
return switch (method) {
case "POST" -> EventType.CREATE;
case "GET" -> EventType.READ;
case "DELETE" -> EventType.DELETE;
case "PUT", "PATCH" -> EventType.UPDATE;
default -> EventType.OTHER;
};
}
public static String getRequestBody(
HttpServletRequest servletRequest, ContentCachingRequestWrapper contentWrapper) {
StringBuilder resultBody = new StringBuilder();
// GET, form-urlencoded POST 파라미터
Map<String, String[]> paramMap = servletRequest.getParameterMap();
String queryParams =
paramMap.entrySet().stream()
.map(e -> e.getKey() + "=" + String.join(",", e.getValue()))
.collect(Collectors.joining("&"));
resultBody.append(queryParams.isEmpty() ? "" : queryParams);
// JSON Body
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
&& servletRequest.getContentType() != null
&& servletRequest.getContentType().contains("application/json")) {
try {
// json인 경우는 Wrapper를 통해 가져오기
resultBody.append(getBodyData(contentWrapper));
} catch (Exception e) {
resultBody.append("cannot read JSON body ").append(e.toString());
}
}
// Multipart form-data
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
&& servletRequest.getContentType() != null
&& servletRequest.getContentType().startsWith("multipart/form-data")) {
resultBody.append("multipart/form-data request");
}
return resultBody.toString();
}
// JSON Body 읽기
public static String getBodyData(ContentCachingRequestWrapper request) {
byte[] buf = request.getContentAsByteArray();
if (buf.length == 0) {
return null;
}
try {
return new String(buf, request.getCharacterEncoding());
} catch (UnsupportedEncodingException e) {
return new String(buf);
}
}
// ApiResponse 의 Status가 2xx 범위이면 SUCCESS, 아니면 FAILED
public static EventStatus isSuccessFail(ApiResponseDto<?> apiResponse) {
return apiResponse.getHttpStatus().is2xxSuccessful() ? EventStatus.SUCCESS : EventStatus.FAILED;
}
public static String getUriMenuInfo(List<MenuDto.Basic> menuList, String uri) {
MenuDto.Basic m =
menuList.stream()
.filter(menu -> menu.getMenuApiUrl() != null && uri.contains(menu.getMenuApiUrl()))
.findFirst()
.orElse(null);
return m != null ? m.getMenuUid() : "SYSTEM";
}
}
package com.kamco.cd.training.config.api;
import com.kamco.cd.training.log.dto.EventStatus;
import com.kamco.cd.training.log.dto.EventType;
import com.kamco.cd.training.menu.dto.MenuDto;
import jakarta.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.web.util.ContentCachingRequestWrapper;
public class ApiLogFunction {
// 클라이언트 IP 추출
public static String getClientIp(HttpServletRequest request) {
String[] headers = {
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_CLIENT_IP",
"HTTP_X_FORWARDED_FOR"
};
for (String header : headers) {
String ip = request.getHeader(header);
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
return ip.split(",")[0];
}
}
String ip = request.getRemoteAddr();
if ("0:0:0:0:0:0:0:1".equals(ip)) { // local 일 때
ip = "127.0.0.1";
}
return ip;
}
// 사용자 ID 추출 예시 (Spring Security 기준)
public static String getUserId(HttpServletRequest request) {
try {
return request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : null;
} catch (Exception e) {
return null;
}
}
public static EventType getEventType(HttpServletRequest request) {
String method = request.getMethod().toUpperCase();
String uri = request.getRequestURI().toLowerCase();
// URL 기반 DOWNLOAD/PRINT 분류
if (uri.contains("/download") || uri.contains("/export")) {
return EventType.DOWNLOAD;
}
if (uri.contains("/print")) {
return EventType.PRINT;
}
// 일반 CRUD
return switch (method) {
case "POST" -> EventType.CREATE;
case "GET" -> EventType.READ;
case "DELETE" -> EventType.DELETE;
case "PUT", "PATCH" -> EventType.UPDATE;
default -> EventType.OTHER;
};
}
public static String getRequestBody(
HttpServletRequest servletRequest, ContentCachingRequestWrapper contentWrapper) {
StringBuilder resultBody = new StringBuilder();
// GET, form-urlencoded POST 파라미터
Map<String, String[]> paramMap = servletRequest.getParameterMap();
String queryParams =
paramMap.entrySet().stream()
.map(e -> e.getKey() + "=" + String.join(",", e.getValue()))
.collect(Collectors.joining("&"));
resultBody.append(queryParams.isEmpty() ? "" : queryParams);
// JSON Body
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
&& servletRequest.getContentType() != null
&& servletRequest.getContentType().contains("application/json")) {
try {
// json인 경우는 Wrapper를 통해 가져오기
resultBody.append(getBodyData(contentWrapper));
} catch (Exception e) {
resultBody.append("cannot read JSON body ").append(e.toString());
}
}
// Multipart form-data
if ("POST".equalsIgnoreCase(servletRequest.getMethod())
&& servletRequest.getContentType() != null
&& servletRequest.getContentType().startsWith("multipart/form-data")) {
resultBody.append("multipart/form-data request");
}
return resultBody.toString();
}
// JSON Body 읽기
public static String getBodyData(ContentCachingRequestWrapper request) {
byte[] buf = request.getContentAsByteArray();
if (buf.length == 0) {
return null;
}
try {
return new String(buf, request.getCharacterEncoding());
} catch (UnsupportedEncodingException e) {
return new String(buf);
}
}
// ApiResponse 의 Status가 2xx 범위이면 SUCCESS, 아니면 FAILED
public static EventStatus isSuccessFail(ApiResponseDto<?> apiResponse) {
return apiResponse.getHttpStatus().is2xxSuccessful() ? EventStatus.SUCCESS : EventStatus.FAILED;
}
public static String getUriMenuInfo(List<MenuDto.Basic> menuList, String uri) {
MenuDto.Basic m =
menuList.stream()
.filter(menu -> menu.getMenuApiUrl() != null && uri.contains(menu.getMenuApiUrl()))
.findFirst()
.orElse(null);
return m != null ? m.getMenuUid() : "SYSTEM";
}
}

View File

@@ -1,124 +1,124 @@
package com.kamco.cd.training.config.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.training.auth.CustomUserDetails;
import com.kamco.cd.training.menu.service.MenuService;
import com.kamco.cd.training.postgres.entity.AuditLogEntity;
import com.kamco.cd.training.postgres.repository.log.AuditLogRepository;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.springframework.web.util.ContentCachingRequestWrapper;
/**
* ApiResponseDto의 내장된 HTTP 상태 코드를 실제 HTTP 응답에 적용하는 Advice
*
* <p>createOK() → 201 CREATED ok() → 200 OK deleteOk() → 204 NO_CONTENT
*/
@RestControllerAdvice
public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
private final AuditLogRepository auditLogRepository;
private final MenuService menuService;
@Autowired private ObjectMapper objectMapper;
public ApiResponseAdvice(AuditLogRepository auditLogRepository, MenuService menuService) {
this.auditLogRepository = auditLogRepository;
this.menuService = menuService;
}
@Override
public boolean supports(
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// ApiResponseDto를 반환하는 경우에만 적용
return returnType.getParameterType().equals(ApiResponseDto.class);
}
@Override
public Object beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
ContentCachingRequestWrapper contentWrapper = null;
if (servletRequest instanceof ContentCachingRequestWrapper wrapper) {
contentWrapper = wrapper;
}
if (body instanceof ApiResponseDto<?> apiResponse) {
response.setStatusCode(apiResponse.getHttpStatus());
String ip = ApiLogFunction.getClientIp(servletRequest);
Long userid = null;
if (servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth
&& auth.getPrincipal() instanceof CustomUserDetails customUserDetails) {
userid = customUserDetails.getMember().getId();
}
String requestBody;
// 멀티파트 요청은 바디 로깅을 생략 (파일 바이너리로 인한 문제 예방)
MediaType reqContentType = null;
try {
String ct = servletRequest.getContentType();
reqContentType = ct != null ? MediaType.valueOf(ct) : null;
} catch (Exception ignored) {
}
if (reqContentType != null && MediaType.MULTIPART_FORM_DATA.includes(reqContentType)) {
requestBody = "(multipart omitted)";
} else {
requestBody = ApiLogFunction.getRequestBody(servletRequest, contentWrapper);
requestBody = maskSensitiveFields(requestBody);
}
AuditLogEntity log =
new AuditLogEntity(
userid,
ApiLogFunction.getEventType(servletRequest),
ApiLogFunction.isSuccessFail(apiResponse),
ApiLogFunction.getUriMenuInfo(
menuService.getFindAll(), servletRequest.getRequestURI()),
ip,
servletRequest.getRequestURI(),
requestBody,
apiResponse.getErrorLogUid());
auditLogRepository.save(log);
}
return body;
}
/**
* 마스킹
*
* @param json
* @return
*/
private String maskSensitiveFields(String json) {
if (json == null) {
return null;
}
// password 마스킹
json = json.replaceAll("\"password\"\\s*:\\s*\"[^\"]*\"", "\"password\":\"****\"");
// 토큰 마스킹
json = json.replaceAll("\"accessToken\"\\s*:\\s*\"[^\"]*\"", "\"accessToken\":\"****\"");
json = json.replaceAll("\"refreshToken\"\\s*:\\s*\"[^\"]*\"", "\"refreshToken\":\"****\"");
return json;
}
}
package com.kamco.cd.training.config.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.training.auth.CustomUserDetails;
import com.kamco.cd.training.menu.service.MenuService;
import com.kamco.cd.training.postgres.entity.AuditLogEntity;
import com.kamco.cd.training.postgres.repository.log.AuditLogRepository;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.springframework.web.util.ContentCachingRequestWrapper;
/**
* ApiResponseDto의 내장된 HTTP 상태 코드를 실제 HTTP 응답에 적용하는 Advice
*
* <p>createOK() → 201 CREATED ok() → 200 OK deleteOk() → 204 NO_CONTENT
*/
@RestControllerAdvice
public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {
private final AuditLogRepository auditLogRepository;
private final MenuService menuService;
@Autowired private ObjectMapper objectMapper;
public ApiResponseAdvice(AuditLogRepository auditLogRepository, MenuService menuService) {
this.auditLogRepository = auditLogRepository;
this.menuService = menuService;
}
@Override
public boolean supports(
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// ApiResponseDto를 반환하는 경우에만 적용
return returnType.getParameterType().equals(ApiResponseDto.class);
}
@Override
public Object beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
ContentCachingRequestWrapper contentWrapper = null;
if (servletRequest instanceof ContentCachingRequestWrapper wrapper) {
contentWrapper = wrapper;
}
if (body instanceof ApiResponseDto<?> apiResponse) {
response.setStatusCode(apiResponse.getHttpStatus());
String ip = ApiLogFunction.getClientIp(servletRequest);
Long userid = null;
if (servletRequest.getUserPrincipal() instanceof UsernamePasswordAuthenticationToken auth
&& auth.getPrincipal() instanceof CustomUserDetails customUserDetails) {
userid = customUserDetails.getMember().getId();
}
String requestBody;
// 멀티파트 요청은 바디 로깅을 생략 (파일 바이너리로 인한 문제 예방)
MediaType reqContentType = null;
try {
String ct = servletRequest.getContentType();
reqContentType = ct != null ? MediaType.valueOf(ct) : null;
} catch (Exception ignored) {
}
if (reqContentType != null && MediaType.MULTIPART_FORM_DATA.includes(reqContentType)) {
requestBody = "(multipart omitted)";
} else {
requestBody = ApiLogFunction.getRequestBody(servletRequest, contentWrapper);
requestBody = maskSensitiveFields(requestBody);
}
AuditLogEntity log =
new AuditLogEntity(
userid,
ApiLogFunction.getEventType(servletRequest),
ApiLogFunction.isSuccessFail(apiResponse),
ApiLogFunction.getUriMenuInfo(
menuService.getFindAll(), servletRequest.getRequestURI()),
ip,
servletRequest.getRequestURI(),
requestBody,
apiResponse.getErrorLogUid());
auditLogRepository.save(log);
}
return body;
}
/**
* 마스킹
*
* @param json
* @return
*/
private String maskSensitiveFields(String json) {
if (json == null) {
return null;
}
// password 마스킹
json = json.replaceAll("\"password\"\\s*:\\s*\"[^\"]*\"", "\"password\":\"****\"");
// 토큰 마스킹
json = json.replaceAll("\"accessToken\"\\s*:\\s*\"[^\"]*\"", "\"accessToken\":\"****\"");
json = json.replaceAll("\"refreshToken\"\\s*:\\s*\"[^\"]*\"", "\"refreshToken\":\"****\"");
return json;
}
}

View File

@@ -1,199 +1,199 @@
package com.kamco.cd.training.config.api;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.springframework.http.HttpStatus;
@Getter
@ToString
public class ApiResponseDto<T> {
private T data;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Error error;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T errorData;
@JsonIgnore private HttpStatus httpStatus;
@JsonIgnore private Long errorLogUid;
public ApiResponseDto(T data) {
this.data = data;
}
private ApiResponseDto(T data, HttpStatus httpStatus) {
this.data = data;
this.httpStatus = httpStatus;
}
public ApiResponseDto(ApiResponseCode code) {
this.error = new Error(code.getId(), code.getMessage());
}
public ApiResponseDto(ApiResponseCode code, String message) {
this.error = new Error(code.getId(), message);
}
public ApiResponseDto(ApiResponseCode code, String message, HttpStatus httpStatus) {
this.error = new Error(code.getId(), message);
this.httpStatus = httpStatus;
}
public ApiResponseDto(
ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) {
this.error = new Error(code.getId(), message);
this.httpStatus = httpStatus;
this.errorLogUid = errorLogUid;
}
public ApiResponseDto(ApiResponseCode code, String message, T errorData) {
this.error = new Error(code.getId(), message);
this.errorData = errorData;
}
// HTTP 상태 코드가 내장된 ApiResponseDto 반환 메서드들
public static <T> ApiResponseDto<T> createOK(T data) {
return new ApiResponseDto<>(data, HttpStatus.CREATED);
}
public static <T> ApiResponseDto<T> ok(T data) {
return new ApiResponseDto<>(data, HttpStatus.OK);
}
public static <T> ApiResponseDto<ResponseObj> okObject(ResponseObj data) {
if (data.getCode().equals(ApiResponseCode.OK)) {
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
} else {
return new ApiResponseDto<>(data.getCode(), data.getMessage(), HttpStatus.CONFLICT);
}
}
public static <T> ApiResponseDto<T> deleteOk(T data) {
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
}
public static ApiResponseDto<String> createException(ApiResponseCode code) {
return new ApiResponseDto<>(code);
}
public static ApiResponseDto<String> createException(ApiResponseCode code, String message) {
return new ApiResponseDto<>(code, message);
}
public static ApiResponseDto<String> createException(
ApiResponseCode code, String message, HttpStatus httpStatus) {
return new ApiResponseDto<>(code, message, httpStatus);
}
public static ApiResponseDto<String> createException(
ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) {
return new ApiResponseDto<>(code, message, httpStatus, errorLogUid);
}
public static <T> ApiResponseDto<T> createException(
ApiResponseCode code, String message, T data) {
return new ApiResponseDto<>(code, message, data);
}
@Getter
public static class Error {
private final String code;
private final String message;
public Error(String code, String message) {
this.code = code;
this.message = message;
}
}
/** Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object */
@Getter
public static class ResponseObj {
private final ApiResponseCode code;
private final String message;
public ResponseObj(ApiResponseCode code, String message) {
this.code = code;
this.message = message;
}
}
@Getter
@RequiredArgsConstructor
public enum ApiResponseCode implements EnumType {
// @formatter:off
OK("요청이 성공하였습니다."),
BAD_REQUEST("요청 파라미터가 잘못되었습니다."),
BAD_GATEWAY("네트워크 상태가 불안정합니다."),
ALREADY_EXIST_MALL("이미 등록된 쇼핑센터입니다."),
NOT_FOUND_MAP("지도를 찾을 수 없습니다."),
UNAUTHORIZED("권한이 없습니다."),
CONFLICT("이미 등록된 컨텐츠입니다."),
NOT_FOUND("Resource를 찾을 수 없습니다."),
NOT_FOUND_DATA("데이터를 찾을 수 없습니다."),
NOT_FOUND_WEATHER_DATA("날씨 데이터를 찾을 수 없습니다."),
FAIL_SEND_MESSAGE("메시지를 전송하지 못했습니다."),
TOO_MANY_CONNECTED_MACHINES("연결된 기기가 너무 많습니다."),
UNAUTHENTICATED("인증에 실패하였습니다."),
INVALID_TOKEN("잘못된 토큰입니다."),
EXPIRED_TOKEN("만료된 토큰입니다."),
INTERNAL_SERVER_ERROR("서버에 문제가 발생 하였습니다."),
FORBIDDEN("권한을 확인해주세요."),
INVALID_PASSWORD("잘못된 비밀번호 입니다."),
NOT_FOUND_CAR_IN("입차정보가 없습니다."),
WRONG_STATUS("잘못된 상태입니다."),
FAIL_VERIFICATION("인증에 실패하였습니다."),
INVALID_EMAIL("잘못된 형식의 이메일입니다."),
REQUIRED_EMAIL("이메일은 필수 항목입니다."),
WRONG_PASSWORD("잘못된 패스워드입니다."),
DUPLICATE_EMAIL("이미 가입된 이메일입니다."),
DUPLICATE_DATA("이미 등록되어 있습니다."),
DATA_INTEGRITY_ERROR("데이터 무결성이 위반되어 요청을 처리할수 없습니다."),
FOREIGN_KEY_ERROR("참조 중인 데이터가 있어 삭제할 수 없습니다."),
DUPLICATE_EMPLOYEEID("이미 가입된 사번입니다."),
NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."),
NOT_FOUND_USER("사용자를 찾을 수 없습니다."),
UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."),
LOGIN_ID_NOT_FOUND("아이디를 잘못 입력하셨습니다."),
LOGIN_PASSWORD_MISMATCH("비밀번호를 잘못 입력하셨습니다."),
LOGIN_PASSWORD_EXCEEDED("비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.\n로그인 오류에 대해 관리자에게 문의하시기 바랍니다."),
REFRESH_TOKEN_EXPIRED_OR_REVOKED("토큰 정보가 만료 되었습니다."),
REFRESH_TOKEN_MISMATCH("토큰 정보가 일치하지 않습니다."),
INACTIVE_ID("사용할 수 없는 계정입니다."),
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"
+ " Password.\""),
;
// @formatter:on
private final String message;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return message;
}
public static ApiResponseCode getCode(String name) {
return ApiResponseCode.valueOf(name.toUpperCase());
}
public static String getMessage(String name) {
return ApiResponseCode.valueOf(name.toUpperCase()).getText();
}
}
}
package com.kamco.cd.training.config.api;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import org.springframework.http.HttpStatus;
@Getter
@ToString
public class ApiResponseDto<T> {
private T data;
@JsonInclude(JsonInclude.Include.NON_NULL)
private Error error;
@JsonInclude(JsonInclude.Include.NON_NULL)
private T errorData;
@JsonIgnore private HttpStatus httpStatus;
@JsonIgnore private Long errorLogUid;
public ApiResponseDto(T data) {
this.data = data;
}
private ApiResponseDto(T data, HttpStatus httpStatus) {
this.data = data;
this.httpStatus = httpStatus;
}
public ApiResponseDto(ApiResponseCode code) {
this.error = new Error(code.getId(), code.getMessage());
}
public ApiResponseDto(ApiResponseCode code, String message) {
this.error = new Error(code.getId(), message);
}
public ApiResponseDto(ApiResponseCode code, String message, HttpStatus httpStatus) {
this.error = new Error(code.getId(), message);
this.httpStatus = httpStatus;
}
public ApiResponseDto(
ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) {
this.error = new Error(code.getId(), message);
this.httpStatus = httpStatus;
this.errorLogUid = errorLogUid;
}
public ApiResponseDto(ApiResponseCode code, String message, T errorData) {
this.error = new Error(code.getId(), message);
this.errorData = errorData;
}
// HTTP 상태 코드가 내장된 ApiResponseDto 반환 메서드들
public static <T> ApiResponseDto<T> createOK(T data) {
return new ApiResponseDto<>(data, HttpStatus.CREATED);
}
public static <T> ApiResponseDto<T> ok(T data) {
return new ApiResponseDto<>(data, HttpStatus.OK);
}
public static <T> ApiResponseDto<ResponseObj> okObject(ResponseObj data) {
if (data.getCode().equals(ApiResponseCode.OK)) {
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
} else {
return new ApiResponseDto<>(data.getCode(), data.getMessage(), HttpStatus.CONFLICT);
}
}
public static <T> ApiResponseDto<T> deleteOk(T data) {
return new ApiResponseDto<>(data, HttpStatus.NO_CONTENT);
}
public static ApiResponseDto<String> createException(ApiResponseCode code) {
return new ApiResponseDto<>(code);
}
public static ApiResponseDto<String> createException(ApiResponseCode code, String message) {
return new ApiResponseDto<>(code, message);
}
public static ApiResponseDto<String> createException(
ApiResponseCode code, String message, HttpStatus httpStatus) {
return new ApiResponseDto<>(code, message, httpStatus);
}
public static ApiResponseDto<String> createException(
ApiResponseCode code, String message, HttpStatus httpStatus, Long errorLogUid) {
return new ApiResponseDto<>(code, message, httpStatus, errorLogUid);
}
public static <T> ApiResponseDto<T> createException(
ApiResponseCode code, String message, T data) {
return new ApiResponseDto<>(code, message, data);
}
@Getter
public static class Error {
private final String code;
private final String message;
public Error(String code, String message) {
this.code = code;
this.message = message;
}
}
/** Error가 아닌 Business상 성공이거나 실패인 경우, 메세지 함께 전달하기 위한 object */
@Getter
public static class ResponseObj {
private final ApiResponseCode code;
private final String message;
public ResponseObj(ApiResponseCode code, String message) {
this.code = code;
this.message = message;
}
}
@Getter
@RequiredArgsConstructor
public enum ApiResponseCode implements EnumType {
// @formatter:off
OK("요청이 성공하였습니다."),
BAD_REQUEST("요청 파라미터가 잘못되었습니다."),
BAD_GATEWAY("네트워크 상태가 불안정합니다."),
ALREADY_EXIST_MALL("이미 등록된 쇼핑센터입니다."),
NOT_FOUND_MAP("지도를 찾을 수 없습니다."),
UNAUTHORIZED("권한이 없습니다."),
CONFLICT("이미 등록된 컨텐츠입니다."),
NOT_FOUND("Resource를 찾을 수 없습니다."),
NOT_FOUND_DATA("데이터를 찾을 수 없습니다."),
NOT_FOUND_WEATHER_DATA("날씨 데이터를 찾을 수 없습니다."),
FAIL_SEND_MESSAGE("메시지를 전송하지 못했습니다."),
TOO_MANY_CONNECTED_MACHINES("연결된 기기가 너무 많습니다."),
UNAUTHENTICATED("인증에 실패하였습니다."),
INVALID_TOKEN("잘못된 토큰입니다."),
EXPIRED_TOKEN("만료된 토큰입니다."),
INTERNAL_SERVER_ERROR("서버에 문제가 발생 하였습니다."),
FORBIDDEN("권한을 확인해주세요."),
INVALID_PASSWORD("잘못된 비밀번호 입니다."),
NOT_FOUND_CAR_IN("입차정보가 없습니다."),
WRONG_STATUS("잘못된 상태입니다."),
FAIL_VERIFICATION("인증에 실패하였습니다."),
INVALID_EMAIL("잘못된 형식의 이메일입니다."),
REQUIRED_EMAIL("이메일은 필수 항목입니다."),
WRONG_PASSWORD("잘못된 패스워드입니다."),
DUPLICATE_EMAIL("이미 가입된 이메일입니다."),
DUPLICATE_DATA("이미 등록되어 있습니다."),
DATA_INTEGRITY_ERROR("데이터 무결성이 위반되어 요청을 처리할수 없습니다."),
FOREIGN_KEY_ERROR("참조 중인 데이터가 있어 삭제할 수 없습니다."),
DUPLICATE_EMPLOYEEID("이미 가입된 사번입니다."),
NOT_FOUND_USER_FOR_EMAIL("이메일로 유저를 찾을 수 없습니다."),
NOT_FOUND_USER("사용자를 찾을 수 없습니다."),
UNPROCESSABLE_ENTITY("이 데이터는 삭제할 수 없습니다."),
LOGIN_ID_NOT_FOUND("아이디를 잘못 입력하셨습니다."),
LOGIN_PASSWORD_MISMATCH("비밀번호를 잘못 입력하셨습니다."),
LOGIN_PASSWORD_EXCEEDED("비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다.\n로그인 오류에 대해 관리자에게 문의하시기 바랍니다."),
REFRESH_TOKEN_EXPIRED_OR_REVOKED("토큰 정보가 만료 되었습니다."),
REFRESH_TOKEN_MISMATCH("토큰 정보가 일치하지 않습니다."),
INACTIVE_ID("사용할 수 없는 계정입니다."),
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"
+ " Password.\""),
;
// @formatter:on
private final String message;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return message;
}
public static ApiResponseCode getCode(String name) {
return ApiResponseCode.valueOf(name.toUpperCase());
}
public static String getMessage(String name) {
return ApiResponseCode.valueOf(name.toUpperCase()).getText();
}
}
}

View File

@@ -1,154 +1,154 @@
package com.kamco.cd.training.dataset;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.dataset.service.DatasetService;
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 java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
@Tag(name = "데이터셋 관리", description = "어드민 홈 > 학습데이터관리 > 전체데이터 API")
@RestController
@RequestMapping("/api/datasets")
@RequiredArgsConstructor
public class DatasetApiController {
private final DatasetService datasetService;
@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
public ApiResponseDto<Page<DatasetDto.Basic>> searchDatasets(
@Parameter(description = "구분", example = "DELIVER(납품), PRODUCTION(제작)")
@RequestParam(required = false)
String groupTitle,
@Parameter(description = "제목", example = "") @RequestParam(required = false) String title,
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
int page,
@Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20")
int size) {
DatasetDto.SearchReq searchReq = new DatasetDto.SearchReq();
searchReq.setTitle(title);
searchReq.setGroupTitle(groupTitle);
searchReq.setPage(page);
searchReq.setSize(size);
return ApiResponseDto.ok(datasetService.searchDatasets(searchReq));
}
@Operation(summary = "데이터셋 상세 조회", description = "데이터셋 상세 정보를 조회합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = DatasetDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/{uuid}")
public ApiResponseDto<DatasetDto.Basic> getDatasetDetail(@PathVariable UUID uuid) {
return ApiResponseDto.ok(datasetService.getDatasetDetail(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 = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/register")
public ApiResponseDto<Long> registerDataset(
@RequestBody @Valid DatasetDto.RegisterReq registerReq) {
Long id = datasetService.registerDataset(registerReq);
return ApiResponseDto.createOK(id);
}
@Operation(summary = "데이터셋 수정", description = "데이터셋 정보를 수정합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
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<UUID> updateDataset(
@PathVariable UUID uuid, @RequestBody DatasetDto.UpdateReq updateReq) {
datasetService.updateDataset(uuid, updateReq);
return ApiResponseDto.ok(uuid);
}
@Operation(summary = "데이터셋 삭제", description = "데이터셋을 삭제합니다.")
@ApiResponses(
value = {
@ApiResponse(responseCode = "201", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/{uuid}")
public ApiResponseDto<UUID> deleteDatasets(@PathVariable UUID uuid) {
datasetService.deleteDatasets(uuid);
return ApiResponseDto.ok(uuid);
}
/*
@Operation(summary = "데이터셋 통계 요약", description = "선택 데이터셋의 통계를 요약합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = DatasetDto.Summary.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/summary")
public ApiResponseDto<DatasetDto.Summary> getDatasetSummary(
@RequestBody @Valid DatasetDto.SummaryReq summaryReq) {
return ApiResponseDto.ok(datasetService.getDatasetSummary(summaryReq));
}
*/
}
package com.kamco.cd.training.dataset;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.dataset.service.DatasetService;
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 java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
@Tag(name = "데이터셋 관리", description = "어드민 홈 > 학습데이터관리 > 전체데이터 API")
@RestController
@RequestMapping("/api/datasets")
@RequiredArgsConstructor
public class DatasetApiController {
private final DatasetService datasetService;
@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
public ApiResponseDto<Page<DatasetDto.Basic>> searchDatasets(
@Parameter(description = "구분", example = "DELIVER(납품), PRODUCTION(제작)")
@RequestParam(required = false)
String groupTitle,
@Parameter(description = "제목", example = "") @RequestParam(required = false) String title,
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
int page,
@Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20")
int size) {
DatasetDto.SearchReq searchReq = new DatasetDto.SearchReq();
searchReq.setTitle(title);
searchReq.setGroupTitle(groupTitle);
searchReq.setPage(page);
searchReq.setSize(size);
return ApiResponseDto.ok(datasetService.searchDatasets(searchReq));
}
@Operation(summary = "데이터셋 상세 조회", description = "데이터셋 상세 정보를 조회합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = DatasetDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/{uuid}")
public ApiResponseDto<DatasetDto.Basic> getDatasetDetail(@PathVariable UUID uuid) {
return ApiResponseDto.ok(datasetService.getDatasetDetail(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 = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/register")
public ApiResponseDto<Long> registerDataset(
@RequestBody @Valid DatasetDto.RegisterReq registerReq) {
Long id = datasetService.registerDataset(registerReq);
return ApiResponseDto.createOK(id);
}
@Operation(summary = "데이터셋 수정", description = "데이터셋 정보를 수정합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
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<UUID> updateDataset(
@PathVariable UUID uuid, @RequestBody DatasetDto.UpdateReq updateReq) {
datasetService.updateDataset(uuid, updateReq);
return ApiResponseDto.ok(uuid);
}
@Operation(summary = "데이터셋 삭제", description = "데이터셋을 삭제합니다.")
@ApiResponses(
value = {
@ApiResponse(responseCode = "201", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/{uuid}")
public ApiResponseDto<UUID> deleteDatasets(@PathVariable UUID uuid) {
datasetService.deleteDatasets(uuid);
return ApiResponseDto.ok(uuid);
}
/*
@Operation(summary = "데이터셋 통계 요약", description = "선택 데이터셋의 통계를 요약합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = DatasetDto.Summary.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/summary")
public ApiResponseDto<DatasetDto.Summary> getDatasetSummary(
@RequestBody @Valid DatasetDto.SummaryReq summaryReq) {
return ApiResponseDto.ok(datasetService.getDatasetSummary(summaryReq));
}
*/
}

View File

@@ -1,56 +1,56 @@
package com.kamco.cd.training.dataset;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.dataset.dto.MapSheetDto;
import com.kamco.cd.training.dataset.service.MapSheetService;
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.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
@Tag(name = "도엽 관리", description = "도엽(MapSheet) 관리 API")
@RestController
@RequiredArgsConstructor
public class MapSheetApiController {
private final MapSheetService mapSheetService;
@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)
})
@PostMapping("/api/datasets/items/search")
public ApiResponseDto<Page<MapSheetDto.Basic>> searchMapSheets(
@RequestBody @Valid MapSheetDto.SearchReq searchReq) {
return ApiResponseDto.ok(mapSheetService.searchMapSheets(searchReq));
}
@Operation(summary = "도엽 삭제", description = "도엽을 삭제합니다 (다건 지원).")
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "404", description = "도엽을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/api/datasets/items/delete")
public ApiResponseDto<Void> deleteMapSheets(@RequestBody @Valid MapSheetDto.DeleteReq deleteReq) {
mapSheetService.deleteMapSheets(deleteReq);
return ApiResponseDto.ok(null);
}
}
package com.kamco.cd.training.dataset;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.dataset.dto.MapSheetDto;
import com.kamco.cd.training.dataset.service.MapSheetService;
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.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;
@Tag(name = "도엽 관리", description = "도엽(MapSheet) 관리 API")
@RestController
@RequiredArgsConstructor
public class MapSheetApiController {
private final MapSheetService mapSheetService;
@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)
})
@PostMapping("/api/datasets/items/search")
public ApiResponseDto<Page<MapSheetDto.Basic>> searchMapSheets(
@RequestBody @Valid MapSheetDto.SearchReq searchReq) {
return ApiResponseDto.ok(mapSheetService.searchMapSheets(searchReq));
}
@Operation(summary = "도엽 삭제", description = "도엽을 삭제합니다 (다건 지원).")
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "404", description = "도엽을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/api/datasets/items/delete")
public ApiResponseDto<Void> deleteMapSheets(@RequestBody @Valid MapSheetDto.DeleteReq deleteReq) {
mapSheetService.deleteMapSheets(deleteReq);
return ApiResponseDto.ok(null);
}
}

View File

@@ -1,212 +1,212 @@
package com.kamco.cd.training.dataset.dto;
import com.kamco.cd.training.common.enums.LearnDataRegister;
import com.kamco.cd.training.common.enums.LearnDataType;
import com.kamco.cd.training.common.utils.enums.Enums;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class DatasetDto {
@Schema(name = "Dataset Basic", description = "데이터셋 기본 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Basic {
private Long id;
private UUID uuid;
private String groupTitle;
private String groupTitleCd;
private String title;
private Long roundNo;
private String totalSize;
private String memo;
@JsonFormatDttm private ZonedDateTime createdDttm;
private String status;
private String statusCd;
private Boolean deleted;
public Basic(
Long id,
UUID uuid,
String groupTitle,
String title,
Long roundNo,
Long totalSize,
String memo,
ZonedDateTime createdDttm,
String status,
Boolean deleted) {
this.id = id;
this.uuid = uuid;
this.groupTitle = getGroupTitle(groupTitle);
this.groupTitleCd = groupTitle;
this.title = title;
this.roundNo = roundNo;
this.totalSize = getTotalSize(totalSize);
this.memo = memo;
this.createdDttm = createdDttm;
this.status = getStatus(status);
this.statusCd = status;
this.deleted = deleted;
}
public String getTotalSize(Long totalSize) {
if (totalSize == null) return "0G";
double giga = totalSize / (1024.0 * 1024 * 1024);
return String.format("%.2fG", giga);
}
public String getGroupTitle(String groupTitleCd) {
LearnDataType type = Enums.fromId(LearnDataType.class, groupTitleCd);
return type == null ? null : type.getText();
}
public String getStatus(String status) {
LearnDataRegister type = Enums.fromId(LearnDataRegister.class, status);
return type == null ? null : type.getText();
}
}
@Schema(name = "Dataset Detail", description = "데이터셋 상세 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Detail {
private Long id;
private String groupTitle;
private String title;
private Long roundNo;
private String totalSize;
private String memo;
@JsonFormatDttm private ZonedDateTime createdDttm;
private String status;
private Boolean deleted;
}
@Schema(name = "DatasetSearchReq", description = "데이터셋 목록 조회 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
@Schema(description = "구분", example = "DELIVER(납품), PRODUCTION(제작)")
private String groupTitle;
@Schema(description = "제목 (부분 검색)", example = "1차")
private String title;
@Schema(description = "페이지 번호 (1부터 시작)", example = "1")
private int page = 1;
@Schema(description = "페이지 크기", example = "20")
private int size = 20;
public Pageable toPageable() {
// API에서는 1부터 시작하지만 내부적으로는 0부터 시작
int pageIndex = Math.max(0, page - 1);
return PageRequest.of(pageIndex, size, Sort.by(Sort.Direction.DESC, "createdDttm"));
}
}
@Schema(name = "DatasetDetailReq", description = "데이터셋 상세 조회 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class DetailReq {
@NotNull(message = "데이터셋 ID는 필수입니다")
@Schema(description = "데이터셋 ID", example = "101")
private Long datasetId;
}
@Schema(name = "DatasetRegisterReq", description = "데이터셋 등록 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class RegisterReq {
@NotBlank(message = "제목은 필수입니다")
@Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다")
@Schema(description = "제목", example = "1차 제작")
private String title;
@NotBlank(message = "연도는 필수입니다")
@Size(max = 4, message = "연도는 4자리입니다")
@Schema(description = "연도 (YYYY)", example = "2024")
private String year;
@Schema(description = "회차", example = "1")
private Long roundNo;
@Schema(description = "메모", example = "데이터셋 설명")
private String memo;
}
@Schema(name = "DatasetUpdateReq", description = "데이터셋 수정 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class UpdateReq {
@Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다")
@Schema(description = "제목", example = "1차 제작")
private String title;
@Schema(description = "메모", example = "데이터셋 설명")
private String memo;
}
@Schema(name = "DatasetSummaryReq", description = "데이터셋 통계 요약 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SummaryReq {
@NotNull(message = "데이터셋 ID 목록은 필수입니다")
@Schema(description = "데이터셋 ID 목록", example = "[101, 105]")
private List<Long> datasetIds;
}
@Schema(name = "DatasetSummary", description = "데이터셋 통계 요약")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Summary {
@Schema(description = "총 데이터셋 수", example = "2")
private int totalDatasets;
@Schema(description = "총 도엽 수", example = "1500")
private long totalMapSheets;
@Schema(description = "총 파일 크기 (bytes)", example = "10737418240")
private long totalFileSize;
@Schema(description = "평균 도엽 수", example = "750")
private double averageMapSheets;
}
}
package com.kamco.cd.training.dataset.dto;
import com.kamco.cd.training.common.enums.LearnDataRegister;
import com.kamco.cd.training.common.enums.LearnDataType;
import com.kamco.cd.training.common.utils.enums.Enums;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class DatasetDto {
@Schema(name = "Dataset Basic", description = "데이터셋 기본 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Basic {
private Long id;
private UUID uuid;
private String groupTitle;
private String groupTitleCd;
private String title;
private Long roundNo;
private String totalSize;
private String memo;
@JsonFormatDttm private ZonedDateTime createdDttm;
private String status;
private String statusCd;
private Boolean deleted;
public Basic(
Long id,
UUID uuid,
String groupTitle,
String title,
Long roundNo,
Long totalSize,
String memo,
ZonedDateTime createdDttm,
String status,
Boolean deleted) {
this.id = id;
this.uuid = uuid;
this.groupTitle = getGroupTitle(groupTitle);
this.groupTitleCd = groupTitle;
this.title = title;
this.roundNo = roundNo;
this.totalSize = getTotalSize(totalSize);
this.memo = memo;
this.createdDttm = createdDttm;
this.status = getStatus(status);
this.statusCd = status;
this.deleted = deleted;
}
public String getTotalSize(Long totalSize) {
if (totalSize == null) return "0G";
double giga = totalSize / (1024.0 * 1024 * 1024);
return String.format("%.2fG", giga);
}
public String getGroupTitle(String groupTitleCd) {
LearnDataType type = Enums.fromId(LearnDataType.class, groupTitleCd);
return type == null ? null : type.getText();
}
public String getStatus(String status) {
LearnDataRegister type = Enums.fromId(LearnDataRegister.class, status);
return type == null ? null : type.getText();
}
}
@Schema(name = "Dataset Detail", description = "데이터셋 상세 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Detail {
private Long id;
private String groupTitle;
private String title;
private Long roundNo;
private String totalSize;
private String memo;
@JsonFormatDttm private ZonedDateTime createdDttm;
private String status;
private Boolean deleted;
}
@Schema(name = "DatasetSearchReq", description = "데이터셋 목록 조회 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
@Schema(description = "구분", example = "DELIVER(납품), PRODUCTION(제작)")
private String groupTitle;
@Schema(description = "제목 (부분 검색)", example = "1차")
private String title;
@Schema(description = "페이지 번호 (1부터 시작)", example = "1")
private int page = 1;
@Schema(description = "페이지 크기", example = "20")
private int size = 20;
public Pageable toPageable() {
// API에서는 1부터 시작하지만 내부적으로는 0부터 시작
int pageIndex = Math.max(0, page - 1);
return PageRequest.of(pageIndex, size, Sort.by(Sort.Direction.DESC, "createdDttm"));
}
}
@Schema(name = "DatasetDetailReq", description = "데이터셋 상세 조회 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class DetailReq {
@NotNull(message = "데이터셋 ID는 필수입니다")
@Schema(description = "데이터셋 ID", example = "101")
private Long datasetId;
}
@Schema(name = "DatasetRegisterReq", description = "데이터셋 등록 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class RegisterReq {
@NotBlank(message = "제목은 필수입니다")
@Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다")
@Schema(description = "제목", example = "1차 제작")
private String title;
@NotBlank(message = "연도는 필수입니다")
@Size(max = 4, message = "연도는 4자리입니다")
@Schema(description = "연도 (YYYY)", example = "2024")
private String year;
@Schema(description = "회차", example = "1")
private Long roundNo;
@Schema(description = "메모", example = "데이터셋 설명")
private String memo;
}
@Schema(name = "DatasetUpdateReq", description = "데이터셋 수정 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class UpdateReq {
@Size(max = 200, message = "제목은 최대 200자까지 입력 가능합니다")
@Schema(description = "제목", example = "1차 제작")
private String title;
@Schema(description = "메모", example = "데이터셋 설명")
private String memo;
}
@Schema(name = "DatasetSummaryReq", description = "데이터셋 통계 요약 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SummaryReq {
@NotNull(message = "데이터셋 ID 목록은 필수입니다")
@Schema(description = "데이터셋 ID 목록", example = "[101, 105]")
private List<Long> datasetIds;
}
@Schema(name = "DatasetSummary", description = "데이터셋 통계 요약")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Summary {
@Schema(description = "총 데이터셋 수", example = "2")
private int totalDatasets;
@Schema(description = "총 도엽 수", example = "1500")
private long totalMapSheets;
@Schema(description = "총 파일 크기 (bytes)", example = "10737418240")
private long totalFileSize;
@Schema(description = "평균 도엽 수", example = "750")
private double averageMapSheets;
}
}

View File

@@ -1,103 +1,103 @@
package com.kamco.cd.training.dataset.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class MapSheetDto {
@Schema(name = "MapSheet Basic", description = "도엽 기본 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Basic {
private Long id;
private Long datasetId;
private String sheetNum;
private String fileName;
private Long fileSize;
private String filePath;
private String status;
private String memo;
private Boolean deleted;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm;
}
@Schema(name = "MapSheetSearchReq", description = "도엽 목록 조회 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
@NotNull(message = "데이터셋 ID는 필수입니다")
@Schema(description = "데이터셋 ID", example = "101")
private Long datasetId;
@Schema(description = "페이지 번호 (1부터 시작)", example = "1")
private int page = 1;
@Schema(description = "페이지 크기", example = "20")
private int size = 20;
public Pageable toPageable() {
int pageIndex = Math.max(0, page - 1);
return PageRequest.of(pageIndex, size, Sort.by(Sort.Direction.DESC, "createdDttm"));
}
}
@Schema(name = "MapSheetDeleteReq", description = "도엽 삭제 요청 (다건)")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class DeleteReq {
@NotNull(message = "삭제할 도엽 ID 목록은 필수입니다")
@Schema(description = "삭제할 도엽 ID 목록", example = "[9991, 9992]")
private List<Long> itemIds;
}
@Schema(name = "MapSheetCheckReq", description = "도엽 번호 유효성 검증 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class CheckReq {
@NotNull(message = "도엽 번호는 필수입니다")
@Schema(description = "도엽 번호", example = "377055")
private String sheetNum;
}
@Schema(name = "MapSheetCheckRes", description = "도엽 번호 유효성 검증 응답")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class CheckRes {
@Schema(description = "유효 여부", example = "true")
private boolean valid;
@Schema(description = "메시지", example = "유효한 도엽 번호입니다")
private String message;
@Schema(description = "중복 여부", example = "false")
private boolean duplicate;
}
}
package com.kamco.cd.training.dataset.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class MapSheetDto {
@Schema(name = "MapSheet Basic", description = "도엽 기본 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Basic {
private Long id;
private Long datasetId;
private String sheetNum;
private String fileName;
private Long fileSize;
private String filePath;
private String status;
private String memo;
private Boolean deleted;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm;
}
@Schema(name = "MapSheetSearchReq", description = "도엽 목록 조회 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
@NotNull(message = "데이터셋 ID는 필수입니다")
@Schema(description = "데이터셋 ID", example = "101")
private Long datasetId;
@Schema(description = "페이지 번호 (1부터 시작)", example = "1")
private int page = 1;
@Schema(description = "페이지 크기", example = "20")
private int size = 20;
public Pageable toPageable() {
int pageIndex = Math.max(0, page - 1);
return PageRequest.of(pageIndex, size, Sort.by(Sort.Direction.DESC, "createdDttm"));
}
}
@Schema(name = "MapSheetDeleteReq", description = "도엽 삭제 요청 (다건)")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class DeleteReq {
@NotNull(message = "삭제할 도엽 ID 목록은 필수입니다")
@Schema(description = "삭제할 도엽 ID 목록", example = "[9991, 9992]")
private List<Long> itemIds;
}
@Schema(name = "MapSheetCheckReq", description = "도엽 번호 유효성 검증 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class CheckReq {
@NotNull(message = "도엽 번호는 필수입니다")
@Schema(description = "도엽 번호", example = "377055")
private String sheetNum;
}
@Schema(name = "MapSheetCheckRes", description = "도엽 번호 유효성 검증 응답")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class CheckRes {
@Schema(description = "유효 여부", example = "true")
private boolean valid;
@Schema(description = "메시지", example = "유효한 도엽 번호입니다")
private String message;
@Schema(description = "중복 여부", example = "false")
private boolean duplicate;
}
}

View File

@@ -1,86 +1,86 @@
package com.kamco.cd.training.dataset.service;
import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.postgres.core.DatasetCoreService;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DatasetService {
private final DatasetCoreService datasetCoreService;
/**
* 데이터셋 목록 조회
*
* @param searchReq 검색 조건
* @return 데이터셋 목록
*/
public Page<DatasetDto.Basic> searchDatasets(DatasetDto.SearchReq searchReq) {
log.info("데이터셋 목록 조회 - 조건: {}", searchReq);
return datasetCoreService.findDatasetList(searchReq);
}
/**
* 데이터셋 상세 조회
*
* @param id 상세 조회할 목록 Id
* @return 데이터셋 상세 정보
*/
public DatasetDto.Basic getDatasetDetail(UUID id) {
return datasetCoreService.getOneByUuid(id);
}
/**
* 데이터셋 등록
*
* @param registerReq 등록 요청
* @return 등록된 데이터셋 ID
*/
@Transactional
public Long registerDataset(DatasetDto.RegisterReq registerReq) {
log.info("데이터셋 등록 - 요청: {}", registerReq);
DatasetDto.Basic saved = datasetCoreService.save(registerReq);
log.info("데이터셋 등록 완료 - ID: {}", saved.getId());
return saved.getId();
}
/**
* 데이터셋 수정
*
* @param updateReq 수정 요청
* @return 수정된 데이터셋 ID
*/
@Transactional
public void updateDataset(UUID uuid, DatasetDto.UpdateReq updateReq) {
datasetCoreService.update(uuid, updateReq);
}
/**
* 데이터셋 삭제 (다건)
*
* @param uuid 삭제 요청
*/
@Transactional
public void deleteDatasets(UUID uuid) {
datasetCoreService.deleteDatasets(uuid);
}
/**
* 데이터셋 통계 요약
*
* @param summaryReq 요약 요청
* @return 통계 요약
*/
public DatasetDto.Summary getDatasetSummary(DatasetDto.SummaryReq summaryReq) {
log.info("데이터셋 통계 요약 - 요청: {}", summaryReq);
return datasetCoreService.getDatasetSummary(summaryReq);
}
}
package com.kamco.cd.training.dataset.service;
import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.postgres.core.DatasetCoreService;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DatasetService {
private final DatasetCoreService datasetCoreService;
/**
* 데이터셋 목록 조회
*
* @param searchReq 검색 조건
* @return 데이터셋 목록
*/
public Page<DatasetDto.Basic> searchDatasets(DatasetDto.SearchReq searchReq) {
log.info("데이터셋 목록 조회 - 조건: {}", searchReq);
return datasetCoreService.findDatasetList(searchReq);
}
/**
* 데이터셋 상세 조회
*
* @param id 상세 조회할 목록 Id
* @return 데이터셋 상세 정보
*/
public DatasetDto.Basic getDatasetDetail(UUID id) {
return datasetCoreService.getOneByUuid(id);
}
/**
* 데이터셋 등록
*
* @param registerReq 등록 요청
* @return 등록된 데이터셋 ID
*/
@Transactional
public Long registerDataset(DatasetDto.RegisterReq registerReq) {
log.info("데이터셋 등록 - 요청: {}", registerReq);
DatasetDto.Basic saved = datasetCoreService.save(registerReq);
log.info("데이터셋 등록 완료 - ID: {}", saved.getId());
return saved.getId();
}
/**
* 데이터셋 수정
*
* @param updateReq 수정 요청
* @return 수정된 데이터셋 ID
*/
@Transactional
public void updateDataset(UUID uuid, DatasetDto.UpdateReq updateReq) {
datasetCoreService.update(uuid, updateReq);
}
/**
* 데이터셋 삭제 (다건)
*
* @param uuid 삭제 요청
*/
@Transactional
public void deleteDatasets(UUID uuid) {
datasetCoreService.deleteDatasets(uuid);
}
/**
* 데이터셋 통계 요약
*
* @param summaryReq 요약 요청
* @return 통계 요약
*/
public DatasetDto.Summary getDatasetSummary(DatasetDto.SummaryReq summaryReq) {
log.info("데이터셋 통계 요약 - 요청: {}", summaryReq);
return datasetCoreService.getDatasetSummary(summaryReq);
}
}

View File

@@ -1,41 +1,41 @@
package com.kamco.cd.training.dataset.service;
import com.kamco.cd.training.dataset.dto.MapSheetDto;
import com.kamco.cd.training.postgres.core.MapSheetCoreService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MapSheetService {
private final MapSheetCoreService mapSheetCoreService;
/**
* 도엽 목록 조회
*
* @param searchReq 검색 조건
* @return 도엽 목록
*/
public Page<MapSheetDto.Basic> searchMapSheets(MapSheetDto.SearchReq searchReq) {
log.info("도엽 목록 조회 - 조건: {}", searchReq);
return mapSheetCoreService.findMapSheetList(searchReq);
}
/**
* 도엽 삭제 (다건)
*
* @param deleteReq 삭제 요청
*/
@Transactional
public void deleteMapSheets(MapSheetDto.DeleteReq deleteReq) {
log.info("도엽 삭제 - 요청: {}", deleteReq);
mapSheetCoreService.deleteMapSheets(deleteReq);
log.info("도엽 삭제 완료 - 개수: {}", deleteReq.getItemIds().size());
}
}
package com.kamco.cd.training.dataset.service;
import com.kamco.cd.training.dataset.dto.MapSheetDto;
import com.kamco.cd.training.postgres.core.MapSheetCoreService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MapSheetService {
private final MapSheetCoreService mapSheetCoreService;
/**
* 도엽 목록 조회
*
* @param searchReq 검색 조건
* @return 도엽 목록
*/
public Page<MapSheetDto.Basic> searchMapSheets(MapSheetDto.SearchReq searchReq) {
log.info("도엽 목록 조회 - 조건: {}", searchReq);
return mapSheetCoreService.findMapSheetList(searchReq);
}
/**
* 도엽 삭제 (다건)
*
* @param deleteReq 삭제 요청
*/
@Transactional
public void deleteMapSheets(MapSheetDto.DeleteReq deleteReq) {
log.info("도엽 삭제 - 요청: {}", deleteReq);
mapSheetCoreService.deleteMapSheets(deleteReq);
log.info("도엽 삭제 완료 - 개수: {}", deleteReq.getItemIds().size());
}
}

View File

@@ -1,229 +1,229 @@
package com.kamco.cd.training.log.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.ZonedDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class AuditLogDto {
@Schema(name = "AuditLogBasic", description = "감사로그 기본 정보")
@Getter
public static class Basic {
@JsonIgnore private final Long id;
private final Long userUid;
private final EventType eventType;
private final EventStatus eventStatus;
private final String menuUid;
private final String ipAddress;
private final String requestUri;
private final String requestBody;
private final Long errorLogUid;
@JsonFormatDttm private final ZonedDateTime createdDttm;
public Basic(
Long id,
Long userUid,
EventType eventType,
EventStatus eventStatus,
String menuUid,
String ipAddress,
String requestUri,
String requestBody,
Long errorLogUid,
ZonedDateTime createdDttm) {
this.id = id;
this.userUid = userUid;
this.eventType = eventType;
this.eventStatus = eventStatus;
this.menuUid = menuUid;
this.ipAddress = ipAddress;
this.requestUri = requestUri;
this.requestBody = requestBody;
this.errorLogUid = errorLogUid;
this.createdDttm = createdDttm;
}
}
@Schema(name = "AuditCommon", description = "목록 공통")
@Getter
@AllArgsConstructor
public static class AuditCommon {
private int readCount;
private int cudCount;
private int printCount;
private int downloadCount;
private Long totalCount;
}
@Schema(name = "DailyAuditList", description = "일자별 목록")
@Getter
public static class DailyAuditList extends AuditCommon {
private final String baseDate;
public DailyAuditList(
int readCount,
int cudCount,
int printCount,
int downloadCount,
Long totalCount,
String baseDate) {
super(readCount, cudCount, printCount, downloadCount, totalCount);
this.baseDate = baseDate;
}
}
@Schema(name = "MenuAuditList", description = "메뉴별 목록")
@Getter
public static class MenuAuditList extends AuditCommon {
private final String menuId;
private final String menuName;
public MenuAuditList(
String menuId,
String menuName,
int readCount,
int cudCount,
int printCount,
int downloadCount,
Long totalCount) {
super(readCount, cudCount, printCount, downloadCount, totalCount);
this.menuId = menuId;
this.menuName = menuName;
}
}
@Schema(name = "UserAuditList", description = "사용자별 목록")
@Getter
public static class UserAuditList extends AuditCommon {
private final Long accountId;
private final String loginId;
private final String username;
public UserAuditList(
Long accountId,
String loginId,
String username,
int readCount,
int cudCount,
int printCount,
int downloadCount,
Long totalCount) {
super(readCount, cudCount, printCount, downloadCount, totalCount);
this.accountId = accountId;
this.loginId = loginId;
this.username = username;
}
}
@Schema(name = "AuditDetail", description = "감사 로그 상세 공통")
@Getter
@AllArgsConstructor
public static class AuditDetail {
private Long logId;
private EventType eventType;
private LogDetail detail;
}
@Schema(name = "DailyDetail", description = "일자별 로그 상세")
@Getter
public static class DailyDetail extends AuditDetail {
private final String userName;
private final String loginId;
private final String menuName;
public DailyDetail(
Long logId,
String userName,
String loginId,
String menuName,
EventType eventType,
LogDetail detail) {
super(logId, eventType, detail);
this.userName = userName;
this.loginId = loginId;
this.menuName = menuName;
}
}
@Schema(name = "MenuDetail", description = "메뉴별 로그 상세")
@Getter
public static class MenuDetail extends AuditDetail {
private final String logDateTime;
private final String userName;
private final String loginId;
public MenuDetail(
Long logId,
String logDateTime,
String userName,
String loginId,
EventType eventType,
LogDetail detail) {
super(logId, eventType, detail);
this.logDateTime = logDateTime;
this.userName = userName;
this.loginId = loginId;
}
}
@Schema(name = "UserDetail", description = "사용자별 로그 상세")
@Getter
public static class UserDetail extends AuditDetail {
private final String logDateTime;
private final String menuNm;
public UserDetail(
Long logId, String logDateTime, String menuNm, EventType eventType, LogDetail detail) {
super(logId, eventType, detail);
this.logDateTime = logDateTime;
this.menuNm = menuNm;
}
}
@Getter
@Setter
@AllArgsConstructor
public static class LogDetail {
String serviceName;
String parentMenuName;
String menuName;
String menuUrl;
String menuDescription;
Long sortOrder;
boolean used;
}
@Schema(name = "searchReq", description = "일자별 로그 검색 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class searchReq {
// 페이징 파라미터
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);
}
}
}
package com.kamco.cd.training.log.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.ZonedDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class AuditLogDto {
@Schema(name = "AuditLogBasic", description = "감사로그 기본 정보")
@Getter
public static class Basic {
@JsonIgnore private final Long id;
private final Long userUid;
private final EventType eventType;
private final EventStatus eventStatus;
private final String menuUid;
private final String ipAddress;
private final String requestUri;
private final String requestBody;
private final Long errorLogUid;
@JsonFormatDttm private final ZonedDateTime createdDttm;
public Basic(
Long id,
Long userUid,
EventType eventType,
EventStatus eventStatus,
String menuUid,
String ipAddress,
String requestUri,
String requestBody,
Long errorLogUid,
ZonedDateTime createdDttm) {
this.id = id;
this.userUid = userUid;
this.eventType = eventType;
this.eventStatus = eventStatus;
this.menuUid = menuUid;
this.ipAddress = ipAddress;
this.requestUri = requestUri;
this.requestBody = requestBody;
this.errorLogUid = errorLogUid;
this.createdDttm = createdDttm;
}
}
@Schema(name = "AuditCommon", description = "목록 공통")
@Getter
@AllArgsConstructor
public static class AuditCommon {
private int readCount;
private int cudCount;
private int printCount;
private int downloadCount;
private Long totalCount;
}
@Schema(name = "DailyAuditList", description = "일자별 목록")
@Getter
public static class DailyAuditList extends AuditCommon {
private final String baseDate;
public DailyAuditList(
int readCount,
int cudCount,
int printCount,
int downloadCount,
Long totalCount,
String baseDate) {
super(readCount, cudCount, printCount, downloadCount, totalCount);
this.baseDate = baseDate;
}
}
@Schema(name = "MenuAuditList", description = "메뉴별 목록")
@Getter
public static class MenuAuditList extends AuditCommon {
private final String menuId;
private final String menuName;
public MenuAuditList(
String menuId,
String menuName,
int readCount,
int cudCount,
int printCount,
int downloadCount,
Long totalCount) {
super(readCount, cudCount, printCount, downloadCount, totalCount);
this.menuId = menuId;
this.menuName = menuName;
}
}
@Schema(name = "UserAuditList", description = "사용자별 목록")
@Getter
public static class UserAuditList extends AuditCommon {
private final Long accountId;
private final String loginId;
private final String username;
public UserAuditList(
Long accountId,
String loginId,
String username,
int readCount,
int cudCount,
int printCount,
int downloadCount,
Long totalCount) {
super(readCount, cudCount, printCount, downloadCount, totalCount);
this.accountId = accountId;
this.loginId = loginId;
this.username = username;
}
}
@Schema(name = "AuditDetail", description = "감사 로그 상세 공통")
@Getter
@AllArgsConstructor
public static class AuditDetail {
private Long logId;
private EventType eventType;
private LogDetail detail;
}
@Schema(name = "DailyDetail", description = "일자별 로그 상세")
@Getter
public static class DailyDetail extends AuditDetail {
private final String userName;
private final String loginId;
private final String menuName;
public DailyDetail(
Long logId,
String userName,
String loginId,
String menuName,
EventType eventType,
LogDetail detail) {
super(logId, eventType, detail);
this.userName = userName;
this.loginId = loginId;
this.menuName = menuName;
}
}
@Schema(name = "MenuDetail", description = "메뉴별 로그 상세")
@Getter
public static class MenuDetail extends AuditDetail {
private final String logDateTime;
private final String userName;
private final String loginId;
public MenuDetail(
Long logId,
String logDateTime,
String userName,
String loginId,
EventType eventType,
LogDetail detail) {
super(logId, eventType, detail);
this.logDateTime = logDateTime;
this.userName = userName;
this.loginId = loginId;
}
}
@Schema(name = "UserDetail", description = "사용자별 로그 상세")
@Getter
public static class UserDetail extends AuditDetail {
private final String logDateTime;
private final String menuNm;
public UserDetail(
Long logId, String logDateTime, String menuNm, EventType eventType, LogDetail detail) {
super(logId, eventType, detail);
this.logDateTime = logDateTime;
this.menuNm = menuNm;
}
}
@Getter
@Setter
@AllArgsConstructor
public static class LogDetail {
String serviceName;
String parentMenuName;
String menuName;
String menuUrl;
String menuDescription;
Long sortOrder;
boolean used;
}
@Schema(name = "searchReq", description = "일자별 로그 검색 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class searchReq {
// 페이징 파라미터
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,101 +1,101 @@
package com.kamco.cd.training.log.dto;
import com.kamco.cd.training.common.utils.enums.EnumType;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class ErrorLogDto {
@Schema(name = "ErrorLogBasic", description = "에러로그 기본 정보")
@Getter
@Setter
@AllArgsConstructor
public static class Basic {
private final Long id;
private final String serviceName;
private final String menuNm;
private final String loginId;
private final String userName;
private final EventType errorType;
private final String errorName;
private final LogErrorLevel errorLevel;
private final String errorCode;
private final String errorMessage;
private final String errorDetail;
private final String createDate; // to_char해서 가져옴
}
@Schema(name = "ErrorSearchReq", description = "에러로그 검색 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class ErrorSearchReq {
LogErrorLevel errorLevel;
EventType eventType;
LocalDate startDate;
LocalDate endDate;
// 페이징 파라미터
private int page = 0;
private int size = 20;
private String sort;
public ErrorSearchReq(
LogErrorLevel errorLevel,
EventType eventType,
LocalDate startDate,
LocalDate endDate,
int page,
int size) {
this.errorLevel = errorLevel;
this.eventType = eventType;
this.startDate = startDate;
this.endDate = endDate;
this.page = page;
this.size = size;
}
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);
}
}
public enum LogErrorLevel implements EnumType {
WARNING("Warning"),
ERROR("Error"),
CRITICAL("Critical");
private final String desc;
LogErrorLevel(String desc) {
this.desc = desc;
}
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
}
package com.kamco.cd.training.log.dto;
import com.kamco.cd.training.common.utils.enums.EnumType;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class ErrorLogDto {
@Schema(name = "ErrorLogBasic", description = "에러로그 기본 정보")
@Getter
@Setter
@AllArgsConstructor
public static class Basic {
private final Long id;
private final String serviceName;
private final String menuNm;
private final String loginId;
private final String userName;
private final EventType errorType;
private final String errorName;
private final LogErrorLevel errorLevel;
private final String errorCode;
private final String errorMessage;
private final String errorDetail;
private final String createDate; // to_char해서 가져옴
}
@Schema(name = "ErrorSearchReq", description = "에러로그 검색 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class ErrorSearchReq {
LogErrorLevel errorLevel;
EventType eventType;
LocalDate startDate;
LocalDate endDate;
// 페이징 파라미터
private int page = 0;
private int size = 20;
private String sort;
public ErrorSearchReq(
LogErrorLevel errorLevel,
EventType eventType,
LocalDate startDate,
LocalDate endDate,
int page,
int size) {
this.errorLevel = errorLevel;
this.eventType = eventType;
this.startDate = startDate;
this.endDate = endDate;
this.page = page;
this.size = size;
}
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);
}
}
public enum LogErrorLevel implements EnumType {
WARNING("Warning"),
ERROR("Error"),
CRITICAL("Critical");
private final String desc;
LogErrorLevel(String desc) {
this.desc = desc;
}
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
}

View File

@@ -1,24 +1,24 @@
package com.kamco.cd.training.log.dto;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum EventStatus implements EnumType {
SUCCESS("이벤트 결과 성공"),
FAILED("이벤트 결과 실패");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
package com.kamco.cd.training.log.dto;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum EventStatus implements EnumType {
SUCCESS("이벤트 결과 성공"),
FAILED("이벤트 결과 실패");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -1,29 +1,29 @@
package com.kamco.cd.training.log.dto;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum EventType implements EnumType {
CREATE("생성"),
READ("조회"),
UPDATE("수정"),
DELETE("삭제"),
DOWNLOAD("다운로드"),
PRINT("출력"),
OTHER("기타");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}
package com.kamco.cd.training.log.dto;
import com.kamco.cd.training.common.utils.enums.EnumType;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum EventType implements EnumType {
CREATE("생성"),
READ("조회"),
UPDATE("수정"),
DELETE("삭제"),
DOWNLOAD("다운로드"),
PRINT("출력"),
OTHER("기타");
private final String desc;
@Override
public String getId() {
return name();
}
@Override
public String getText() {
return desc;
}
}

View File

@@ -1,46 +1,46 @@
package com.kamco.cd.training.log.service;
import com.kamco.cd.training.log.dto.AuditLogDto;
import com.kamco.cd.training.postgres.core.AuditLogCoreService;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuditLogService {
private final AuditLogCoreService auditLogCoreService;
public Page<AuditLogDto.DailyAuditList> getLogByDaily(
AuditLogDto.searchReq searchRange, LocalDate startDate, LocalDate endDate) {
return auditLogCoreService.getLogByDaily(searchRange, startDate, endDate);
}
public Page<AuditLogDto.MenuAuditList> getLogByMenu(
AuditLogDto.searchReq searchRange, String searchValue) {
return auditLogCoreService.getLogByMenu(searchRange, searchValue);
}
public Page<AuditLogDto.UserAuditList> getLogByAccount(
AuditLogDto.searchReq searchRange, String searchValue) {
return auditLogCoreService.getLogByAccount(searchRange, searchValue);
}
public Page<AuditLogDto.DailyDetail> getLogByDailyResult(
AuditLogDto.searchReq searchRange, LocalDate logDate) {
return auditLogCoreService.getLogByDailyResult(searchRange, logDate);
}
public Page<AuditLogDto.MenuDetail> getLogByMenuResult(
AuditLogDto.searchReq searchRange, String menuId) {
return auditLogCoreService.getLogByMenuResult(searchRange, menuId);
}
public Page<AuditLogDto.UserDetail> getLogByAccountResult(
AuditLogDto.searchReq searchRange, Long accountId) {
return auditLogCoreService.getLogByAccountResult(searchRange, accountId);
}
}
package com.kamco.cd.training.log.service;
import com.kamco.cd.training.log.dto.AuditLogDto;
import com.kamco.cd.training.postgres.core.AuditLogCoreService;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuditLogService {
private final AuditLogCoreService auditLogCoreService;
public Page<AuditLogDto.DailyAuditList> getLogByDaily(
AuditLogDto.searchReq searchRange, LocalDate startDate, LocalDate endDate) {
return auditLogCoreService.getLogByDaily(searchRange, startDate, endDate);
}
public Page<AuditLogDto.MenuAuditList> getLogByMenu(
AuditLogDto.searchReq searchRange, String searchValue) {
return auditLogCoreService.getLogByMenu(searchRange, searchValue);
}
public Page<AuditLogDto.UserAuditList> getLogByAccount(
AuditLogDto.searchReq searchRange, String searchValue) {
return auditLogCoreService.getLogByAccount(searchRange, searchValue);
}
public Page<AuditLogDto.DailyDetail> getLogByDailyResult(
AuditLogDto.searchReq searchRange, LocalDate logDate) {
return auditLogCoreService.getLogByDailyResult(searchRange, logDate);
}
public Page<AuditLogDto.MenuDetail> getLogByMenuResult(
AuditLogDto.searchReq searchRange, String menuId) {
return auditLogCoreService.getLogByMenuResult(searchRange, menuId);
}
public Page<AuditLogDto.UserDetail> getLogByAccountResult(
AuditLogDto.searchReq searchRange, Long accountId) {
return auditLogCoreService.getLogByAccountResult(searchRange, accountId);
}
}

View File

@@ -1,19 +1,19 @@
package com.kamco.cd.training.log.service;
import com.kamco.cd.training.log.dto.ErrorLogDto;
import com.kamco.cd.training.postgres.core.ErrorLogCoreService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ErrorLogService {
private final ErrorLogCoreService errorLogCoreService;
public Page<ErrorLogDto.Basic> findLogByError(ErrorLogDto.ErrorSearchReq searchReq) {
return errorLogCoreService.findLogByError(searchReq);
}
}
package com.kamco.cd.training.log.service;
import com.kamco.cd.training.log.dto.ErrorLogDto;
import com.kamco.cd.training.postgres.core.ErrorLogCoreService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ErrorLogService {
private final ErrorLogCoreService errorLogCoreService;
public Page<ErrorLogDto.Basic> findLogByError(ErrorLogDto.ErrorSearchReq searchReq) {
return errorLogCoreService.findLogByError(searchReq);
}
}

View File

@@ -1,241 +1,241 @@
package com.kamco.cd.training.members;
import com.kamco.cd.training.auth.CustomUserDetails;
import com.kamco.cd.training.auth.JwtTokenProvider;
import com.kamco.cd.training.common.enums.StatusType;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.members.dto.MembersDto;
import com.kamco.cd.training.members.dto.SignInRequest;
import com.kamco.cd.training.members.dto.TokenResponse;
import com.kamco.cd.training.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;
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;
import org.springframework.http.HttpStatus;
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 AuthService authService;
@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 = "로그인 실패 (아이디/비밀번호 오류, 계정잠금 등)",
content =
@Content(
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(
name = "사번 입력 오류",
description = "존재하지 않는 아이디",
value =
"""
{
"code": "LOGIN_ID_NOT_FOUND",
"message": "사번을 잘못 입력하셨습니다."
}
"""),
@ExampleObject(
name = "비밀번호 입력 오류 (4회 이하)",
description = "아이디는 정상, 비밀번호를 여러 번 틀린 경우",
value =
"""
{
"code": "LOGIN_PASSWORD_MISMATCH",
"message": "비밀번호를 잘못 입력하셨습니다."
}
"""),
@ExampleObject(
name = "비밀번호 오류 횟수 초과",
description = "비밀번호 5회 이상 오류로 계정 잠김",
value =
"""
{
"code": "LOGIN_PASSWORD_EXCEEDED",
"message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다."
}
"""),
@ExampleObject(
name = "사용 중지 된 계정의 로그인 시도",
description = "사용 중지 된 계정의 로그인 시도",
value =
"""
{
"code": "INACTIVE_ID",
"message": "사용할 수 없는 계정입니다."
}
""")
}))
})
public ApiResponseDto<TokenResponse> signin(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "로그인 요청 정보",
required = true)
@RequestBody
SignInRequest request,
HttpServletResponse response) {
// 사용자 상태 조회
String status = authService.getUserStatus(request);
if(StatusType.INACTIVE.getId().equals(status)) {
throw new CustomApiException("INACTIVE_ID", HttpStatus.UNAUTHORIZED);
}
Authentication authentication = null;
MembersDto.Member member = new MembersDto.Member();
authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
String username = authentication.getName(); // UserDetailsService 에서 사용한 username
String accessToken = jwtTokenProvider.createAccessToken(username);
String refreshToken = jwtTokenProvider.createRefreshToken(username);
// 토큰 저장
authService.tokenSave(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();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
CustomUserDetails user = (CustomUserDetails) authentication.getPrincipal();
member.setId(user.getMember().getId());
member.setName(user.getMember().getName());
member.setEmployeeNo(user.getMember().getEmployeeNo());
// PENDING 비활성 상태(새로운 패스워드 입력 해야함)
if (StatusType.PENDING.getId().equals(status)) {
member.setEmployeeNo(request.getUsername());
return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken, member));
}
// 인증 성공 로그인 시간 저장
authService.saveLogin(UUID.fromString(username));
return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken, member));
}
@PostMapping("/refresh")
@Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "재발급 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse(
responseCode = "403",
description = "만료되었거나 유효하지 않은 리프레시 토큰",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
public ResponseEntity<TokenResponse> refresh(String refreshToken, HttpServletResponse response)
throws AccessDeniedException {
if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) {
throw new AccessDeniedException("만료되었거나 유효하지 않은 리프레시 토큰 입니다.");
}
String username = jwtTokenProvider.getSubject(refreshToken);
// 저장된 RefreshToken과 일치하는지 확인
authService.validateRefreshToken(username, refreshToken);
// 새 토큰 발급
String newAccessToken = jwtTokenProvider.createAccessToken(username);
String newRefreshToken = jwtTokenProvider.createRefreshToken(username);
// 토큰 저장
authService.tokenSave(username, refreshToken, 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());
MembersDto.Member member = new MembersDto.Member();
return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken, member));
}
@PostMapping("/logout")
@Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "로그아웃 성공",
content = @Content(schema = @Schema(implementation = Void.class)))
})
public ApiResponseDto<ResponseEntity<Object>> logout(
Authentication authentication, HttpServletResponse response) {
if (authentication != null) {
String username = authentication.getName();
authService.logout(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 ApiResponseDto.createOK(ResponseEntity.noContent().build());
}
}
package com.kamco.cd.training.members;
import com.kamco.cd.training.auth.CustomUserDetails;
import com.kamco.cd.training.auth.JwtTokenProvider;
import com.kamco.cd.training.common.enums.StatusType;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.members.dto.MembersDto;
import com.kamco.cd.training.members.dto.SignInRequest;
import com.kamco.cd.training.members.dto.TokenResponse;
import com.kamco.cd.training.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;
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;
import org.springframework.http.HttpStatus;
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 AuthService authService;
@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 = "로그인 성공1",
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse(
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 =
"""
{
"code": "LOGIN_PASSWORD_MISMATCH",
"message": "비밀번호를 잘못 입력하셨습니다."
}
"""),
@ExampleObject(
name = "비밀번호 오류 횟수 초과",
description = "비밀번호 5회 이상 오류로 계정 잠김",
value =
"""
{
"code": "LOGIN_PASSWORD_EXCEEDED",
"message": "비밀번호 오류 횟수를 초과하여 이용하실 수 없습니다. 로그인 오류에 대해 관리자에게 문의하시기 바랍니다."
}
"""),
@ExampleObject(
name = "사용 중지 된 계정의 로그인 시도",
description = "사용 중지 된 계정의 로그인 시도",
value =
"""
{
"code": "INACTIVE_ID",
"message": "사용할 수 없는 계정입니다."
}
""")
}))
})
public ApiResponseDto<TokenResponse> signin(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "로그인 요청 정보",
required = true)
@RequestBody
SignInRequest request,
HttpServletResponse response) {
// 사용자 상태 조회
String status = authService.getUserStatus(request);
if (StatusType.INACTIVE.getId().equals(status)) {
throw new CustomApiException("INACTIVE_ID", HttpStatus.UNAUTHORIZED);
}
Authentication authentication = null;
MembersDto.Member member = new MembersDto.Member();
authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
String username = authentication.getName(); // UserDetailsService 에서 사용한 username
String accessToken = jwtTokenProvider.createAccessToken(username);
String refreshToken = jwtTokenProvider.createRefreshToken(username);
// 토큰 저장
authService.tokenSave(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();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
CustomUserDetails user = (CustomUserDetails) authentication.getPrincipal();
member.setId(user.getMember().getId());
member.setName(user.getMember().getName());
member.setEmployeeNo(user.getMember().getEmployeeNo());
// PENDING 비활성 상태(새로운 패스워드 입력 해야함)
if (StatusType.PENDING.getId().equals(status)) {
member.setEmployeeNo(request.getUsername());
return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken, member));
}
// 인증 성공 로그인 시간 저장
authService.saveLogin(UUID.fromString(username));
return ApiResponseDto.ok(new TokenResponse(status, accessToken, refreshToken, member));
}
@PostMapping("/refresh")
@Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "재발급 성공",
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
@ApiResponse(
responseCode = "403",
description = "만료되었거나 유효하지 않은 리프레시 토큰",
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
public ResponseEntity<TokenResponse> refresh(String refreshToken, HttpServletResponse response)
throws AccessDeniedException {
if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) {
throw new AccessDeniedException("만료되었거나 유효하지 않은 리프레시 토큰 입니다.");
}
String username = jwtTokenProvider.getSubject(refreshToken);
// 저장된 RefreshToken과 일치하는지 확인
authService.validateRefreshToken(username, refreshToken);
// 새 토큰 발급
String newAccessToken = jwtTokenProvider.createAccessToken(username);
String newRefreshToken = jwtTokenProvider.createRefreshToken(username);
// 토큰 저장
authService.tokenSave(username, refreshToken, 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());
MembersDto.Member member = new MembersDto.Member();
return ResponseEntity.ok(new TokenResponse("ACTIVE", newAccessToken, newRefreshToken, member));
}
@PostMapping("/logout")
@Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(리프레시 토큰 삭제)합니다.")
@ApiResponses({
@ApiResponse(
responseCode = "200",
description = "로그아웃 성공",
content = @Content(schema = @Schema(implementation = Void.class)))
})
public ApiResponseDto<ResponseEntity<Object>> logout(
Authentication authentication, HttpServletResponse response) {
if (authentication != null) {
String username = authentication.getName();
authService.logout(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 ApiResponseDto.createOK(ResponseEntity.noContent().build());
}
}

View File

@@ -1,69 +1,69 @@
package com.kamco.cd.training.members;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.members.dto.MembersDto;
import com.kamco.cd.training.members.dto.MembersDto.Basic;
import com.kamco.cd.training.members.service.MembersService;
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.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.web.bind.annotation.*;
@Tag(name = "회원정보 관리", description = "회원정보 관리 API")
@RestController
@RequestMapping("/api/members")
@RequiredArgsConstructor
public class MembersApiController {
private final AuthenticationManager authenticationManager;
private final MembersService membersService;
@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)
})
@PostMapping("/search")
public ApiResponseDto<Page<Basic>> getMemberList(
@RequestBody @Valid MembersDto.SearchReq searchReq) {
return ApiResponseDto.ok(membersService.findByMembers(searchReq));
}
@Operation(
summary = "사용자 비밀번호 변경",
description = "로그인 성공후 status가 INACTIVE일때 로그인 id를 memberId로 path 생성필요")
@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<String> resetPassword(
@PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) {
membersService.resetPassword(memberId, initReq);
return ApiResponseDto.createOK(memberId);
}
}
package com.kamco.cd.training.members;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.members.dto.MembersDto;
import com.kamco.cd.training.members.dto.MembersDto.Basic;
import com.kamco.cd.training.members.service.MembersService;
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.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.web.bind.annotation.*;
@Tag(name = "회원정보 관리", description = "회원정보 관리 API")
@RestController
@RequestMapping("/api/members")
@RequiredArgsConstructor
public class MembersApiController {
private final AuthenticationManager authenticationManager;
private final MembersService membersService;
@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)
})
@PostMapping("/search")
public ApiResponseDto<Page<Basic>> getMemberList(
@RequestBody @Valid MembersDto.SearchReq searchReq) {
return ApiResponseDto.ok(membersService.findByMembers(searchReq));
}
@Operation(
summary = "사용자 비밀번호 변경",
description = "로그인 성공후 status가 INACTIVE일때 로그인 id를 memberId로 path 생성필요")
@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<String> resetPassword(
@PathVariable String memberId, @RequestBody @Valid MembersDto.InitReq initReq) {
membersService.resetPassword(memberId, initReq);
return ApiResponseDto.createOK(memberId);
}
}

View File

@@ -1,183 +1,183 @@
package com.kamco.cd.training.members.dto;
import com.kamco.cd.training.common.enums.RoleType;
import com.kamco.cd.training.common.enums.StatusType;
import com.kamco.cd.training.common.utils.enums.Enums;
import com.kamco.cd.training.common.utils.interfaces.EnumValid;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
public class MembersDto {
@Getter
@Setter
public static class Basic {
private Long id;
private UUID uuid;
private String userRole;
private String userRoleName;
private String name;
private String employeeNo;
private String status;
private String statusName;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime firstLoginDttm;
@JsonFormatDttm private ZonedDateTime lastLoginDttm;
@JsonFormatDttm private ZonedDateTime statusChgDttm;
public Basic(
Long id,
UUID uuid,
String userRole,
String name,
String employeeNo,
String status,
ZonedDateTime createdDttm,
ZonedDateTime firstLoginDttm,
ZonedDateTime lastLoginDttm,
ZonedDateTime statusChgDttm,
Boolean pwdResetYn) {
this.id = id;
this.uuid = uuid;
this.userRole = userRole;
this.userRoleName = getUserRoleName(userRole);
this.name = name;
this.employeeNo = employeeNo;
this.status = status;
this.statusName = getStatusName(status, pwdResetYn);
this.createdDttm = createdDttm;
this.firstLoginDttm = firstLoginDttm;
this.lastLoginDttm = lastLoginDttm;
this.statusChgDttm = statusChgDttm;
}
private String getUserRoleName(String roleId) {
RoleType type = Enums.fromId(RoleType.class, roleId);
return type.getText();
}
private String getStatusName(String status, Boolean pwdResetYn) {
StatusType type = Enums.fromId(StatusType.class, status);
pwdResetYn = pwdResetYn != null && pwdResetYn;
if (type.equals(StatusType.PENDING) && pwdResetYn) {
type = StatusType.ACTIVE;
}
return type.getText();
}
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
@Schema(description = "전체, 관리자(ADMIN), 라벨러(LABELER), 검수자(REVIEWER)", example = "")
private String userRole;
@Schema(description = "키워드", example = "홍길동")
private String keyword;
// 페이징 파라미터
@Schema(description = "페이지 번호 (0부터 시작) ", example = "0")
private int page = 0;
@Schema(description = "페이지 크기", example = "20")
private int size = 20;
public Pageable toPageable() {
return PageRequest.of(page, size);
}
}
@Getter
@Setter
public static class AddReq {
@Schema(description = "관리자 유형", example = "ADMIN")
@NotBlank
@EnumValid(enumClass = RoleType.class, message = "userRole은 ADMIN, LABELER, REVIEWER 만 가능합니다.")
private String userRole;
@Schema(description = "사번", example = "K20251212001")
@Size(max = 50)
private String employeeNo;
@Schema(description = "이름", example = "홍길동")
@NotBlank
@Size(min = 2, max = 100)
private String name;
@NotBlank
@Schema(description = "패스워드", example = "")
@Size(max = 255)
private String password;
public AddReq(String userRole, String employeeNo, String name, String password) {
this.userRole = userRole;
this.employeeNo = employeeNo;
this.name = name;
this.password = password;
}
}
@Getter
@Setter
public static class UpdateReq {
@Schema(description = "이름", example = "홍길동")
@Size(min = 2, max = 100)
private String name;
@Schema(description = "상태", example = "ACTIVE")
@EnumValid(enumClass = StatusType.class, message = "status는 ACTIVE, INACTIVE, DELETED 만 가능합니다.")
private String status;
@Schema(description = "패스워드", example = "")
@Size(max = 255)
private String password;
public UpdateReq(String name, String status, String password) {
this.name = name;
this.status = status;
this.password = password;
}
}
@Getter
@Setter
public static class InitReq {
@Schema(description = "기존 패스워드", example = "")
@Size(max = 255)
@NotBlank
private String oldPassword;
@Schema(description = "신규 패스워드", example = "")
@Size(max = 255)
@NotBlank
private String newPassword;
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class Member {
private Long id;
private String name;
private String employeeNo;
}
}
package com.kamco.cd.training.members.dto;
import com.kamco.cd.training.common.enums.RoleType;
import com.kamco.cd.training.common.enums.StatusType;
import com.kamco.cd.training.common.utils.enums.Enums;
import com.kamco.cd.training.common.utils.interfaces.EnumValid;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.ZonedDateTime;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
public class MembersDto {
@Getter
@Setter
public static class Basic {
private Long id;
private UUID uuid;
private String userRole;
private String userRoleName;
private String name;
private String employeeNo;
private String status;
private String statusName;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime firstLoginDttm;
@JsonFormatDttm private ZonedDateTime lastLoginDttm;
@JsonFormatDttm private ZonedDateTime statusChgDttm;
public Basic(
Long id,
UUID uuid,
String userRole,
String name,
String employeeNo,
String status,
ZonedDateTime createdDttm,
ZonedDateTime firstLoginDttm,
ZonedDateTime lastLoginDttm,
ZonedDateTime statusChgDttm,
Boolean pwdResetYn) {
this.id = id;
this.uuid = uuid;
this.userRole = userRole;
this.userRoleName = getUserRoleName(userRole);
this.name = name;
this.employeeNo = employeeNo;
this.status = status;
this.statusName = getStatusName(status, pwdResetYn);
this.createdDttm = createdDttm;
this.firstLoginDttm = firstLoginDttm;
this.lastLoginDttm = lastLoginDttm;
this.statusChgDttm = statusChgDttm;
}
private String getUserRoleName(String roleId) {
RoleType type = Enums.fromId(RoleType.class, roleId);
return type.getText();
}
private String getStatusName(String status, Boolean pwdResetYn) {
StatusType type = Enums.fromId(StatusType.class, status);
pwdResetYn = pwdResetYn != null && pwdResetYn;
if (type.equals(StatusType.PENDING) && pwdResetYn) {
type = StatusType.ACTIVE;
}
return type.getText();
}
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
@Schema(description = "전체, 관리자(ADMIN), 라벨러(LABELER), 검수자(REVIEWER)", example = "")
private String userRole;
@Schema(description = "키워드", example = "홍길동")
private String keyword;
// 페이징 파라미터
@Schema(description = "페이지 번호 (0부터 시작) ", example = "0")
private int page = 0;
@Schema(description = "페이지 크기", example = "20")
private int size = 20;
public Pageable toPageable() {
return PageRequest.of(page, size);
}
}
@Getter
@Setter
public static class AddReq {
@Schema(description = "관리자 유형", example = "ADMIN")
@NotBlank
@EnumValid(enumClass = RoleType.class, message = "userRole은 ADMIN, LABELER, REVIEWER 만 가능합니다.")
private String userRole;
@Schema(description = "사번", example = "K20251212001")
@Size(max = 50)
private String employeeNo;
@Schema(description = "이름", example = "홍길동")
@NotBlank
@Size(min = 2, max = 100)
private String name;
@NotBlank
@Schema(description = "패스워드", example = "")
@Size(max = 255)
private String password;
public AddReq(String userRole, String employeeNo, String name, String password) {
this.userRole = userRole;
this.employeeNo = employeeNo;
this.name = name;
this.password = password;
}
}
@Getter
@Setter
public static class UpdateReq {
@Schema(description = "이름", example = "홍길동")
@Size(min = 2, max = 100)
private String name;
@Schema(description = "상태", example = "ACTIVE")
@EnumValid(enumClass = StatusType.class, message = "status는 ACTIVE, INACTIVE, DELETED 만 가능합니다.")
private String status;
@Schema(description = "패스워드", example = "")
@Size(max = 255)
private String password;
public UpdateReq(String name, String status, String password) {
this.name = name;
this.status = status;
this.password = password;
}
}
@Getter
@Setter
public static class InitReq {
@Schema(description = "기존 패스워드", example = "")
@Size(max = 255)
@NotBlank
private String oldPassword;
@Schema(description = "신규 패스워드", example = "")
@Size(max = 255)
@NotBlank
private String newPassword;
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class Member {
private Long id;
private String name;
private String employeeNo;
}
}

View File

@@ -1,20 +1,20 @@
package com.kamco.cd.training.members.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString(exclude = "password")
public class SignInRequest {
@Schema(description = "사용자 ID", example = "1234567")
private String username;
@Schema(description = "비밀번호", example = "Admin2!@#")
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
}
package com.kamco.cd.training.members.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString(exclude = "password")
public class SignInRequest {
@Schema(description = "사용자 ID", example = "1234567")
private String username;
@Schema(description = "비밀번호", example = "Admin2!@#")
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
}

View File

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

View File

@@ -1,50 +1,50 @@
package com.kamco.cd.training.members.exception;
import lombok.Getter;
@Getter
public class MemberException {
// *** Duplicate Member Exception ***
@Getter
public static class DuplicateMemberException extends RuntimeException {
public enum Field {
USER_ID,
EMPLOYEE_NO,
DEFAULT
}
private final Field field;
private final String value;
public DuplicateMemberException(Field field, String value) {
super(field.name() + " duplicate: " + value);
this.field = field;
this.value = value;
}
}
// *** Member Not Found Exception ***
public static class MemberNotFoundException extends RuntimeException {
public MemberNotFoundException() {
super("Member not found");
}
public MemberNotFoundException(String message) {
super(message);
}
}
public static class PasswordNotFoundException extends RuntimeException {
public PasswordNotFoundException() {
super("Password not found");
}
public PasswordNotFoundException(String message) {
super(message);
}
}
}
package com.kamco.cd.training.members.exception;
import lombok.Getter;
@Getter
public class MemberException {
// *** Duplicate Member Exception ***
@Getter
public static class DuplicateMemberException extends RuntimeException {
public enum Field {
USER_ID,
EMPLOYEE_NO,
DEFAULT
}
private final Field field;
private final String value;
public DuplicateMemberException(Field field, String value) {
super(field.name() + " duplicate: " + value);
this.field = field;
this.value = value;
}
}
// *** Member Not Found Exception ***
public static class MemberNotFoundException extends RuntimeException {
public MemberNotFoundException() {
super("Member not found");
}
public MemberNotFoundException(String message) {
super(message);
}
}
public static class PasswordNotFoundException extends RuntimeException {
public PasswordNotFoundException() {
super("Password not found");
}
public PasswordNotFoundException(String message) {
super(message);
}
}
}

View File

@@ -1,77 +1,77 @@
package com.kamco.cd.training.members.service;
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.members.dto.SignInRequest;
import com.kamco.cd.training.postgres.core.MembersCoreService;
import com.kamco.cd.training.postgres.core.TokenCoreService;
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;
private final TokenCoreService tokenCoreService;
/**
* 토큰 저장
*
* @param subject
* @param refreshToken
* @param validityMs
*/
@Transactional
public void tokenSave(String subject, String refreshToken, long validityMs) {
tokenCoreService.save(subject, refreshToken, validityMs);
}
/**
* refreshToken을 DB와 비교 검증
*
* @param subject 사용자 식별(UUID)
* @param requestRefreshToken refresh token
*/
public void validateRefreshToken(String subject, String requestRefreshToken) {
String savedToken = tokenCoreService.getValidTokenOrThrow(subject);
if (!savedToken.equals(requestRefreshToken)) {
throw new CustomApiException(AuthErrorCode.REFRESH_TOKEN_MISMATCH);
}
}
/**
* 로그아웃(토큰폐기)
*
* @param subject 사용자 식별(UUID)
*/
@Transactional
public void logout(String subject) {
// RefreshToken 폐기
tokenCoreService.revokeBySubject(subject);
}
/**
* 로그인 일시 저장
*
* @param uuid
*/
@Transactional
public void saveLogin(UUID uuid) {
membersCoreService.saveLogin(uuid);
}
/**
* 사용자 상태 조회
*
* @param request
* @return
*/
public String getUserStatus(SignInRequest request) {
return membersCoreService.getUserStatus(request);
}
}
package com.kamco.cd.training.members.service;
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.members.dto.SignInRequest;
import com.kamco.cd.training.postgres.core.MembersCoreService;
import com.kamco.cd.training.postgres.core.TokenCoreService;
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;
private final TokenCoreService tokenCoreService;
/**
* 토큰 저장
*
* @param subject
* @param refreshToken
* @param validityMs
*/
@Transactional
public void tokenSave(String subject, String refreshToken, long validityMs) {
tokenCoreService.save(subject, refreshToken, validityMs);
}
/**
* refreshToken을 DB와 비교 검증
*
* @param subject 사용자 식별(UUID)
* @param requestRefreshToken refresh token
*/
public void validateRefreshToken(String subject, String requestRefreshToken) {
String savedToken = tokenCoreService.getValidTokenOrThrow(subject);
if (!savedToken.equals(requestRefreshToken)) {
throw new CustomApiException(AuthErrorCode.REFRESH_TOKEN_MISMATCH);
}
}
/**
* 로그아웃(토큰폐기)
*
* @param subject 사용자 식별(UUID)
*/
@Transactional
public void logout(String subject) {
// RefreshToken 폐기
tokenCoreService.revokeBySubject(subject);
}
/**
* 로그인 일시 저장
*
* @param uuid
*/
@Transactional
public void saveLogin(UUID uuid) {
membersCoreService.saveLogin(uuid);
}
/**
* 사용자 상태 조회
*
* @param request
* @return
*/
public String getUserStatus(SignInRequest request) {
return membersCoreService.getUserStatus(request);
}
}

View File

@@ -1,29 +1,29 @@
package com.kamco.cd.training.members.service;
import com.kamco.cd.training.auth.CustomUserDetails;
import com.kamco.cd.training.postgres.entity.MemberEntity;
import com.kamco.cd.training.postgres.repository.members.MembersRepository;
import java.util.UUID;
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 {
UUID uuid = UUID.fromString(username);
MemberEntity member =
membersRepository
.findByUUID(uuid)
.orElseThrow(() -> new UsernameNotFoundException("USER NOT FOUND"));
return new CustomUserDetails(member);
}
}
package com.kamco.cd.training.members.service;
import com.kamco.cd.training.auth.CustomUserDetails;
import com.kamco.cd.training.postgres.entity.MemberEntity;
import com.kamco.cd.training.postgres.repository.members.MembersRepository;
import java.util.UUID;
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 {
UUID uuid = UUID.fromString(username);
MemberEntity member =
membersRepository
.findByUUID(uuid)
.orElseThrow(() -> new UsernameNotFoundException("USER NOT FOUND"));
return new CustomUserDetails(member);
}
}

View File

@@ -1,45 +1,45 @@
package com.kamco.cd.training.members.service;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.common.utils.CommonStringUtils;
import com.kamco.cd.training.members.dto.MembersDto;
import com.kamco.cd.training.members.dto.MembersDto.Basic;
import com.kamco.cd.training.postgres.core.MembersCoreService;
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;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MembersService {
private final MembersCoreService membersCoreService;
/**
* 회원목록 조회
*
* @param searchReq
* @return
*/
public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) {
return membersCoreService.findByMembers(searchReq);
}
/**
* 패스워드 사용자 변경
*
* @param id
* @param initReq
*/
@Transactional
public void resetPassword(String id, MembersDto.InitReq initReq) {
if (!CommonStringUtils.isValidPassword(initReq.getNewPassword())) {
throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST);
}
membersCoreService.resetPassword(id, initReq);
}
}
package com.kamco.cd.training.members.service;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.common.utils.CommonStringUtils;
import com.kamco.cd.training.members.dto.MembersDto;
import com.kamco.cd.training.members.dto.MembersDto.Basic;
import com.kamco.cd.training.postgres.core.MembersCoreService;
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;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MembersService {
private final MembersCoreService membersCoreService;
/**
* 회원목록 조회
*
* @param searchReq
* @return
*/
public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) {
return membersCoreService.findByMembers(searchReq);
}
/**
* 패스워드 사용자 변경
*
* @param id
* @param initReq
*/
@Transactional
public void resetPassword(String id, MembersDto.InitReq initReq) {
if (!CommonStringUtils.isValidPassword(initReq.getNewPassword())) {
throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST);
}
membersCoreService.resetPassword(id, initReq);
}
}

View File

@@ -1,62 +1,62 @@
package com.kamco.cd.training.menu;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.menu.dto.MenuDto;
import com.kamco.cd.training.menu.service.MenuService;
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 java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "메뉴 관리", description = "메뉴 관리 API")
@RestController
@RequestMapping("/api/menu")
@RequiredArgsConstructor
public class MenuApiController {
private final MenuService menuService;
@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
public ApiResponseDto<List<MenuDto.Basic>> getFindAll() {
return ApiResponseDto.ok(menuService.getFindAll());
}
@Operation(summary = "캐시 초기화", description = "메뉴관리 캐시를 초기화합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "캐시 초기화 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/cache/refresh")
public ApiResponseDto<String> refreshCommonCodeCache() {
menuService.refresh();
return ApiResponseDto.ok("메뉴관리 캐시가 초기화되었습니다.");
}
}
package com.kamco.cd.training.menu;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.menu.dto.MenuDto;
import com.kamco.cd.training.menu.service.MenuService;
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 java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "메뉴 관리", description = "메뉴 관리 API")
@RestController
@RequestMapping("/api/menu")
@RequiredArgsConstructor
public class MenuApiController {
private final MenuService menuService;
@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
public ApiResponseDto<List<MenuDto.Basic>> getFindAll() {
return ApiResponseDto.ok(menuService.getFindAll());
}
@Operation(summary = "캐시 초기화", description = "메뉴관리 캐시를 초기화합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "캐시 초기화 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/cache/refresh")
public ApiResponseDto<String> refreshCommonCodeCache() {
menuService.refresh();
return ApiResponseDto.ok("메뉴관리 캐시가 초기화되었습니다.");
}
}

View File

@@ -1,64 +1,64 @@
package com.kamco.cd.training.menu.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
public class MenuDto {
@Schema(name = "Menu Basic", description = "메뉴 기본 정보")
@Getter
@NoArgsConstructor
public static class Basic {
private String menuUid;
private String menuNm;
private String menuUrl;
private String description;
private Long menuOrder;
private Boolean isUse;
private Boolean deleted;
private Long createdUid;
private Long updatedUid;
private List<Basic> children;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm;
private String menuApiUrl;
public Basic(
String menuUid,
String menuNm,
String menuUrl,
String description,
Long menuOrder,
Boolean isUse,
Boolean deleted,
Long createdUid,
Long updatedUid,
List<Basic> children,
ZonedDateTime createdDttm,
ZonedDateTime updatedDttm,
String menuApiUrl) {
this.menuUid = menuUid;
this.menuNm = menuNm;
this.menuUrl = menuUrl;
this.description = description;
this.menuOrder = menuOrder;
this.isUse = isUse;
this.deleted = deleted;
this.createdUid = createdUid;
this.updatedUid = updatedUid;
this.children = children;
this.createdDttm = createdDttm;
this.updatedDttm = updatedDttm;
this.menuApiUrl = menuApiUrl;
}
}
}
package com.kamco.cd.training.menu.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
public class MenuDto {
@Schema(name = "Menu Basic", description = "메뉴 기본 정보")
@Getter
@NoArgsConstructor
public static class Basic {
private String menuUid;
private String menuNm;
private String menuUrl;
private String description;
private Long menuOrder;
private Boolean isUse;
private Boolean deleted;
private Long createdUid;
private Long updatedUid;
private List<Basic> children;
@JsonFormatDttm private ZonedDateTime createdDttm;
@JsonFormatDttm private ZonedDateTime updatedDttm;
private String menuApiUrl;
public Basic(
String menuUid,
String menuNm,
String menuUrl,
String description,
Long menuOrder,
Boolean isUse,
Boolean deleted,
Long createdUid,
Long updatedUid,
List<Basic> children,
ZonedDateTime createdDttm,
ZonedDateTime updatedDttm,
String menuApiUrl) {
this.menuUid = menuUid;
this.menuNm = menuNm;
this.menuUrl = menuUrl;
this.description = description;
this.menuOrder = menuOrder;
this.isUse = isUse;
this.deleted = deleted;
this.createdUid = createdUid;
this.updatedUid = updatedUid;
this.children = children;
this.createdDttm = createdDttm;
this.updatedDttm = updatedDttm;
this.menuApiUrl = menuApiUrl;
}
}
}

View File

@@ -1,27 +1,27 @@
package com.kamco.cd.training.menu.service;
import com.kamco.cd.training.menu.dto.MenuDto;
import com.kamco.cd.training.postgres.core.MenuCoreService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
// => org.springframework.cache.annotation.Cacheable
@Service
@RequiredArgsConstructor
public class MenuService {
private final MenuCoreService menuCoreService;
@Cacheable("trainMenuFindAll")
public List<MenuDto.Basic> getFindAll() {
return menuCoreService.getFindAll();
}
/** 메모리 캐시 초기화 */
@CacheEvict(value = "trainMenuFindAll", allEntries = true)
public void refresh() {}
}
package com.kamco.cd.training.menu.service;
import com.kamco.cd.training.menu.dto.MenuDto;
import com.kamco.cd.training.postgres.core.MenuCoreService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
// training 서버는 Redis 사용하지 않고 Spring Boot 메모리 캐시를 사용함
// => org.springframework.cache.annotation.Cacheable
@Service
@RequiredArgsConstructor
public class MenuService {
private final MenuCoreService menuCoreService;
@Cacheable("trainMenuFindAll")
public List<MenuDto.Basic> getFindAll() {
return menuCoreService.getFindAll();
}
/** 메모리 캐시 초기화 */
@CacheEvict(value = "trainMenuFindAll", allEntries = true)
public void refresh() {}
}

View File

@@ -1,286 +1,286 @@
package com.kamco.cd.training.model;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.model.dto.ModelMngDto.Basic;
import com.kamco.cd.training.model.service.ModelMngService;
import com.kamco.cd.training.model.service.ModelTrainService;
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 java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.DeleteMapping;
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;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@Tag(name = "모델관리", description = "모델관리 (학습 모델, 하이퍼파라미터, 메모)")
@RequestMapping("/api/models")
public class ModelMngApiController {
private final ModelMngService modelMngService;
private final ModelTrainService modelTrainService;
@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
public ApiResponseDto<Page<Basic>> findByModels(
@Parameter(description = "상태 코드") @RequestParam(required = false) String status,
@Parameter(description = "페이지 번호") @RequestParam(defaultValue = "0") int page,
@Parameter(description = "페이지 크기") @RequestParam(defaultValue = "20") int size) {
ModelMngDto.SearchReq searchReq = new ModelMngDto.SearchReq(status, page, size);
return ApiResponseDto.ok(modelMngService.findByModels(searchReq));
}
@Operation(summary = "학습 모델 상세 조회", description = "학습 모델의 상세 정보를 UUID로 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.Detail.class))),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/{uuid}")
public ApiResponseDto<ModelMngDto.Detail> getModelDetail(
@Parameter(description = "모델 UUID", example = "b7e99739-6736-45f9-a224-8161ecddf287")
@PathVariable
String uuid) {
return ApiResponseDto.ok(modelMngService.getModelDetailByUuid(uuid));
}
// ==================== 학습 모델학습관리 API (5종) ====================
@Operation(summary = "학습 모델 통합 조회", description = "학습 관리 화면에서 학습 이력 리스트와 현재 상태를 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = List.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/train")
public ApiResponseDto<List<ModelMngDto.TrainListRes>> getTrainModelList() {
return ApiResponseDto.ok(modelTrainService.getTrainModelList());
}
@Operation(summary = "학습 설정 통합 조회", description = "학습 실행 팝업 구성에 필요한 모든 데이터를 한 번에 반환합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.FormConfigRes.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/train/form-config")
public ApiResponseDto<ModelMngDto.FormConfigRes> getFormConfig() {
return ApiResponseDto.ok(modelTrainService.getFormConfig());
}
@Operation(summary = "하이퍼파라미터 등록", description = "Step 1 에서 파라미터를 수정하여 신규 버전으로 저장합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "등록 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/hyper-params")
public ApiResponseDto<String> createHyperParam(
@Valid @RequestBody ModelMngDto.HyperParamCreateReq createReq) {
String newVersion = modelTrainService.createHyperParam(createReq);
return ApiResponseDto.ok(newVersion);
}
@Operation(summary = "하이퍼파라미터 단건 조회", description = "특정 버전의 하이퍼파라미터 상세 정보를 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.HyperParamInfo.class))),
@ApiResponse(responseCode = "404", description = "하이퍼파라미터를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/hyper-params/{hyperVer}")
public ApiResponseDto<ModelMngDto.HyperParamInfo> getHyperParam(
@Parameter(description = "하이퍼파라미터 버전", example = "H1") @PathVariable String hyperVer) {
return ApiResponseDto.ok(modelTrainService.getHyperParam(hyperVer));
}
@Operation(summary = "하이퍼파라미터 삭제", description = "특정 버전의 하이퍼파라미터를 삭제합니다 (H1은 삭제 불가)")
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "400", description = "H1은 삭제 불가", content = @Content),
@ApiResponse(responseCode = "404", description = "하이퍼파라미터를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/hyper-params/{hyperVer}")
public ApiResponseDto<Void> deleteHyperParam(
@Parameter(description = "하이퍼파라미터 버전", example = "V3.99.251221.120518") @PathVariable
String hyperVer) {
modelTrainService.deleteHyperParam(hyperVer);
return ApiResponseDto.ok(null);
}
@Operation(summary = "학습 시작", description = "모든 설정(Step 1~3)을 마치고 최종적으로 학습 프로세스를 시작합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "학습 시작 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.TrainStartRes.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/train")
public ApiResponseDto<ModelMngDto.TrainStartRes> startTraining(
@Valid @RequestBody ModelMngDto.TrainStartReq trainReq) {
return ApiResponseDto.ok(modelTrainService.startTraining(trainReq));
}
@Operation(summary = "학습 모델 삭제", description = "목록에서 특정 학습 모델을 삭제합니다")
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "400", description = "진행 중인 모델은 삭제 불가", content = @Content),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/train/{uuid}")
public ApiResponseDto<Void> deleteTrainModel(
@Parameter(description = "모델 UUID") @PathVariable String uuid) {
modelTrainService.deleteTrainModel(uuid);
return ApiResponseDto.ok(null);
}
// ==================== Resume Training (학습 재시작) ====================
@Operation(summary = "학습 재시작 정보 조회", description = "중단된 학습의 재시작 가능 여부와 Checkpoint 정보를 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.ResumeInfo.class))),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/train/{uuid}/resume-info")
public ApiResponseDto<ModelMngDto.ResumeInfo> getResumeInfo(
@Parameter(description = "모델 UUID") @PathVariable String uuid) {
return ApiResponseDto.ok(modelTrainService.getResumeInfo(uuid));
}
@Operation(summary = "학습 재시작", description = "중단된 지점(Checkpoint)부터 학습을 재개합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "재시작 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.ResumeResponse.class))),
@ApiResponse(responseCode = "400", description = "재시작 불가능한 상태", content = @Content),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/train/{uuid}/resume")
public ApiResponseDto<ModelMngDto.ResumeResponse> resumeTraining(
@Parameter(description = "모델 UUID") @PathVariable String uuid,
@Valid @RequestBody ModelMngDto.ResumeRequest resumeReq) {
return ApiResponseDto.ok(modelTrainService.resumeTraining(uuid, resumeReq));
}
// ==================== Best Epoch Setting (Best Epoch 설정) ====================
@Operation(summary = "Best Epoch 설정", description = "사용자가 직접 Best Epoch를 선택하여 설정합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "설정 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.BestEpochResponse.class))),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/train/{uuid}/best-epoch")
public ApiResponseDto<ModelMngDto.BestEpochResponse> setBestEpoch(
@Parameter(description = "모델 UUID") @PathVariable String uuid,
@Valid @RequestBody ModelMngDto.BestEpochRequest bestEpochReq) {
return ApiResponseDto.ok(modelTrainService.setBestEpoch(uuid, bestEpochReq));
}
@Operation(summary = "Epoch별 성능 지표 조회", description = "학습된 모델의 Epoch별 성능 지표를 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = List.class))),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/train/{uuid}/epoch-metrics")
public ApiResponseDto<List<ModelMngDto.EpochMetric>> getEpochMetrics(
@Parameter(description = "모델 UUID") @PathVariable String uuid) {
return ApiResponseDto.ok(modelTrainService.getEpochMetrics(uuid));
}
}
package com.kamco.cd.training.model;
import com.kamco.cd.training.config.api.ApiResponseDto;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.model.dto.ModelMngDto.Basic;
import com.kamco.cd.training.model.service.ModelMngService;
import com.kamco.cd.training.model.service.ModelTrainService;
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 java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.DeleteMapping;
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;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@Tag(name = "모델관리", description = "모델관리 (학습 모델, 하이퍼파라미터, 메모)")
@RequestMapping("/api/models")
public class ModelMngApiController {
private final ModelMngService modelMngService;
private final ModelTrainService modelTrainService;
@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
public ApiResponseDto<Page<Basic>> findByModels(
@Parameter(description = "상태 코드") @RequestParam(required = false) String status,
@Parameter(description = "페이지 번호") @RequestParam(defaultValue = "0") int page,
@Parameter(description = "페이지 크기") @RequestParam(defaultValue = "20") int size) {
ModelMngDto.SearchReq searchReq = new ModelMngDto.SearchReq(status, page, size);
return ApiResponseDto.ok(modelMngService.findByModels(searchReq));
}
@Operation(summary = "학습 모델 상세 조회", description = "학습 모델의 상세 정보를 UUID로 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.Detail.class))),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/{uuid}")
public ApiResponseDto<ModelMngDto.Detail> getModelDetail(
@Parameter(description = "모델 UUID", example = "b7e99739-6736-45f9-a224-8161ecddf287")
@PathVariable
String uuid) {
return ApiResponseDto.ok(modelMngService.getModelDetailByUuid(uuid));
}
// ==================== 학습 모델학습관리 API (5종) ====================
@Operation(summary = "학습 모델 통합 조회", description = "학습 관리 화면에서 학습 이력 리스트와 현재 상태를 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = List.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/train")
public ApiResponseDto<List<ModelMngDto.TrainListRes>> getTrainModelList() {
return ApiResponseDto.ok(modelTrainService.getTrainModelList());
}
@Operation(summary = "학습 설정 통합 조회", description = "학습 실행 팝업 구성에 필요한 모든 데이터를 한 번에 반환합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.FormConfigRes.class))),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/train/form-config")
public ApiResponseDto<ModelMngDto.FormConfigRes> getFormConfig() {
return ApiResponseDto.ok(modelTrainService.getFormConfig());
}
@Operation(summary = "하이퍼파라미터 등록", description = "Step 1 에서 파라미터를 수정하여 신규 버전으로 저장합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "등록 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/hyper-params")
public ApiResponseDto<String> createHyperParam(
@Valid @RequestBody ModelMngDto.HyperParamCreateReq createReq) {
String newVersion = modelTrainService.createHyperParam(createReq);
return ApiResponseDto.ok(newVersion);
}
@Operation(summary = "하이퍼파라미터 단건 조회", description = "특정 버전의 하이퍼파라미터 상세 정보를 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.HyperParamInfo.class))),
@ApiResponse(responseCode = "404", description = "하이퍼파라미터를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/hyper-params/{hyperVer}")
public ApiResponseDto<ModelMngDto.HyperParamInfo> getHyperParam(
@Parameter(description = "하이퍼파라미터 버전", example = "H1") @PathVariable String hyperVer) {
return ApiResponseDto.ok(modelTrainService.getHyperParam(hyperVer));
}
@Operation(summary = "하이퍼파라미터 삭제", description = "특정 버전의 하이퍼파라미터를 삭제합니다 (H1은 삭제 불가)")
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "400", description = "H1은 삭제 불가", content = @Content),
@ApiResponse(responseCode = "404", description = "하이퍼파라미터를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/hyper-params/{hyperVer}")
public ApiResponseDto<Void> deleteHyperParam(
@Parameter(description = "하이퍼파라미터 버전", example = "V3.99.251221.120518") @PathVariable
String hyperVer) {
modelTrainService.deleteHyperParam(hyperVer);
return ApiResponseDto.ok(null);
}
@Operation(summary = "학습 시작", description = "모든 설정(Step 1~3)을 마치고 최종적으로 학습 프로세스를 시작합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "학습 시작 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.TrainStartRes.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/train")
public ApiResponseDto<ModelMngDto.TrainStartRes> startTraining(
@Valid @RequestBody ModelMngDto.TrainStartReq trainReq) {
return ApiResponseDto.ok(modelTrainService.startTraining(trainReq));
}
@Operation(summary = "학습 모델 삭제", description = "목록에서 특정 학습 모델을 삭제합니다")
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "삭제 성공", content = @Content),
@ApiResponse(responseCode = "400", description = "진행 중인 모델은 삭제 불가", content = @Content),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@DeleteMapping("/train/{uuid}")
public ApiResponseDto<Void> deleteTrainModel(
@Parameter(description = "모델 UUID") @PathVariable String uuid) {
modelTrainService.deleteTrainModel(uuid);
return ApiResponseDto.ok(null);
}
// ==================== Resume Training (학습 재시작) ====================
@Operation(summary = "학습 재시작 정보 조회", description = "중단된 학습의 재시작 가능 여부와 Checkpoint 정보를 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.ResumeInfo.class))),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/train/{uuid}/resume-info")
public ApiResponseDto<ModelMngDto.ResumeInfo> getResumeInfo(
@Parameter(description = "모델 UUID") @PathVariable String uuid) {
return ApiResponseDto.ok(modelTrainService.getResumeInfo(uuid));
}
@Operation(summary = "학습 재시작", description = "중단된 지점(Checkpoint)부터 학습을 재개합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "재시작 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.ResumeResponse.class))),
@ApiResponse(responseCode = "400", description = "재시작 불가능한 상태", content = @Content),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/train/{uuid}/resume")
public ApiResponseDto<ModelMngDto.ResumeResponse> resumeTraining(
@Parameter(description = "모델 UUID") @PathVariable String uuid,
@Valid @RequestBody ModelMngDto.ResumeRequest resumeReq) {
return ApiResponseDto.ok(modelTrainService.resumeTraining(uuid, resumeReq));
}
// ==================== Best Epoch Setting (Best Epoch 설정) ====================
@Operation(summary = "Best Epoch 설정", description = "사용자가 직접 Best Epoch를 선택하여 설정합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "설정 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ModelMngDto.BestEpochResponse.class))),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/train/{uuid}/best-epoch")
public ApiResponseDto<ModelMngDto.BestEpochResponse> setBestEpoch(
@Parameter(description = "모델 UUID") @PathVariable String uuid,
@Valid @RequestBody ModelMngDto.BestEpochRequest bestEpochReq) {
return ApiResponseDto.ok(modelTrainService.setBestEpoch(uuid, bestEpochReq));
}
@Operation(summary = "Epoch별 성능 지표 조회", description = "학습된 모델의 Epoch별 성능 지표를 조회합니다")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = List.class))),
@ApiResponse(responseCode = "404", description = "모델을 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/train/{uuid}/epoch-metrics")
public ApiResponseDto<List<ModelMngDto.EpochMetric>> getEpochMetrics(
@Parameter(description = "모델 UUID") @PathVariable String uuid) {
return ApiResponseDto.ok(modelTrainService.getEpochMetrics(uuid));
}
}

View File

@@ -1,219 +1,219 @@
package com.kamco.cd.training.model.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import com.kamco.cd.training.postgres.entity.ModelHyperParamEntity;
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.Setter;
public class HyperParamDto {
@Schema(name = "HyperParam Basic", description = "하이퍼파라미터 기본 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Basic {
private String hyperVer;
// Important
private String backbone;
private String inputSize;
private String cropSize;
private Integer epochCnt;
private Integer batchSize;
// Architecture
private Double dropPathRate;
private Integer frozenStages;
private String neckPolicy;
private String decoderChannels;
private String classWeight;
private Integer numLayers;
// Optimization
private Double learningRate;
private Double weightDecay;
private Double layerDecayRate;
private Boolean ddpFindUnusedParams;
private Integer ignoreIndex;
// Data
private Integer trainNumWorkers;
private Integer valNumWorkers;
private Integer testNumWorkers;
private Boolean trainShuffle;
private Boolean trainPersistent;
private Boolean valPersistent;
// Evaluation
private String metrics;
private String saveBest;
private String saveBestRule;
private Integer valInterval;
private Integer logInterval;
private Integer visInterval;
// Hardware
private Integer gpuCnt;
private String gpuIds;
private Integer masterPort;
// Augmentation
private Double rotProb;
private Double flipProb;
private String rotDegree;
private Double exchangeProb;
private Integer brightnessDelta;
private String contrastRange;
private String saturationRange;
private Integer hueDelta;
// Legacy (deprecated)
private Double dropoutRatio;
private Integer cnnFilterCnt;
// Common
private String memo;
@JsonFormatDttm private ZonedDateTime createdDttm;
public Basic(ModelHyperParamEntity entity) {
this.hyperVer = entity.getHyperVer();
// Important
this.backbone = entity.getBackbone();
this.inputSize = entity.getInputSize();
this.cropSize = entity.getCropSize();
this.epochCnt = entity.getEpochCnt();
this.batchSize = entity.getBatchSize();
// Architecture
this.dropPathRate = entity.getDropPathRate();
this.frozenStages = entity.getFrozenStages();
this.neckPolicy = entity.getNeckPolicy();
this.decoderChannels = entity.getDecoderChannels();
this.classWeight = entity.getClassWeight();
this.numLayers = entity.getNumLayers();
// Optimization
this.learningRate = entity.getLearningRate();
this.weightDecay = entity.getWeightDecay();
this.layerDecayRate = entity.getLayerDecayRate();
this.ddpFindUnusedParams = entity.getDdpFindUnusedParams();
this.ignoreIndex = entity.getIgnoreIndex();
// Data
this.trainNumWorkers = entity.getTrainNumWorkers();
this.valNumWorkers = entity.getValNumWorkers();
this.testNumWorkers = entity.getTestNumWorkers();
this.trainShuffle = entity.getTrainShuffle();
this.trainPersistent = entity.getTrainPersistent();
this.valPersistent = entity.getValPersistent();
// Evaluation
this.metrics = entity.getMetrics();
this.saveBest = entity.getSaveBest();
this.saveBestRule = entity.getSaveBestRule();
this.valInterval = entity.getValInterval();
this.logInterval = entity.getLogInterval();
this.visInterval = entity.getVisInterval();
// Hardware
this.gpuCnt = entity.getGpuCnt();
this.gpuIds = entity.getGpuIds();
this.masterPort = entity.getMasterPort();
// Augmentation
this.rotProb = entity.getRotProb();
this.flipProb = entity.getFlipProb();
this.rotDegree = entity.getRotDegree();
this.exchangeProb = entity.getExchangeProb();
this.brightnessDelta = entity.getBrightnessDelta();
this.contrastRange = entity.getContrastRange();
this.saturationRange = entity.getSaturationRange();
this.hueDelta = entity.getHueDelta();
// Legacy
this.dropoutRatio = entity.getDropoutRatio();
this.cnnFilterCnt = entity.getCnnFilterCnt();
// Common
this.memo = entity.getMemo();
this.createdDttm = entity.getCreatedDttm();
}
}
@Schema(name = "HyperParam AddReq", description = "하이퍼파라미터 등록 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class AddReq {
@NotBlank(message = "버전명은 필수입니다")
private String hyperVer;
// Important
private String backbone;
private String inputSize;
private String cropSize;
private Integer epochCnt;
private Integer batchSize;
// Architecture
private Double dropPathRate;
private Integer frozenStages;
private String neckPolicy;
private String decoderChannels;
private String classWeight;
private Integer numLayers;
// Optimization
private Double learningRate;
private Double weightDecay;
private Double layerDecayRate;
private Boolean ddpFindUnusedParams;
private Integer ignoreIndex;
// Data
private Integer trainNumWorkers;
private Integer valNumWorkers;
private Integer testNumWorkers;
private Boolean trainShuffle;
private Boolean trainPersistent;
private Boolean valPersistent;
// Evaluation
private String metrics;
private String saveBest;
private String saveBestRule;
private Integer valInterval;
private Integer logInterval;
private Integer visInterval;
// Hardware
private Integer gpuCnt;
private String gpuIds;
private Integer masterPort;
// Augmentation
private Double rotProb;
private Double flipProb;
private String rotDegree;
private Double exchangeProb;
private Integer brightnessDelta;
private String contrastRange;
private String saturationRange;
private Integer hueDelta;
// Legacy (deprecated)
private Double dropoutRatio;
private Integer cnnFilterCnt;
// Common
private String memo;
}
}
package com.kamco.cd.training.model.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import com.kamco.cd.training.postgres.entity.ModelHyperParamEntity;
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.Setter;
public class HyperParamDto {
@Schema(name = "HyperParam Basic", description = "하이퍼파라미터 기본 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class Basic {
private String hyperVer;
// Important
private String backbone;
private String inputSize;
private String cropSize;
private Integer epochCnt;
private Integer batchSize;
// Architecture
private Double dropPathRate;
private Integer frozenStages;
private String neckPolicy;
private String decoderChannels;
private String classWeight;
private Integer numLayers;
// Optimization
private Double learningRate;
private Double weightDecay;
private Double layerDecayRate;
private Boolean ddpFindUnusedParams;
private Integer ignoreIndex;
// Data
private Integer trainNumWorkers;
private Integer valNumWorkers;
private Integer testNumWorkers;
private Boolean trainShuffle;
private Boolean trainPersistent;
private Boolean valPersistent;
// Evaluation
private String metrics;
private String saveBest;
private String saveBestRule;
private Integer valInterval;
private Integer logInterval;
private Integer visInterval;
// Hardware
private Integer gpuCnt;
private String gpuIds;
private Integer masterPort;
// Augmentation
private Double rotProb;
private Double flipProb;
private String rotDegree;
private Double exchangeProb;
private Integer brightnessDelta;
private String contrastRange;
private String saturationRange;
private Integer hueDelta;
// Legacy (deprecated)
private Double dropoutRatio;
private Integer cnnFilterCnt;
// Common
private String memo;
@JsonFormatDttm private ZonedDateTime createdDttm;
public Basic(ModelHyperParamEntity entity) {
this.hyperVer = entity.getHyperVer();
// Important
this.backbone = entity.getBackbone();
this.inputSize = entity.getInputSize();
this.cropSize = entity.getCropSize();
this.epochCnt = entity.getEpochCnt();
this.batchSize = entity.getBatchSize();
// Architecture
this.dropPathRate = entity.getDropPathRate();
this.frozenStages = entity.getFrozenStages();
this.neckPolicy = entity.getNeckPolicy();
this.decoderChannels = entity.getDecoderChannels();
this.classWeight = entity.getClassWeight();
this.numLayers = entity.getNumLayers();
// Optimization
this.learningRate = entity.getLearningRate();
this.weightDecay = entity.getWeightDecay();
this.layerDecayRate = entity.getLayerDecayRate();
this.ddpFindUnusedParams = entity.getDdpFindUnusedParams();
this.ignoreIndex = entity.getIgnoreIndex();
// Data
this.trainNumWorkers = entity.getTrainNumWorkers();
this.valNumWorkers = entity.getValNumWorkers();
this.testNumWorkers = entity.getTestNumWorkers();
this.trainShuffle = entity.getTrainShuffle();
this.trainPersistent = entity.getTrainPersistent();
this.valPersistent = entity.getValPersistent();
// Evaluation
this.metrics = entity.getMetrics();
this.saveBest = entity.getSaveBest();
this.saveBestRule = entity.getSaveBestRule();
this.valInterval = entity.getValInterval();
this.logInterval = entity.getLogInterval();
this.visInterval = entity.getVisInterval();
// Hardware
this.gpuCnt = entity.getGpuCnt();
this.gpuIds = entity.getGpuIds();
this.masterPort = entity.getMasterPort();
// Augmentation
this.rotProb = entity.getRotProb();
this.flipProb = entity.getFlipProb();
this.rotDegree = entity.getRotDegree();
this.exchangeProb = entity.getExchangeProb();
this.brightnessDelta = entity.getBrightnessDelta();
this.contrastRange = entity.getContrastRange();
this.saturationRange = entity.getSaturationRange();
this.hueDelta = entity.getHueDelta();
// Legacy
this.dropoutRatio = entity.getDropoutRatio();
this.cnnFilterCnt = entity.getCnnFilterCnt();
// Common
this.memo = entity.getMemo();
this.createdDttm = entity.getCreatedDttm();
}
}
@Schema(name = "HyperParam AddReq", description = "하이퍼파라미터 등록 요청")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class AddReq {
@NotBlank(message = "버전명은 필수입니다")
private String hyperVer;
// Important
private String backbone;
private String inputSize;
private String cropSize;
private Integer epochCnt;
private Integer batchSize;
// Architecture
private Double dropPathRate;
private Integer frozenStages;
private String neckPolicy;
private String decoderChannels;
private String classWeight;
private Integer numLayers;
// Optimization
private Double learningRate;
private Double weightDecay;
private Double layerDecayRate;
private Boolean ddpFindUnusedParams;
private Integer ignoreIndex;
// Data
private Integer trainNumWorkers;
private Integer valNumWorkers;
private Integer testNumWorkers;
private Boolean trainShuffle;
private Boolean trainPersistent;
private Boolean valPersistent;
// Evaluation
private String metrics;
private String saveBest;
private String saveBestRule;
private Integer valInterval;
private Integer logInterval;
private Integer visInterval;
// Hardware
private Integer gpuCnt;
private String gpuIds;
private Integer masterPort;
// Augmentation
private Double rotProb;
private Double flipProb;
private String rotDegree;
private Double exchangeProb;
private Integer brightnessDelta;
private String contrastRange;
private String saturationRange;
private Integer hueDelta;
// Legacy (deprecated)
private Double dropoutRatio;
private Integer cnnFilterCnt;
// Common
private String memo;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,61 +1,61 @@
package com.kamco.cd.training.model.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.ZonedDateTime;
import lombok.Getter;
public class ModelVerDto {
@Schema(name = "modelVer Basic", description = "모델버전 엔티티 기본 정보")
@Getter
public static class Basic {
private final Long id;
private final Long modelUid;
private final String modelCate;
private final String modelVer;
private final String usedState;
private final String modelState;
private final Double qualityProb;
private final String deployState;
private final String modelPath;
@JsonFormatDttm private final ZonedDateTime createdDttm;
private final Long createdUid;
@JsonFormatDttm private final ZonedDateTime updatedDttm;
private final Long updatedUid;
public Basic(
Long id,
Long modelUid,
String modelCate,
String modelVer,
String usedState,
String modelState,
Double qualityProb,
String deployState,
String modelPath,
ZonedDateTime createdDttm,
Long createdUid,
ZonedDateTime updatedDttm,
Long updatedUid) {
this.id = id;
this.modelUid = modelUid;
this.modelCate = modelCate;
this.modelVer = modelVer;
this.usedState = usedState;
this.modelState = modelState;
this.qualityProb = qualityProb;
this.deployState = deployState;
this.modelPath = modelPath;
this.createdDttm = createdDttm;
this.createdUid = createdUid;
this.updatedDttm = updatedDttm;
this.updatedUid = updatedUid;
}
}
}
package com.kamco.cd.training.model.dto;
import com.kamco.cd.training.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.ZonedDateTime;
import lombok.Getter;
public class ModelVerDto {
@Schema(name = "modelVer Basic", description = "모델버전 엔티티 기본 정보")
@Getter
public static class Basic {
private final Long id;
private final Long modelUid;
private final String modelCate;
private final String modelVer;
private final String usedState;
private final String modelState;
private final Double qualityProb;
private final String deployState;
private final String modelPath;
@JsonFormatDttm private final ZonedDateTime createdDttm;
private final Long createdUid;
@JsonFormatDttm private final ZonedDateTime updatedDttm;
private final Long updatedUid;
public Basic(
Long id,
Long modelUid,
String modelCate,
String modelVer,
String usedState,
String modelState,
Double qualityProb,
String deployState,
String modelPath,
ZonedDateTime createdDttm,
Long createdUid,
ZonedDateTime updatedDttm,
Long updatedUid) {
this.id = id;
this.modelUid = modelUid;
this.modelCate = modelCate;
this.modelVer = modelVer;
this.usedState = usedState;
this.modelState = modelState;
this.qualityProb = qualityProb;
this.deployState = deployState;
this.modelPath = modelPath;
this.createdDttm = createdDttm;
this.createdUid = createdUid;
this.updatedDttm = updatedDttm;
this.updatedUid = updatedUid;
}
}
}

View File

@@ -1,50 +1,50 @@
package com.kamco.cd.training.model.service;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.model.dto.ModelMngDto.Basic;
import com.kamco.cd.training.model.dto.ModelMngDto.SearchReq;
import com.kamco.cd.training.postgres.core.ModelMngCoreService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class ModelMngService {
private final ModelMngCoreService modelMngCoreService;
/**
* 모델 목록 조회
*
* @param searchReq 검색 조건
* @return 페이징 처리된 모델 목록
*/
public Page<Basic> findByModels(SearchReq searchReq) {
return modelMngCoreService.findByModels(searchReq);
}
/**
* 모델 상세 조회
*
* @param modelUid 모델 UID
* @return 모델 상세 정보
*/
public ModelMngDto.Detail getModelDetail(Long modelUid) {
return modelMngCoreService.getModelDetail(modelUid);
}
/**
* 모델 상세 조회 (UUID 기반)
*
* @param uuid 모델 UUID
* @return 모델 상세 정보
*/
public ModelMngDto.Detail getModelDetailByUuid(String uuid) {
return modelMngCoreService.getModelDetailByUuid(uuid);
}
}
package com.kamco.cd.training.model.service;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.model.dto.ModelMngDto.Basic;
import com.kamco.cd.training.model.dto.ModelMngDto.SearchReq;
import com.kamco.cd.training.postgres.core.ModelMngCoreService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class ModelMngService {
private final ModelMngCoreService modelMngCoreService;
/**
* 모델 목록 조회
*
* @param searchReq 검색 조건
* @return 페이징 처리된 모델 목록
*/
public Page<Basic> findByModels(SearchReq searchReq) {
return modelMngCoreService.findByModels(searchReq);
}
/**
* 모델 상세 조회
*
* @param modelUid 모델 UID
* @return 모델 상세 정보
*/
public ModelMngDto.Detail getModelDetail(Long modelUid) {
return modelMngCoreService.getModelDetail(modelUid);
}
/**
* 모델 상세 조회 (UUID 기반)
*
* @param uuid 모델 UUID
* @return 모델 상세 정보
*/
public ModelMngDto.Detail getModelDetailByUuid(String uuid) {
return modelMngCoreService.getModelDetailByUuid(uuid);
}
}

View File

@@ -1,393 +1,393 @@
package com.kamco.cd.training.model.service;
import com.kamco.cd.training.common.exception.BadRequestException;
import com.kamco.cd.training.common.exception.NotFoundException;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.postgres.core.DatasetCoreService;
import com.kamco.cd.training.postgres.core.HyperParamCoreService;
import com.kamco.cd.training.postgres.core.ModelMngCoreService;
import com.kamco.cd.training.postgres.core.SystemMetricsCoreService;
import com.kamco.cd.training.postgres.entity.ModelTrainMasterEntity;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class ModelTrainService {
private final ModelMngCoreService modelMngCoreService;
private final HyperParamCoreService hyperParamCoreService;
private final DatasetCoreService datasetCoreService;
private final SystemMetricsCoreService systemMetricsCoreService;
/**
* 학습 모델 목록 조회
*
* @return 학습 모델 목록
*/
public List<ModelMngDto.TrainListRes> getTrainModelList() {
return modelMngCoreService.findAllTrainModels();
}
/**
* 학습 설정 통합 조회
*
* @return 학습 설정 폼 데이터
*/
public ModelMngDto.FormConfigRes getFormConfig() {
// 1. 현재 실행 중인 모델 확인
String runningModelUuid = modelMngCoreService.findRunningModelUuid();
boolean isTrainAvailable = (runningModelUuid == null);
// 2. 저장공간 체크 (10GB 미만 시 학습 불가)
if (isTrainAvailable) {
isTrainAvailable = systemMetricsCoreService.isStorageAvailableForTraining();
long availableMB = systemMetricsCoreService.getAvailableStorageMB();
log.info("저장공간 체크 완료: {}MB 사용 가능, 학습 가능 여부: {}", availableMB, isTrainAvailable);
}
// 3. 하이퍼파라미터 목록
List<ModelMngDto.HyperParamInfo> hyperParams = hyperParamCoreService.findAllActiveHyperParams();
// 4. 데이터셋 목록
List<ModelMngDto.DatasetInfo> datasets = datasetCoreService.findAllActiveDatasetsForTraining();
return ModelMngDto.FormConfigRes.builder()
.isTrainAvailable(isTrainAvailable)
.hyperParams(hyperParams)
.datasets(datasets)
.runningModelUuid(runningModelUuid)
.build();
}
/**
* 하이퍼파라미터 등록
*
* @param createReq 등록 요청
* @return 생성된 버전명
*/
@Transactional
public String createHyperParam(ModelMngDto.HyperParamCreateReq createReq) {
// 신규 버전 추가 시 baseHyperVer가 없으면 H1으로 설정
if (createReq.getBaseHyperVer() == null || createReq.getBaseHyperVer().isEmpty()) {
String firstVersion = hyperParamCoreService.getFirstHyperParamVersion();
createReq.setBaseHyperVer(firstVersion);
log.info("baseHyperVer가 없어 첫 번째 버전으로 설정: {}", firstVersion);
}
String newVersion = hyperParamCoreService.createHyperParam(createReq);
log.info("하이퍼파라미터 등록 완료: {}", newVersion);
return newVersion;
}
/**
* 하이퍼파라미터 단건 조회
*
* @param hyperVer 하이퍼파라미터 버전
* @return 하이퍼파라미터 정보
*/
public ModelMngDto.HyperParamInfo getHyperParam(String hyperVer) {
return hyperParamCoreService.findByHyperVer(hyperVer);
}
/**
* 하이퍼파라미터 삭제
*
* @param hyperVer 하이퍼파라미터 버전
*/
@Transactional
public void deleteHyperParam(String hyperVer) {
hyperParamCoreService.deleteHyperParam(hyperVer);
log.info("하이퍼파라미터 삭제 완료: {}", hyperVer);
}
/**
* 학습 시작
*
* @param trainReq 학습 시작 요청
* @return 학습 시작 응답
*/
@Transactional
public ModelMngDto.TrainStartRes startTraining(ModelMngDto.TrainStartReq trainReq) {
// 1. 동시 실행 방지 검증
String runningModelUuid = modelMngCoreService.findRunningModelUuid();
if (runningModelUuid != null) {
throw new BadRequestException(
"이미 실행 중인 학습이 있습니다. 학습은 한 번에 한 개만 실행할 수 있습니다. (실행 중인 모델: " + runningModelUuid + ")");
}
// 2. 저장공간 체크 (10GB 미만 시 학습 불가)
if (!systemMetricsCoreService.isStorageAvailableForTraining()) {
long availableMB = systemMetricsCoreService.getAvailableStorageMB();
long requiredMB = 10 * 1024; // 10GB
throw new BadRequestException(
String.format(
"저장공간이 부족하여 학습을 시작할 수 없습니다. (필요: %dMB, 사용 가능: %dMB)", requiredMB, availableMB));
}
// 3. 데이터셋 상태 검증 (COMPLETED 상태만 학습 가능)
validateDatasetStatus(trainReq.getDatasetIds());
// 4. 데이터 분할 비율 검증 (예: "7:2:1" 형식)
if (trainReq.getDatasetRatio() != null && !trainReq.getDatasetRatio().isEmpty()) {
validateDatasetRatio(trainReq.getDatasetRatio());
}
// 5. 학습 마스터 생성
ModelTrainMasterEntity entity = modelMngCoreService.createTrainMaster(trainReq);
// 5. 데이터셋 매핑 생성
modelMngCoreService.createDatasetMappings(entity.getId(), trainReq.getDatasetIds());
// 6. 실제 UUID 사용
String uuid = entity.getUuid().toString();
log.info(
"학습 시작: uuid={}, hyperVer={}, epoch={}, datasets={}",
uuid,
trainReq.getHyperVer(),
trainReq.getEpoch(),
trainReq.getDatasetIds());
// TODO: 비동기 GPU 학습 프로세스 트리거 로직 추가
return ModelMngDto.TrainStartRes.builder().uuid(uuid).status(entity.getStatusCd()).build();
}
/**
* 데이터셋 상태 검증
*
* @param datasetIds 데이터셋 ID 목록
*/
private void validateDatasetStatus(List<Long> datasetIds) {
for (Long datasetId : datasetIds) {
try {
var dataset = datasetCoreService.getOneById(datasetId);
// COMPLETED 상태가 아닌 데이터셋이 포함되어 있으면 예외 발생
if (dataset.getStatus() == null || !"COMPLETED".equals(dataset.getStatus())) {
throw new BadRequestException(
String.format(
"학습에 사용할 수 없는 데이터셋입니다. (ID: %d, 상태: %s). COMPLETED 상태의 데이터셋만 선택 가능합니다.",
datasetId, dataset.getStatus() != null ? dataset.getStatus() : "NULL"));
}
log.debug("데이터셋 상태 검증 통과: ID={}, Status={}", datasetId, dataset.getStatus());
} catch (NotFoundException e) {
throw new BadRequestException("존재하지 않는 데이터셋입니다. ID: " + datasetId);
}
}
log.info("모든 데이터셋 상태 검증 완료: {} 개", datasetIds.size());
}
/**
* 데이터 분할 비율 검증
*
* @param datasetRatio 데이터셋 비율 (예: "7:2:1")
*/
private void validateDatasetRatio(String datasetRatio) {
try {
String[] parts = datasetRatio.split(":");
if (parts.length != 3) {
throw new BadRequestException("데이터 분할 비율은 'Training:Validation:Test' 형식이어야 합니다 (예: 7:2:1)");
}
int train = Integer.parseInt(parts[0].trim());
int validation = Integer.parseInt(parts[1].trim());
int test = Integer.parseInt(parts[2].trim());
int sum = train + validation + test;
if (sum != 10) {
throw new BadRequestException(
String.format("데이터 분할 비율의 합계는 10이어야 합니다. (현재 합계: %d, 입력값: %s)", sum, datasetRatio));
}
if (train <= 0 || validation < 0 || test < 0) {
throw new BadRequestException("데이터 분할 비율은 모두 0 이상이어야 합니다 (Training은 1 이상)");
}
log.info(
"데이터 분할 비율 검증 완료: Training={}0%, Validation={}0%, Test={}0%", train, validation, test);
} catch (NumberFormatException e) {
throw new BadRequestException("데이터 분할 비율은 숫자로만 구성되어야 합니다: " + datasetRatio);
}
}
/**
* 학습 모델 삭제
*
* @param uuid 모델 UUID
*/
@Transactional
public void deleteTrainModel(String uuid) {
modelMngCoreService.deleteByUuid(uuid);
log.info("학습 모델 삭제 완료: uuid={}", uuid);
}
// ==================== Resume Training (학습 재시작) ====================
/**
* 학습 재시작 정보 조회
*
* @param uuid 모델 UUID
* @return 재시작 정보
*/
public ModelMngDto.ResumeInfo getResumeInfo(String uuid) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
return ModelMngDto.ResumeInfo.builder()
.canResume(entity.getCanResume() != null && entity.getCanResume())
.lastEpoch(entity.getLastCheckpointEpoch())
.totalEpoch(entity.getEpochCnt())
.checkpointPath(entity.getCheckpointPath())
.failedAt(
entity.getStopDttm() != null
? entity.getStopDttm().atZone(java.time.ZoneId.systemDefault())
: null)
.build();
}
/**
* 학습 재시작
*
* @param uuid 모델 UUID
* @param resumeReq 재시작 요청
* @return 재시작 응답
*/
@Transactional
public ModelMngDto.ResumeResponse resumeTraining(
String uuid, ModelMngDto.ResumeRequest resumeReq) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
// 재시작 가능 여부 검증
if (entity.getCanResume() == null || !entity.getCanResume()) {
throw new IllegalStateException("학습 재시작이 불가능한 모델입니다: " + uuid);
}
if (entity.getLastCheckpointEpoch() == null) {
throw new IllegalStateException("Checkpoint가 존재하지 않습니다: " + uuid);
}
// 상태 업데이트
entity.setStatusCd("RUNNING");
entity.setProgressRate(0);
// 총 Epoch 수 변경 (선택사항)
if (resumeReq.getNewTotalEpoch() != null) {
entity.setEpochCnt(resumeReq.getNewTotalEpoch());
}
log.info(
"학습 재시작: uuid={}, resumeFromEpoch={}, totalEpoch={}",
uuid,
resumeReq.getResumeFromEpoch(),
entity.getEpochCnt());
// TODO: 비동기 GPU 학습 재시작 프로세스 트리거 로직 추가
// - Checkpoint 파일 로드
// - 지정된 Epoch부터 학습 재개
return ModelMngDto.ResumeResponse.builder()
.uuid(uuid)
.status(entity.getStatusCd())
.resumedFromEpoch(resumeReq.getResumeFromEpoch())
.build();
}
// ==================== Best Epoch Setting (Best Epoch 설정) ====================
/**
* Best Epoch 설정
*
* @param uuid 모델 UUID
* @param bestEpochReq Best Epoch 요청
* @return Best Epoch 응답
*/
@Transactional
public ModelMngDto.BestEpochResponse setBestEpoch(
String uuid, ModelMngDto.BestEpochRequest bestEpochReq) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
// 1차 학습 완료 상태 검증
if (!"STEP1_COMPLETED".equals(entity.getStatusCd())
&& !"STEP1".equals(entity.getProcessStep())) {
log.warn(
"Best Epoch 설정 시도: 현재 상태={}, processStep={}",
entity.getStatusCd(),
entity.getProcessStep());
}
Integer previousBestEpoch = entity.getConfirmedBestEpoch();
// 사용자가 확정한 Best Epoch 설정
entity.setConfirmedBestEpoch(bestEpochReq.getBestEpoch());
// 2차 학습(Test) 단계로 상태 전이
entity.setProcessStep("STEP2");
entity.setStatusCd("STEP2_RUNNING");
entity.setProgressRate(0);
entity.setUpdatedDttm(java.time.ZonedDateTime.now());
log.info(
"Best Epoch 설정 및 2차 학습 시작: uuid={}, newBestEpoch={}, previousBestEpoch={}, reason={}, newStatus={}",
uuid,
bestEpochReq.getBestEpoch(),
previousBestEpoch,
bestEpochReq.getReason(),
entity.getStatusCd());
// TODO: 비동기 GPU 2차 학습(Test) 프로세스 트리거 로직 추가
// - Best Epoch 모델 로드
// - Test 데이터셋으로 성능 평가 실행
// - 완료 시 STEP2_COMPLETED 상태로 전환
return ModelMngDto.BestEpochResponse.builder()
.uuid(uuid)
.bestEpoch(entity.getBestEpoch()) // 자동 선택된 값
.confirmedBestEpoch(entity.getConfirmedBestEpoch()) // 사용자 확정 값
.previousBestEpoch(previousBestEpoch)
.build();
}
/**
* Epoch별 성능 지표 조회
*
* @param uuid 모델 UUID
* @return Epoch별 성능 지표 목록
*/
public List<ModelMngDto.EpochMetric> getEpochMetrics(String uuid) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
// TODO: 실제 학습 로그 파일이나 DB에서 Epoch별 성능 지표 조회
// 현재는 샘플 데이터 반환
List<ModelMngDto.EpochMetric> metrics = new java.util.ArrayList<>();
if (entity.getEpochCnt() != null && entity.getBestEpoch() != null) {
// 샘플 데이터 생성 (실제로는 학습 로그 파일 파싱 또는 별도 테이블 조회)
for (int i = 1; i <= Math.min(entity.getEpochCnt(), 10); i++) {
int epoch = entity.getBestEpoch() - 5 + i;
if (epoch <= 0 || epoch > entity.getEpochCnt()) {
continue;
}
metrics.add(
ModelMngDto.EpochMetric.builder()
.epoch(epoch)
.mIoU(0.80 + (Math.random() * 0.15)) // 샘플 데이터
.mFscore(0.85 + (Math.random() * 0.10)) // 샘플 데이터
.loss(0.3 - (Math.random() * 0.15)) // 샘플 데이터
.isBest(entity.getBestEpoch() != null && epoch == entity.getBestEpoch())
.build());
}
}
log.info("Epoch별 성능 지표 조회: uuid={}, metricsCount={}", uuid, metrics.size());
return metrics;
}
}
package com.kamco.cd.training.model.service;
import com.kamco.cd.training.common.exception.BadRequestException;
import com.kamco.cd.training.common.exception.NotFoundException;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.postgres.core.DatasetCoreService;
import com.kamco.cd.training.postgres.core.HyperParamCoreService;
import com.kamco.cd.training.postgres.core.ModelMngCoreService;
import com.kamco.cd.training.postgres.core.SystemMetricsCoreService;
import com.kamco.cd.training.postgres.entity.ModelTrainMasterEntity;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class ModelTrainService {
private final ModelMngCoreService modelMngCoreService;
private final HyperParamCoreService hyperParamCoreService;
private final DatasetCoreService datasetCoreService;
private final SystemMetricsCoreService systemMetricsCoreService;
/**
* 학습 모델 목록 조회
*
* @return 학습 모델 목록
*/
public List<ModelMngDto.TrainListRes> getTrainModelList() {
return modelMngCoreService.findAllTrainModels();
}
/**
* 학습 설정 통합 조회
*
* @return 학습 설정 폼 데이터
*/
public ModelMngDto.FormConfigRes getFormConfig() {
// 1. 현재 실행 중인 모델 확인
String runningModelUuid = modelMngCoreService.findRunningModelUuid();
boolean isTrainAvailable = (runningModelUuid == null);
// 2. 저장공간 체크 (10GB 미만 시 학습 불가)
if (isTrainAvailable) {
isTrainAvailable = systemMetricsCoreService.isStorageAvailableForTraining();
long availableMB = systemMetricsCoreService.getAvailableStorageMB();
log.info("저장공간 체크 완료: {}MB 사용 가능, 학습 가능 여부: {}", availableMB, isTrainAvailable);
}
// 3. 하이퍼파라미터 목록
List<ModelMngDto.HyperParamInfo> hyperParams = hyperParamCoreService.findAllActiveHyperParams();
// 4. 데이터셋 목록
List<ModelMngDto.DatasetInfo> datasets = datasetCoreService.findAllActiveDatasetsForTraining();
return ModelMngDto.FormConfigRes.builder()
.isTrainAvailable(isTrainAvailable)
.hyperParams(hyperParams)
.datasets(datasets)
.runningModelUuid(runningModelUuid)
.build();
}
/**
* 하이퍼파라미터 등록
*
* @param createReq 등록 요청
* @return 생성된 버전명
*/
@Transactional
public String createHyperParam(ModelMngDto.HyperParamCreateReq createReq) {
// 신규 버전 추가 시 baseHyperVer가 없으면 H1으로 설정
if (createReq.getBaseHyperVer() == null || createReq.getBaseHyperVer().isEmpty()) {
String firstVersion = hyperParamCoreService.getFirstHyperParamVersion();
createReq.setBaseHyperVer(firstVersion);
log.info("baseHyperVer가 없어 첫 번째 버전으로 설정: {}", firstVersion);
}
String newVersion = hyperParamCoreService.createHyperParam(createReq);
log.info("하이퍼파라미터 등록 완료: {}", newVersion);
return newVersion;
}
/**
* 하이퍼파라미터 단건 조회
*
* @param hyperVer 하이퍼파라미터 버전
* @return 하이퍼파라미터 정보
*/
public ModelMngDto.HyperParamInfo getHyperParam(String hyperVer) {
return hyperParamCoreService.findByHyperVer(hyperVer);
}
/**
* 하이퍼파라미터 삭제
*
* @param hyperVer 하이퍼파라미터 버전
*/
@Transactional
public void deleteHyperParam(String hyperVer) {
hyperParamCoreService.deleteHyperParam(hyperVer);
log.info("하이퍼파라미터 삭제 완료: {}", hyperVer);
}
/**
* 학습 시작
*
* @param trainReq 학습 시작 요청
* @return 학습 시작 응답
*/
@Transactional
public ModelMngDto.TrainStartRes startTraining(ModelMngDto.TrainStartReq trainReq) {
// 1. 동시 실행 방지 검증
String runningModelUuid = modelMngCoreService.findRunningModelUuid();
if (runningModelUuid != null) {
throw new BadRequestException(
"이미 실행 중인 학습이 있습니다. 학습은 한 번에 한 개만 실행할 수 있습니다. (실행 중인 모델: " + runningModelUuid + ")");
}
// 2. 저장공간 체크 (10GB 미만 시 학습 불가)
if (!systemMetricsCoreService.isStorageAvailableForTraining()) {
long availableMB = systemMetricsCoreService.getAvailableStorageMB();
long requiredMB = 10 * 1024; // 10GB
throw new BadRequestException(
String.format(
"저장공간이 부족하여 학습을 시작할 수 없습니다. (필요: %dMB, 사용 가능: %dMB)", requiredMB, availableMB));
}
// 3. 데이터셋 상태 검증 (COMPLETED 상태만 학습 가능)
validateDatasetStatus(trainReq.getDatasetIds());
// 4. 데이터 분할 비율 검증 (예: "7:2:1" 형식)
if (trainReq.getDatasetRatio() != null && !trainReq.getDatasetRatio().isEmpty()) {
validateDatasetRatio(trainReq.getDatasetRatio());
}
// 5. 학습 마스터 생성
ModelTrainMasterEntity entity = modelMngCoreService.createTrainMaster(trainReq);
// 5. 데이터셋 매핑 생성
modelMngCoreService.createDatasetMappings(entity.getId(), trainReq.getDatasetIds());
// 6. 실제 UUID 사용
String uuid = entity.getUuid().toString();
log.info(
"학습 시작: uuid={}, hyperVer={}, epoch={}, datasets={}",
uuid,
trainReq.getHyperVer(),
trainReq.getEpoch(),
trainReq.getDatasetIds());
// TODO: 비동기 GPU 학습 프로세스 트리거 로직 추가
return ModelMngDto.TrainStartRes.builder().uuid(uuid).status(entity.getStatusCd()).build();
}
/**
* 데이터셋 상태 검증
*
* @param datasetIds 데이터셋 ID 목록
*/
private void validateDatasetStatus(List<Long> datasetIds) {
for (Long datasetId : datasetIds) {
try {
var dataset = datasetCoreService.getOneById(datasetId);
// COMPLETED 상태가 아닌 데이터셋이 포함되어 있으면 예외 발생
if (dataset.getStatus() == null || !"COMPLETED".equals(dataset.getStatus())) {
throw new BadRequestException(
String.format(
"학습에 사용할 수 없는 데이터셋입니다. (ID: %d, 상태: %s). COMPLETED 상태의 데이터셋만 선택 가능합니다.",
datasetId, dataset.getStatus() != null ? dataset.getStatus() : "NULL"));
}
log.debug("데이터셋 상태 검증 통과: ID={}, Status={}", datasetId, dataset.getStatus());
} catch (NotFoundException e) {
throw new BadRequestException("존재하지 않는 데이터셋입니다. ID: " + datasetId);
}
}
log.info("모든 데이터셋 상태 검증 완료: {} 개", datasetIds.size());
}
/**
* 데이터 분할 비율 검증
*
* @param datasetRatio 데이터셋 비율 (예: "7:2:1")
*/
private void validateDatasetRatio(String datasetRatio) {
try {
String[] parts = datasetRatio.split(":");
if (parts.length != 3) {
throw new BadRequestException("데이터 분할 비율은 'Training:Validation:Test' 형식이어야 합니다 (예: 7:2:1)");
}
int train = Integer.parseInt(parts[0].trim());
int validation = Integer.parseInt(parts[1].trim());
int test = Integer.parseInt(parts[2].trim());
int sum = train + validation + test;
if (sum != 10) {
throw new BadRequestException(
String.format("데이터 분할 비율의 합계는 10이어야 합니다. (현재 합계: %d, 입력값: %s)", sum, datasetRatio));
}
if (train <= 0 || validation < 0 || test < 0) {
throw new BadRequestException("데이터 분할 비율은 모두 0 이상이어야 합니다 (Training은 1 이상)");
}
log.info(
"데이터 분할 비율 검증 완료: Training={}0%, Validation={}0%, Test={}0%", train, validation, test);
} catch (NumberFormatException e) {
throw new BadRequestException("데이터 분할 비율은 숫자로만 구성되어야 합니다: " + datasetRatio);
}
}
/**
* 학습 모델 삭제
*
* @param uuid 모델 UUID
*/
@Transactional
public void deleteTrainModel(String uuid) {
modelMngCoreService.deleteByUuid(uuid);
log.info("학습 모델 삭제 완료: uuid={}", uuid);
}
// ==================== Resume Training (학습 재시작) ====================
/**
* 학습 재시작 정보 조회
*
* @param uuid 모델 UUID
* @return 재시작 정보
*/
public ModelMngDto.ResumeInfo getResumeInfo(String uuid) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
return ModelMngDto.ResumeInfo.builder()
.canResume(entity.getCanResume() != null && entity.getCanResume())
.lastEpoch(entity.getLastCheckpointEpoch())
.totalEpoch(entity.getEpochCnt())
.checkpointPath(entity.getCheckpointPath())
.failedAt(
entity.getStopDttm() != null
? entity.getStopDttm().atZone(java.time.ZoneId.systemDefault())
: null)
.build();
}
/**
* 학습 재시작
*
* @param uuid 모델 UUID
* @param resumeReq 재시작 요청
* @return 재시작 응답
*/
@Transactional
public ModelMngDto.ResumeResponse resumeTraining(
String uuid, ModelMngDto.ResumeRequest resumeReq) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
// 재시작 가능 여부 검증
if (entity.getCanResume() == null || !entity.getCanResume()) {
throw new IllegalStateException("학습 재시작이 불가능한 모델입니다: " + uuid);
}
if (entity.getLastCheckpointEpoch() == null) {
throw new IllegalStateException("Checkpoint가 존재하지 않습니다: " + uuid);
}
// 상태 업데이트
entity.setStatusCd("RUNNING");
entity.setProgressRate(0);
// 총 Epoch 수 변경 (선택사항)
if (resumeReq.getNewTotalEpoch() != null) {
entity.setEpochCnt(resumeReq.getNewTotalEpoch());
}
log.info(
"학습 재시작: uuid={}, resumeFromEpoch={}, totalEpoch={}",
uuid,
resumeReq.getResumeFromEpoch(),
entity.getEpochCnt());
// TODO: 비동기 GPU 학습 재시작 프로세스 트리거 로직 추가
// - Checkpoint 파일 로드
// - 지정된 Epoch부터 학습 재개
return ModelMngDto.ResumeResponse.builder()
.uuid(uuid)
.status(entity.getStatusCd())
.resumedFromEpoch(resumeReq.getResumeFromEpoch())
.build();
}
// ==================== Best Epoch Setting (Best Epoch 설정) ====================
/**
* Best Epoch 설정
*
* @param uuid 모델 UUID
* @param bestEpochReq Best Epoch 요청
* @return Best Epoch 응답
*/
@Transactional
public ModelMngDto.BestEpochResponse setBestEpoch(
String uuid, ModelMngDto.BestEpochRequest bestEpochReq) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
// 1차 학습 완료 상태 검증
if (!"STEP1_COMPLETED".equals(entity.getStatusCd())
&& !"STEP1".equals(entity.getProcessStep())) {
log.warn(
"Best Epoch 설정 시도: 현재 상태={}, processStep={}",
entity.getStatusCd(),
entity.getProcessStep());
}
Integer previousBestEpoch = entity.getConfirmedBestEpoch();
// 사용자가 확정한 Best Epoch 설정
entity.setConfirmedBestEpoch(bestEpochReq.getBestEpoch());
// 2차 학습(Test) 단계로 상태 전이
entity.setProcessStep("STEP2");
entity.setStatusCd("STEP2_RUNNING");
entity.setProgressRate(0);
entity.setUpdatedDttm(java.time.ZonedDateTime.now());
log.info(
"Best Epoch 설정 및 2차 학습 시작: uuid={}, newBestEpoch={}, previousBestEpoch={}, reason={}, newStatus={}",
uuid,
bestEpochReq.getBestEpoch(),
previousBestEpoch,
bestEpochReq.getReason(),
entity.getStatusCd());
// TODO: 비동기 GPU 2차 학습(Test) 프로세스 트리거 로직 추가
// - Best Epoch 모델 로드
// - Test 데이터셋으로 성능 평가 실행
// - 완료 시 STEP2_COMPLETED 상태로 전환
return ModelMngDto.BestEpochResponse.builder()
.uuid(uuid)
.bestEpoch(entity.getBestEpoch()) // 자동 선택된 값
.confirmedBestEpoch(entity.getConfirmedBestEpoch()) // 사용자 확정 값
.previousBestEpoch(previousBestEpoch)
.build();
}
/**
* Epoch별 성능 지표 조회
*
* @param uuid 모델 UUID
* @return Epoch별 성능 지표 목록
*/
public List<ModelMngDto.EpochMetric> getEpochMetrics(String uuid) {
ModelTrainMasterEntity entity = modelMngCoreService.findByUuid(uuid);
// TODO: 실제 학습 로그 파일이나 DB에서 Epoch별 성능 지표 조회
// 현재는 샘플 데이터 반환
List<ModelMngDto.EpochMetric> metrics = new java.util.ArrayList<>();
if (entity.getEpochCnt() != null && entity.getBestEpoch() != null) {
// 샘플 데이터 생성 (실제로는 학습 로그 파일 파싱 또는 별도 테이블 조회)
for (int i = 1; i <= Math.min(entity.getEpochCnt(), 10); i++) {
int epoch = entity.getBestEpoch() - 5 + i;
if (epoch <= 0 || epoch > entity.getEpochCnt()) {
continue;
}
metrics.add(
ModelMngDto.EpochMetric.builder()
.epoch(epoch)
.mIoU(0.80 + (Math.random() * 0.15)) // 샘플 데이터
.mFscore(0.85 + (Math.random() * 0.10)) // 샘플 데이터
.loss(0.3 - (Math.random() * 0.15)) // 샘플 데이터
.isBest(entity.getBestEpoch() != null && epoch == entity.getBestEpoch())
.build());
}
}
log.info("Epoch별 성능 지표 조회: uuid={}, metricsCount={}", uuid, metrics.size());
return metrics;
}
}

View File

@@ -1,22 +1,22 @@
package com.kamco.cd.training.postgres;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PrePersist;
import java.time.ZonedDateTime;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
@Getter
@MappedSuperclass
public class CommonCreateEntity {
@CreatedDate
@Column(name = "created_dttm", updatable = false, nullable = false)
private ZonedDateTime createdDate;
@PrePersist
protected void onPersist() {
this.createdDate = ZonedDateTime.now();
}
}
package com.kamco.cd.training.postgres;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PrePersist;
import java.time.ZonedDateTime;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
@Getter
@MappedSuperclass
public class CommonCreateEntity {
@CreatedDate
@Column(name = "created_dttm", updatable = false, nullable = false)
private ZonedDateTime createdDate;
@PrePersist
protected void onPersist() {
this.createdDate = ZonedDateTime.now();
}
}

View File

@@ -1,34 +1,34 @@
package com.kamco.cd.training.postgres;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import java.time.ZonedDateTime;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
@Getter
@MappedSuperclass
public class CommonDateEntity {
@CreatedDate
@Column(name = "created_dttm", updatable = false, nullable = false)
private ZonedDateTime createdDate;
@LastModifiedDate
@Column(name = "updated_dttm", nullable = false)
private ZonedDateTime modifiedDate;
@PrePersist
protected void onPersist() {
this.createdDate = ZonedDateTime.now();
this.modifiedDate = ZonedDateTime.now();
}
@PreUpdate
protected void onUpdate() {
this.modifiedDate = ZonedDateTime.now();
}
}
package com.kamco.cd.training.postgres;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import java.time.ZonedDateTime;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
@Getter
@MappedSuperclass
public class CommonDateEntity {
@CreatedDate
@Column(name = "created_dttm", updatable = false, nullable = false)
private ZonedDateTime createdDate;
@LastModifiedDate
@Column(name = "updated_dttm", nullable = false)
private ZonedDateTime modifiedDate;
@PrePersist
protected void onPersist() {
this.createdDate = ZonedDateTime.now();
this.modifiedDate = ZonedDateTime.now();
}
@PreUpdate
protected void onUpdate() {
this.modifiedDate = ZonedDateTime.now();
}
}

View File

@@ -1,19 +1,19 @@
package com.kamco.cd.training.postgres;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@RequiredArgsConstructor
@Configuration
public class QueryDslConfig {
private final EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
package com.kamco.cd.training.postgres;
import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@RequiredArgsConstructor
@Configuration
public class QueryDslConfig {
private final EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}

View File

@@ -1,30 +1,30 @@
package com.kamco.cd.training.postgres;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.PathBuilder;
import org.springframework.data.domain.Pageable;
public class QuerydslOrderUtil {
/**
* Pageable의 Sort 정보를 QueryDSL OrderSpecifier 배열로 변환
*
* @param pageable Spring Pageable
* @param entityClass 엔티티 클래스 (예: User.class)
* @param alias Q 엔티티 alias (예: "user")
*/
public static <T> OrderSpecifier<?>[] getOrderSpecifiers(
Pageable pageable, Class<T> entityClass, String alias) {
PathBuilder<T> entityPath = new PathBuilder<>(entityClass, alias);
return pageable.getSort().stream()
.map(
sort -> {
Order order = sort.isAscending() ? Order.ASC : Order.DESC;
// PathBuilder.get()는 컬럼명(String)을 동적 Path로 반환
return new OrderSpecifier<>(
order, entityPath.get(sort.getProperty(), Comparable.class));
})
.toArray(OrderSpecifier[]::new);
}
}
package com.kamco.cd.training.postgres;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.PathBuilder;
import org.springframework.data.domain.Pageable;
public class QuerydslOrderUtil {
/**
* Pageable의 Sort 정보를 QueryDSL OrderSpecifier 배열로 변환
*
* @param pageable Spring Pageable
* @param entityClass 엔티티 클래스 (예: User.class)
* @param alias Q 엔티티 alias (예: "user")
*/
public static <T> OrderSpecifier<?>[] getOrderSpecifiers(
Pageable pageable, Class<T> entityClass, String alias) {
PathBuilder<T> entityPath = new PathBuilder<>(entityClass, alias);
return pageable.getSort().stream()
.map(
sort -> {
Order order = sort.isAscending() ? Order.ASC : Order.DESC;
// PathBuilder.get()는 컬럼명(String)을 동적 Path로 반환
return new OrderSpecifier<>(
order, entityPath.get(sort.getProperty(), Comparable.class));
})
.toArray(OrderSpecifier[]::new);
}
}

View File

@@ -1,77 +1,77 @@
// package com.kamco.cd.kamcoback.postgres.core;
//
// import com.kamco.cd.kamcoback.common.service.BaseCoreService;
// import com.kamco.cd.kamcoback.postgres.entity.AnimalEntity;
// import com.kamco.cd.kamcoback.postgres.entity.ZooEntity;
// import com.kamco.cd.kamcoback.postgres.repository.AnimalRepository;
// import com.kamco.cd.kamcoback.postgres.repository.ZooRepository;
// import com.kamco.cd.kamcoback.zoo.dto.AnimalDto;
// import jakarta.persistence.EntityNotFoundException;
// import lombok.RequiredArgsConstructor;
// import org.springframework.data.domain.Page;
// import org.springframework.stereotype.Service;
// import org.springframework.transaction.annotation.Transactional;
//
// @Service
// @RequiredArgsConstructor
// @Transactional(readOnly = true)
// public class AnimalCoreService
// implements BaseCoreService<AnimalDto.Basic, Long, AnimalDto.SearchReq> {
//
// private final AnimalRepository animalRepository;
// private final ZooRepository zooRepository;
//
// @Transactional(readOnly = true)
// public AnimalDto.Basic getDataByUuid(String uuid) {
// AnimalEntity getZoo =
// animalRepository
// .getAnimalByUuid(uuid)
// .orElseThrow(() -> new EntityNotFoundException("Zoo not found with uuid: " + uuid));
// return getZoo.toDto();
// }
//
// // AddReq를 받는 추가 메서드
// @Transactional
// public AnimalDto.Basic create(AnimalDto.AddReq req) {
// ZooEntity zoo = null;
// if (req.getZooUuid() != null) {
// zoo =
// zooRepository
// .getZooByUuid(req.getZooUuid())
// .orElseThrow(
// () ->
// new EntityNotFoundException("Zoo not found with uuid: " +
// req.getZooUuid()));
// }
// AnimalEntity entity = new AnimalEntity(req.getCategory(), req.getSpecies(), req.getName(),
// zoo);
// AnimalEntity saved = animalRepository.save(entity);
// return saved.toDto();
// }
//
// @Override
// @Transactional
// public void remove(Long id) {
// AnimalEntity getAnimal =
// animalRepository
// .getAnimalByUid(id)
// .orElseThrow(() -> new EntityNotFoundException("getAnimal not found with id: " + id));
// getAnimal.deleted();
// }
//
// @Override
// public AnimalDto.Basic getOneById(Long id) {
// AnimalEntity getAnimal =
// animalRepository
// .getAnimalByUid(id)
// .orElseThrow(() -> new EntityNotFoundException("Zoo not found with id: " + id));
// return getAnimal.toDto();
// }
//
// @Override
// public Page<AnimalDto.Basic> search(AnimalDto.SearchReq searchReq) {
//
// Page<AnimalEntity> animalEntities = animalRepository.listAnimal(searchReq);
// return animalEntities.map(AnimalEntity::toDto);
// }
// }
// package com.kamco.cd.kamcoback.postgres.core;
//
// import com.kamco.cd.kamcoback.common.service.BaseCoreService;
// import com.kamco.cd.kamcoback.postgres.entity.AnimalEntity;
// import com.kamco.cd.kamcoback.postgres.entity.ZooEntity;
// import com.kamco.cd.kamcoback.postgres.repository.AnimalRepository;
// import com.kamco.cd.kamcoback.postgres.repository.ZooRepository;
// import com.kamco.cd.kamcoback.zoo.dto.AnimalDto;
// import jakarta.persistence.EntityNotFoundException;
// import lombok.RequiredArgsConstructor;
// import org.springframework.data.domain.Page;
// import org.springframework.stereotype.Service;
// import org.springframework.transaction.annotation.Transactional;
//
// @Service
// @RequiredArgsConstructor
// @Transactional(readOnly = true)
// public class AnimalCoreService
// implements BaseCoreService<AnimalDto.Basic, Long, AnimalDto.SearchReq> {
//
// private final AnimalRepository animalRepository;
// private final ZooRepository zooRepository;
//
// @Transactional(readOnly = true)
// public AnimalDto.Basic getDataByUuid(String uuid) {
// AnimalEntity getZoo =
// animalRepository
// .getAnimalByUuid(uuid)
// .orElseThrow(() -> new EntityNotFoundException("Zoo not found with uuid: " + uuid));
// return getZoo.toDto();
// }
//
// // AddReq를 받는 추가 메서드
// @Transactional
// public AnimalDto.Basic create(AnimalDto.AddReq req) {
// ZooEntity zoo = null;
// if (req.getZooUuid() != null) {
// zoo =
// zooRepository
// .getZooByUuid(req.getZooUuid())
// .orElseThrow(
// () ->
// new EntityNotFoundException("Zoo not found with uuid: " +
// req.getZooUuid()));
// }
// AnimalEntity entity = new AnimalEntity(req.getCategory(), req.getSpecies(), req.getName(),
// zoo);
// AnimalEntity saved = animalRepository.save(entity);
// return saved.toDto();
// }
//
// @Override
// @Transactional
// public void remove(Long id) {
// AnimalEntity getAnimal =
// animalRepository
// .getAnimalByUid(id)
// .orElseThrow(() -> new EntityNotFoundException("getAnimal not found with id: " + id));
// getAnimal.deleted();
// }
//
// @Override
// public AnimalDto.Basic getOneById(Long id) {
// AnimalEntity getAnimal =
// animalRepository
// .getAnimalByUid(id)
// .orElseThrow(() -> new EntityNotFoundException("Zoo not found with id: " + id));
// return getAnimal.toDto();
// }
//
// @Override
// public Page<AnimalDto.Basic> search(AnimalDto.SearchReq searchReq) {
//
// Page<AnimalEntity> animalEntities = animalRepository.listAnimal(searchReq);
// return animalEntities.map(AnimalEntity::toDto);
// }
// }

View File

@@ -1,62 +1,62 @@
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.common.service.BaseCoreService;
import com.kamco.cd.training.log.dto.AuditLogDto;
import com.kamco.cd.training.postgres.repository.log.AuditLogRepository;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuditLogCoreService
implements BaseCoreService<AuditLogDto.DailyAuditList, Long, AuditLogDto.searchReq> {
private final AuditLogRepository auditLogRepository;
@Override
public void remove(Long aLong) {}
@Override
public AuditLogDto.DailyAuditList getOneById(Long aLong) {
return null;
}
@Override
public Page<AuditLogDto.DailyAuditList> search(AuditLogDto.searchReq searchReq) {
return null;
}
public Page<AuditLogDto.DailyAuditList> getLogByDaily(
AuditLogDto.searchReq searchRange, LocalDate startDate, LocalDate endDate) {
return auditLogRepository.findLogByDaily(searchRange, startDate, endDate);
}
public Page<AuditLogDto.MenuAuditList> getLogByMenu(
AuditLogDto.searchReq searchRange, String searchValue) {
return auditLogRepository.findLogByMenu(searchRange, searchValue);
}
public Page<AuditLogDto.UserAuditList> getLogByAccount(
AuditLogDto.searchReq searchRange, String searchValue) {
return auditLogRepository.findLogByAccount(searchRange, searchValue);
}
public Page<AuditLogDto.DailyDetail> getLogByDailyResult(
AuditLogDto.searchReq searchRange, LocalDate logDate) {
return auditLogRepository.findLogByDailyResult(searchRange, logDate);
}
public Page<AuditLogDto.MenuDetail> getLogByMenuResult(
AuditLogDto.searchReq searchRange, String menuId) {
return auditLogRepository.findLogByMenuResult(searchRange, menuId);
}
public Page<AuditLogDto.UserDetail> getLogByAccountResult(
AuditLogDto.searchReq searchRange, Long accountId) {
return auditLogRepository.findLogByAccountResult(searchRange, accountId);
}
}
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.common.service.BaseCoreService;
import com.kamco.cd.training.log.dto.AuditLogDto;
import com.kamco.cd.training.postgres.repository.log.AuditLogRepository;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuditLogCoreService
implements BaseCoreService<AuditLogDto.DailyAuditList, Long, AuditLogDto.searchReq> {
private final AuditLogRepository auditLogRepository;
@Override
public void remove(Long aLong) {}
@Override
public AuditLogDto.DailyAuditList getOneById(Long aLong) {
return null;
}
@Override
public Page<AuditLogDto.DailyAuditList> search(AuditLogDto.searchReq searchReq) {
return null;
}
public Page<AuditLogDto.DailyAuditList> getLogByDaily(
AuditLogDto.searchReq searchRange, LocalDate startDate, LocalDate endDate) {
return auditLogRepository.findLogByDaily(searchRange, startDate, endDate);
}
public Page<AuditLogDto.MenuAuditList> getLogByMenu(
AuditLogDto.searchReq searchRange, String searchValue) {
return auditLogRepository.findLogByMenu(searchRange, searchValue);
}
public Page<AuditLogDto.UserAuditList> getLogByAccount(
AuditLogDto.searchReq searchRange, String searchValue) {
return auditLogRepository.findLogByAccount(searchRange, searchValue);
}
public Page<AuditLogDto.DailyDetail> getLogByDailyResult(
AuditLogDto.searchReq searchRange, LocalDate logDate) {
return auditLogRepository.findLogByDailyResult(searchRange, logDate);
}
public Page<AuditLogDto.MenuDetail> getLogByMenuResult(
AuditLogDto.searchReq searchRange, String menuId) {
return auditLogRepository.findLogByMenuResult(searchRange, menuId);
}
public Page<AuditLogDto.UserDetail> getLogByAccountResult(
AuditLogDto.searchReq searchRange, Long accountId) {
return auditLogRepository.findLogByAccountResult(searchRange, accountId);
}
}

View File

@@ -1,217 +1,217 @@
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.code.dto.CommonCodeDto;
import com.kamco.cd.training.code.dto.CommonCodeDto.Basic;
import com.kamco.cd.training.code.dto.CommonCodeDto.SearchReq;
import com.kamco.cd.training.common.service.BaseCoreService;
import com.kamco.cd.training.config.api.ApiResponseDto.ApiResponseCode;
import com.kamco.cd.training.config.api.ApiResponseDto.ResponseObj;
import com.kamco.cd.training.postgres.entity.CommonCodeEntity;
import com.kamco.cd.training.postgres.repository.code.CommonCodeRepository;
import jakarta.persistence.EntityNotFoundException;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CommonCodeCoreService
implements BaseCoreService<CommonCodeDto.Basic, Long, SearchReq> {
private final CommonCodeRepository commonCodeRepository;
/**
* 모두 조회
*
* @return
*/
// @Cacheable(value = "commonCodes")
public List<CommonCodeDto.Basic> findAll() {
return commonCodeRepository.findByAll().stream().map(CommonCodeEntity::toDto).toList();
}
/**
* 등록
*
* @param req
* @return
*/
// @CacheEvict(value = "commonCodes", allEntries = true)
public ResponseObj save(CommonCodeDto.AddReq req) {
String regex = "^[A-Z0-9]+(_[A-Z0-9]+)*$";
boolean isValid = req.getCode().matches(regex);
if (!isValid) {
return new ResponseObj(ApiResponseCode.CONFLICT, "공통코드에 영문 대문자, 숫자, 언더바(_)만 입력 가능합니다.");
}
Long existsCount =
commonCodeRepository.findByParentIdCodeExists(req.getParentId(), req.getCode());
if (existsCount > 0) {
return new ResponseObj(ApiResponseCode.DUPLICATE_DATA, "이미 사용중인 공통코드ID 입니다.");
}
CommonCodeEntity entity =
new CommonCodeEntity(
req.getCode(),
req.getName(),
req.getDescription(),
req.getOrder(),
req.isUsed(),
req.getProps1(),
req.getProps2(),
req.getProps3());
if (req.getParentId() != null) {
CommonCodeEntity parentCommonCodeEntity =
commonCodeRepository
.findById(req.getParentId())
.orElseThrow(
() ->
new EntityNotFoundException(
"parent id 를 찾을 수 없습니다. id : " + req.getParentId()));
entity.addParent(parentCommonCodeEntity);
} else {
entity.addParent(null);
}
commonCodeRepository.save(entity).toDto();
return new ResponseObj(ApiResponseCode.OK, "");
}
/**
* 수정
*
* @param id
* @param req
* @return
*/
// @CacheEvict(value = "commonCodes", allEntries = true)
public ResponseObj update(Long id, CommonCodeDto.ModifyReq req) {
CommonCodeEntity found =
commonCodeRepository
.findByCodeId(id)
.orElseThrow(() -> new EntityNotFoundException("common code 를 찾을 수 없습니다. id : " + id));
found.update(
req.getName(),
req.getDescription(),
req.isUsed(),
req.getProps1(),
req.getProps2(),
req.getProps3());
return new ResponseObj(ApiResponseCode.OK, "");
}
/**
* 순서 변경
*
* @param req
* @return
*/
// @CacheEvict(value = "commonCodes", allEntries = true)
public ResponseObj updateOrder(CommonCodeDto.OrderReq req) {
CommonCodeEntity found =
commonCodeRepository
.findByCodeId(req.getId())
.orElseThrow(
() -> new EntityNotFoundException("common code 를 찾을 수 없습니다. id : " + req.getId()));
found.updateOrder(req.getOrder());
return new ResponseObj(ApiResponseCode.OK, "");
}
public List<CommonCodeDto.Basic> findByCode(String code) {
return commonCodeRepository.findByCode(code).stream().map(CommonCodeEntity::toDto).toList();
}
/**
* 공통코드 이름 조회
*
* @param parentCodeCd
* @param childCodeCd
* @return
*/
public Optional<String> getCode(String parentCodeCd, String childCodeCd) {
return commonCodeRepository.getCode(parentCodeCd, childCodeCd);
}
/**
* 공통코드 삭제
*
* @param id
* @return
*/
// @CacheEvict(value = "commonCodes", allEntries = true)
public ResponseObj removeCode(Long id) {
CommonCodeEntity entity =
commonCodeRepository
.findByCodeId(id)
.orElseThrow(() -> new EntityNotFoundException("code를 찾을 수 없습니다. id " + id));
// 하위코드가 있으면 삭제 불가
if (!entity.getChildren().isEmpty()) {
return new ResponseObj(
ApiResponseCode.UNPROCESSABLE_ENTITY,
"하위에 다른 공통코드를 가지고 있습니다.<br/>하위공통 코드를 이동한 후 삭제할 수 있습니다.");
}
// id 코드 deleted = false 업데이트
entity.deleted();
return new ResponseObj(ApiResponseCode.OK, "");
}
@Override
public void remove(Long aLong) {
// 미사용
}
/**
* id 로 단건 조회
*
* @param id
* @return
*/
@Override
public Basic getOneById(Long id) {
CommonCodeEntity entity =
commonCodeRepository
.findByCodeId(id)
.orElseThrow(() -> new EntityNotFoundException("code를 찾을 수 없습니다. id " + id));
return entity.toDto();
}
@Override
public Page<Basic> search(SearchReq searchReq) {
return null;
}
/**
* 중복 체크
*
* @param parentId
* @param code
* @return
*/
public ResponseObj getCodeCheckDuplicate(Long parentId, String code) {
Long existsCount = commonCodeRepository.findByParentIdCodeExists(parentId, code);
String regex = "^[A-Z0-9]+(_[A-Z0-9]+)*$";
boolean isValid = code.matches(regex);
if (!isValid) {
return new ResponseObj(ApiResponseCode.CONFLICT, "공통코드에 영문 대문자, 숫자, 언더바(_)만 입력 가능합니다.");
}
if (existsCount > 0) {
return new ResponseObj(ApiResponseCode.DUPLICATE_DATA, "이미 사용중인 공통코드ID 입니다.");
}
return new ResponseObj(ApiResponseCode.OK, "");
}
}
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.code.dto.CommonCodeDto;
import com.kamco.cd.training.code.dto.CommonCodeDto.Basic;
import com.kamco.cd.training.code.dto.CommonCodeDto.SearchReq;
import com.kamco.cd.training.common.service.BaseCoreService;
import com.kamco.cd.training.config.api.ApiResponseDto.ApiResponseCode;
import com.kamco.cd.training.config.api.ApiResponseDto.ResponseObj;
import com.kamco.cd.training.postgres.entity.CommonCodeEntity;
import com.kamco.cd.training.postgres.repository.code.CommonCodeRepository;
import jakarta.persistence.EntityNotFoundException;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CommonCodeCoreService
implements BaseCoreService<CommonCodeDto.Basic, Long, SearchReq> {
private final CommonCodeRepository commonCodeRepository;
/**
* 모두 조회
*
* @return
*/
// @Cacheable(value = "commonCodes")
public List<CommonCodeDto.Basic> findAll() {
return commonCodeRepository.findByAll().stream().map(CommonCodeEntity::toDto).toList();
}
/**
* 등록
*
* @param req
* @return
*/
// @CacheEvict(value = "commonCodes", allEntries = true)
public ResponseObj save(CommonCodeDto.AddReq req) {
String regex = "^[A-Z0-9]+(_[A-Z0-9]+)*$";
boolean isValid = req.getCode().matches(regex);
if (!isValid) {
return new ResponseObj(ApiResponseCode.CONFLICT, "공통코드에 영문 대문자, 숫자, 언더바(_)만 입력 가능합니다.");
}
Long existsCount =
commonCodeRepository.findByParentIdCodeExists(req.getParentId(), req.getCode());
if (existsCount > 0) {
return new ResponseObj(ApiResponseCode.DUPLICATE_DATA, "이미 사용중인 공통코드ID 입니다.");
}
CommonCodeEntity entity =
new CommonCodeEntity(
req.getCode(),
req.getName(),
req.getDescription(),
req.getOrder(),
req.isUsed(),
req.getProps1(),
req.getProps2(),
req.getProps3());
if (req.getParentId() != null) {
CommonCodeEntity parentCommonCodeEntity =
commonCodeRepository
.findById(req.getParentId())
.orElseThrow(
() ->
new EntityNotFoundException(
"parent id 를 찾을 수 없습니다. id : " + req.getParentId()));
entity.addParent(parentCommonCodeEntity);
} else {
entity.addParent(null);
}
commonCodeRepository.save(entity).toDto();
return new ResponseObj(ApiResponseCode.OK, "");
}
/**
* 수정
*
* @param id
* @param req
* @return
*/
// @CacheEvict(value = "commonCodes", allEntries = true)
public ResponseObj update(Long id, CommonCodeDto.ModifyReq req) {
CommonCodeEntity found =
commonCodeRepository
.findByCodeId(id)
.orElseThrow(() -> new EntityNotFoundException("common code 를 찾을 수 없습니다. id : " + id));
found.update(
req.getName(),
req.getDescription(),
req.isUsed(),
req.getProps1(),
req.getProps2(),
req.getProps3());
return new ResponseObj(ApiResponseCode.OK, "");
}
/**
* 순서 변경
*
* @param req
* @return
*/
// @CacheEvict(value = "commonCodes", allEntries = true)
public ResponseObj updateOrder(CommonCodeDto.OrderReq req) {
CommonCodeEntity found =
commonCodeRepository
.findByCodeId(req.getId())
.orElseThrow(
() -> new EntityNotFoundException("common code 를 찾을 수 없습니다. id : " + req.getId()));
found.updateOrder(req.getOrder());
return new ResponseObj(ApiResponseCode.OK, "");
}
public List<CommonCodeDto.Basic> findByCode(String code) {
return commonCodeRepository.findByCode(code).stream().map(CommonCodeEntity::toDto).toList();
}
/**
* 공통코드 이름 조회
*
* @param parentCodeCd
* @param childCodeCd
* @return
*/
public Optional<String> getCode(String parentCodeCd, String childCodeCd) {
return commonCodeRepository.getCode(parentCodeCd, childCodeCd);
}
/**
* 공통코드 삭제
*
* @param id
* @return
*/
// @CacheEvict(value = "commonCodes", allEntries = true)
public ResponseObj removeCode(Long id) {
CommonCodeEntity entity =
commonCodeRepository
.findByCodeId(id)
.orElseThrow(() -> new EntityNotFoundException("code를 찾을 수 없습니다. id " + id));
// 하위코드가 있으면 삭제 불가
if (!entity.getChildren().isEmpty()) {
return new ResponseObj(
ApiResponseCode.UNPROCESSABLE_ENTITY,
"하위에 다른 공통코드를 가지고 있습니다.<br/>하위공통 코드를 이동한 후 삭제할 수 있습니다.");
}
// id 코드 deleted = false 업데이트
entity.deleted();
return new ResponseObj(ApiResponseCode.OK, "");
}
@Override
public void remove(Long aLong) {
// 미사용
}
/**
* id 로 단건 조회
*
* @param id
* @return
*/
@Override
public Basic getOneById(Long id) {
CommonCodeEntity entity =
commonCodeRepository
.findByCodeId(id)
.orElseThrow(() -> new EntityNotFoundException("code를 찾을 수 없습니다. id " + id));
return entity.toDto();
}
@Override
public Page<Basic> search(SearchReq searchReq) {
return null;
}
/**
* 중복 체크
*
* @param parentId
* @param code
* @return
*/
public ResponseObj getCodeCheckDuplicate(Long parentId, String code) {
Long existsCount = commonCodeRepository.findByParentIdCodeExists(parentId, code);
String regex = "^[A-Z0-9]+(_[A-Z0-9]+)*$";
boolean isValid = code.matches(regex);
if (!isValid) {
return new ResponseObj(ApiResponseCode.CONFLICT, "공통코드에 영문 대문자, 숫자, 언더바(_)만 입력 가능합니다.");
}
if (existsCount > 0) {
return new ResponseObj(ApiResponseCode.DUPLICATE_DATA, "이미 사용중인 공통코드ID 입니다.");
}
return new ResponseObj(ApiResponseCode.OK, "");
}
}

View File

@@ -1,276 +1,276 @@
package com.kamco.cd.training.postgres.core;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.training.common.exception.NotFoundException;
import com.kamco.cd.training.common.service.BaseCoreService;
import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.postgres.entity.DatasetEntity;
import com.kamco.cd.training.postgres.repository.dataset.DatasetRepository;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class DatasetCoreService
implements BaseCoreService<DatasetDto.Basic, Long, DatasetDto.SearchReq> {
private final DatasetRepository datasetRepository;
private final ObjectMapper objectMapper;
/**
* 학습 데이터 삭제
*
* @param id 데이터셋 ID
*/
@Override
public void remove(Long id) {
DatasetEntity entity =
datasetRepository
.findById(id)
.orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + id));
entity.setDeleted(true);
datasetRepository.save(entity);
}
public void remove(UUID id) {
DatasetEntity entity =
datasetRepository
.findByUuid(id)
.orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + id));
entity.setDeleted(true);
datasetRepository.save(entity);
}
@Override
public DatasetDto.Basic getOneById(Long id) {
DatasetEntity entity =
datasetRepository
.findById(id)
.orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + id));
if (entity.getDeleted()) {
throw new NotFoundException("삭제된 데이터셋입니다. ID: " + id);
}
return entity.toDto();
}
public DatasetDto.Basic getOneByUuid(UUID id) {
DatasetEntity entity =
datasetRepository
.findByUuid(id)
.orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. uuid: " + id));
return entity.toDto();
}
@Override
public Page<DatasetDto.Basic> search(DatasetDto.SearchReq searchReq) {
Page<DatasetEntity> entityPage = datasetRepository.findDatasetList(searchReq);
return entityPage.map(DatasetEntity::toDto);
}
/**
* 학습데이터 조회
*
* @param searchReq 검색 조건
* @return 페이징 처리된 데이터셋 목록
*/
public Page<DatasetDto.Basic> findDatasetList(DatasetDto.SearchReq searchReq) {
return search(searchReq);
}
/**
* 학습데이터 등록
*
* @param registerReq 등록 요청 데이터
* @return 등록된 데이터셋 정보
*/
public DatasetDto.Basic save(DatasetDto.RegisterReq registerReq) {
// 먼저 id1 필드를 임시값(0)으로 설정하여 저장
DatasetEntity entity = new DatasetEntity();
entity.setTitle(registerReq.getTitle());
entity.setYear(registerReq.getYear());
entity.setGroupTitle("PRODUCTION");
entity.setDataYear(registerReq.getYear());
entity.setRoundNo(registerReq.getRoundNo() != null ? registerReq.getRoundNo() : 1L);
entity.setMemo(registerReq.getMemo());
entity.setStatus("READY");
entity.setDataType("CREATE");
entity.setTotalItems(0L);
entity.setTotalSize(0L);
entity.setItemCount(0L);
entity.setDeleted(false);
entity.setCreatedDttm(ZonedDateTime.now());
entity.setId1(0L); // 임시값
DatasetEntity savedEntity = datasetRepository.save(entity);
// 저장 후 id1을 dataset_uid와 동일하게 업데이트
savedEntity.setId1(savedEntity.getId());
savedEntity = datasetRepository.save(savedEntity);
return savedEntity.toDto();
}
/**
* 학습 데이터 수정
*
* @param updateReq 수정 요청 데이터
* @return 수정된 데이터셋 정보
*/
public void update(UUID uuid, DatasetDto.UpdateReq updateReq) {
DatasetEntity entity =
datasetRepository
.findByUuid(uuid)
.orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. uuid: " + uuid));
if (StringUtils.isNotBlank(updateReq.getTitle())) {
entity.setTitle(updateReq.getTitle());
}
if (StringUtils.isNotBlank(updateReq.getMemo())) {
entity.setMemo(updateReq.getMemo());
}
datasetRepository.save(entity);
}
/**
* 학습데이터 삭제
*
* @param uuid 삭제 요청 (데이터셋 ID 목록)
*/
public void deleteDatasets(UUID uuid) {
remove(uuid);
}
public DatasetDto.Summary getDatasetSummary(DatasetDto.SummaryReq summaryReq) {
long totalMapSheets = 0;
long totalFileSize = 0;
for (Long datasetId : summaryReq.getDatasetIds()) {
DatasetEntity entity =
datasetRepository
.findById(datasetId)
.orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + datasetId));
if (!entity.getDeleted()) {
totalMapSheets += entity.getTotalItems() != null ? entity.getTotalItems() : 0;
totalFileSize += entity.getTotalSize() != null ? entity.getTotalSize() : 0;
}
}
double averageMapSheets =
!summaryReq.getDatasetIds().isEmpty()
? (double) totalMapSheets / summaryReq.getDatasetIds().size()
: 0;
return new DatasetDto.Summary(
summaryReq.getDatasetIds().size(), totalMapSheets, totalFileSize, averageMapSheets);
}
/**
* 활성 데이터셋 전체 조회 (학습 관리용)
*
* @return 데이터셋 정보 목록
*/
public List<ModelMngDto.DatasetInfo> findAllActiveDatasetsForTraining() {
List<DatasetEntity> entities = datasetRepository.findByDeletedOrderByCreatedDttmDesc(false);
return entities.stream()
.map(
entity -> {
// totalSize를 읽기 쉬운 형식으로 변환
String totalSizeStr = formatFileSize(entity.getTotalSize());
// classCounts JSON 파싱
Map<String, Integer> classCounts = entity.getClassCounts();
return ModelMngDto.DatasetInfo.builder()
.id(entity.getId())
.title(entity.getTitle())
.groupTitle(entity.getGroupTitle())
.totalItems(entity.getTotalItems())
.totalSize(totalSizeStr)
.classCounts(classCounts)
.memo(entity.getMemo())
.createdDttm(entity.getCreatedDttm())
.build();
})
.toList();
}
/**
* JSON 문자열을 Map으로 파싱
*
* @param jsonStr JSON 문자열
* @return 클래스별 카운트 맵
*/
private Map<String, Integer> parseClassCounts(String jsonStr) {
if (jsonStr == null || jsonStr.trim().isEmpty()) {
return new HashMap<>();
}
try {
return objectMapper.readValue(jsonStr, new TypeReference<Map<String, Integer>>() {});
} catch (Exception e) {
log.warn("클래스 통계 JSON 파싱 실패: {}", jsonStr, e);
return new HashMap<>();
}
}
/**
* 데이터셋의 클래스 통계 계산 및 저장
*
* @param datasetId 데이터셋 ID
* @param classCounts 클래스별 카운트
*/
public void updateClassCounts(Long datasetId, Map<String, Integer> classCounts) {
DatasetEntity entity =
datasetRepository
.findById(datasetId)
.orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + datasetId));
try {
entity.setClassCounts(classCounts);
datasetRepository.save(entity);
log.info("데이터셋 클래스 통계 업데이트 완료: datasetId={}, classes={}", datasetId, classCounts.keySet());
} catch (Exception e) {
log.error("클래스 통계 JSON 변환 실패: datasetId={}", datasetId, e);
}
}
/**
* 파일 크기를 읽기 쉬운 형식으로 변환
*
* @param size 바이트 단위 크기
* @return 형식화된 문자열 (예: "1.5GB")
*/
private String formatFileSize(Long size) {
if (size == null || size == 0) {
return "0 GB";
}
double gb = size / (1024.0 * 1024.0 * 1024.0);
if (gb >= 1.0) {
return String.format("%.2f GB", gb);
}
double mb = size / (1024.0 * 1024.0);
if (mb >= 1.0) {
return String.format("%.2f MB", mb);
}
double kb = size / 1024.0;
return String.format("%.2f KB", kb);
}
}
package com.kamco.cd.training.postgres.core;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.training.common.exception.NotFoundException;
import com.kamco.cd.training.common.service.BaseCoreService;
import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.postgres.entity.DatasetEntity;
import com.kamco.cd.training.postgres.repository.dataset.DatasetRepository;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class DatasetCoreService
implements BaseCoreService<DatasetDto.Basic, Long, DatasetDto.SearchReq> {
private final DatasetRepository datasetRepository;
private final ObjectMapper objectMapper;
/**
* 학습 데이터 삭제
*
* @param id 데이터셋 ID
*/
@Override
public void remove(Long id) {
DatasetEntity entity =
datasetRepository
.findById(id)
.orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + id));
entity.setDeleted(true);
datasetRepository.save(entity);
}
public void remove(UUID id) {
DatasetEntity entity =
datasetRepository
.findByUuid(id)
.orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + id));
entity.setDeleted(true);
datasetRepository.save(entity);
}
@Override
public DatasetDto.Basic getOneById(Long id) {
DatasetEntity entity =
datasetRepository
.findById(id)
.orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + id));
if (entity.getDeleted()) {
throw new NotFoundException("삭제된 데이터셋입니다. ID: " + id);
}
return entity.toDto();
}
public DatasetDto.Basic getOneByUuid(UUID id) {
DatasetEntity entity =
datasetRepository
.findByUuid(id)
.orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. uuid: " + id));
return entity.toDto();
}
@Override
public Page<DatasetDto.Basic> search(DatasetDto.SearchReq searchReq) {
Page<DatasetEntity> entityPage = datasetRepository.findDatasetList(searchReq);
return entityPage.map(DatasetEntity::toDto);
}
/**
* 학습데이터 조회
*
* @param searchReq 검색 조건
* @return 페이징 처리된 데이터셋 목록
*/
public Page<DatasetDto.Basic> findDatasetList(DatasetDto.SearchReq searchReq) {
return search(searchReq);
}
/**
* 학습데이터 등록
*
* @param registerReq 등록 요청 데이터
* @return 등록된 데이터셋 정보
*/
public DatasetDto.Basic save(DatasetDto.RegisterReq registerReq) {
// 먼저 id1 필드를 임시값(0)으로 설정하여 저장
DatasetEntity entity = new DatasetEntity();
entity.setTitle(registerReq.getTitle());
entity.setYear(registerReq.getYear());
entity.setGroupTitle("PRODUCTION");
entity.setDataYear(registerReq.getYear());
entity.setRoundNo(registerReq.getRoundNo() != null ? registerReq.getRoundNo() : 1L);
entity.setMemo(registerReq.getMemo());
entity.setStatus("READY");
entity.setDataType("CREATE");
entity.setTotalItems(0L);
entity.setTotalSize(0L);
entity.setItemCount(0L);
entity.setDeleted(false);
entity.setCreatedDttm(ZonedDateTime.now());
entity.setId1(0L); // 임시값
DatasetEntity savedEntity = datasetRepository.save(entity);
// 저장 후 id1을 dataset_uid와 동일하게 업데이트
savedEntity.setId1(savedEntity.getId());
savedEntity = datasetRepository.save(savedEntity);
return savedEntity.toDto();
}
/**
* 학습 데이터 수정
*
* @param updateReq 수정 요청 데이터
* @return 수정된 데이터셋 정보
*/
public void update(UUID uuid, DatasetDto.UpdateReq updateReq) {
DatasetEntity entity =
datasetRepository
.findByUuid(uuid)
.orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. uuid: " + uuid));
if (StringUtils.isNotBlank(updateReq.getTitle())) {
entity.setTitle(updateReq.getTitle());
}
if (StringUtils.isNotBlank(updateReq.getMemo())) {
entity.setMemo(updateReq.getMemo());
}
datasetRepository.save(entity);
}
/**
* 학습데이터 삭제
*
* @param uuid 삭제 요청 (데이터셋 ID 목록)
*/
public void deleteDatasets(UUID uuid) {
remove(uuid);
}
public DatasetDto.Summary getDatasetSummary(DatasetDto.SummaryReq summaryReq) {
long totalMapSheets = 0;
long totalFileSize = 0;
for (Long datasetId : summaryReq.getDatasetIds()) {
DatasetEntity entity =
datasetRepository
.findById(datasetId)
.orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + datasetId));
if (!entity.getDeleted()) {
totalMapSheets += entity.getTotalItems() != null ? entity.getTotalItems() : 0;
totalFileSize += entity.getTotalSize() != null ? entity.getTotalSize() : 0;
}
}
double averageMapSheets =
!summaryReq.getDatasetIds().isEmpty()
? (double) totalMapSheets / summaryReq.getDatasetIds().size()
: 0;
return new DatasetDto.Summary(
summaryReq.getDatasetIds().size(), totalMapSheets, totalFileSize, averageMapSheets);
}
/**
* 활성 데이터셋 전체 조회 (학습 관리용)
*
* @return 데이터셋 정보 목록
*/
public List<ModelMngDto.DatasetInfo> findAllActiveDatasetsForTraining() {
List<DatasetEntity> entities = datasetRepository.findByDeletedOrderByCreatedDttmDesc(false);
return entities.stream()
.map(
entity -> {
// totalSize를 읽기 쉬운 형식으로 변환
String totalSizeStr = formatFileSize(entity.getTotalSize());
// classCounts JSON 파싱
Map<String, Integer> classCounts = entity.getClassCounts();
return ModelMngDto.DatasetInfo.builder()
.id(entity.getId())
.title(entity.getTitle())
.groupTitle(entity.getGroupTitle())
.totalItems(entity.getTotalItems())
.totalSize(totalSizeStr)
.classCounts(classCounts)
.memo(entity.getMemo())
.createdDttm(entity.getCreatedDttm())
.build();
})
.toList();
}
/**
* JSON 문자열을 Map으로 파싱
*
* @param jsonStr JSON 문자열
* @return 클래스별 카운트 맵
*/
private Map<String, Integer> parseClassCounts(String jsonStr) {
if (jsonStr == null || jsonStr.trim().isEmpty()) {
return new HashMap<>();
}
try {
return objectMapper.readValue(jsonStr, new TypeReference<Map<String, Integer>>() {});
} catch (Exception e) {
log.warn("클래스 통계 JSON 파싱 실패: {}", jsonStr, e);
return new HashMap<>();
}
}
/**
* 데이터셋의 클래스 통계 계산 및 저장
*
* @param datasetId 데이터셋 ID
* @param classCounts 클래스별 카운트
*/
public void updateClassCounts(Long datasetId, Map<String, Integer> classCounts) {
DatasetEntity entity =
datasetRepository
.findById(datasetId)
.orElseThrow(() -> new NotFoundException("데이터셋을 찾을 수 없습니다. ID: " + datasetId));
try {
entity.setClassCounts(classCounts);
datasetRepository.save(entity);
log.info("데이터셋 클래스 통계 업데이트 완료: datasetId={}, classes={}", datasetId, classCounts.keySet());
} catch (Exception e) {
log.error("클래스 통계 JSON 변환 실패: datasetId={}", datasetId, e);
}
}
/**
* 파일 크기를 읽기 쉬운 형식으로 변환
*
* @param size 바이트 단위 크기
* @return 형식화된 문자열 (예: "1.5GB")
*/
private String formatFileSize(Long size) {
if (size == null || size == 0) {
return "0 GB";
}
double gb = size / (1024.0 * 1024.0 * 1024.0);
if (gb >= 1.0) {
return String.format("%.2f GB", gb);
}
double mb = size / (1024.0 * 1024.0);
if (mb >= 1.0) {
return String.format("%.2f MB", mb);
}
double kb = size / 1024.0;
return String.format("%.2f KB", kb);
}
}

View File

@@ -1,35 +1,35 @@
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.common.service.BaseCoreService;
import com.kamco.cd.training.log.dto.ErrorLogDto;
import com.kamco.cd.training.postgres.repository.log.ErrorLogRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ErrorLogCoreService
implements BaseCoreService<ErrorLogDto.Basic, Long, ErrorLogDto.ErrorSearchReq> {
private final ErrorLogRepository errorLogRepository;
public Page<ErrorLogDto.Basic> findLogByError(ErrorLogDto.ErrorSearchReq searchReq) {
return errorLogRepository.findLogByError(searchReq);
}
@Override
public void remove(Long aLong) {}
@Override
public ErrorLogDto.Basic getOneById(Long aLong) {
return null;
}
@Override
public Page<ErrorLogDto.Basic> search(ErrorLogDto.ErrorSearchReq searchReq) {
return null;
}
}
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.common.service.BaseCoreService;
import com.kamco.cd.training.log.dto.ErrorLogDto;
import com.kamco.cd.training.postgres.repository.log.ErrorLogRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ErrorLogCoreService
implements BaseCoreService<ErrorLogDto.Basic, Long, ErrorLogDto.ErrorSearchReq> {
private final ErrorLogRepository errorLogRepository;
public Page<ErrorLogDto.Basic> findLogByError(ErrorLogDto.ErrorSearchReq searchReq) {
return errorLogRepository.findLogByError(searchReq);
}
@Override
public void remove(Long aLong) {}
@Override
public ErrorLogDto.Basic getOneById(Long aLong) {
return null;
}
@Override
public Page<ErrorLogDto.Basic> search(ErrorLogDto.ErrorSearchReq searchReq) {
return null;
}
}

View File

@@ -1,337 +1,337 @@
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.common.exception.BadRequestException;
import com.kamco.cd.training.common.exception.NotFoundException;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.postgres.entity.ModelHyperParamEntity;
import com.kamco.cd.training.postgres.repository.model.ModelHyperParamRepository;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class HyperParamCoreService {
private final ModelHyperParamRepository hyperParamRepository;
/**
* 하이퍼파라미터 전체 조회 (삭제되지 않은 것만)
*
* @return 하이퍼파라미터 목록
*/
public List<ModelMngDto.HyperParamInfo> findAllActiveHyperParams() {
List<ModelHyperParamEntity> entities =
hyperParamRepository.findByDelYnOrderByCreatedDttmDesc("N");
return entities.stream().map(this::mapToHyperParamInfo).toList();
}
private ModelMngDto.HyperParamInfo mapToHyperParamInfo(ModelHyperParamEntity entity) {
return ModelMngDto.HyperParamInfo.builder()
.hyperVer(entity.getHyperVer())
// Important
.backbone(entity.getBackbone())
.inputSize(entity.getInputSize())
.cropSize(entity.getCropSize())
.epochCnt(entity.getEpochCnt())
.batchSize(entity.getBatchSize())
// Architecture
.dropPathRate(entity.getDropPathRate())
.frozenStages(entity.getFrozenStages())
.neckPolicy(entity.getNeckPolicy())
.decoderChannels(entity.getDecoderChannels())
.classWeight(entity.getClassWeight())
.numLayers(entity.getNumLayers())
// Optimization
.learningRate(entity.getLearningRate())
.weightDecay(entity.getWeightDecay())
.layerDecayRate(entity.getLayerDecayRate())
.ddpFindUnusedParams(entity.getDdpFindUnusedParams())
.ignoreIndex(entity.getIgnoreIndex())
// Data
.trainNumWorkers(entity.getTrainNumWorkers())
.valNumWorkers(entity.getValNumWorkers())
.testNumWorkers(entity.getTestNumWorkers())
.trainShuffle(entity.getTrainShuffle())
.trainPersistent(entity.getTrainPersistent())
.valPersistent(entity.getValPersistent())
// Evaluation
.metrics(entity.getMetrics())
.saveBest(entity.getSaveBest())
.saveBestRule(entity.getSaveBestRule())
.valInterval(entity.getValInterval())
.logInterval(entity.getLogInterval())
.visInterval(entity.getVisInterval())
// Hardware
.gpuCnt(entity.getGpuCnt())
.gpuIds(entity.getGpuIds())
.masterPort(entity.getMasterPort())
// Augmentation
.rotProb(entity.getRotProb())
.flipProb(entity.getFlipProb())
.rotDegree(entity.getRotDegree())
.exchangeProb(entity.getExchangeProb())
.brightnessDelta(entity.getBrightnessDelta())
.contrastRange(entity.getContrastRange())
.saturationRange(entity.getSaturationRange())
.hueDelta(entity.getHueDelta())
// Legacy
.dropoutRatio(entity.getDropoutRatio())
.cnnFilterCnt(entity.getCnnFilterCnt())
// Common
.memo(entity.getMemo())
.createdDttm(entity.getCreatedDttm())
.build();
}
/**
* 하이퍼파라미터 등록
*
* @param createReq 등록 요청
* @return 등록된 버전명
*/
public String createHyperParam(ModelMngDto.HyperParamCreateReq createReq) {
// 중복 체크
if (hyperParamRepository.existsById(createReq.getNewHyperVer())) {
throw new BadRequestException("이미 존재하는 버전입니다: " + createReq.getNewHyperVer());
}
// 기준 버전 조회
ModelHyperParamEntity baseEntity =
hyperParamRepository
.findById(createReq.getBaseHyperVer())
.orElseThrow(
() -> new NotFoundException("기준 버전을 찾을 수 없습니다: " + createReq.getBaseHyperVer()));
// 신규 엔티티 생성 (기준 값 복사 후 변경된 값만 적용)
ModelHyperParamEntity entity = new ModelHyperParamEntity();
entity.setHyperVer(createReq.getNewHyperVer());
// Important
entity.setBackbone(
createReq.getBackbone() != null ? createReq.getBackbone() : baseEntity.getBackbone());
entity.setInputSize(
createReq.getInputSize() != null ? createReq.getInputSize() : baseEntity.getInputSize());
entity.setCropSize(
createReq.getCropSize() != null ? createReq.getCropSize() : baseEntity.getCropSize());
entity.setEpochCnt(
createReq.getEpochCnt() != null ? createReq.getEpochCnt() : baseEntity.getEpochCnt());
entity.setBatchSize(
createReq.getBatchSize() != null ? createReq.getBatchSize() : baseEntity.getBatchSize());
// Architecture
entity.setDropPathRate(
createReq.getDropPathRate() != null
? createReq.getDropPathRate()
: baseEntity.getDropPathRate());
entity.setFrozenStages(
createReq.getFrozenStages() != null
? createReq.getFrozenStages()
: baseEntity.getFrozenStages());
entity.setNeckPolicy(
createReq.getNeckPolicy() != null ? createReq.getNeckPolicy() : baseEntity.getNeckPolicy());
entity.setDecoderChannels(
createReq.getDecoderChannels() != null
? createReq.getDecoderChannels()
: baseEntity.getDecoderChannels());
entity.setClassWeight(
createReq.getClassWeight() != null
? createReq.getClassWeight()
: baseEntity.getClassWeight());
entity.setNumLayers(
createReq.getNumLayers() != null ? createReq.getNumLayers() : baseEntity.getNumLayers());
// Optimization
entity.setLearningRate(
createReq.getLearningRate() != null
? createReq.getLearningRate()
: baseEntity.getLearningRate());
entity.setWeightDecay(
createReq.getWeightDecay() != null
? createReq.getWeightDecay()
: baseEntity.getWeightDecay());
entity.setLayerDecayRate(
createReq.getLayerDecayRate() != null
? createReq.getLayerDecayRate()
: baseEntity.getLayerDecayRate());
entity.setDdpFindUnusedParams(
createReq.getDdpFindUnusedParams() != null
? createReq.getDdpFindUnusedParams()
: baseEntity.getDdpFindUnusedParams());
entity.setIgnoreIndex(
createReq.getIgnoreIndex() != null
? createReq.getIgnoreIndex()
: baseEntity.getIgnoreIndex());
// Data
entity.setTrainNumWorkers(
createReq.getTrainNumWorkers() != null
? createReq.getTrainNumWorkers()
: baseEntity.getTrainNumWorkers());
entity.setValNumWorkers(
createReq.getValNumWorkers() != null
? createReq.getValNumWorkers()
: baseEntity.getValNumWorkers());
entity.setTestNumWorkers(
createReq.getTestNumWorkers() != null
? createReq.getTestNumWorkers()
: baseEntity.getTestNumWorkers());
entity.setTrainShuffle(
createReq.getTrainShuffle() != null
? createReq.getTrainShuffle()
: baseEntity.getTrainShuffle());
entity.setTrainPersistent(
createReq.getTrainPersistent() != null
? createReq.getTrainPersistent()
: baseEntity.getTrainPersistent());
entity.setValPersistent(
createReq.getValPersistent() != null
? createReq.getValPersistent()
: baseEntity.getValPersistent());
// Evaluation
entity.setMetrics(
createReq.getMetrics() != null ? createReq.getMetrics() : baseEntity.getMetrics());
entity.setSaveBest(
createReq.getSaveBest() != null ? createReq.getSaveBest() : baseEntity.getSaveBest());
entity.setSaveBestRule(
createReq.getSaveBestRule() != null
? createReq.getSaveBestRule()
: baseEntity.getSaveBestRule());
entity.setValInterval(
createReq.getValInterval() != null
? createReq.getValInterval()
: baseEntity.getValInterval());
entity.setLogInterval(
createReq.getLogInterval() != null
? createReq.getLogInterval()
: baseEntity.getLogInterval());
entity.setVisInterval(
createReq.getVisInterval() != null
? createReq.getVisInterval()
: baseEntity.getVisInterval());
// Hardware
entity.setGpuCnt(
createReq.getGpuCnt() != null ? createReq.getGpuCnt() : baseEntity.getGpuCnt());
entity.setGpuIds(
createReq.getGpuIds() != null ? createReq.getGpuIds() : baseEntity.getGpuIds());
entity.setMasterPort(
createReq.getMasterPort() != null ? createReq.getMasterPort() : baseEntity.getMasterPort());
// Augmentation
entity.setRotProb(
createReq.getRotProb() != null ? createReq.getRotProb() : baseEntity.getRotProb());
entity.setFlipProb(
createReq.getFlipProb() != null ? createReq.getFlipProb() : baseEntity.getFlipProb());
entity.setRotDegree(
createReq.getRotDegree() != null ? createReq.getRotDegree() : baseEntity.getRotDegree());
entity.setExchangeProb(
createReq.getExchangeProb() != null
? createReq.getExchangeProb()
: baseEntity.getExchangeProb());
entity.setBrightnessDelta(
createReq.getBrightnessDelta() != null
? createReq.getBrightnessDelta()
: baseEntity.getBrightnessDelta());
entity.setContrastRange(
createReq.getContrastRange() != null
? createReq.getContrastRange()
: baseEntity.getContrastRange());
entity.setSaturationRange(
createReq.getSaturationRange() != null
? createReq.getSaturationRange()
: baseEntity.getSaturationRange());
entity.setHueDelta(
createReq.getHueDelta() != null ? createReq.getHueDelta() : baseEntity.getHueDelta());
// Legacy
entity.setDropoutRatio(
createReq.getDropoutRatio() != null
? createReq.getDropoutRatio()
: baseEntity.getDropoutRatio());
entity.setCnnFilterCnt(
createReq.getCnnFilterCnt() != null
? createReq.getCnnFilterCnt()
: baseEntity.getCnnFilterCnt());
// Common
entity.setMemo(createReq.getMemo());
entity.setDelYn("N");
entity.setCreatedDttm(ZonedDateTime.now());
ModelHyperParamEntity saved = hyperParamRepository.save(entity);
return saved.getHyperVer();
}
/**
* 하이퍼파라미터 단건 조회
*
* @param hyperVer 하이퍼파라미터 버전
* @return 하이퍼파라미터 정보
*/
public ModelMngDto.HyperParamInfo findByHyperVer(String hyperVer) {
ModelHyperParamEntity entity =
hyperParamRepository
.findById(hyperVer)
.orElseThrow(() -> new NotFoundException("하이퍼파라미터를 찾을 수 없습니다: " + hyperVer));
if ("Y".equals(entity.getDelYn())) {
throw new NotFoundException("삭제된 하이퍼파라미터입니다: " + hyperVer);
}
return mapToHyperParamInfo(entity);
}
/**
* 하이퍼파라미터 수정 (기존 버전은 수정 불가)
*
* @param hyperVer 하이퍼파라미터 버전
* @param updateReq 수정 요청
*/
public void updateHyperParam(String hyperVer, ModelMngDto.HyperParamCreateReq updateReq) {
// 기존 버전은 수정 불가
throw new BadRequestException("기존 버전은 수정할 수 없습니다. 신규 버전을 생성해주세요.");
}
/**
* 하이퍼파라미터 삭제 (논리 삭제)
*
* @param hyperVer 하이퍼파라미터 버전
*/
public void deleteHyperParam(String hyperVer) {
// H1은 디폴트 버전이므로 삭제 불가
if ("H1".equals(hyperVer)) {
throw new BadRequestException("H1은 디폴트 하이퍼파라미터 버전이므로 삭제할 수 없습니다.");
}
ModelHyperParamEntity entity =
hyperParamRepository
.findById(hyperVer)
.orElseThrow(() -> new NotFoundException("하이퍼파라미터를 찾을 수 없습니다: " + hyperVer));
if ("Y".equals(entity.getDelYn())) {
throw new BadRequestException("이미 삭제된 하이퍼파라미터입니다: " + hyperVer);
}
// 논리 삭제 처리
entity.setDelYn("Y");
hyperParamRepository.save(entity);
}
/**
* 첫 번째 하이퍼파라미터 버전 조회 (H1 확인용)
*
* @return 첫 번째 하이퍼파라미터 버전
*/
public String getFirstHyperParamVersion() {
List<ModelHyperParamEntity> entities =
hyperParamRepository.findByDelYnOrderByCreatedDttmDesc("N");
if (entities.isEmpty()) {
throw new NotFoundException("하이퍼파라미터가 존재하지 않습니다.");
}
// 가장 오래된 것이 H1이므로 리스트의 마지막 요소 반환
return entities.get(entities.size() - 1).getHyperVer();
}
}
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.common.exception.BadRequestException;
import com.kamco.cd.training.common.exception.NotFoundException;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.postgres.entity.ModelHyperParamEntity;
import com.kamco.cd.training.postgres.repository.model.ModelHyperParamRepository;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class HyperParamCoreService {
private final ModelHyperParamRepository hyperParamRepository;
/**
* 하이퍼파라미터 전체 조회 (삭제되지 않은 것만)
*
* @return 하이퍼파라미터 목록
*/
public List<ModelMngDto.HyperParamInfo> findAllActiveHyperParams() {
List<ModelHyperParamEntity> entities =
hyperParamRepository.findByDelYnOrderByCreatedDttmDesc("N");
return entities.stream().map(this::mapToHyperParamInfo).toList();
}
private ModelMngDto.HyperParamInfo mapToHyperParamInfo(ModelHyperParamEntity entity) {
return ModelMngDto.HyperParamInfo.builder()
.hyperVer(entity.getHyperVer())
// Important
.backbone(entity.getBackbone())
.inputSize(entity.getInputSize())
.cropSize(entity.getCropSize())
.epochCnt(entity.getEpochCnt())
.batchSize(entity.getBatchSize())
// Architecture
.dropPathRate(entity.getDropPathRate())
.frozenStages(entity.getFrozenStages())
.neckPolicy(entity.getNeckPolicy())
.decoderChannels(entity.getDecoderChannels())
.classWeight(entity.getClassWeight())
.numLayers(entity.getNumLayers())
// Optimization
.learningRate(entity.getLearningRate())
.weightDecay(entity.getWeightDecay())
.layerDecayRate(entity.getLayerDecayRate())
.ddpFindUnusedParams(entity.getDdpFindUnusedParams())
.ignoreIndex(entity.getIgnoreIndex())
// Data
.trainNumWorkers(entity.getTrainNumWorkers())
.valNumWorkers(entity.getValNumWorkers())
.testNumWorkers(entity.getTestNumWorkers())
.trainShuffle(entity.getTrainShuffle())
.trainPersistent(entity.getTrainPersistent())
.valPersistent(entity.getValPersistent())
// Evaluation
.metrics(entity.getMetrics())
.saveBest(entity.getSaveBest())
.saveBestRule(entity.getSaveBestRule())
.valInterval(entity.getValInterval())
.logInterval(entity.getLogInterval())
.visInterval(entity.getVisInterval())
// Hardware
.gpuCnt(entity.getGpuCnt())
.gpuIds(entity.getGpuIds())
.masterPort(entity.getMasterPort())
// Augmentation
.rotProb(entity.getRotProb())
.flipProb(entity.getFlipProb())
.rotDegree(entity.getRotDegree())
.exchangeProb(entity.getExchangeProb())
.brightnessDelta(entity.getBrightnessDelta())
.contrastRange(entity.getContrastRange())
.saturationRange(entity.getSaturationRange())
.hueDelta(entity.getHueDelta())
// Legacy
.dropoutRatio(entity.getDropoutRatio())
.cnnFilterCnt(entity.getCnnFilterCnt())
// Common
.memo(entity.getMemo())
.createdDttm(entity.getCreatedDttm())
.build();
}
/**
* 하이퍼파라미터 등록
*
* @param createReq 등록 요청
* @return 등록된 버전명
*/
public String createHyperParam(ModelMngDto.HyperParamCreateReq createReq) {
// 중복 체크
if (hyperParamRepository.existsById(createReq.getNewHyperVer())) {
throw new BadRequestException("이미 존재하는 버전입니다: " + createReq.getNewHyperVer());
}
// 기준 버전 조회
ModelHyperParamEntity baseEntity =
hyperParamRepository
.findById(createReq.getBaseHyperVer())
.orElseThrow(
() -> new NotFoundException("기준 버전을 찾을 수 없습니다: " + createReq.getBaseHyperVer()));
// 신규 엔티티 생성 (기준 값 복사 후 변경된 값만 적용)
ModelHyperParamEntity entity = new ModelHyperParamEntity();
entity.setHyperVer(createReq.getNewHyperVer());
// Important
entity.setBackbone(
createReq.getBackbone() != null ? createReq.getBackbone() : baseEntity.getBackbone());
entity.setInputSize(
createReq.getInputSize() != null ? createReq.getInputSize() : baseEntity.getInputSize());
entity.setCropSize(
createReq.getCropSize() != null ? createReq.getCropSize() : baseEntity.getCropSize());
entity.setEpochCnt(
createReq.getEpochCnt() != null ? createReq.getEpochCnt() : baseEntity.getEpochCnt());
entity.setBatchSize(
createReq.getBatchSize() != null ? createReq.getBatchSize() : baseEntity.getBatchSize());
// Architecture
entity.setDropPathRate(
createReq.getDropPathRate() != null
? createReq.getDropPathRate()
: baseEntity.getDropPathRate());
entity.setFrozenStages(
createReq.getFrozenStages() != null
? createReq.getFrozenStages()
: baseEntity.getFrozenStages());
entity.setNeckPolicy(
createReq.getNeckPolicy() != null ? createReq.getNeckPolicy() : baseEntity.getNeckPolicy());
entity.setDecoderChannels(
createReq.getDecoderChannels() != null
? createReq.getDecoderChannels()
: baseEntity.getDecoderChannels());
entity.setClassWeight(
createReq.getClassWeight() != null
? createReq.getClassWeight()
: baseEntity.getClassWeight());
entity.setNumLayers(
createReq.getNumLayers() != null ? createReq.getNumLayers() : baseEntity.getNumLayers());
// Optimization
entity.setLearningRate(
createReq.getLearningRate() != null
? createReq.getLearningRate()
: baseEntity.getLearningRate());
entity.setWeightDecay(
createReq.getWeightDecay() != null
? createReq.getWeightDecay()
: baseEntity.getWeightDecay());
entity.setLayerDecayRate(
createReq.getLayerDecayRate() != null
? createReq.getLayerDecayRate()
: baseEntity.getLayerDecayRate());
entity.setDdpFindUnusedParams(
createReq.getDdpFindUnusedParams() != null
? createReq.getDdpFindUnusedParams()
: baseEntity.getDdpFindUnusedParams());
entity.setIgnoreIndex(
createReq.getIgnoreIndex() != null
? createReq.getIgnoreIndex()
: baseEntity.getIgnoreIndex());
// Data
entity.setTrainNumWorkers(
createReq.getTrainNumWorkers() != null
? createReq.getTrainNumWorkers()
: baseEntity.getTrainNumWorkers());
entity.setValNumWorkers(
createReq.getValNumWorkers() != null
? createReq.getValNumWorkers()
: baseEntity.getValNumWorkers());
entity.setTestNumWorkers(
createReq.getTestNumWorkers() != null
? createReq.getTestNumWorkers()
: baseEntity.getTestNumWorkers());
entity.setTrainShuffle(
createReq.getTrainShuffle() != null
? createReq.getTrainShuffle()
: baseEntity.getTrainShuffle());
entity.setTrainPersistent(
createReq.getTrainPersistent() != null
? createReq.getTrainPersistent()
: baseEntity.getTrainPersistent());
entity.setValPersistent(
createReq.getValPersistent() != null
? createReq.getValPersistent()
: baseEntity.getValPersistent());
// Evaluation
entity.setMetrics(
createReq.getMetrics() != null ? createReq.getMetrics() : baseEntity.getMetrics());
entity.setSaveBest(
createReq.getSaveBest() != null ? createReq.getSaveBest() : baseEntity.getSaveBest());
entity.setSaveBestRule(
createReq.getSaveBestRule() != null
? createReq.getSaveBestRule()
: baseEntity.getSaveBestRule());
entity.setValInterval(
createReq.getValInterval() != null
? createReq.getValInterval()
: baseEntity.getValInterval());
entity.setLogInterval(
createReq.getLogInterval() != null
? createReq.getLogInterval()
: baseEntity.getLogInterval());
entity.setVisInterval(
createReq.getVisInterval() != null
? createReq.getVisInterval()
: baseEntity.getVisInterval());
// Hardware
entity.setGpuCnt(
createReq.getGpuCnt() != null ? createReq.getGpuCnt() : baseEntity.getGpuCnt());
entity.setGpuIds(
createReq.getGpuIds() != null ? createReq.getGpuIds() : baseEntity.getGpuIds());
entity.setMasterPort(
createReq.getMasterPort() != null ? createReq.getMasterPort() : baseEntity.getMasterPort());
// Augmentation
entity.setRotProb(
createReq.getRotProb() != null ? createReq.getRotProb() : baseEntity.getRotProb());
entity.setFlipProb(
createReq.getFlipProb() != null ? createReq.getFlipProb() : baseEntity.getFlipProb());
entity.setRotDegree(
createReq.getRotDegree() != null ? createReq.getRotDegree() : baseEntity.getRotDegree());
entity.setExchangeProb(
createReq.getExchangeProb() != null
? createReq.getExchangeProb()
: baseEntity.getExchangeProb());
entity.setBrightnessDelta(
createReq.getBrightnessDelta() != null
? createReq.getBrightnessDelta()
: baseEntity.getBrightnessDelta());
entity.setContrastRange(
createReq.getContrastRange() != null
? createReq.getContrastRange()
: baseEntity.getContrastRange());
entity.setSaturationRange(
createReq.getSaturationRange() != null
? createReq.getSaturationRange()
: baseEntity.getSaturationRange());
entity.setHueDelta(
createReq.getHueDelta() != null ? createReq.getHueDelta() : baseEntity.getHueDelta());
// Legacy
entity.setDropoutRatio(
createReq.getDropoutRatio() != null
? createReq.getDropoutRatio()
: baseEntity.getDropoutRatio());
entity.setCnnFilterCnt(
createReq.getCnnFilterCnt() != null
? createReq.getCnnFilterCnt()
: baseEntity.getCnnFilterCnt());
// Common
entity.setMemo(createReq.getMemo());
entity.setDelYn("N");
entity.setCreatedDttm(ZonedDateTime.now());
ModelHyperParamEntity saved = hyperParamRepository.save(entity);
return saved.getHyperVer();
}
/**
* 하이퍼파라미터 단건 조회
*
* @param hyperVer 하이퍼파라미터 버전
* @return 하이퍼파라미터 정보
*/
public ModelMngDto.HyperParamInfo findByHyperVer(String hyperVer) {
ModelHyperParamEntity entity =
hyperParamRepository
.findById(hyperVer)
.orElseThrow(() -> new NotFoundException("하이퍼파라미터를 찾을 수 없습니다: " + hyperVer));
if ("Y".equals(entity.getDelYn())) {
throw new NotFoundException("삭제된 하이퍼파라미터입니다: " + hyperVer);
}
return mapToHyperParamInfo(entity);
}
/**
* 하이퍼파라미터 수정 (기존 버전은 수정 불가)
*
* @param hyperVer 하이퍼파라미터 버전
* @param updateReq 수정 요청
*/
public void updateHyperParam(String hyperVer, ModelMngDto.HyperParamCreateReq updateReq) {
// 기존 버전은 수정 불가
throw new BadRequestException("기존 버전은 수정할 수 없습니다. 신규 버전을 생성해주세요.");
}
/**
* 하이퍼파라미터 삭제 (논리 삭제)
*
* @param hyperVer 하이퍼파라미터 버전
*/
public void deleteHyperParam(String hyperVer) {
// H1은 디폴트 버전이므로 삭제 불가
if ("H1".equals(hyperVer)) {
throw new BadRequestException("H1은 디폴트 하이퍼파라미터 버전이므로 삭제할 수 없습니다.");
}
ModelHyperParamEntity entity =
hyperParamRepository
.findById(hyperVer)
.orElseThrow(() -> new NotFoundException("하이퍼파라미터를 찾을 수 없습니다: " + hyperVer));
if ("Y".equals(entity.getDelYn())) {
throw new BadRequestException("이미 삭제된 하이퍼파라미터입니다: " + hyperVer);
}
// 논리 삭제 처리
entity.setDelYn("Y");
hyperParamRepository.save(entity);
}
/**
* 첫 번째 하이퍼파라미터 버전 조회 (H1 확인용)
*
* @return 첫 번째 하이퍼파라미터 버전
*/
public String getFirstHyperParamVersion() {
List<ModelHyperParamEntity> entities =
hyperParamRepository.findByDelYnOrderByCreatedDttmDesc("N");
if (entities.isEmpty()) {
throw new NotFoundException("하이퍼파라미터가 존재하지 않습니다.");
}
// 가장 오래된 것이 H1이므로 리스트의 마지막 요소 반환
return entities.get(entities.size() - 1).getHyperVer();
}
}

View File

@@ -1,72 +1,72 @@
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.common.exception.NotFoundException;
import com.kamco.cd.training.common.service.BaseCoreService;
import com.kamco.cd.training.dataset.dto.MapSheetDto;
import com.kamco.cd.training.postgres.entity.MapSheetEntity;
import com.kamco.cd.training.postgres.repository.dataset.MapSheetRepository;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class MapSheetCoreService
implements BaseCoreService<MapSheetDto.Basic, Long, MapSheetDto.SearchReq> {
private final MapSheetRepository mapSheetRepository;
@Override
public void remove(Long id) {
Optional<MapSheetEntity> mapSheet = mapSheetRepository.findById(id);
if (mapSheet.isEmpty()) {
return;
}
MapSheetEntity entity = mapSheet.get();
entity.setDeleted(true);
mapSheetRepository.save(entity);
}
@Override
public MapSheetDto.Basic getOneById(Long id) {
MapSheetEntity entity =
mapSheetRepository
.findById(id)
.orElseThrow(() -> new NotFoundException("도엽을 찾을 수 없습니다. ID: " + id));
if (entity.getDeleted()) {
throw new NotFoundException("삭제된 도엽입니다. ID: " + id);
}
return entity.toDto();
}
@Override
public Page<MapSheetDto.Basic> search(MapSheetDto.SearchReq searchReq) {
Page<MapSheetEntity> entityPage = mapSheetRepository.findMapSheetList(searchReq);
return entityPage.map(MapSheetEntity::toDto);
}
/**
* 도엽 목록 조회
*
* @param searchReq 검색 조건
* @return 페이징 처리된 도엽 목록
*/
public Page<MapSheetDto.Basic> findMapSheetList(MapSheetDto.SearchReq searchReq) {
return search(searchReq);
}
/**
* 도엽 삭제 (다건)
*
* @param deleteReq 삭제 요청
*/
public void deleteMapSheets(MapSheetDto.DeleteReq deleteReq) {
for (Long id : deleteReq.getItemIds()) {
remove(id);
}
}
}
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.common.exception.NotFoundException;
import com.kamco.cd.training.common.service.BaseCoreService;
import com.kamco.cd.training.dataset.dto.MapSheetDto;
import com.kamco.cd.training.postgres.entity.MapSheetEntity;
import com.kamco.cd.training.postgres.repository.dataset.MapSheetRepository;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class MapSheetCoreService
implements BaseCoreService<MapSheetDto.Basic, Long, MapSheetDto.SearchReq> {
private final MapSheetRepository mapSheetRepository;
@Override
public void remove(Long id) {
Optional<MapSheetEntity> mapSheet = mapSheetRepository.findById(id);
if (mapSheet.isEmpty()) {
return;
}
MapSheetEntity entity = mapSheet.get();
entity.setDeleted(true);
mapSheetRepository.save(entity);
}
@Override
public MapSheetDto.Basic getOneById(Long id) {
MapSheetEntity entity =
mapSheetRepository
.findById(id)
.orElseThrow(() -> new NotFoundException("도엽을 찾을 수 없습니다. ID: " + id));
if (entity.getDeleted()) {
throw new NotFoundException("삭제된 도엽입니다. ID: " + id);
}
return entity.toDto();
}
@Override
public Page<MapSheetDto.Basic> search(MapSheetDto.SearchReq searchReq) {
Page<MapSheetEntity> entityPage = mapSheetRepository.findMapSheetList(searchReq);
return entityPage.map(MapSheetEntity::toDto);
}
/**
* 도엽 목록 조회
*
* @param searchReq 검색 조건
* @return 페이징 처리된 도엽 목록
*/
public Page<MapSheetDto.Basic> findMapSheetList(MapSheetDto.SearchReq searchReq) {
return search(searchReq);
}
/**
* 도엽 삭제 (다건)
*
* @param deleteReq 삭제 요청
*/
public void deleteMapSheets(MapSheetDto.DeleteReq deleteReq) {
for (Long id : deleteReq.getItemIds()) {
remove(id);
}
}
}

View File

@@ -1,167 +1,168 @@
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.auth.BCryptSaltGenerator;
import com.kamco.cd.training.common.enums.StatusType;
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.common.utils.CommonStringUtils;
import com.kamco.cd.training.common.utils.UserUtil;
import com.kamco.cd.training.members.dto.MembersDto;
import com.kamco.cd.training.members.dto.MembersDto.AddReq;
import com.kamco.cd.training.members.dto.MembersDto.Basic;
import com.kamco.cd.training.members.dto.SignInRequest;
import com.kamco.cd.training.members.exception.MemberException.DuplicateMemberException;
import com.kamco.cd.training.members.exception.MemberException.DuplicateMemberException.Field;
import com.kamco.cd.training.members.exception.MemberException.MemberNotFoundException;
import com.kamco.cd.training.postgres.entity.MemberEntity;
import com.kamco.cd.training.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.http.HttpStatus;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class MembersCoreService {
private final MembersRepository membersRepository;
private final UserUtil userUtil;
/**
* 관리자 계정 등록
*
* @param addReq
* @return
*/
public Long saveMembers(AddReq addReq) {
if (membersRepository.existsByEmployeeNo(addReq.getEmployeeNo())) {
throw new DuplicateMemberException(Field.EMPLOYEE_NO, addReq.getEmployeeNo());
}
// salt 생성, 사번이 salt
String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(addReq.getEmployeeNo().trim());
// 패스워드 암호화, 초기 패스워드 고정
String hashedPassword = BCrypt.hashpw(addReq.getPassword(), salt);
MemberEntity memberEntity = new MemberEntity();
memberEntity.setUserId(addReq.getEmployeeNo());
memberEntity.setUserRole(addReq.getUserRole());
memberEntity.setPassword(hashedPassword);
memberEntity.setName(addReq.getName());
memberEntity.setEmployeeNo(addReq.getEmployeeNo());
memberEntity.setRgstrUidl(userUtil.getId());
memberEntity.setStatus(StatusType.PENDING.getId());
return membersRepository.save(memberEntity).getId();
}
/**
* 관리자 계정 수정
*
* @param uuid
* @param updateReq
*/
public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) {
MemberEntity memberEntity =
membersRepository.findByUUID(uuid).orElseThrow(MemberNotFoundException::new);
if (StringUtils.isNotBlank(updateReq.getName())) {
memberEntity.setName(updateReq.getName());
}
if (StringUtils.isNotBlank(updateReq.getStatus())) {
memberEntity.changeStatus(updateReq.getStatus());
}
if (StringUtils.isNotBlank(updateReq.getPassword())) {
// 패스워드 유효성 검사
if (!CommonStringUtils.isValidPassword(updateReq.getPassword())) {
throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST);
}
String password =
CommonStringUtils.hashPassword(updateReq.getPassword(), memberEntity.getEmployeeNo());
memberEntity.setStatus(StatusType.PENDING.getId());
memberEntity.setLoginFailCount(0);
memberEntity.setPassword(password);
memberEntity.setPwdResetYn(true);
}
memberEntity.setUpdtrUid(userUtil.getId());
membersRepository.save(memberEntity);
}
/**
* 패스워드 변경
*
* @param id
*/
public void resetPassword(String id, MembersDto.InitReq initReq) {
MemberEntity memberEntity =
membersRepository.findByEmployeeNo(id).orElseThrow(() -> new MemberNotFoundException());
// 기존 패스워드 확인
if (!BCrypt.checkpw(initReq.getOldPassword(), memberEntity.getPassword())) {
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH);
}
String password =
CommonStringUtils.hashPassword(initReq.getOldPassword(), memberEntity.getEmployeeNo());
memberEntity.setPassword(password);
memberEntity.setStatus(StatusType.ACTIVE.getId());
memberEntity.setUpdatedDttm(ZonedDateTime.now());
memberEntity.setUpdtrUid(memberEntity.getId());
memberEntity.setPwdResetYn(false);
membersRepository.save(memberEntity);
}
//
/**
* 회원목록 조회
*
* @param searchReq
* @return
*/
public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) {
Page<MemberEntity> entityPage = membersRepository.findByMembers(searchReq);
return entityPage.map(MemberEntity::toDto);
}
/**
* 사용자 상태 조회
*
* @param request
* @return
*/
public String getUserStatus(SignInRequest request) {
MemberEntity memberEntity =
membersRepository
.findByEmployeeNo(request.getUsername())
.orElseThrow(() -> new CustomApiException("LOGIN_ID_NOT_FOUND", HttpStatus.UNAUTHORIZED));
return memberEntity.getStatus();
}
/**
* 최초 로그인 저장 마지막 로그인 저장
*
* @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);
}
}
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.auth.BCryptSaltGenerator;
import com.kamco.cd.training.common.enums.StatusType;
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.common.utils.CommonStringUtils;
import com.kamco.cd.training.common.utils.UserUtil;
import com.kamco.cd.training.members.dto.MembersDto;
import com.kamco.cd.training.members.dto.MembersDto.AddReq;
import com.kamco.cd.training.members.dto.MembersDto.Basic;
import com.kamco.cd.training.members.dto.SignInRequest;
import com.kamco.cd.training.members.exception.MemberException.DuplicateMemberException;
import com.kamco.cd.training.members.exception.MemberException.DuplicateMemberException.Field;
import com.kamco.cd.training.members.exception.MemberException.MemberNotFoundException;
import com.kamco.cd.training.postgres.entity.MemberEntity;
import com.kamco.cd.training.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.http.HttpStatus;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class MembersCoreService {
private final MembersRepository membersRepository;
private final UserUtil userUtil;
/**
* 관리자 계정 등록
*
* @param addReq
* @return
*/
public Long saveMembers(AddReq addReq) {
if (membersRepository.existsByEmployeeNo(addReq.getEmployeeNo())) {
throw new DuplicateMemberException(Field.EMPLOYEE_NO, addReq.getEmployeeNo());
}
// salt 생성, 사번이 salt
String salt = BCryptSaltGenerator.generateSaltWithEmployeeNo(addReq.getEmployeeNo().trim());
// 패스워드 암호화, 초기 패스워드 고정
String hashedPassword = BCrypt.hashpw(addReq.getPassword(), salt);
MemberEntity memberEntity = new MemberEntity();
memberEntity.setUserId(addReq.getEmployeeNo());
memberEntity.setUserRole(addReq.getUserRole());
memberEntity.setPassword(hashedPassword);
memberEntity.setName(addReq.getName());
memberEntity.setEmployeeNo(addReq.getEmployeeNo());
memberEntity.setRgstrUidl(userUtil.getId());
memberEntity.setStatus(StatusType.PENDING.getId());
return membersRepository.save(memberEntity).getId();
}
/**
* 관리자 계정 수정
*
* @param uuid
* @param updateReq
*/
public void updateMembers(UUID uuid, MembersDto.UpdateReq updateReq) {
MemberEntity memberEntity =
membersRepository.findByUUID(uuid).orElseThrow(MemberNotFoundException::new);
if (StringUtils.isNotBlank(updateReq.getName())) {
memberEntity.setName(updateReq.getName());
}
if (StringUtils.isNotBlank(updateReq.getStatus())) {
memberEntity.changeStatus(updateReq.getStatus());
}
if (StringUtils.isNotBlank(updateReq.getPassword())) {
// 패스워드 유효성 검사
if (!CommonStringUtils.isValidPassword(updateReq.getPassword())) {
throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST);
}
String password =
CommonStringUtils.hashPassword(updateReq.getPassword(), memberEntity.getEmployeeNo());
memberEntity.setStatus(StatusType.PENDING.getId());
memberEntity.setLoginFailCount(0);
memberEntity.setPassword(password);
memberEntity.setPwdResetYn(true);
}
memberEntity.setUpdtrUid(userUtil.getId());
membersRepository.save(memberEntity);
}
/**
* 패스워드 변경
*
* @param id
*/
public void resetPassword(String id, MembersDto.InitReq initReq) {
MemberEntity memberEntity =
membersRepository.findByEmployeeNo(id).orElseThrow(() -> new MemberNotFoundException());
// 기존 패스워드 확인
if (!BCrypt.checkpw(initReq.getOldPassword(), memberEntity.getPassword())) {
throw new CustomApiException(AuthErrorCode.LOGIN_PASSWORD_MISMATCH);
}
String password =
CommonStringUtils.hashPassword(initReq.getOldPassword(), memberEntity.getEmployeeNo());
memberEntity.setPassword(password);
memberEntity.setStatus(StatusType.ACTIVE.getId());
memberEntity.setUpdatedDttm(ZonedDateTime.now());
memberEntity.setUpdtrUid(memberEntity.getId());
memberEntity.setPwdResetYn(false);
membersRepository.save(memberEntity);
}
//
/**
* 회원목록 조회
*
* @param searchReq
* @return
*/
public Page<Basic> findByMembers(MembersDto.SearchReq searchReq) {
Page<MemberEntity> entityPage = membersRepository.findByMembers(searchReq);
return entityPage.map(MemberEntity::toDto);
}
/**
* 사용자 상태 조회
*
* @param request
* @return
*/
public String getUserStatus(SignInRequest request) {
MemberEntity memberEntity =
membersRepository
.findByEmployeeNo(request.getUsername())
.orElseThrow(
() -> new CustomApiException("LOGIN_ID_NOT_FOUND", HttpStatus.UNAUTHORIZED));
return memberEntity.getStatus();
}
/**
* 최초 로그인 저장 마지막 로그인 저장
*
* @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);
}
}

View File

@@ -1,19 +1,19 @@
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.menu.dto.MenuDto;
import com.kamco.cd.training.postgres.entity.MenuEntity;
import com.kamco.cd.training.postgres.repository.menu.MenuRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class MenuCoreService {
private final MenuRepository menuRepository;
public List<MenuDto.Basic> getFindAll() {
return menuRepository.getFindAll().stream().map(MenuEntity::toDto).toList();
}
}
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.menu.dto.MenuDto;
import com.kamco.cd.training.postgres.entity.MenuEntity;
import com.kamco.cd.training.postgres.repository.menu.MenuRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class MenuCoreService {
private final MenuRepository menuRepository;
public List<MenuDto.Basic> getFindAll() {
return menuRepository.getFindAll().stream().map(MenuEntity::toDto).toList();
}
}

View File

@@ -1,202 +1,202 @@
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.common.exception.BadRequestException;
import com.kamco.cd.training.common.exception.NotFoundException;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.model.dto.ModelMngDto.Basic;
import com.kamco.cd.training.postgres.entity.ModelDatasetMappEntity;
import com.kamco.cd.training.postgres.entity.ModelTrainMasterEntity;
import com.kamco.cd.training.postgres.repository.model.ModelDatasetMappRepository;
import com.kamco.cd.training.postgres.repository.model.ModelMngRepository;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ModelMngCoreService {
private final ModelMngRepository modelMngRepository;
private final ModelDatasetMappRepository modelDatasetMappRepository;
/**
* 모델 목록 조회
*
* @param searchReq 검색 조건
* @return 페이징 처리된 모델 목록
*/
public Page<Basic> findByModels(ModelMngDto.SearchReq searchReq) {
Page<ModelTrainMasterEntity> entityPage = modelMngRepository.findByModels(searchReq);
return entityPage.map(ModelTrainMasterEntity::toDto);
}
/**
* 모델 상세 조회
*
* @param modelUid 모델 UID
* @return 모델 상세 정보
*/
public ModelMngDto.Detail getModelDetail(Long modelUid) {
ModelTrainMasterEntity entity =
modelMngRepository
.findById(modelUid)
.orElseThrow(() -> new NotFoundException("모델을 찾을 수 없습니다. ID: " + modelUid));
if (Boolean.TRUE.equals(entity.getDelYn())) {
throw new NotFoundException("삭제된 모델입니다. ID: " + modelUid);
}
return ModelMngDto.Detail.builder()
.uuid(entity.getUuid().toString())
.modelVer(entity.getModelVer())
.hyperVer(entity.getHyperVer())
.epochVer(entity.getEpochVer())
.processStep(entity.getProcessStep())
.statusCd(entity.getStatusCd())
.trainStartDttm(entity.getTrainStartDttm())
.epochCnt(entity.getEpochCnt())
.datasetRatio(entity.getDatasetRatio())
.bestEpoch(entity.getBestEpoch())
.confirmedBestEpoch(entity.getConfirmedBestEpoch())
.step1EndDttm(entity.getStep1EndDttm())
.step1Duration(entity.getStep1Duration())
.step2EndDttm(entity.getStep2EndDttm())
.step2Duration(entity.getStep2Duration())
.progressRate(entity.getProgressRate())
.createdDttm(entity.getCreatedDttm())
.updatedDttm(entity.getUpdatedDttm())
.modelPath(entity.getModelPath())
.errorMsg(entity.getErrorMsg())
.build();
}
/**
* 모델 상세 조회 (UUID 기반)
*
* @param uuid 모델 UUID
* @return 모델 상세 정보
*/
public ModelMngDto.Detail getModelDetailByUuid(String uuid) {
ModelTrainMasterEntity entity = findByUuid(uuid);
return getModelDetail(entity.getId());
}
/**
* 학습 모델 전체 목록 조회 (삭제되지 않은 것만)
*
* @return 학습 모델 목록
*/
public List<ModelMngDto.TrainListRes> findAllTrainModels() {
List<ModelTrainMasterEntity> entities =
modelMngRepository.findByDelYnOrderByCreatedDttmDesc(false);
return entities.stream()
.map(
entity ->
ModelMngDto.TrainListRes.builder()
.uuid(entity.getUuid().toString())
.modelVer(entity.getModelVer())
.status(entity.getStatusCd())
.processStep(entity.getProcessStep())
.trainStartDttm(entity.getTrainStartDttm())
.progressRate(entity.getProgressRate())
.epochCnt(entity.getEpochCnt())
.step1EndDttm(entity.getStep1EndDttm())
.step1Duration(entity.getStep1Duration())
.step2EndDttm(entity.getStep2EndDttm())
.step2Duration(entity.getStep2Duration())
.createdDttm(entity.getCreatedDttm())
.errorMsg(entity.getErrorMsg())
.canResume(entity.getCanResume())
.lastCheckpointEpoch(entity.getLastCheckpointEpoch())
.build())
.toList();
}
/**
* 현재 실행 중인 모델 확인
*
* @return 실행 중인 모델 UUID (없으면 null)
*/
public String findRunningModelUuid() {
return modelMngRepository
.findFirstByStatusCdAndDelYn("RUNNING", false)
.map(entity -> entity.getUuid().toString())
.orElse(null);
}
/**
* 학습 마스터 생성
*
* @param trainReq 학습 시작 요청
* @return 생성된 모델 Entity
*/
public ModelTrainMasterEntity createTrainMaster(ModelMngDto.TrainStartReq trainReq) {
ModelTrainMasterEntity entity = new ModelTrainMasterEntity();
entity.setModelVer(trainReq.getHyperVer());
entity.setHyperVer(trainReq.getHyperVer());
entity.setEpochVer(String.valueOf(trainReq.getEpoch()));
entity.setProcessStep("STEP1");
entity.setStatusCd("READY");
entity.setTrainStartDttm(ZonedDateTime.now());
entity.setEpochCnt(trainReq.getEpoch());
entity.setDatasetRatio(trainReq.getDatasetRatio());
entity.setDelYn(false);
entity.setCreatedDttm(ZonedDateTime.now());
entity.setProgressRate(0);
return modelMngRepository.save(entity);
}
/**
* 데이터셋 매핑 생성
*
* @param modelUid 모델 UID
* @param datasetIds 데이터셋 ID 목록
*/
public void createDatasetMappings(Long modelUid, List<Long> datasetIds) {
for (Long datasetId : datasetIds) {
ModelDatasetMappEntity mapping = new ModelDatasetMappEntity();
mapping.setModelUid(modelUid);
mapping.setDatasetUid(datasetId);
mapping.setDatasetType("TRAIN");
modelDatasetMappRepository.save(mapping);
}
}
/**
* UUID로 모델 조회
*
* @param uuid UUID
* @return 모델 Entity
*/
public ModelTrainMasterEntity findByUuid(String uuid) {
try {
java.util.UUID uuidObj = java.util.UUID.fromString(uuid);
return modelMngRepository
.findByUuid(uuidObj)
.orElseThrow(() -> new NotFoundException("모델을 찾을 수 없습니다. UUID: " + uuid));
} catch (IllegalArgumentException e) {
throw new BadRequestException("잘못된 UUID 형식입니다: " + uuid);
}
}
/**
* 모델 삭제 (논리 삭제)
*
* @param uuid UUID
*/
public void deleteByUuid(String uuid) {
ModelTrainMasterEntity entity = findByUuid(uuid);
// 진행 중인 모델은 삭제 불가
if ("RUNNING".equals(entity.getStatusCd())) {
throw new BadRequestException("진행 중인 모델은 삭제할 수 없습니다.");
}
entity.setDelYn(true);
entity.setUpdatedDttm(ZonedDateTime.now());
modelMngRepository.save(entity);
}
}
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.common.exception.BadRequestException;
import com.kamco.cd.training.common.exception.NotFoundException;
import com.kamco.cd.training.model.dto.ModelMngDto;
import com.kamco.cd.training.model.dto.ModelMngDto.Basic;
import com.kamco.cd.training.postgres.entity.ModelDatasetMappEntity;
import com.kamco.cd.training.postgres.entity.ModelTrainMasterEntity;
import com.kamco.cd.training.postgres.repository.model.ModelDatasetMappRepository;
import com.kamco.cd.training.postgres.repository.model.ModelMngRepository;
import java.time.ZonedDateTime;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ModelMngCoreService {
private final ModelMngRepository modelMngRepository;
private final ModelDatasetMappRepository modelDatasetMappRepository;
/**
* 모델 목록 조회
*
* @param searchReq 검색 조건
* @return 페이징 처리된 모델 목록
*/
public Page<Basic> findByModels(ModelMngDto.SearchReq searchReq) {
Page<ModelTrainMasterEntity> entityPage = modelMngRepository.findByModels(searchReq);
return entityPage.map(ModelTrainMasterEntity::toDto);
}
/**
* 모델 상세 조회
*
* @param modelUid 모델 UID
* @return 모델 상세 정보
*/
public ModelMngDto.Detail getModelDetail(Long modelUid) {
ModelTrainMasterEntity entity =
modelMngRepository
.findById(modelUid)
.orElseThrow(() -> new NotFoundException("모델을 찾을 수 없습니다. ID: " + modelUid));
if (Boolean.TRUE.equals(entity.getDelYn())) {
throw new NotFoundException("삭제된 모델입니다. ID: " + modelUid);
}
return ModelMngDto.Detail.builder()
.uuid(entity.getUuid().toString())
.modelVer(entity.getModelVer())
.hyperVer(entity.getHyperVer())
.epochVer(entity.getEpochVer())
.processStep(entity.getProcessStep())
.statusCd(entity.getStatusCd())
.trainStartDttm(entity.getTrainStartDttm())
.epochCnt(entity.getEpochCnt())
.datasetRatio(entity.getDatasetRatio())
.bestEpoch(entity.getBestEpoch())
.confirmedBestEpoch(entity.getConfirmedBestEpoch())
.step1EndDttm(entity.getStep1EndDttm())
.step1Duration(entity.getStep1Duration())
.step2EndDttm(entity.getStep2EndDttm())
.step2Duration(entity.getStep2Duration())
.progressRate(entity.getProgressRate())
.createdDttm(entity.getCreatedDttm())
.updatedDttm(entity.getUpdatedDttm())
.modelPath(entity.getModelPath())
.errorMsg(entity.getErrorMsg())
.build();
}
/**
* 모델 상세 조회 (UUID 기반)
*
* @param uuid 모델 UUID
* @return 모델 상세 정보
*/
public ModelMngDto.Detail getModelDetailByUuid(String uuid) {
ModelTrainMasterEntity entity = findByUuid(uuid);
return getModelDetail(entity.getId());
}
/**
* 학습 모델 전체 목록 조회 (삭제되지 않은 것만)
*
* @return 학습 모델 목록
*/
public List<ModelMngDto.TrainListRes> findAllTrainModels() {
List<ModelTrainMasterEntity> entities =
modelMngRepository.findByDelYnOrderByCreatedDttmDesc(false);
return entities.stream()
.map(
entity ->
ModelMngDto.TrainListRes.builder()
.uuid(entity.getUuid().toString())
.modelVer(entity.getModelVer())
.status(entity.getStatusCd())
.processStep(entity.getProcessStep())
.trainStartDttm(entity.getTrainStartDttm())
.progressRate(entity.getProgressRate())
.epochCnt(entity.getEpochCnt())
.step1EndDttm(entity.getStep1EndDttm())
.step1Duration(entity.getStep1Duration())
.step2EndDttm(entity.getStep2EndDttm())
.step2Duration(entity.getStep2Duration())
.createdDttm(entity.getCreatedDttm())
.errorMsg(entity.getErrorMsg())
.canResume(entity.getCanResume())
.lastCheckpointEpoch(entity.getLastCheckpointEpoch())
.build())
.toList();
}
/**
* 현재 실행 중인 모델 확인
*
* @return 실행 중인 모델 UUID (없으면 null)
*/
public String findRunningModelUuid() {
return modelMngRepository
.findFirstByStatusCdAndDelYn("RUNNING", false)
.map(entity -> entity.getUuid().toString())
.orElse(null);
}
/**
* 학습 마스터 생성
*
* @param trainReq 학습 시작 요청
* @return 생성된 모델 Entity
*/
public ModelTrainMasterEntity createTrainMaster(ModelMngDto.TrainStartReq trainReq) {
ModelTrainMasterEntity entity = new ModelTrainMasterEntity();
entity.setModelVer(trainReq.getHyperVer());
entity.setHyperVer(trainReq.getHyperVer());
entity.setEpochVer(String.valueOf(trainReq.getEpoch()));
entity.setProcessStep("STEP1");
entity.setStatusCd("READY");
entity.setTrainStartDttm(ZonedDateTime.now());
entity.setEpochCnt(trainReq.getEpoch());
entity.setDatasetRatio(trainReq.getDatasetRatio());
entity.setDelYn(false);
entity.setCreatedDttm(ZonedDateTime.now());
entity.setProgressRate(0);
return modelMngRepository.save(entity);
}
/**
* 데이터셋 매핑 생성
*
* @param modelUid 모델 UID
* @param datasetIds 데이터셋 ID 목록
*/
public void createDatasetMappings(Long modelUid, List<Long> datasetIds) {
for (Long datasetId : datasetIds) {
ModelDatasetMappEntity mapping = new ModelDatasetMappEntity();
mapping.setModelUid(modelUid);
mapping.setDatasetUid(datasetId);
mapping.setDatasetType("TRAIN");
modelDatasetMappRepository.save(mapping);
}
}
/**
* UUID로 모델 조회
*
* @param uuid UUID
* @return 모델 Entity
*/
public ModelTrainMasterEntity findByUuid(String uuid) {
try {
java.util.UUID uuidObj = java.util.UUID.fromString(uuid);
return modelMngRepository
.findByUuid(uuidObj)
.orElseThrow(() -> new NotFoundException("모델을 찾을 수 없습니다. UUID: " + uuid));
} catch (IllegalArgumentException e) {
throw new BadRequestException("잘못된 UUID 형식입니다: " + uuid);
}
}
/**
* 모델 삭제 (논리 삭제)
*
* @param uuid UUID
*/
public void deleteByUuid(String uuid) {
ModelTrainMasterEntity entity = findByUuid(uuid);
// 진행 중인 모델은 삭제 불가
if ("RUNNING".equals(entity.getStatusCd())) {
throw new BadRequestException("진행 중인 모델은 삭제할 수 없습니다.");
}
entity.setDelYn(true);
entity.setUpdatedDttm(ZonedDateTime.now());
modelMngRepository.save(entity);
}
}

View File

@@ -1,66 +1,66 @@
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.postgres.entity.SystemMetricsEntity;
import com.kamco.cd.training.postgres.repository.SystemMetricsRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class SystemMetricsCoreService {
private final SystemMetricsRepository systemMetricsRepository;
/**
* 현재 사용 가능한 저장공간 조회 (MB 단위)
*
* @return 사용 가능한 저장공간 (MB)
*/
public long getAvailableStorageMB() {
SystemMetricsEntity latestMetrics =
systemMetricsRepository
.findLatestMetrics()
.orElseThrow(() -> new IllegalStateException("시스템 메트릭 정보를 조회할 수 없습니다"));
Long kbmemfree = latestMetrics.getKbmemfree();
Long kbmemused = latestMetrics.getKbmemused();
if (kbmemfree == null || kbmemused == null) {
log.warn("시스템 메트릭에 메모리 정보가 없습니다");
return 0L;
}
// 남은 용량 = kbmemfree - kbmemused (KB 단위)
long availableKB = kbmemfree - kbmemused;
// KB를 MB로 변환
long availableMB = availableKB / 1024;
log.info(
"사용 가능한 저장공간: {}MB (kbmemfree: {}KB, kbmemused: {}KB)", availableMB, kbmemfree, kbmemused);
return Math.max(0L, availableMB);
}
/**
* 학습 실행 가능 여부 확인 (10GB 이상 필요)
*
* @return 학습 실행 가능 여부
*/
public boolean isStorageAvailableForTraining() {
long availableMB = getAvailableStorageMB();
long requiredMB = 10 * 1024; // 10GB = 10,240MB
boolean isAvailable = availableMB >= requiredMB;
if (!isAvailable) {
log.warn("저장공간 부족: 현재 {}MB, 필요 {}MB", availableMB, requiredMB);
}
return isAvailable;
}
}
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.postgres.entity.SystemMetricsEntity;
import com.kamco.cd.training.postgres.repository.SystemMetricsRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class SystemMetricsCoreService {
private final SystemMetricsRepository systemMetricsRepository;
/**
* 현재 사용 가능한 저장공간 조회 (MB 단위)
*
* @return 사용 가능한 저장공간 (MB)
*/
public long getAvailableStorageMB() {
SystemMetricsEntity latestMetrics =
systemMetricsRepository
.findLatestMetrics()
.orElseThrow(() -> new IllegalStateException("시스템 메트릭 정보를 조회할 수 없습니다"));
Long kbmemfree = latestMetrics.getKbmemfree();
Long kbmemused = latestMetrics.getKbmemused();
if (kbmemfree == null || kbmemused == null) {
log.warn("시스템 메트릭에 메모리 정보가 없습니다");
return 0L;
}
// 남은 용량 = kbmemfree - kbmemused (KB 단위)
long availableKB = kbmemfree - kbmemused;
// KB를 MB로 변환
long availableMB = availableKB / 1024;
log.info(
"사용 가능한 저장공간: {}MB (kbmemfree: {}KB, kbmemused: {}KB)", availableMB, kbmemfree, kbmemused);
return Math.max(0L, availableMB);
}
/**
* 학습 실행 가능 여부 확인 (10GB 이상 필요)
*
* @return 학습 실행 가능 여부
*/
public boolean isStorageAvailableForTraining() {
long availableMB = getAvailableStorageMB();
long requiredMB = 10 * 1024; // 10GB = 10,240MB
boolean isAvailable = availableMB >= requiredMB;
if (!isAvailable) {
log.warn("저장공간 부족: 현재 {}MB, 필요 {}MB", availableMB, requiredMB);
}
return isAvailable;
}
}

View File

@@ -1,57 +1,57 @@
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.postgres.entity.AuthRefreshTokenEntity;
import com.kamco.cd.training.postgres.repository.members.AuthRefreshTokenRepository;
import java.time.ZonedDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class TokenCoreService {
private final AuthRefreshTokenRepository tokenRepository;
public void save(String subject, String refreshToken, long validityMs) {
ZonedDateTime expiresAt = ZonedDateTime.now().plusSeconds(validityMs / 1000);
tokenRepository
.findBySubject(subject)
.ifPresentOrElse(
entity -> entity.rotate(refreshToken, expiresAt),
() ->
tokenRepository.save(new AuthRefreshTokenEntity(subject, refreshToken, expiresAt)));
}
/**
* refreshToken을 DB와 비교 검증
*
* @param subject 사용자 식별(UUID)
* @return
*/
public String getValidTokenOrThrow(String subject) {
// DB 기준 유효한 RefreshToken 문자열 반환 (없으면 401 예외)
return tokenRepository
.findBySubjectAndRevokedDttmIsNullAndExpiresDttmAfter(subject, ZonedDateTime.now())
.orElseThrow(() -> new CustomApiException(AuthErrorCode.REFRESH_TOKEN_EXPIRED_OR_REVOKED))
.getToken();
}
/**
* 로그아웃
*
* @param subject
*/
public void revokeBySubject(String subject) {
AuthRefreshTokenEntity token =
tokenRepository
.findBySubjectAndRevokedDttmIsNullAndExpiresDttmAfter(subject, ZonedDateTime.now())
.orElseThrow(() -> new CustomApiException(AuthErrorCode.REFRESH_TOKEN_MISMATCH));
// save() 호출 안 해도 됨 (영속 상태 + 트랜잭션)
token.revoke();
}
}
package com.kamco.cd.training.postgres.core;
import com.kamco.cd.training.common.enums.error.AuthErrorCode;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.postgres.entity.AuthRefreshTokenEntity;
import com.kamco.cd.training.postgres.repository.members.AuthRefreshTokenRepository;
import java.time.ZonedDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class TokenCoreService {
private final AuthRefreshTokenRepository tokenRepository;
public void save(String subject, String refreshToken, long validityMs) {
ZonedDateTime expiresAt = ZonedDateTime.now().plusSeconds(validityMs / 1000);
tokenRepository
.findBySubject(subject)
.ifPresentOrElse(
entity -> entity.rotate(refreshToken, expiresAt),
() ->
tokenRepository.save(new AuthRefreshTokenEntity(subject, refreshToken, expiresAt)));
}
/**
* refreshToken을 DB와 비교 검증
*
* @param subject 사용자 식별(UUID)
* @return
*/
public String getValidTokenOrThrow(String subject) {
// DB 기준 유효한 RefreshToken 문자열 반환 (없으면 401 예외)
return tokenRepository
.findBySubjectAndRevokedDttmIsNullAndExpiresDttmAfter(subject, ZonedDateTime.now())
.orElseThrow(() -> new CustomApiException(AuthErrorCode.REFRESH_TOKEN_EXPIRED_OR_REVOKED))
.getToken();
}
/**
* 로그아웃
*
* @param subject
*/
public void revokeBySubject(String subject) {
AuthRefreshTokenEntity token =
tokenRepository
.findBySubjectAndRevokedDttmIsNullAndExpiresDttmAfter(subject, ZonedDateTime.now())
.orElseThrow(() -> new CustomApiException(AuthErrorCode.REFRESH_TOKEN_MISMATCH));
// save() 호출 안 해도 됨 (영속 상태 + 트랜잭션)
token.revoke();
}
}

Some files were not shown because too many files have changed in this diff Show More