Merge pull request '년도목록조회 수정,log 파일 추가, 권한별 메뉴 기능 추가' (#89) from feat/dev_251201 into develop
Reviewed-on: https://kamco.gitea.gs.dabeeo.com/dabeeo/kamco-dabeeo-backoffice/pulls/89
This commit is contained in:
@@ -0,0 +1,188 @@
|
|||||||
|
package com.kamco.cd.kamcoback.auth;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.kamco.cd.kamcoback.common.enums.RoleType;
|
||||||
|
import com.kamco.cd.kamcoback.menu.dto.MenuDto;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.security.authorization.AuthorizationDecision;
|
||||||
|
import org.springframework.security.authorization.AuthorizationManager;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/** redis에 등록된 메뉴별 권한 확인 */
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RedisAuthorizationManager
|
||||||
|
implements AuthorizationManager<RequestAuthorizationContext> {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(RedisAuthorizationManager.class);
|
||||||
|
private static final String REDIS_KEY = "auth:api:role";
|
||||||
|
|
||||||
|
private final StringRedisTemplate redisTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthorizationDecision check(
|
||||||
|
Supplier<Authentication> authenticationSupplier, RequestAuthorizationContext context) {
|
||||||
|
|
||||||
|
HttpServletRequest request = context.getRequest();
|
||||||
|
String requestPath = normalizePath(request.getRequestURI());
|
||||||
|
|
||||||
|
Authentication authentication = authenticationSupplier.get();
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
return new AuthorizationDecision(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADMIN 은 무조건 허용
|
||||||
|
if (hasAdmin(authentication)) {
|
||||||
|
return new AuthorizationDecision(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사용자 role 목록
|
||||||
|
List<String> userRoles = extractRoles(authentication);
|
||||||
|
if (userRoles.isEmpty()) {
|
||||||
|
return new AuthorizationDecision(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis 조회 (1회)
|
||||||
|
String json = redisTemplate.opsForValue().get(REDIS_KEY);
|
||||||
|
if (json == null || json.isBlank()) {
|
||||||
|
return new AuthorizationDecision(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON 파싱 (1회)
|
||||||
|
List<MenuDto.MenuWithRolesDto> menus = parseMenus(json);
|
||||||
|
if (menus.isEmpty()) {
|
||||||
|
return new AuthorizationDecision(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// role + prefix URI 매칭
|
||||||
|
for (String role : userRoles) {
|
||||||
|
if (isAllowed(menus, role, requestPath)) {
|
||||||
|
return new AuthorizationDecision(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AuthorizationDecision(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* 핵심 권한 판별 로직
|
||||||
|
* ========================= */
|
||||||
|
private boolean isAllowed(List<MenuDto.MenuWithRolesDto> menus, String role, String requestPath) {
|
||||||
|
|
||||||
|
for (MenuDto.MenuWithRolesDto menu : menus) {
|
||||||
|
if (menu == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String baseUri = menu.getMenuApiUri();
|
||||||
|
if (baseUri == null || baseUri.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// role 포함 여부
|
||||||
|
if (!hasRole(menu.getRoles(), role)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefix URI 허용
|
||||||
|
if (matchUri(baseUri, requestPath)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* URI prefix 매칭
|
||||||
|
* ========================= */
|
||||||
|
private boolean matchUri(String baseUri, String requestPath) {
|
||||||
|
String base = normalizePath(baseUri);
|
||||||
|
String req = normalizePath(requestPath);
|
||||||
|
|
||||||
|
// /api/log/audit → /api/log/audit/**
|
||||||
|
return req.equals(base) || req.startsWith(base + "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* role 문자열 처리
|
||||||
|
* ========================= */
|
||||||
|
private boolean hasRole(String rolesCsv, String targetRole) {
|
||||||
|
if (rolesCsv == null || rolesCsv.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (targetRole == null || targetRole.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String r : rolesCsv.split(",")) {
|
||||||
|
if (targetRole.equalsIgnoreCase(r.trim())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* Redis JSON 파싱
|
||||||
|
* ========================= */
|
||||||
|
private List<MenuDto.MenuWithRolesDto> parseMenus(String json) {
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(json, new TypeReference<List<MenuDto.MenuWithRolesDto>>() {});
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to parse redis menu json. key={}", REDIS_KEY, e);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* ADMIN 판별
|
||||||
|
* ========================= */
|
||||||
|
private boolean hasAdmin(Authentication authentication) {
|
||||||
|
for (GrantedAuthority ga : authentication.getAuthorities()) {
|
||||||
|
if (ga == null || ga.getAuthority() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String auth = ga.getAuthority();
|
||||||
|
String admin = RoleType.ADMIN.getId();
|
||||||
|
|
||||||
|
if (auth.equals(admin) || auth.equals("ROLE_" + admin)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* ROLE 목록 추출
|
||||||
|
* ========================= */
|
||||||
|
private List<String> extractRoles(Authentication authentication) {
|
||||||
|
return authentication.getAuthorities().stream()
|
||||||
|
.map(GrantedAuthority::getAuthority)
|
||||||
|
.filter(a -> a != null && !a.isBlank())
|
||||||
|
.map(a -> a.startsWith("ROLE_") ? a.substring(5) : a)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
* URI 정규화
|
||||||
|
* ========================= */
|
||||||
|
private String normalizePath(String path) {
|
||||||
|
if (path == null || path.isBlank()) {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
return path.replaceAll("//+", "/");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package com.kamco.cd.kamcoback.config;
|
|||||||
|
|
||||||
import com.kamco.cd.kamcoback.auth.CustomAuthenticationProvider;
|
import com.kamco.cd.kamcoback.auth.CustomAuthenticationProvider;
|
||||||
import com.kamco.cd.kamcoback.auth.JwtAuthenticationFilter;
|
import com.kamco.cd.kamcoback.auth.JwtAuthenticationFilter;
|
||||||
|
import com.kamco.cd.kamcoback.auth.RedisAuthorizationManager;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@@ -11,6 +12,7 @@ import org.springframework.security.authentication.AuthenticationManager;
|
|||||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
@@ -27,16 +29,17 @@ public class SecurityConfig {
|
|||||||
|
|
||||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||||
private final CustomAuthenticationProvider customAuthenticationProvider;
|
private final CustomAuthenticationProvider customAuthenticationProvider;
|
||||||
|
private final RedisAuthorizationManager redisAuthorizationManager;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
|
||||||
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||||
.csrf(csrf -> csrf.disable())
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.formLogin(form -> form.disable())
|
.formLogin(AbstractHttpConfigurer::disable)
|
||||||
.httpBasic(basic -> basic.disable())
|
.httpBasic(AbstractHttpConfigurer::disable)
|
||||||
.logout(logout -> logout.disable())
|
.logout(AbstractHttpConfigurer::disable)
|
||||||
.authenticationProvider(customAuthenticationProvider)
|
.authenticationProvider(customAuthenticationProvider)
|
||||||
.authorizeHttpRequests(
|
.authorizeHttpRequests(
|
||||||
auth ->
|
auth ->
|
||||||
@@ -74,10 +77,10 @@ public class SecurityConfig {
|
|||||||
"/api/auth/logout",
|
"/api/auth/logout",
|
||||||
"/swagger-ui/**",
|
"/swagger-ui/**",
|
||||||
"/api/members/*/password",
|
"/api/members/*/password",
|
||||||
"/api/code/type/**",
|
|
||||||
"/v3/api-docs/**")
|
"/v3/api-docs/**")
|
||||||
.permitAll()
|
.permitAll()
|
||||||
.anyRequest()
|
.anyRequest()
|
||||||
|
// .access(redisAuthorizationManager)
|
||||||
.authenticated())
|
.authenticated())
|
||||||
.addFilterBefore(
|
.addFilterBefore(
|
||||||
jwtAuthenticationFilter,
|
jwtAuthenticationFilter,
|
||||||
|
|||||||
@@ -85,12 +85,12 @@ public class MenuApiController {
|
|||||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||||
})
|
})
|
||||||
@PostMapping("/auth")
|
@PostMapping("/auth")
|
||||||
public ApiResponseDto<Void> getFindByRoleRedis() {
|
public ApiResponseDto<String> getFindByRoleRedis() {
|
||||||
menuService.getFindByRoleRedis();
|
menuService.getFindByRoleRedis();
|
||||||
return ApiResponseDto.createOK(null);
|
return ApiResponseDto.createOK("ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "권한별 메뉴 조회", description = "권한별 메뉴 조회")
|
@Operation(summary = "권한별 메뉴 조회", description = "로그인 권한별 메뉴 목록")
|
||||||
@ApiResponses(
|
@ApiResponses(
|
||||||
value = {
|
value = {
|
||||||
@ApiResponse(
|
@ApiResponse(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@@ -61,4 +62,15 @@ public class MenuDto {
|
|||||||
this.menuApiUrl = menuApiUrl;
|
this.menuApiUrl = menuApiUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class MenuWithRolesDto {
|
||||||
|
|
||||||
|
private String menuUid;
|
||||||
|
private String menuNm;
|
||||||
|
private String menuUrl;
|
||||||
|
private String menuApiUri;
|
||||||
|
private String roles; // "ROLE_A,ROLE_B"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,16 @@ public class MenuService {
|
|||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<MenuDto.MenuWithRolesDto> menusWithRoles = menuCoreService.getMenuWithRoles();
|
||||||
|
|
||||||
|
try {
|
||||||
|
String key = "auth:api:role";
|
||||||
|
String value = objectMapper.writeValueAsString(menusWithRoles);
|
||||||
|
redisTemplate.opsForValue().set(key, value);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -26,4 +26,13 @@ public class MenuCoreService {
|
|||||||
public List<MenuDto.Basic> getFindByRole(String role) {
|
public List<MenuDto.Basic> getFindByRole(String role) {
|
||||||
return menuRepository.getFindByRole(role).stream().map(MenuEntity::toDto).toList();
|
return menuRepository.getFindByRole(role).stream().map(MenuEntity::toDto).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴별 권한 조회
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public List<MenuDto.MenuWithRolesDto> getMenuWithRoles() {
|
||||||
|
return menuRepository.getFindByMenuWithRoles();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.kamco.cd.kamcoback.postgres.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Entity
|
||||||
|
@Table(name = "tb_year")
|
||||||
|
public class YearEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "yyyy", nullable = false)
|
||||||
|
private Integer yyyy;
|
||||||
|
|
||||||
|
@Size(max = 20)
|
||||||
|
@Column(name = "status", length = 20)
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.kamco.cd.kamcoback.postgres.repository.menu;
|
package com.kamco.cd.kamcoback.postgres.repository.menu;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.menu.dto.MenuDto.MenuWithRolesDto;
|
||||||
import com.kamco.cd.kamcoback.postgres.entity.MenuEntity;
|
import com.kamco.cd.kamcoback.postgres.entity.MenuEntity;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -14,4 +15,11 @@ public interface MenuRepositoryCustom {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
List<MenuEntity> getFindByRole(String role);
|
List<MenuEntity> getFindByRole(String role);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 메뉴별 권한 조회
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
List<MenuWithRolesDto> getFindByMenuWithRoles();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ package com.kamco.cd.kamcoback.postgres.repository.menu;
|
|||||||
import static com.kamco.cd.kamcoback.postgres.entity.QMenuEntity.menuEntity;
|
import static com.kamco.cd.kamcoback.postgres.entity.QMenuEntity.menuEntity;
|
||||||
import static com.kamco.cd.kamcoback.postgres.entity.QMenuMappEntity.menuMappEntity;
|
import static com.kamco.cd.kamcoback.postgres.entity.QMenuMappEntity.menuMappEntity;
|
||||||
|
|
||||||
|
import com.kamco.cd.kamcoback.menu.dto.MenuDto.MenuWithRolesDto;
|
||||||
import com.kamco.cd.kamcoback.postgres.entity.MenuEntity;
|
import com.kamco.cd.kamcoback.postgres.entity.MenuEntity;
|
||||||
import com.kamco.cd.kamcoback.postgres.entity.QMenuEntity;
|
import com.kamco.cd.kamcoback.postgres.entity.QMenuEntity;
|
||||||
|
import com.kamco.cd.kamcoback.postgres.entity.QMenuMappEntity;
|
||||||
|
import com.querydsl.core.types.Expression;
|
||||||
|
import com.querydsl.core.types.Projections;
|
||||||
import com.querydsl.core.types.dsl.Expressions;
|
import com.querydsl.core.types.dsl.Expressions;
|
||||||
import com.querydsl.core.types.dsl.StringExpression;
|
|
||||||
import com.querydsl.jpa.impl.JPAQueryFactory;
|
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -17,7 +20,6 @@ import org.springframework.stereotype.Repository;
|
|||||||
public class MenuRepositoryImpl implements MenuRepositoryCustom {
|
public class MenuRepositoryImpl implements MenuRepositoryCustom {
|
||||||
|
|
||||||
private final JPAQueryFactory queryFactory;
|
private final JPAQueryFactory queryFactory;
|
||||||
private final StringExpression NULL_STRING = Expressions.stringTemplate("cast(null as text)");
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<MenuEntity> getFindAll() {
|
public List<MenuEntity> getFindAll() {
|
||||||
@@ -57,4 +59,31 @@ public class MenuRepositoryImpl implements MenuRepositoryCustom {
|
|||||||
|
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MenuWithRolesDto> getFindByMenuWithRoles() {
|
||||||
|
QMenuEntity tm = menuEntity;
|
||||||
|
QMenuMappEntity tmm = menuMappEntity;
|
||||||
|
|
||||||
|
Expression<String> roleAgg =
|
||||||
|
Expressions.stringTemplate("string_agg({0}, {1})", tmm.roleCode, Expressions.constant(","));
|
||||||
|
|
||||||
|
List<MenuWithRolesDto> content =
|
||||||
|
queryFactory
|
||||||
|
.select(
|
||||||
|
Projections.constructor(
|
||||||
|
MenuWithRolesDto.class,
|
||||||
|
tm.menuUid,
|
||||||
|
tm.menuNm,
|
||||||
|
tm.menuUrl,
|
||||||
|
tm.menuApiUri,
|
||||||
|
roleAgg))
|
||||||
|
.from(tm)
|
||||||
|
.leftJoin(tmm)
|
||||||
|
.on(tmm.menuUid.eq(tm).and(tmm.deleted.isFalse()))
|
||||||
|
.where(tm.deleted.isFalse())
|
||||||
|
.groupBy(tm.menuUid, tm.menuNm, tm.menuUrl, tm.menuApiUri)
|
||||||
|
.fetch();
|
||||||
|
return content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
85
src/main/resources/logback-spring.xml
Normal file
85
src/main/resources/logback-spring.xml
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration scan="true" scanPeriod="30 seconds">
|
||||||
|
|
||||||
|
<!-- 공통 변수 -->
|
||||||
|
<property name="LOG_PATH" value="./logs"/>
|
||||||
|
<property name="LOG_PATTERN"
|
||||||
|
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- 콘솔 로그 (공통) -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>${LOG_PATTERN}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- 파일 로그 (공통) -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<file>${LOG_PATH}/application.log</file>
|
||||||
|
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<!-- 일 단위 로테이션 -->
|
||||||
|
<fileNamePattern>${LOG_PATH}/application.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
|
<!-- 보관 기간 -->
|
||||||
|
<maxHistory>30</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
|
||||||
|
<encoder>
|
||||||
|
<pattern>${LOG_PATTERN}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- 개발 서버 설정 (local) -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<springProfile name="local">
|
||||||
|
<!-- SQL, 디버그 로그 상세 -->
|
||||||
|
<logger name="org.springframework" level="INFO"/>
|
||||||
|
<logger name="org.hibernate.SQL" level="DEBUG"/>
|
||||||
|
<logger name="org.hibernate.type.descriptor.sql" level="TRACE"/>
|
||||||
|
<logger name="com.zaxxer.hikari" level="DEBUG"/>
|
||||||
|
<logger name="jdbc.sqlonly" level="DEBUG"/>
|
||||||
|
|
||||||
|
<root level="DEBUG">
|
||||||
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
<appender-ref ref="FILE"/>
|
||||||
|
</root>
|
||||||
|
</springProfile>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- 개발 서버 설정 (dev) -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<springProfile name="dev">
|
||||||
|
<!-- SQL, 디버그 로그 상세 -->
|
||||||
|
<logger name="org.springframework" level="INFO"/>
|
||||||
|
<logger name="org.hibernate.SQL" level="DEBUG"/>
|
||||||
|
<logger name="org.hibernate.type.descriptor.sql" level="TRACE"/>
|
||||||
|
<logger name="com.zaxxer.hikari" level="DEBUG"/>
|
||||||
|
<logger name="jdbc.sqlonly" level="DEBUG"/>
|
||||||
|
|
||||||
|
<root level="DEBUG">
|
||||||
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
<appender-ref ref="FILE"/>
|
||||||
|
</root>
|
||||||
|
</springProfile>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- 운영 서버 설정 (prod) -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<springProfile name="prod">
|
||||||
|
<!-- 불필요한 로그 최소화 -->
|
||||||
|
<logger name="org.springframework" level="WARN"/>
|
||||||
|
<logger name="org.hibernate" level="WARN"/>
|
||||||
|
<logger name="com.zaxxer.hikari" level="WARN"/>
|
||||||
|
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="FILE"/>
|
||||||
|
</root>
|
||||||
|
</springProfile>
|
||||||
|
|
||||||
|
</configuration>
|
||||||
Reference in New Issue
Block a user