This commit is contained in:
lsh9672
2025-06-11 16:31:06 +09:00
commit f0fbb47c51
164 changed files with 8667 additions and 0 deletions
@@ -0,0 +1,18 @@
package com.ktds.hi.common;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
/**
* Common 모듈 설정 클래스
* 다른 모듈에서 Common 모듈을 사용할 때 필요한 설정을 자동으로 적용
*/
@Configuration
@ComponentScan(basePackages = "com.ktds.hi.common")
@EntityScan(basePackages = "com.ktds.hi.common.entity")
@EnableJpaRepositories(basePackages = "com.ktds.hi.common.repository")
public class CommonModuleConfiguration {
// 설정 클래스는 어노테이션만으로도 충분
}
@@ -0,0 +1,62 @@
package com.ktds.hi.common.aspect;
import com.ktds.hi.common.service.AuditLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* 감사 로깅 AOP
* 특정 메서드 실행 시 자동으로 감사 로그를 기록
*/
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class AuditAspect {
private final AuditLogService auditLogService;
/**
* 서비스 메서드 실행 후 감사 로그 기록
*/
@AfterReturning(
pointcut = "execution(* com.ktds.hi.*.biz.service.*Service.create*(..))",
returning = "result"
)
public void auditCreate(JoinPoint joinPoint, Object result) {
try {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
auditLogService.logCreate(
className.replace("Service", ""),
extractEntityId(result),
String.format("%s.%s 실행", className, methodName)
);
} catch (Exception e) {
log.warn("Failed to audit create operation", e);
}
}
/**
* 결과 객체에서 ID 추출
*/
private String extractEntityId(Object result) {
if (result == null) {
return "UNKNOWN";
}
try {
// 리플렉션을 사용하여 getId() 메서드 호출
var method = result.getClass().getMethod("getId");
Object id = method.invoke(result);
return id != null ? id.toString() : "UNKNOWN";
} catch (Exception e) {
return "UNKNOWN";
}
}
}
@@ -0,0 +1,55 @@
package com.ktds.hi.common.repository;
import com.ktds.hi.common.audit.AuditAction;
import com.ktds.hi.common.audit.AuditLog;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
/**
* 감사 로그 리포지토리
*/
@Repository
public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
/**
* 사용자별 감사 로그 조회
*/
Page<AuditLog> findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable);
/**
* 액션별 감사 로그 조회
*/
Page<AuditLog> findByActionOrderByCreatedAtDesc(AuditAction action, Pageable pageable);
/**
* 엔티티별 감사 로그 조회
*/
Page<AuditLog> findByEntityTypeAndEntityIdOrderByCreatedAtDesc(String entityType, String entityId, Pageable pageable);
/**
* 기간별 감사 로그 조회
*/
@Query("SELECT al FROM AuditLog al WHERE al.createdAt BETWEEN :startDate AND :endDate ORDER BY al.createdAt DESC")
Page<AuditLog> findByCreatedAtBetween(@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate,
Pageable pageable);
/**
* 사용자 액션 통계 조회
*/
@Query("SELECT al.action, COUNT(al) FROM AuditLog al WHERE al.userId = :userId GROUP BY al.action")
List<Object[]> findActionStatsByUserId(@Param("userId") Long userId);
/**
* 일별 로그 수 조회
*/
@Query("SELECT DATE(al.createdAt), COUNT(al) FROM AuditLog al WHERE al.createdAt >= :startDate GROUP BY DATE(al.createdAt) ORDER BY DATE(al.createdAt)")
List<Object[]> findDailyLogCounts(@Param("startDate") LocalDateTime startDate);
}
@@ -0,0 +1,89 @@
package com.ktds.hi.common.security;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* 커스텀 사용자 상세 정보
* Spring Security UserDetails 인터페이스 구현
*/
@Getter
@AllArgsConstructor
public class CustomUserDetails implements UserDetails {
private final Long id;
private final String username;
private final String email;
private final String password;
private final List<String> roles;
private final boolean enabled;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return enabled;
}
/**
* 기본 생성자 (활성화된 사용자)
*/
public static CustomUserDetails of(Long id, String username, String email, String password, List<String> roles) {
return new CustomUserDetails(
id, username, email, password, roles,
true, true, true, true
);
}
/**
* 상태를 지정한 생성자
*/
public static CustomUserDetails of(Long id, String username, String email, String password, List<String> roles,
boolean enabled, boolean accountNonExpired,
boolean accountNonLocked, boolean credentialsNonExpired) {
return new CustomUserDetails(
id, username, email, password, roles,
enabled, accountNonExpired, accountNonLocked, credentialsNonExpired
);
}
}
@@ -0,0 +1,114 @@
package com.ktds.hi.common.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ktds.hi.common.constants.SecurityConstants;
import com.ktds.hi.common.response.ApiResponse;
import com.ktds.hi.common.response.ResponseCode;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* JWT 인증 필터
* HTTP 요청에서 JWT 토큰을 추출하고 검증하여 인증 처리
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final ObjectMapper objectMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
// 액세스 토큰인지 확인
if (!jwtTokenProvider.isAccessToken(jwt)) {
sendErrorResponse(response, ResponseCode.UNAUTHORIZED, "액세스 토큰이 아닙니다.");
return;
}
String userId = jwtTokenProvider.getUserIdFromToken(jwt);
String roles = jwtTokenProvider.getRolesFromToken(jwt);
if (StringUtils.hasText(userId)) {
List<SimpleGrantedAuthority> authorities = Arrays.stream(roles.split(","))
.map(String::trim)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userId, null, authorities);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (Exception e) {
log.error("JWT authentication failed", e);
sendErrorResponse(response, ResponseCode.UNAUTHORIZED, "인증에 실패했습니다.");
return;
}
filterChain.doFilter(request, response);
}
/**
* 요청에서 JWT 토큰 추출
*/
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader(SecurityConstants.JWT_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(SecurityConstants.JWT_PREFIX)) {
return bearerToken.substring(SecurityConstants.JWT_PREFIX.length());
}
return null;
}
/**
* 에러 응답 전송
*/
private void sendErrorResponse(HttpServletResponse response, ResponseCode responseCode, String message)
throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
ApiResponse<Void> errorResponse = ApiResponse.error(responseCode, message);
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}
/**
* 공개 경로인지 확인
*/
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String path = request.getRequestURI();
return Arrays.stream(SecurityConstants.PUBLIC_ENDPOINTS)
.anyMatch(pattern -> path.matches(pattern.replace("**", ".*")));
}
}
@@ -0,0 +1,204 @@
package com.ktds.hi.common.security;
import com.ktds.hi.common.constants.SecurityConstants;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.stream.Collectors;
/**
* JWT 토큰 생성 및 검증 제공자
* JWT 토큰의 생성, 파싱, 검증 기능을 담당
*/
@Component
@Slf4j
public class JwtTokenProvider {
private final SecretKey secretKey;
private final long accessTokenValidityInMilliseconds;
private final long refreshTokenValidityInMilliseconds;
public JwtTokenProvider(
@Value("${app.jwt.secret-key:hiorder-secret-key-for-jwt-token-generation-2024}") String secretKeyString,
@Value("${app.jwt.access-token-validity:3600000}") long accessTokenValidity,
@Value("${app.jwt.refresh-token-validity:604800000}") long refreshTokenValidity) {
// 비밀키 생성 (256비트 이상이어야 함)
byte[] keyBytes = secretKeyString.getBytes(StandardCharsets.UTF_8);
if (keyBytes.length < 32) {
// 32바이트 미만이면 패딩
byte[] paddedKey = new byte[32];
System.arraycopy(keyBytes, 0, paddedKey, 0, Math.min(keyBytes.length, 32));
keyBytes = paddedKey;
}
this.secretKey = Keys.hmacShaKeyFor(keyBytes);
this.accessTokenValidityInMilliseconds = accessTokenValidity;
this.refreshTokenValidityInMilliseconds = refreshTokenValidity;
}
/**
* 액세스 토큰 생성
*/
public String createAccessToken(Authentication authentication) {
return createToken(authentication, accessTokenValidityInMilliseconds, "access");
}
/**
* 리프레시 토큰 생성
*/
public String createRefreshToken(Authentication authentication) {
return createToken(authentication, refreshTokenValidityInMilliseconds, "refresh");
}
/**
* 사용자 정보로 액세스 토큰 생성
*/
public String createAccessToken(String userId, String username, String roles) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + accessTokenValidityInMilliseconds);
return Jwts.builder()
.setSubject(userId)
.claim("username", username)
.claim("roles", roles)
.claim("type", "access")
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(secretKey, SignatureAlgorithm.HS512)
.compact();
}
/**
* 토큰 생성 공통 메서드
*/
private String createToken(Authentication authentication, long validityInMilliseconds, String tokenType) {
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
Date now = new Date();
Date expiryDate = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()
.setSubject(authentication.getName())
.claim("roles", authorities)
.claim("type", tokenType)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(secretKey, SignatureAlgorithm.HS512)
.compact();
}
/**
* 토큰에서 사용자 ID 추출
*/
public String getUserIdFromToken(String token) {
Claims claims = parseClaimsFromToken(token);
return claims.getSubject();
}
/**
* 토큰에서 사용자명 추출
*/
public String getUsernameFromToken(String token) {
Claims claims = parseClaimsFromToken(token);
return claims.get("username", String.class);
}
/**
* 토큰에서 권한 추출
*/
public String getRolesFromToken(String token) {
Claims claims = parseClaimsFromToken(token);
return claims.get("roles", String.class);
}
/**
* 토큰에서 만료일 추출
*/
public Date getExpirationDateFromToken(String token) {
Claims claims = parseClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 토큰 유효성 검증
*/
public boolean validateToken(String token) {
try {
parseClaimsFromToken(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.debug("Invalid JWT token: {}", e.getMessage());
return false;
}
}
/**
* 액세스 토큰인지 확인
*/
public boolean isAccessToken(String token) {
try {
Claims claims = parseClaimsFromToken(token);
return "access".equals(claims.get("type", String.class));
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
/**
* 리프레시 토큰인지 확인
*/
public boolean isRefreshToken(String token) {
try {
Claims claims = parseClaimsFromToken(token);
return "refresh".equals(claims.get("type", String.class));
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
/**
* 토큰 만료 확인
*/
public boolean isTokenExpired(String token) {
try {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
} catch (JwtException | IllegalArgumentException e) {
return true;
}
}
/**
* 토큰에서 Claims 파싱
*/
private Claims parseClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 토큰 만료 시간까지 남은 시간 (밀리초)
*/
public long getTimeUntilExpiration(String token) {
try {
Date expiration = getExpirationDateFromToken(token);
return Math.max(0, expiration.getTime() - System.currentTimeMillis());
} catch (JwtException | IllegalArgumentException e) {
return 0;
}
}
}
@@ -0,0 +1,135 @@
package com.ktds.hi.common.security;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 보안 관련 유틸리티 클래스
* 현재 인증된 사용자 정보 조회 등의 기능을 제공
*/
public final class SecurityUtil {
private SecurityUtil() {
// 인스턴스 생성 방지
}
/**
* 현재 인증된 사용자 ID 조회
*/
public static Optional<Long> getCurrentUserId() {
return getCurrentAuthentication()
.map(Authentication::getName)
.map(Long::parseLong);
}
/**
* 현재 인증된 사용자명 조회
*/
public static Optional<String> getCurrentUsername() {
return getCurrentAuthentication()
.map(Authentication::getName);
}
/**
* 현재 인증된 사용자의 권한 조회
*/
public static Set<String> getCurrentUserRoles() {
return getCurrentAuthentication()
.map(auth -> auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet()))
.orElse(Set.of());
}
/**
* 현재 사용자가 특정 권한을 가지는지 확인
*/
public static boolean hasRole(String role) {
return getCurrentUserRoles().contains(role);
}
/**
* 현재 사용자가 여러 권한 중 하나라도 가지는지 확인
*/
public static boolean hasAnyRole(String... roles) {
Set<String> userRoles = getCurrentUserRoles();
for (String role : roles) {
if (userRoles.contains(role)) {
return true;
}
}
return false;
}
/**
* 현재 사용자가 모든 권한을 가지는지 확인
*/
public static boolean hasAllRoles(String... roles) {
Set<String> userRoles = getCurrentUserRoles();
for (String role : roles) {
if (!userRoles.contains(role)) {
return false;
}
}
return true;
}
/**
* 현재 사용자가 인증되었는지 확인
*/
public static boolean isAuthenticated() {
return getCurrentAuthentication()
.map(Authentication::isAuthenticated)
.orElse(false);
}
/**
* 현재 사용자가 익명 사용자인지 확인
*/
public static boolean isAnonymous() {
return !isAuthenticated();
}
/**
* 현재 Authentication 객체 조회
*/
public static Optional<Authentication> getCurrentAuthentication() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return Optional.ofNullable(authentication);
}
/**
* 현재 사용자가 리소스 소유자인지 확인
*/
public static boolean isOwner(Long resourceOwnerId) {
return getCurrentUserId()
.map(currentUserId -> currentUserId.equals(resourceOwnerId))
.orElse(false);
}
/**
* 현재 사용자가 관리자인지 확인
*/
public static boolean isAdmin() {
return hasRole("ROLE_ADMIN");
}
/**
* 현재 사용자가 매장 소유자인지 확인
*/
public static boolean isStoreOwner() {
return hasRole("ROLE_STORE_OWNER");
}
/**
* 현재 사용자가 일반 사용자인지 확인
*/
public static boolean isUser() {
return hasRole("ROLE_USER");
}
}
@@ -0,0 +1,92 @@
package com.ktds.hi.common.service;
import com.ktds.hi.common.audit.AuditAction;
import com.ktds.hi.common.audit.AuditLog;
import com.ktds.hi.common.repository.AuditLogRepository;
import com.ktds.hi.common.security.SecurityUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 감사 로그 서비스
* 시스템의 중요한 액션들을 비동기적으로 로깅
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class AuditLogService {
private final AuditLogRepository auditLogRepository;
/**
* 감사 로그 기록 (비동기)
*/
@Async
@Transactional
public void logAsync(AuditAction action, String entityType, String entityId, String description) {
log(action, entityType, entityId, description);
}
/**
* 감사 로그 기록 (동기)
*/
@Transactional
public void log(AuditAction action, String entityType, String entityId, String description) {
try {
Long userId = SecurityUtil.getCurrentUserId().orElse(null);
String username = SecurityUtil.getCurrentUsername().orElse("SYSTEM");
AuditLog auditLog = AuditLog.create(userId, username, action, entityType, entityId, description);
auditLogRepository.save(auditLog);
} catch (Exception e) {
log.error("Failed to save audit log: action={}, entityType={}, entityId={}",
action, entityType, entityId, e);
}
}
/**
* 생성 로그
*/
public void logCreate(String entityType, String entityId, String description) {
logAsync(AuditAction.CREATE, entityType, entityId, description);
}
/**
* 수정 로그
*/
public void logUpdate(String entityType, String entityId, String description) {
logAsync(AuditAction.UPDATE, entityType, entityId, description);
}
/**
* 삭제 로그
*/
public void logDelete(String entityType, String entityId, String description) {
logAsync(AuditAction.DELETE, entityType, entityId, description);
}
/**
* 접근 로그
*/
public void logAccess(String entityType, String entityId, String description) {
logAsync(AuditAction.ACCESS, entityType, entityId, description);
}
/**
* 로그인 로그
*/
public void logLogin(String description) {
logAsync(AuditAction.LOGIN, "USER", SecurityUtil.getCurrentUserId().map(String::valueOf).orElse("UNKNOWN"), description);
}
/**
* 로그아웃 로그
*/
public void logLogout(String description) {
logAsync(AuditAction.LOGOUT, "USER", SecurityUtil.getCurrentUserId().map(String::valueOf).orElse("UNKNOWN"), description);
}
}
@@ -0,0 +1,94 @@
spring:
# JPA 설정
jpa:
hibernate:
ddl-auto: ${JPA_DDL_AUTO:validate}
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
show-sql: ${JPA_SHOW_SQL:false}
properties:
hibernate:
format_sql: true
use_sql_comments: true
jdbc:
batch_size: 20
order_inserts: true
order_updates: true
batch_versioned_data: true
# Redis 설정
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
timeout: 2000ms
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
# Jackson 설정
jackson:
time-zone: Asia/Seoul
date-format: yyyy-MM-dd HH:mm:ss
serialization:
write-dates-as-timestamps: false
deserialization:
fail-on-unknown-properties: false
# 트랜잭션 설정
transaction:
default-timeout: 30
# 애플리케이션 설정
app:
# JWT 설정
jwt:
secret-key: ${JWT_SECRET_KEY:hiorder-secret-key-for-jwt-token-generation-2024-very-long-secret-key}
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:3600000} # 1시간
refresh-token-validity: ${JWT_REFRESH_TOKEN_VALIDITY:604800000} # 7일
# CORS 설정
cors:
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:8080}
allowed-methods: ${CORS_ALLOWED_METHODS:GET,POST,PUT,DELETE,OPTIONS}
allowed-headers: ${CORS_ALLOWED_HEADERS:*}
exposed-headers: ${CORS_EXPOSED_HEADERS:Authorization}
allow-credentials: ${CORS_ALLOW_CREDENTIALS:true}
max-age: ${CORS_MAX_AGE:3600}
# 캐시 설정
cache:
default-ttl: ${CACHE_DEFAULT_TTL:3600} # 1시간
# Swagger 설정
swagger:
title: ${SWAGGER_TITLE:하이오더 API}
description: ${SWAGGER_DESCRIPTION:하이오더 백엔드 API 문서}
version: ${SWAGGER_VERSION:1.0.0}
server-url: ${SWAGGER_SERVER_URL:http://localhost:8080}
# 로깅 설정
logging:
level:
com.ktds.hi: ${LOG_LEVEL:INFO}
org.springframework.security: ${SECURITY_LOG_LEVEL:INFO}
org.hibernate.SQL: ${SQL_LOG_LEVEL:INFO}
org.hibernate.type.descriptor.sql.BasicBinder: ${SQL_PARAM_LOG_LEVEL:INFO}
pattern:
console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
# 관리 엔드포인트 설정
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when-authorized