mirror of
https://github.com/cna-bootcamp/phonebill.git
synced 2026-06-12 19:49:10 +00:00
kos-mock 상품변경 실제 DB 업데이트 기능 추가
- MockDataService에 updateCustomerProduct 메서드 추가 - KosMockService에 실제 고객 데이터 업데이트 로직 추가 - 상품변경 시 고객의 current_product_code를 실제로 업데이트하도록 수정 - 트랜잭션 처리로 데이터 일관성 보장 - product-service Hibernate dialect 설정 추가 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,64 +3,35 @@
|
||||
<ExternalSystemSettings>
|
||||
<option name="env">
|
||||
<map>
|
||||
<!-- Database Connection -->
|
||||
<entry key="DB_HOST" value="20.249.175.46" />
|
||||
<entry key="DB_PORT" value="5432" />
|
||||
<entry key="DB_NAME" value="bill_inquiry_db" />
|
||||
<entry key="DB_USERNAME" value="bill_inquiry_user" />
|
||||
<entry key="DB_PASSWORD" value="BillUser2025!" />
|
||||
<entry key="DB_KIND" value="postgresql" />
|
||||
|
||||
<!-- Redis Connection -->
|
||||
<entry key="REDIS_HOST" value="20.249.193.103" />
|
||||
<entry key="REDIS_PORT" value="6379" />
|
||||
<entry key="REDIS_PASSWORD" value="Redis2025Dev!" />
|
||||
<entry key="REDIS_DATABASE" value="1" />
|
||||
|
||||
<!-- Server Configuration -->
|
||||
<entry key="SERVER_PORT" value="8082" />
|
||||
<entry key="SPRING_PROFILES_ACTIVE" value="dev" />
|
||||
|
||||
<!-- JWT Configuration -->
|
||||
<entry key="JWT_SECRET" value="phonebill-jwt-secret-key-2025-dev" />
|
||||
|
||||
<!-- JPA Configuration -->
|
||||
<entry key="JPA_DDL_AUTO" value="update" />
|
||||
<entry key="SHOW_SQL" value="true" />
|
||||
|
||||
<!-- Logging Configuration -->
|
||||
<entry key="LOG_FILE_NAME" value="logs/bill-service.log" />
|
||||
<entry key="LOG_LEVEL_APP" value="DEBUG" />
|
||||
|
||||
<!-- KOS Mock URL -->
|
||||
<entry key="KOS_BASE_URL" value="http://localhost:8084" />
|
||||
|
||||
<!-- Development optimized settings -->
|
||||
<entry key="JPA_SHOW_SQL" value="true" />
|
||||
<entry key="JPA_FORMAT_SQL" value="true" />
|
||||
<entry key="JPA_SQL_COMMENTS" value="true" />
|
||||
<entry key="LOG_LEVEL_ROOT" value="INFO" />
|
||||
<entry key="LOG_LEVEL_SERVICE" value="DEBUG" />
|
||||
<entry key="LOG_LEVEL_REPOSITORY" value="DEBUG" />
|
||||
<entry key="LOG_LEVEL_SQL" value="DEBUG" />
|
||||
<entry key="LOG_LEVEL_WEB" value="DEBUG" />
|
||||
<entry key="LOG_LEVEL_CACHE" value="DEBUG" />
|
||||
<entry key="LOG_LEVEL_SECURITY" value="DEBUG" />
|
||||
|
||||
<!-- Connection Pool Settings -->
|
||||
<entry key="DB_MIN_IDLE" value="5" />
|
||||
<entry key="DB_MAX_POOL" value="20" />
|
||||
<entry key="CORS_ALLOWED_ORIGINS" value="http://localhost:3000" />
|
||||
<entry key="DB_CONNECTION_TIMEOUT" value="30000" />
|
||||
<entry key="DB_HOST" value="20.249.175.46" />
|
||||
<entry key="DB_IDLE_TIMEOUT" value="600000" />
|
||||
<entry key="DB_MAX_LIFETIME" value="1800000" />
|
||||
<entry key="DB_KIND" value="postgresql" />
|
||||
<entry key="DB_LEAK_DETECTION" value="60000" />
|
||||
|
||||
<!-- Redis Pool Settings -->
|
||||
<entry key="DB_MAX_LIFETIME" value="1800000" />
|
||||
<entry key="DB_MAX_POOL" value="20" />
|
||||
<entry key="DB_MIN_IDLE" value="5" />
|
||||
<entry key="DB_NAME" value="bill_inquiry_db" />
|
||||
<entry key="DB_PASSWORD" value="BillUser2025!" />
|
||||
<entry key="DB_PORT" value="5432" />
|
||||
<entry key="DB_USERNAME" value="bill_inquiry_user" />
|
||||
<entry key="JWT_ACCESS_TOKEN_VALIDITY" value="18000000" />
|
||||
<entry key="JWT_REFRESH_TOKEN_VALIDITY" value="86400000" />
|
||||
<entry key="JWT_SECRET" value="nwe5Yo9qaJ6FBD/Thl2/j6/SFAfNwUorAY1ZcWO2KI7uA4bmVLOCPxE9hYuUpRCOkgV2UF2DdHXtqHi3+BU/ecbz2zpHyf/720h48UbA3XOMYOX1sdM+dQ==" />
|
||||
<entry key="KOS_BASE_URL" value="http://localhost:8084" />
|
||||
<entry key="LOG_FILE_NAME" value="logs/bill-service.log" />
|
||||
<entry key="REDIS_DATABASE" value="1" />
|
||||
<entry key="REDIS_HOST" value="20.249.193.103" />
|
||||
<entry key="REDIS_MAX_ACTIVE" value="8" />
|
||||
<entry key="REDIS_MAX_IDLE" value="8" />
|
||||
<entry key="REDIS_MIN_IDLE" value="0" />
|
||||
<entry key="REDIS_MAX_WAIT" value="-1" />
|
||||
<entry key="REDIS_MIN_IDLE" value="0" />
|
||||
<entry key="REDIS_PASSWORD" value="Redis2025Dev!" />
|
||||
<entry key="REDIS_PORT" value="6379" />
|
||||
<entry key="REDIS_TIMEOUT" value="2000" />
|
||||
<entry key="SERVER_PORT" value="8082" />
|
||||
<entry key="SPRING_PROFILES_ACTIVE" value="dev" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="executionName" />
|
||||
@@ -80,7 +51,7 @@
|
||||
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<ForceTestExec>false</ForceTestExec>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -28,6 +28,7 @@ dependencies {
|
||||
|
||||
// Common modules (로컬 의존성)
|
||||
implementation project(':common')
|
||||
implementation project(':kos-mock')
|
||||
|
||||
// Test Dependencies (bill service specific)
|
||||
testImplementation 'org.testcontainers:postgresql'
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.phonebill.bill.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
|
||||
/**
|
||||
* JPA Auditing 설정
|
||||
*
|
||||
* BaseTimeEntity의 @CreatedDate, @LastModifiedDate 자동 설정을 위한 구성
|
||||
* - 엔티티 저장/수정 시 자동으로 시간 정보 설정
|
||||
* - 모든 엔티티의 생성/수정 시간 추적 가능
|
||||
*
|
||||
* @author 이개발(백엔더)
|
||||
* @version 1.0.0
|
||||
* @since 2025-09-09
|
||||
*/
|
||||
@Configuration
|
||||
@EnableJpaAuditing
|
||||
public class JpaAuditingConfig {
|
||||
// JPA Auditing 활성화를 위한 설정 클래스
|
||||
// 별도의 Bean 정의 없이 @EnableJpaAuditing만으로 충분
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.phonebill.bill.config;
|
||||
|
||||
import com.phonebill.common.security.JwtTokenProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* JWT 설정
|
||||
*
|
||||
* Bill Service의 JWT 토큰 검증을 위한 설정
|
||||
* User Service와 동일한 시크릿 키를 사용하여 토큰 검증
|
||||
*
|
||||
* @author 이개발(백엔더)
|
||||
* @version 1.0.0
|
||||
* @since 2025-09-09
|
||||
*/
|
||||
@Configuration
|
||||
public class JwtConfig {
|
||||
|
||||
/**
|
||||
* JwtTokenProvider 빈 생성
|
||||
*
|
||||
* @param secret JWT 시크릿 키
|
||||
* @param expirationInSeconds JWT 만료 시간 (초)
|
||||
* @return JwtTokenProvider 인스턴스
|
||||
*/
|
||||
@Bean
|
||||
public JwtTokenProvider jwtTokenProvider(
|
||||
@Value("${jwt.secret:dev-jwt-secret-key-for-development-only}") String secret,
|
||||
@Value("${jwt.access-token-validity:86400000}") long expirationInMillis) {
|
||||
|
||||
// 만료 시간을 초 단위로 변환
|
||||
long expirationInSeconds = expirationInMillis / 1000;
|
||||
|
||||
return new JwtTokenProvider(secret, expirationInSeconds);
|
||||
}
|
||||
}
|
||||
@@ -98,8 +98,8 @@ public class RedisConfig {
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
|
||||
// Value 직렬화: JSON 사용
|
||||
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(objectMapper());
|
||||
// Value 직렬화: Redis 전용 ObjectMapper 사용
|
||||
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(redisObjectMapper());
|
||||
template.setValueSerializer(jsonSerializer);
|
||||
template.setHashValueSerializer(jsonSerializer);
|
||||
|
||||
@@ -128,8 +128,8 @@ public class RedisConfig {
|
||||
.serializeKeysWith(org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair
|
||||
.fromSerializer(new StringRedisSerializer()))
|
||||
.serializeValuesWith(org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair
|
||||
.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper())))
|
||||
.disableCachingNullValues(); // null 값 캐싱 비활성화
|
||||
.fromSerializer(new GenericJackson2JsonRedisSerializer(redisObjectMapper())));
|
||||
// null 값 캐싱은 @Cacheable unless 조건으로 처리
|
||||
|
||||
// 캐시별 개별 설정
|
||||
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
|
||||
@@ -163,25 +163,24 @@ public class RedisConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* ObjectMapper 구성
|
||||
* Redis 전용 ObjectMapper 구성
|
||||
*
|
||||
* @return JSON 직렬화용 ObjectMapper
|
||||
* @return Redis 직렬화용 ObjectMapper (다형성 타입 정보 포함)
|
||||
*/
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
private ObjectMapper redisObjectMapper() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
// Java Time 모듈 등록 (LocalDateTime 등 지원)
|
||||
mapper.registerModule(new JavaTimeModule());
|
||||
|
||||
// 타입 정보 포함 (다형성 지원)
|
||||
// 타입 정보 포함 (다형성 지원) - Redis 캐싱에만 필요
|
||||
mapper.activateDefaultTyping(
|
||||
LaissezFaireSubTypeValidator.instance,
|
||||
ObjectMapper.DefaultTyping.NON_FINAL,
|
||||
JsonTypeInfo.As.PROPERTY
|
||||
);
|
||||
|
||||
log.debug("ObjectMapper 구성 완료");
|
||||
log.debug("Redis 전용 ObjectMapper 구성 완료");
|
||||
return mapper;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.phonebill.bill.config;
|
||||
|
||||
import com.phonebill.common.security.JwtAuthenticationFilter;
|
||||
import com.phonebill.common.security.JwtTokenProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -21,6 +24,7 @@ import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Spring Security 설정
|
||||
@@ -42,6 +46,11 @@ import java.util.Arrays;
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
@Value("${cors.allowed-origins")
|
||||
private String allowedOrigins;
|
||||
|
||||
/**
|
||||
* 보안 필터 체인 구성
|
||||
*
|
||||
@@ -53,76 +62,55 @@ public class SecurityConfig {
|
||||
log.info("Security Filter Chain 구성 시작");
|
||||
|
||||
http
|
||||
// CSRF 비활성화 (REST API는 CSRF 불필요)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
// CSRF 비활성화 (JWT 사용으로 불필요)
|
||||
.csrf(csrf -> csrf.disable())
|
||||
|
||||
// CORS 설정 활성화
|
||||
// CORS 설정
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
|
||||
// 세션 관리 - Stateless (JWT 사용)
|
||||
// 세션 비활성화 (JWT 기반 Stateless)
|
||||
.sessionManagement(session ->
|
||||
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
|
||||
// 요청별 인증/인가 설정
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// 공개 엔드포인트 - 인증 불필요
|
||||
// 권한 설정
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
// Public endpoints (인증 불필요)
|
||||
.requestMatchers(
|
||||
// Health Check
|
||||
"/actuator/**",
|
||||
// Swagger UI
|
||||
"/swagger-ui/**",
|
||||
"/actuator/health",
|
||||
"/actuator/info",
|
||||
"/actuator/prometheus",
|
||||
"/v3/api-docs/**",
|
||||
"/api-docs/**",
|
||||
"/swagger-ui/**",
|
||||
"/swagger-ui.html",
|
||||
"/swagger-resources/**",
|
||||
"/webjars/**",
|
||||
// 정적 리소스
|
||||
"/favicon.ico",
|
||||
"/error"
|
||||
"/webjars/**"
|
||||
).permitAll()
|
||||
|
||||
// OPTIONS 요청은 모두 허용 (CORS Preflight)
|
||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||
|
||||
// Actuator endpoints (관리용)
|
||||
.requestMatchers("/actuator/**").hasRole("ADMIN")
|
||||
|
||||
// 요금 조회 API - 인증 필요
|
||||
.requestMatchers("/api/bills/**").authenticated()
|
||||
|
||||
// 나머지 모든 요청 - 인증 필요
|
||||
// 나머지 모든 요청 인증 필요
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
||||
// JWT 인증 필터 추가
|
||||
// TODO: JWT 필터 구현 후 활성화
|
||||
// .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
|
||||
// JWT 필터 추가
|
||||
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
|
||||
|
||||
// 예외 처리
|
||||
.exceptionHandling(exception -> exception
|
||||
// 인증 실패 시 처리
|
||||
// Exception 처리
|
||||
.exceptionHandling(exceptions -> exceptions
|
||||
.authenticationEntryPoint((request, response, authException) -> {
|
||||
log.warn("인증 실패 - URI: {}, 오류: {}",
|
||||
request.getRequestURI(), authException.getMessage());
|
||||
response.setStatus(401);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("""
|
||||
{
|
||||
"success": false,
|
||||
"message": "인증이 필요합니다",
|
||||
"timestamp": "%s"
|
||||
}
|
||||
""".formatted(java.time.LocalDateTime.now()));
|
||||
response.getWriter().write("{\"success\":false,\"error\":{\"code\":\"UNAUTHORIZED\",\"message\":\"인증이 필요합니다.\",\"details\":\"유효한 토큰이 필요합니다.\"}}");
|
||||
})
|
||||
|
||||
// 권한 부족 시 처리
|
||||
.accessDeniedHandler((request, response, accessDeniedException) -> {
|
||||
log.warn("접근 거부 - URI: {}, 오류: {}",
|
||||
request.getRequestURI(), accessDeniedException.getMessage());
|
||||
response.setStatus(403);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.getWriter().write("""
|
||||
{
|
||||
"success": false,
|
||||
"message": "접근 권한이 없습니다",
|
||||
"timestamp": "%s"
|
||||
}
|
||||
""".formatted(java.time.LocalDateTime.now()));
|
||||
response.getWriter().write("{\"success\":false,\"error\":{\"code\":\"ACCESS_DENIED\",\"message\":\"접근이 거부되었습니다.\",\"details\":\"권한이 부족합니다.\"}}");
|
||||
})
|
||||
);
|
||||
|
||||
@@ -130,99 +118,37 @@ public class SecurityConfig {
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS 설정
|
||||
*
|
||||
* @return CORS 설정 소스
|
||||
*/
|
||||
@Bean
|
||||
public JwtAuthenticationFilter jwtAuthenticationFilter() {
|
||||
return new JwtAuthenticationFilter(jwtTokenProvider);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
log.debug("CORS 설정 구성 시작");
|
||||
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
|
||||
// 허용할 Origin 설정 (개발환경)
|
||||
configuration.setAllowedOriginPatterns(Arrays.asList(
|
||||
"http://localhost:*",
|
||||
"https://localhost:*",
|
||||
"http://127.0.0.1:*",
|
||||
"https://127.0.0.1:*"
|
||||
// TODO: 운영환경 도메인 추가
|
||||
));
|
||||
|
||||
// 허용할 HTTP 메소드
|
||||
configuration.setAllowedMethods(Arrays.asList(
|
||||
"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"
|
||||
));
|
||||
|
||||
// 허용할 헤더
|
||||
configuration.setAllowedHeaders(Arrays.asList(
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"X-Requested-With",
|
||||
"Accept",
|
||||
"Origin",
|
||||
"Access-Control-Request-Method",
|
||||
"Access-Control-Request-Headers"
|
||||
));
|
||||
|
||||
// 자격 증명 허용 (쿠키, Authorization 헤더 등)
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
// Preflight 요청 캐시 시간 (초)
|
||||
configuration.setMaxAge(3600L);
|
||||
|
||||
// 환경변수에서 허용할 Origin 패턴 설정
|
||||
String[] origins = allowedOrigins.split(",");
|
||||
configuration.setAllowedOriginPatterns(Arrays.asList(origins));
|
||||
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(Arrays.asList("*"));
|
||||
configuration.setAllowCredentials(true);
|
||||
configuration.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
|
||||
log.debug("CORS 설정 구성 완료");
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 인코더 구성
|
||||
*
|
||||
* @return BCrypt 패스워드 인코더
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
log.debug("Password Encoder 구성 - BCrypt 사용");
|
||||
return new BCryptPasswordEncoder();
|
||||
return new BCryptPasswordEncoder(12); // 기본 설정에서 강도 12 사용
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증 매니저 구성
|
||||
*
|
||||
* @param config 인증 설정
|
||||
* @return 인증 매니저
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||
log.debug("Authentication Manager 구성");
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 인증 필터 구성
|
||||
*
|
||||
* TODO: JWT 토큰 검증 필터 구현
|
||||
*
|
||||
* @return JWT 인증 필터
|
||||
*/
|
||||
// @Bean
|
||||
// public JwtAuthenticationFilter jwtAuthenticationFilter() {
|
||||
// return new JwtAuthenticationFilter();
|
||||
// }
|
||||
|
||||
/**
|
||||
* JWT 토큰 제공자 구성
|
||||
*
|
||||
* TODO: JWT 토큰 생성/검증 서비스 구현
|
||||
*
|
||||
* @return JWT 토큰 제공자
|
||||
*/
|
||||
// @Bean
|
||||
// public JwtTokenProvider jwtTokenProvider() {
|
||||
// return new JwtTokenProvider();
|
||||
// }
|
||||
}
|
||||
@@ -39,9 +39,9 @@ public class SwaggerConfig {
|
||||
.addServerVariable("port", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||
._default("8082")
|
||||
.description("Server port"))))
|
||||
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
|
||||
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes("Bearer Authentication", createAPIKeyScheme()));
|
||||
.addSecuritySchemes("bearerAuth", createAPIKeyScheme()));
|
||||
}
|
||||
|
||||
private Info apiInfo() {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.phonebill.bill.controller;
|
||||
|
||||
import com.phonebill.bill.dto.*;
|
||||
import com.phonebill.kosmock.dto.KosCommonResponse;
|
||||
import com.phonebill.kosmock.dto.KosBillInquiryResponse;
|
||||
import com.phonebill.bill.service.BillInquiryService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -32,7 +34,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/bills")
|
||||
@RequestMapping("/api/v1/bills")
|
||||
@RequiredArgsConstructor
|
||||
@Validated
|
||||
@Tag(name = "Bill Inquiry", description = "요금조회 관련 API")
|
||||
@@ -115,63 +117,22 @@ public class BillController {
|
||||
description = "KOS 시스템 장애 (Circuit Breaker Open)"
|
||||
)
|
||||
})
|
||||
public ResponseEntity<ApiResponse<BillInquiryResponse>> inquireBill(
|
||||
public ResponseEntity<KosCommonResponse<KosBillInquiryResponse>> inquireBill(
|
||||
@Valid @RequestBody BillInquiryRequest request) {
|
||||
log.info("요금조회 요청 - 회선번호: {}, 조회월: {}",
|
||||
request.getLineNumber(), request.getInquiryMonth());
|
||||
|
||||
BillInquiryResponse response = billInquiryService.inquireBill(request);
|
||||
KosBillInquiryResponse response = billInquiryService.inquireBill(request);
|
||||
|
||||
if (response.getStatus() == BillInquiryResponse.ProcessStatus.COMPLETED) {
|
||||
log.info("요금조회 완료 - 요청ID: {}, 회선: {}",
|
||||
response.getRequestId(), request.getLineNumber());
|
||||
return ResponseEntity.ok(
|
||||
ApiResponse.success(response, "요금조회가 완료되었습니다")
|
||||
);
|
||||
} else {
|
||||
log.info("요금조회 비동기 처리 - 요청ID: {}, 상태: {}",
|
||||
response.getRequestId(), response.getStatus());
|
||||
return ResponseEntity.accepted().body(
|
||||
ApiResponse.success(response, "요금조회 요청이 접수되었습니다")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 요금조회 결과 확인
|
||||
*
|
||||
* 비동기로 처리된 요금조회 결과를 확인합니다.
|
||||
* requestId를 통해 조회 상태와 결과를 반환합니다.
|
||||
*/
|
||||
@GetMapping("/inquiry/{requestId}")
|
||||
@Operation(
|
||||
summary = "요금조회 결과 확인",
|
||||
description = "비동기로 처리된 요금조회의 상태와 결과를 확인합니다.",
|
||||
security = @SecurityRequirement(name = "bearerAuth")
|
||||
)
|
||||
@ApiResponses(value = {
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "요금조회 결과 조회 성공"
|
||||
),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(
|
||||
responseCode = "404",
|
||||
description = "요청 ID를 찾을 수 없음"
|
||||
)
|
||||
})
|
||||
public ResponseEntity<ApiResponse<BillInquiryResponse>> getBillInquiryResult(
|
||||
@Parameter(description = "요금조회 요청 ID", example = "REQ_20240308_001")
|
||||
@PathVariable String requestId) {
|
||||
log.info("요금조회 결과 확인 - 요청ID: {}", requestId);
|
||||
log.info("요금조회 완료 - 요청ID: {}, 회선: {}",
|
||||
response.getRequestId(), request.getLineNumber());
|
||||
|
||||
BillInquiryResponse response = billInquiryService.getBillInquiryResult(requestId);
|
||||
|
||||
log.info("요금조회 결과 반환 - 요청ID: {}, 상태: {}", requestId, response.getStatus());
|
||||
return ResponseEntity.ok(
|
||||
ApiResponse.success(response, "요금조회 결과를 조회했습니다")
|
||||
KosCommonResponse.success(response, "요금 조회가 완료되었습니다")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 요금조회 이력 조회
|
||||
*
|
||||
|
||||
@@ -13,10 +13,12 @@ import java.time.LocalDateTime;
|
||||
*
|
||||
* 모든 API 응답에 대한 공통 구조를 제공
|
||||
* - success: 성공/실패 여부
|
||||
* - resultCode: 결과 코드
|
||||
* - resultMessage: 결과 메시지
|
||||
* - data: 실제 응답 데이터 (성공시)
|
||||
* - error: 오류 정보 (실패시)
|
||||
* - message: 응답 메시지
|
||||
* - timestamp: 응답 시간
|
||||
* - traceId: 추적 ID
|
||||
*
|
||||
* @param <T> 응답 데이터 타입
|
||||
* @author 이개발(백엔더)
|
||||
@@ -35,6 +37,16 @@ public class ApiResponse<T> {
|
||||
*/
|
||||
private boolean success;
|
||||
|
||||
/**
|
||||
* 결과 코드
|
||||
*/
|
||||
private String resultCode;
|
||||
|
||||
/**
|
||||
* 결과 메시지
|
||||
*/
|
||||
private String resultMessage;
|
||||
|
||||
/**
|
||||
* 응답 데이터 (성공시에만 포함)
|
||||
*/
|
||||
@@ -45,17 +57,17 @@ public class ApiResponse<T> {
|
||||
*/
|
||||
private ErrorDetail error;
|
||||
|
||||
/**
|
||||
* 응답 메시지
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 응답 시간
|
||||
*/
|
||||
@Builder.Default
|
||||
private LocalDateTime timestamp = LocalDateTime.now();
|
||||
|
||||
/**
|
||||
* 추적 ID
|
||||
*/
|
||||
private String traceId;
|
||||
|
||||
/**
|
||||
* 성공 응답 생성
|
||||
*
|
||||
@@ -67,8 +79,9 @@ public class ApiResponse<T> {
|
||||
public static <T> ApiResponse<T> success(T data, String message) {
|
||||
return ApiResponse.<T>builder()
|
||||
.success(true)
|
||||
.resultCode("0000")
|
||||
.resultMessage(message)
|
||||
.data(data)
|
||||
.message(message)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -93,8 +106,9 @@ public class ApiResponse<T> {
|
||||
public static ApiResponse<Void> failure(ErrorDetail error, String message) {
|
||||
return ApiResponse.<Void>builder()
|
||||
.success(false)
|
||||
.resultCode(error.getCode())
|
||||
.resultMessage(message)
|
||||
.error(error)
|
||||
.message(message)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -110,7 +124,12 @@ public class ApiResponse<T> {
|
||||
.code(code)
|
||||
.message(message)
|
||||
.build();
|
||||
return failure(error, message);
|
||||
return ApiResponse.<Void>builder()
|
||||
.success(false)
|
||||
.resultCode(code)
|
||||
.resultMessage(message)
|
||||
.error(error)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.phonebill.bill.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -28,6 +29,7 @@ public class BillInquiryRequest {
|
||||
* 조회할 회선번호 (필수)
|
||||
* 010-XXXX-XXXX 형식만 허용
|
||||
*/
|
||||
@JsonProperty("lineNumber")
|
||||
@NotBlank(message = "회선번호는 필수입니다")
|
||||
@Pattern(
|
||||
regexp = "^010-\\d{4}-\\d{4}$",
|
||||
@@ -37,11 +39,12 @@ public class BillInquiryRequest {
|
||||
|
||||
/**
|
||||
* 조회월 (선택)
|
||||
* YYYY-MM 형식, 미입력시 당월 조회
|
||||
* YYYYMM 형식, 미입력시 당월 조회
|
||||
*/
|
||||
@JsonProperty("inquiryMonth")
|
||||
@Pattern(
|
||||
regexp = "^\\d{4}-\\d{2}$",
|
||||
message = "조회월은 YYYY-MM 형식이어야 합니다"
|
||||
regexp = "^\\d{6}$",
|
||||
message = "조회월은 YYYYMM 형식이어야 합니다"
|
||||
)
|
||||
private String inquiryMonth;
|
||||
}
|
||||
@@ -108,7 +108,7 @@ public class GlobalExceptionHandler {
|
||||
.body(ApiResponse.<Map<String, String>>builder()
|
||||
.success(false)
|
||||
.data(errors)
|
||||
.message("입력값이 올바르지 않습니다")
|
||||
.resultMessage("입력값이 올바르지 않습니다")
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build());
|
||||
}
|
||||
@@ -129,7 +129,7 @@ public class GlobalExceptionHandler {
|
||||
.body(ApiResponse.<Map<String, String>>builder()
|
||||
.success(false)
|
||||
.data(errors)
|
||||
.message("입력값이 올바르지 않습니다")
|
||||
.resultMessage("입력값이 올바르지 않습니다")
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build());
|
||||
}
|
||||
@@ -152,7 +152,7 @@ public class GlobalExceptionHandler {
|
||||
.body(ApiResponse.<Map<String, String>>builder()
|
||||
.success(false)
|
||||
.data(errors)
|
||||
.message("입력값이 올바르지 않습니다")
|
||||
.resultMessage("입력값이 올바르지 않습니다")
|
||||
.timestamp(LocalDateTime.now())
|
||||
.build());
|
||||
}
|
||||
|
||||
@@ -27,17 +27,23 @@ import java.time.LocalDateTime;
|
||||
public class KosRequest {
|
||||
|
||||
/**
|
||||
* 회선번호 (KOS 필드명: lineNum)
|
||||
* 회선번호 (KOS 필드명: lineNumber)
|
||||
*/
|
||||
@JsonProperty("lineNum")
|
||||
@JsonProperty("lineNumber")
|
||||
private String lineNumber;
|
||||
|
||||
/**
|
||||
* 조회월 (KOS 필드명: searchMonth, YYYY-MM 형식)
|
||||
* 조회월 (KOS 필드명: billingMonth, YYYYMM 형식)
|
||||
*/
|
||||
@JsonProperty("searchMonth")
|
||||
@JsonProperty("billingMonth")
|
||||
private String inquiryMonth;
|
||||
|
||||
/**
|
||||
* 요청 ID (KOS 필드명: requestId)
|
||||
*/
|
||||
@JsonProperty("requestId")
|
||||
private String requestId;
|
||||
|
||||
/**
|
||||
* 서비스 구분 코드 (KOS 필드명: svcDiv)
|
||||
* - BILL_INQ: 요금조회
|
||||
@@ -122,9 +128,14 @@ public class KosRequest {
|
||||
* @return KOS 요청 객체
|
||||
*/
|
||||
public static KosRequest createBillInquiryRequest(String lineNumber, String inquiryMonth, String requestUserId) {
|
||||
// YYYY-MM 형식을 YYYYMM 형식으로 변환
|
||||
String billingMonth = inquiryMonth.replace("-", "");
|
||||
String requestId = "REQ_" + java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
|
||||
|
||||
return KosRequest.builder()
|
||||
.lineNumber(lineNumber)
|
||||
.inquiryMonth(inquiryMonth)
|
||||
.lineNumber(lineNumber.replace("-", "")) // 하이픈 제거
|
||||
.inquiryMonth(billingMonth)
|
||||
.requestId(requestId)
|
||||
.requestUserId(requestUserId)
|
||||
.requestTime(LocalDateTime.now())
|
||||
.requestSequenceNumber(generateSequenceNumber())
|
||||
|
||||
+85
-5
@@ -95,12 +95,92 @@ public interface BillInquiryHistoryRepository extends JpaRepository<BillInquiryH
|
||||
*/
|
||||
@Query("SELECT h FROM BillInquiryHistoryEntity h WHERE " +
|
||||
"h.lineNumber IN :lineNumbers " +
|
||||
"AND (:lineNumber IS NULL OR h.lineNumber = :lineNumber) " +
|
||||
"AND (:startTime IS NULL OR h.requestTime >= :startTime) " +
|
||||
"AND (:endTime IS NULL OR h.requestTime <= :endTime) " +
|
||||
"AND (:status IS NULL OR h.status = :status) " +
|
||||
"ORDER BY h.requestTime DESC")
|
||||
Page<BillInquiryHistoryEntity> findBillHistoryWithFilters(
|
||||
Page<BillInquiryHistoryEntity> findBillHistoryByLineNumbers(
|
||||
@Param("lineNumbers") List<String> lineNumbers,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
@Query("SELECT h FROM BillInquiryHistoryEntity h WHERE " +
|
||||
"h.lineNumber IN :lineNumbers " +
|
||||
"AND h.lineNumber = :lineNumber " +
|
||||
"ORDER BY h.requestTime DESC")
|
||||
Page<BillInquiryHistoryEntity> findBillHistoryByLineNumbersAndLineNumber(
|
||||
@Param("lineNumbers") List<String> lineNumbers,
|
||||
@Param("lineNumber") String lineNumber,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
@Query("SELECT h FROM BillInquiryHistoryEntity h WHERE " +
|
||||
"h.lineNumber IN :lineNumbers " +
|
||||
"AND h.requestTime >= :startTime " +
|
||||
"AND h.requestTime <= :endTime " +
|
||||
"ORDER BY h.requestTime DESC")
|
||||
Page<BillInquiryHistoryEntity> findBillHistoryByLineNumbersAndDateRange(
|
||||
@Param("lineNumbers") List<String> lineNumbers,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
@Query("SELECT h FROM BillInquiryHistoryEntity h WHERE " +
|
||||
"h.lineNumber IN :lineNumbers " +
|
||||
"AND h.status = :status " +
|
||||
"ORDER BY h.requestTime DESC")
|
||||
Page<BillInquiryHistoryEntity> findBillHistoryByLineNumbersAndStatus(
|
||||
@Param("lineNumbers") List<String> lineNumbers,
|
||||
@Param("status") String status,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
@Query("SELECT h FROM BillInquiryHistoryEntity h WHERE " +
|
||||
"h.lineNumber IN :lineNumbers " +
|
||||
"AND h.lineNumber = :lineNumber " +
|
||||
"AND h.requestTime >= :startTime " +
|
||||
"AND h.requestTime <= :endTime " +
|
||||
"ORDER BY h.requestTime DESC")
|
||||
Page<BillInquiryHistoryEntity> findBillHistoryByLineNumbersAndLineNumberAndDateRange(
|
||||
@Param("lineNumbers") List<String> lineNumbers,
|
||||
@Param("lineNumber") String lineNumber,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
@Query("SELECT h FROM BillInquiryHistoryEntity h WHERE " +
|
||||
"h.lineNumber IN :lineNumbers " +
|
||||
"AND h.lineNumber = :lineNumber " +
|
||||
"AND h.status = :status " +
|
||||
"ORDER BY h.requestTime DESC")
|
||||
Page<BillInquiryHistoryEntity> findBillHistoryByLineNumbersAndLineNumberAndStatus(
|
||||
@Param("lineNumbers") List<String> lineNumbers,
|
||||
@Param("lineNumber") String lineNumber,
|
||||
@Param("status") String status,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
@Query("SELECT h FROM BillInquiryHistoryEntity h WHERE " +
|
||||
"h.lineNumber IN :lineNumbers " +
|
||||
"AND h.requestTime >= :startTime " +
|
||||
"AND h.requestTime <= :endTime " +
|
||||
"AND h.status = :status " +
|
||||
"ORDER BY h.requestTime DESC")
|
||||
Page<BillInquiryHistoryEntity> findBillHistoryByLineNumbersAndDateRangeAndStatus(
|
||||
@Param("lineNumbers") List<String> lineNumbers,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime,
|
||||
@Param("status") String status,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
@Query("SELECT h FROM BillInquiryHistoryEntity h WHERE " +
|
||||
"h.lineNumber IN :lineNumbers " +
|
||||
"AND h.lineNumber = :lineNumber " +
|
||||
"AND h.requestTime >= :startTime " +
|
||||
"AND h.requestTime <= :endTime " +
|
||||
"AND h.status = :status " +
|
||||
"ORDER BY h.requestTime DESC")
|
||||
Page<BillInquiryHistoryEntity> findBillHistoryWithAllFilters(
|
||||
@Param("lineNumbers") List<String> lineNumbers,
|
||||
@Param("lineNumber") String lineNumber,
|
||||
@Param("startTime") LocalDateTime startTime,
|
||||
|
||||
@@ -55,7 +55,7 @@ public class BillCacheService {
|
||||
* @param inquiryMonth 조회월
|
||||
* @return 캐시된 요금 데이터 (없으면 null)
|
||||
*/
|
||||
@Cacheable(value = "billData", key = "#lineNumber + ':' + #inquiryMonth")
|
||||
@Cacheable(value = "billData", key = "#lineNumber + ':' + #inquiryMonth", unless = "#result == null")
|
||||
public BillInquiryResponse getCachedBillData(String lineNumber, String inquiryMonth) {
|
||||
log.debug("요금 데이터 캐시 조회 - 회선: {}, 조회월: {}", lineNumber, inquiryMonth);
|
||||
|
||||
@@ -90,6 +90,12 @@ public class BillCacheService {
|
||||
public void cacheBillData(String lineNumber, String inquiryMonth, BillInquiryResponse billData) {
|
||||
log.debug("요금 데이터 캐시 저장 - 회선: {}, 조회월: {}", lineNumber, inquiryMonth);
|
||||
|
||||
// null 값은 캐시하지 않음
|
||||
if (billData == null) {
|
||||
log.debug("요금 데이터가 null이므로 캐시 저장을 건너뜀 - 회선: {}, 조회월: {}", lineNumber, inquiryMonth);
|
||||
return;
|
||||
}
|
||||
|
||||
String cacheKey = BILL_DATA_PREFIX + lineNumber + ":" + inquiryMonth;
|
||||
|
||||
try {
|
||||
|
||||
@@ -194,11 +194,9 @@ public class BillHistoryService {
|
||||
endDateTime = LocalDate.parse(endDate).atTime(23, 59, 59);
|
||||
}
|
||||
|
||||
String statusFilter = status != null ? status.name() : null;
|
||||
|
||||
// 이력 조회
|
||||
Page<BillInquiryHistoryEntity> historyPage = historyRepository.findBillHistoryWithFilters(
|
||||
userLineNumbers, lineNumber, startDateTime, endDateTime, statusFilter, pageable
|
||||
// 조건에 따라 적절한 쿼리 선택
|
||||
Page<BillInquiryHistoryEntity> historyPage = getBillHistoryByConditions(
|
||||
userLineNumbers, lineNumber, startDateTime, endDateTime, status, pageable
|
||||
);
|
||||
|
||||
// 응답 데이터 변환
|
||||
@@ -233,6 +231,64 @@ public class BillHistoryService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건에 따라 적절한 쿼리를 선택하여 이력 조회
|
||||
*/
|
||||
private Page<BillInquiryHistoryEntity> getBillHistoryByConditions(
|
||||
List<String> userLineNumbers, String lineNumber,
|
||||
LocalDateTime startDateTime, LocalDateTime endDateTime,
|
||||
BillInquiryResponse.ProcessStatus status, Pageable pageable) {
|
||||
|
||||
boolean hasLineNumber = lineNumber != null && !lineNumber.trim().isEmpty();
|
||||
boolean hasDateRange = startDateTime != null && endDateTime != null;
|
||||
boolean hasStatus = status != null;
|
||||
|
||||
String statusFilter = hasStatus ? status.name() : null;
|
||||
|
||||
// 8가지 경우의 수에 따라 적절한 쿼리 선택
|
||||
if (hasLineNumber && hasDateRange && hasStatus) {
|
||||
// 모든 필터 적용
|
||||
return historyRepository.findBillHistoryWithAllFilters(
|
||||
userLineNumbers, lineNumber, startDateTime, endDateTime, statusFilter, pageable
|
||||
);
|
||||
} else if (hasLineNumber && hasDateRange) {
|
||||
// 회선번호 + 날짜 범위
|
||||
return historyRepository.findBillHistoryByLineNumbersAndLineNumberAndDateRange(
|
||||
userLineNumbers, lineNumber, startDateTime, endDateTime, pageable
|
||||
);
|
||||
} else if (hasLineNumber && hasStatus) {
|
||||
// 회선번호 + 상태
|
||||
return historyRepository.findBillHistoryByLineNumbersAndLineNumberAndStatus(
|
||||
userLineNumbers, lineNumber, statusFilter, pageable
|
||||
);
|
||||
} else if (hasDateRange && hasStatus) {
|
||||
// 날짜 범위 + 상태
|
||||
return historyRepository.findBillHistoryByLineNumbersAndDateRangeAndStatus(
|
||||
userLineNumbers, startDateTime, endDateTime, statusFilter, pageable
|
||||
);
|
||||
} else if (hasLineNumber) {
|
||||
// 회선번호만
|
||||
return historyRepository.findBillHistoryByLineNumbersAndLineNumber(
|
||||
userLineNumbers, lineNumber, pageable
|
||||
);
|
||||
} else if (hasDateRange) {
|
||||
// 날짜 범위만
|
||||
return historyRepository.findBillHistoryByLineNumbersAndDateRange(
|
||||
userLineNumbers, startDateTime, endDateTime, pageable
|
||||
);
|
||||
} else if (hasStatus) {
|
||||
// 상태만
|
||||
return historyRepository.findBillHistoryByLineNumbersAndStatus(
|
||||
userLineNumbers, statusFilter, pageable
|
||||
);
|
||||
} else {
|
||||
// 필터 없음 (기본)
|
||||
return historyRepository.findBillHistoryByLineNumbers(
|
||||
userLineNumbers, pageable
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 엔티티를 이력 아이템으로 변환
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.phonebill.bill.service;
|
||||
|
||||
import com.phonebill.bill.dto.*;
|
||||
import com.phonebill.kosmock.dto.KosBillInquiryResponse;
|
||||
|
||||
/**
|
||||
* 요금조회 서비스 인터페이스
|
||||
@@ -41,20 +42,8 @@ public interface BillInquiryService {
|
||||
* @param request 요금조회 요청 데이터
|
||||
* @return 요금조회 응답 데이터
|
||||
*/
|
||||
BillInquiryResponse inquireBill(BillInquiryRequest request);
|
||||
KosBillInquiryResponse inquireBill(BillInquiryRequest request);
|
||||
|
||||
/**
|
||||
* 요금조회 결과 확인
|
||||
*
|
||||
* 비동기로 처리된 요금조회의 상태와 결과를 반환
|
||||
* - PROCESSING: 처리 중 상태
|
||||
* - COMPLETED: 처리 완료 (요금 정보 포함)
|
||||
* - FAILED: 처리 실패 (오류 메시지 포함)
|
||||
*
|
||||
* @param requestId 요금조회 요청 ID
|
||||
* @return 요금조회 응답 데이터
|
||||
*/
|
||||
BillInquiryResponse getBillInquiryResult(String requestId);
|
||||
|
||||
/**
|
||||
* 요금조회 이력 조회
|
||||
|
||||
+96
-121
@@ -2,8 +2,12 @@ package com.phonebill.bill.service;
|
||||
|
||||
import com.phonebill.bill.dto.*;
|
||||
import com.phonebill.bill.exception.BillInquiryException;
|
||||
import com.phonebill.common.security.UserPrincipal;
|
||||
import com.phonebill.kosmock.dto.KosBillInquiryResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -47,12 +51,11 @@ public class BillInquiryServiceImpl implements BillInquiryService {
|
||||
log.info("요금조회 메뉴 조회 시작");
|
||||
|
||||
// 현재 인증된 사용자의 고객 정보 조회 (JWT에서 추출)
|
||||
// TODO: SecurityContext에서 사용자 정보 추출 로직 구현
|
||||
String customerId = getCurrentCustomerId();
|
||||
String lineNumber = getCurrentLineNumber();
|
||||
|
||||
// 조회 가능한 월 목록 생성 (최근 12개월)
|
||||
List<String> availableMonths = generateAvailableMonths();
|
||||
// 실제 요금 데이터가 있는 월 목록 조회
|
||||
List<String> availableMonths = getAvailableMonthsWithData(lineNumber);
|
||||
|
||||
// 현재 월
|
||||
String currentMonth = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
|
||||
@@ -77,7 +80,7 @@ public class BillInquiryServiceImpl implements BillInquiryService {
|
||||
*/
|
||||
@Override
|
||||
@Transactional
|
||||
public BillInquiryResponse inquireBill(BillInquiryRequest request) {
|
||||
public KosBillInquiryResponse inquireBill(BillInquiryRequest request) {
|
||||
log.info("요금조회 요청 처리 시작 - 회선: {}, 조회월: {}",
|
||||
request.getLineNumber(), request.getInquiryMonth());
|
||||
|
||||
@@ -87,125 +90,33 @@ public class BillInquiryServiceImpl implements BillInquiryService {
|
||||
// 조회월 기본값 설정 (미입력시 당월)
|
||||
String inquiryMonth = request.getInquiryMonth();
|
||||
if (inquiryMonth == null || inquiryMonth.trim().isEmpty()) {
|
||||
inquiryMonth = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));
|
||||
inquiryMonth = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
|
||||
}
|
||||
|
||||
try {
|
||||
// 1단계: 캐시에서 데이터 확인 (Cache-Aside 패턴)
|
||||
BillInquiryResponse cachedResponse = billCacheService.getCachedBillData(
|
||||
// KOS Mock 서비스 직접 호출
|
||||
KosBillInquiryResponse response = kosClientService.inquireBillFromKosDirect(
|
||||
request.getLineNumber(), inquiryMonth
|
||||
);
|
||||
|
||||
if (cachedResponse != null) {
|
||||
log.info("캐시에서 요금 데이터 조회 완료 - 요청ID: {}", requestId);
|
||||
cachedResponse = BillInquiryResponse.builder()
|
||||
.requestId(requestId)
|
||||
.status(BillInquiryResponse.ProcessStatus.COMPLETED)
|
||||
.billInfo(cachedResponse.getBillInfo())
|
||||
.build();
|
||||
|
||||
// 이력 저장 (비동기)
|
||||
billHistoryService.saveInquiryHistoryAsync(requestId, request, cachedResponse);
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// 2단계: KOS 시스템 연동 (Circuit Breaker 적용)
|
||||
CompletableFuture<BillInquiryResponse> kosResponseFuture = kosClientService.inquireBillFromKos(
|
||||
request.getLineNumber(), inquiryMonth
|
||||
);
|
||||
BillInquiryResponse kosResponse;
|
||||
try {
|
||||
kosResponse = kosResponseFuture.get();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new BillInquiryException("요금조회 처리가 중단되었습니다", e);
|
||||
} catch (Exception e) {
|
||||
throw new BillInquiryException("요금조회 처리 중 오류가 발생했습니다", e);
|
||||
}
|
||||
|
||||
if (kosResponse != null && kosResponse.getStatus() == BillInquiryResponse.ProcessStatus.COMPLETED) {
|
||||
// 3단계: 캐시에 저장 (1시간 TTL)
|
||||
billCacheService.cacheBillData(request.getLineNumber(), inquiryMonth, kosResponse);
|
||||
|
||||
// 응답 데이터 구성
|
||||
BillInquiryResponse response = BillInquiryResponse.builder()
|
||||
.requestId(requestId)
|
||||
.status(BillInquiryResponse.ProcessStatus.COMPLETED)
|
||||
.billInfo(kosResponse.getBillInfo())
|
||||
.build();
|
||||
|
||||
// 이력 저장 (비동기)
|
||||
billHistoryService.saveInquiryHistoryAsync(requestId, request, response);
|
||||
|
||||
log.info("KOS 연동을 통한 요금조회 완료 - 요청ID: {}", requestId);
|
||||
return response;
|
||||
} else {
|
||||
// KOS에서 비동기 처리 중인 경우
|
||||
BillInquiryResponse response = BillInquiryResponse.builder()
|
||||
.requestId(requestId)
|
||||
.status(BillInquiryResponse.ProcessStatus.PROCESSING)
|
||||
.build();
|
||||
|
||||
// 이력 저장 (처리 중 상태)
|
||||
billHistoryService.saveInquiryHistoryAsync(requestId, request, response);
|
||||
|
||||
log.info("KOS 연동 비동기 처리 - 요청ID: {}", requestId);
|
||||
return response;
|
||||
}
|
||||
log.info("KOS Mock 요금조회 완료 - 요청ID: {}, 상태: {}",
|
||||
response.getRequestId(), response.getProcStatus());
|
||||
return response;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("요금조회 처리 중 오류 발생 - 요청ID: {}, 오류: {}", requestId, e.getMessage(), e);
|
||||
log.error("KOS Mock 요금조회 실패 - 회선: {}, 오류: {}",
|
||||
request.getLineNumber(), e.getMessage(), e);
|
||||
|
||||
// 실패 응답 생성
|
||||
BillInquiryResponse errorResponse = BillInquiryResponse.builder()
|
||||
// 실패 시 기본 응답 반환
|
||||
return KosBillInquiryResponse.builder()
|
||||
.requestId(requestId)
|
||||
.status(BillInquiryResponse.ProcessStatus.FAILED)
|
||||
.procStatus("FAILED")
|
||||
.resultCode("9999")
|
||||
.resultMessage("요금 조회 중 오류가 발생했습니다")
|
||||
.build();
|
||||
|
||||
// 이력 저장 (실패 상태)
|
||||
billHistoryService.saveInquiryHistoryAsync(requestId, request, errorResponse);
|
||||
|
||||
// 비즈니스 예외는 그대로 던지고, 시스템 예외는 래핑
|
||||
if (e instanceof BillInquiryException) {
|
||||
throw e;
|
||||
} else {
|
||||
throw new BillInquiryException("요금조회 처리 중 시스템 오류가 발생했습니다", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 요금조회 결과 확인
|
||||
*/
|
||||
@Override
|
||||
public BillInquiryResponse getBillInquiryResult(String requestId) {
|
||||
log.info("요금조회 결과 확인 - 요청ID: {}", requestId);
|
||||
|
||||
// 이력에서 요청 정보 조회
|
||||
BillInquiryResponse response = billHistoryService.getBillInquiryResult(requestId);
|
||||
|
||||
if (response == null) {
|
||||
throw BillInquiryException.billDataNotFound(requestId, "요청 ID");
|
||||
}
|
||||
|
||||
// 처리 중인 경우 KOS에서 최신 상태 확인
|
||||
if (response.getStatus() == BillInquiryResponse.ProcessStatus.PROCESSING) {
|
||||
try {
|
||||
BillInquiryResponse latestResponse = kosClientService.checkInquiryStatus(requestId);
|
||||
if (latestResponse != null) {
|
||||
// 상태 업데이트
|
||||
billHistoryService.updateInquiryStatus(requestId, latestResponse);
|
||||
response = latestResponse;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("KOS 상태 확인 중 오류 발생 - 요청ID: {}, 오류: {}", requestId, e.getMessage());
|
||||
// 상태 확인 실패해도 기존 상태 그대로 반환
|
||||
}
|
||||
}
|
||||
|
||||
log.info("요금조회 결과 반환 - 요청ID: {}, 상태: {}", requestId, response.getStatus());
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 요금조회 이력 조회
|
||||
@@ -244,8 +155,21 @@ public class BillInquiryServiceImpl implements BillInquiryService {
|
||||
* 현재 인증된 사용자의 고객 ID 조회
|
||||
*/
|
||||
private String getCurrentCustomerId() {
|
||||
// TODO: SecurityContext에서 JWT 토큰을 파싱하여 고객 ID 추출
|
||||
// 현재는 더미 데이터 반환
|
||||
// JWT에서 인증된 사용자의 고객 ID 추출
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (authentication != null && authentication.getPrincipal() instanceof UserPrincipal) {
|
||||
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
|
||||
String customerId = userPrincipal.getCustomerId();
|
||||
|
||||
if (customerId != null && !customerId.trim().isEmpty()) {
|
||||
log.debug("사용자 {}의 고객 ID: {}", userPrincipal.getUserId(), customerId);
|
||||
return customerId;
|
||||
}
|
||||
}
|
||||
|
||||
// 인증 정보가 없거나 고객 ID가 없는 경우 기본값 반환
|
||||
log.warn("사용자의 고객 ID 정보를 찾을 수 없습니다. 기본값 사용");
|
||||
return "CUST001";
|
||||
}
|
||||
|
||||
@@ -253,8 +177,21 @@ public class BillInquiryServiceImpl implements BillInquiryService {
|
||||
* 현재 인증된 사용자의 회선번호 조회
|
||||
*/
|
||||
private String getCurrentLineNumber() {
|
||||
// TODO: SecurityContext에서 JWT 토큰을 파싱하여 회선번호 추출
|
||||
// 현재는 더미 데이터 반환
|
||||
// JWT에서 인증된 사용자의 회선번호 추출
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (authentication != null && authentication.getPrincipal() instanceof UserPrincipal) {
|
||||
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
|
||||
String lineNumber = userPrincipal.getLineNumber();
|
||||
|
||||
if (lineNumber != null && !lineNumber.trim().isEmpty()) {
|
||||
log.debug("사용자 {}의 회선번호: {}", userPrincipal.getUserId(), lineNumber);
|
||||
return lineNumber;
|
||||
}
|
||||
}
|
||||
|
||||
// 인증 정보가 없거나 회선번호가 없는 경우 기본값 반환
|
||||
log.warn("사용자의 회선번호 정보를 찾을 수 없습니다. 기본값 사용");
|
||||
return "010-1234-5678";
|
||||
}
|
||||
|
||||
@@ -262,22 +199,60 @@ public class BillInquiryServiceImpl implements BillInquiryService {
|
||||
* 현재 사용자의 모든 회선번호 목록 조회
|
||||
*/
|
||||
private List<String> getCurrentUserLineNumbers() {
|
||||
// TODO: 사용자 권한에 따른 회선번호 목록 조회
|
||||
// 현재는 더미 데이터 반환
|
||||
List<String> lineNumbers = new ArrayList<>();
|
||||
lineNumbers.add("010-1234-5678");
|
||||
return lineNumbers;
|
||||
// JWT에서 인증된 사용자의 회선번호 추출
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (authentication != null && authentication.getPrincipal() instanceof UserPrincipal) {
|
||||
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
|
||||
String lineNumber = userPrincipal.getLineNumber();
|
||||
|
||||
if (lineNumber != null) {
|
||||
List<String> lineNumbers = new ArrayList<>();
|
||||
lineNumbers.add(lineNumber);
|
||||
log.debug("사용자 {}의 회선번호: {}", userPrincipal.getUserId(), lineNumber);
|
||||
return lineNumbers;
|
||||
}
|
||||
}
|
||||
|
||||
// 인증 정보가 없거나 회선번호가 없는 경우 빈 목록 반환
|
||||
log.warn("사용자의 회선번호 정보를 찾을 수 없습니다");
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 조회 가능한 월 목록 생성 (최근 12개월)
|
||||
* 실제 요금 데이터가 있는 월 목록 조회
|
||||
*/
|
||||
private List<String> generateAvailableMonths() {
|
||||
private List<String> getAvailableMonthsWithData(String lineNumber) {
|
||||
try {
|
||||
log.info("회선 {}의 실제 요금 데이터가 있는 월 목록 조회", lineNumber);
|
||||
|
||||
// KOS Mock 서비스를 통해 실제 데이터가 있는 월 목록 조회
|
||||
List<String> availableMonths = kosClientService.getAvailableMonths(lineNumber);
|
||||
|
||||
if (availableMonths == null || availableMonths.isEmpty()) {
|
||||
log.warn("KOS에서 조회 가능한 월 정보가 없음. 기본 최근 3개월 반환");
|
||||
return generateDefaultMonths(3);
|
||||
}
|
||||
|
||||
log.info("KOS에서 조회된 데이터 보유 월: {}", availableMonths);
|
||||
return availableMonths;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("KOS 시스템에서 조회 가능한 월 정보 조회 실패: {}", e.getMessage(), e);
|
||||
// 실패 시 기본 최근 3개월 반환
|
||||
return generateDefaultMonths(3);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 월 목록 생성 (fallback용)
|
||||
*/
|
||||
private List<String> generateDefaultMonths(int monthCount) {
|
||||
List<String> months = new ArrayList<>();
|
||||
LocalDate currentDate = LocalDate.now();
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
|
||||
|
||||
for (int i = 0; i < 12; i++) {
|
||||
for (int i = 0; i < monthCount; i++) {
|
||||
LocalDate monthDate = currentDate.minusMonths(i);
|
||||
months.add(monthDate.format(formatter));
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ import com.phonebill.bill.exception.CircuitBreakerException;
|
||||
import com.phonebill.bill.exception.KosConnectionException;
|
||||
import com.phonebill.bill.external.KosRequest;
|
||||
import com.phonebill.bill.external.KosResponse;
|
||||
import com.phonebill.kosmock.dto.KosCommonResponse;
|
||||
import com.phonebill.kosmock.dto.KosBillInquiryResponse;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.Map;
|
||||
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
|
||||
import io.github.resilience4j.retry.annotation.Retry;
|
||||
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
|
||||
@@ -48,7 +52,146 @@ public class KosClientService {
|
||||
private final KosProperties kosProperties;
|
||||
|
||||
/**
|
||||
* KOS 시스템에서 요금 정보 조회
|
||||
* KOS Mock 시스템에서 요금 정보 조회 (KosBillInquiryResponse 직접 반환)
|
||||
*
|
||||
* @param lineNumber 회선번호
|
||||
* @param inquiryMonth 조회월
|
||||
* @return KOS 원본 응답 데이터
|
||||
*/
|
||||
@CircuitBreaker(name = "kos-bill-inquiry-direct", fallbackMethod = "inquireBillDirectFallback")
|
||||
@Retry(name = "kos-bill-inquiry-direct")
|
||||
public KosBillInquiryResponse inquireBillFromKosDirect(String lineNumber, String inquiryMonth) {
|
||||
log.info("KOS Mock 직접 호출 - 회선: {}, 조회월: {}", lineNumber, inquiryMonth);
|
||||
|
||||
try {
|
||||
// 회선번호 형식 변환 (010-1234-5678 → 01012345678)
|
||||
String formattedLineNumber = lineNumber.replaceAll("-", "");
|
||||
|
||||
// KOS Mock 요청 데이터 구성 (KosBillInquiryRequest 형식)
|
||||
Map<String, Object> kosRequest = Map.of(
|
||||
"lineNumber", formattedLineNumber,
|
||||
"billingMonth", inquiryMonth,
|
||||
"requestId", generateRequestId()
|
||||
);
|
||||
|
||||
// HTTP 헤더 설정
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Content-Type", "application/json");
|
||||
headers.set("X-Service-Name", "MVNO-BILL-INQUIRY");
|
||||
headers.set("X-Request-ID", java.util.UUID.randomUUID().toString());
|
||||
|
||||
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(kosRequest, headers);
|
||||
|
||||
// KOS Mock API 호출
|
||||
String kosUrl = kosProperties.getBaseUrl() + "/api/v1/kos/bill/inquiry";
|
||||
ResponseEntity<Map> responseEntity = restTemplate.exchange(
|
||||
kosUrl, HttpMethod.POST, requestEntity, Map.class
|
||||
);
|
||||
|
||||
Map<String, Object> response = responseEntity.getBody();
|
||||
|
||||
if (response == null) {
|
||||
throw KosConnectionException.apiError("KOS-BILL-INQUIRY",
|
||||
String.valueOf(responseEntity.getStatusCodeValue()), "응답 데이터가 없습니다");
|
||||
}
|
||||
|
||||
// KosCommonResponse의 data 부분에서 KosBillInquiryResponse 추출
|
||||
Map<String, Object> data = (Map<String, Object>) response.get("data");
|
||||
if (data == null) {
|
||||
throw KosConnectionException.apiError("KOS-BILL-INQUIRY",
|
||||
"NO_DATA", "응답에서 data를 찾을 수 없습니다");
|
||||
}
|
||||
|
||||
// KosBillInquiryResponse 객체 구성
|
||||
KosBillInquiryResponse result = convertMapToKosBillInquiryResponse(data);
|
||||
|
||||
log.info("KOS Mock 직접 호출 성공 - 요청ID: {}", result.getRequestId());
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("KOS Mock 직접 호출 오류 - 회선: {}, 오류: {}", lineNumber, e.getMessage(), e);
|
||||
throw new KosConnectionException("KOS-BILL-INQUIRY-DIRECT",
|
||||
"KOS Mock 시스템 연동 중 오류가 발생했습니다", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KOS 시스템에서 요금 정보 조회 (동기 처리)
|
||||
*
|
||||
* Circuit Breaker, Retry 패턴 적용
|
||||
*
|
||||
* @param lineNumber 회선번호
|
||||
* @param inquiryMonth 조회월
|
||||
* @return 요금조회 응답
|
||||
*/
|
||||
@CircuitBreaker(name = "kos-bill-inquiry", fallbackMethod = "inquireBillSyncFallback")
|
||||
@Retry(name = "kos-bill-inquiry")
|
||||
public BillInquiryResponse inquireBillFromKosSync(String lineNumber, String inquiryMonth) {
|
||||
log.info("KOS 요금조회 요청 (동기) - 회선: {}, 조회월: {}", lineNumber, inquiryMonth);
|
||||
|
||||
try {
|
||||
// KOS 요청 데이터 구성
|
||||
KosRequest kosRequest = KosRequest.createBillInquiryRequest(lineNumber, inquiryMonth, "system");
|
||||
|
||||
log.info("KOS Mock으로 전송하는 요청: lineNumber={}, inquiryMonth={}, requestId={}",
|
||||
kosRequest.getLineNumber(), kosRequest.getInquiryMonth(), kosRequest.getRequestId());
|
||||
|
||||
// HTTP 헤더 설정
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Content-Type", "application/json");
|
||||
headers.set("X-Service-Name", "MVNO-BILL-INQUIRY");
|
||||
headers.set("X-Request-ID", java.util.UUID.randomUUID().toString());
|
||||
|
||||
HttpEntity<KosRequest> requestEntity = new HttpEntity<>(kosRequest, headers);
|
||||
|
||||
// KOS API 호출 (KOS Mock 응답 구조에 맞게 수정)
|
||||
String kosUrl = kosProperties.getBaseUrl() + "/api/v1/kos/bill/inquiry";
|
||||
ResponseEntity<Map> responseEntity = restTemplate.exchange(
|
||||
kosUrl, HttpMethod.POST, requestEntity, Map.class
|
||||
);
|
||||
|
||||
Map<String, Object> kosCommonResponse = responseEntity.getBody();
|
||||
|
||||
log.info("KOS Mock 응답 받음: {}", kosCommonResponse);
|
||||
|
||||
if (kosCommonResponse == null) {
|
||||
throw KosConnectionException.apiError("KOS-BILL-INQUIRY",
|
||||
String.valueOf(responseEntity.getStatusCodeValue()), "응답 데이터가 없습니다");
|
||||
}
|
||||
|
||||
// KOS Mock 응답을 내부 모델로 변환
|
||||
BillInquiryResponse response = convertKosMockResponseToBillResponse(kosCommonResponse);
|
||||
|
||||
log.info("KOS 요금조회 성공 (동기) - 회선: {}, 조회월: {}, 상태: {}",
|
||||
lineNumber, inquiryMonth, response.getStatus());
|
||||
|
||||
return response;
|
||||
|
||||
} catch (HttpClientErrorException e) {
|
||||
log.error("KOS API 클라이언트 오류 - 회선: {}, 상태: {}, 응답: {}",
|
||||
lineNumber, e.getStatusCode(), e.getResponseBodyAsString());
|
||||
throw KosConnectionException.apiError("KOS-BILL-INQUIRY",
|
||||
String.valueOf(e.getStatusCode().value()), e.getResponseBodyAsString());
|
||||
|
||||
} catch (HttpServerErrorException e) {
|
||||
log.error("KOS API 서버 오류 - 회선: {}, 상태: {}, 응답: {}",
|
||||
lineNumber, e.getStatusCode(), e.getResponseBodyAsString());
|
||||
throw KosConnectionException.apiError("KOS-BILL-INQUIRY",
|
||||
String.valueOf(e.getStatusCode().value()), e.getResponseBodyAsString());
|
||||
|
||||
} catch (ResourceAccessException e) {
|
||||
log.error("KOS 네트워크 연결 오류 - 회선: {}, 오류: {}", lineNumber, e.getMessage());
|
||||
throw KosConnectionException.networkError("KOS-BILL-INQUIRY", e);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("KOS 연동 중 예상치 못한 오류 - 회선: {}, 오류: {}", lineNumber, e.getMessage(), e);
|
||||
throw new KosConnectionException("KOS-BILL-INQUIRY",
|
||||
"KOS 시스템 연동 중 오류가 발생했습니다", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KOS 시스템에서 요금 정보 조회 (비동기 처리)
|
||||
*
|
||||
* Circuit Breaker, Retry, TimeLimiter 패턴 적용
|
||||
*
|
||||
@@ -65,11 +208,7 @@ public class KosClientService {
|
||||
|
||||
try {
|
||||
// KOS 요청 데이터 구성
|
||||
KosRequest kosRequest = KosRequest.builder()
|
||||
.lineNumber(lineNumber)
|
||||
.inquiryMonth(inquiryMonth)
|
||||
.requestTime(LocalDateTime.now())
|
||||
.build();
|
||||
KosRequest kosRequest = KosRequest.createBillInquiryRequest(lineNumber, inquiryMonth, "system");
|
||||
|
||||
// HTTP 헤더 설정
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
@@ -80,7 +219,7 @@ public class KosClientService {
|
||||
HttpEntity<KosRequest> requestEntity = new HttpEntity<>(kosRequest, headers);
|
||||
|
||||
// KOS API 호출
|
||||
String kosUrl = kosProperties.getBaseUrl() + "/api/bill/inquiry";
|
||||
String kosUrl = kosProperties.getBaseUrl() + "/api/v1/kos/bill/inquiry";
|
||||
ResponseEntity<KosResponse> responseEntity = restTemplate.exchange(
|
||||
kosUrl, HttpMethod.POST, requestEntity, KosResponse.class
|
||||
);
|
||||
@@ -124,6 +263,27 @@ public class KosClientService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* KOS 요금조회 동기 처리 Circuit Breaker Fallback 메소드
|
||||
*/
|
||||
public BillInquiryResponse inquireBillSyncFallback(String lineNumber, String inquiryMonth, Exception ex) {
|
||||
log.warn("KOS 요금조회 동기 처리 Circuit Breaker 작동 - 회선: {}, 조회월: {}, 오류: {}",
|
||||
lineNumber, inquiryMonth, ex.getMessage());
|
||||
|
||||
// Circuit Breaker가 Open 상태인 경우
|
||||
if (ex.getClass().getSimpleName().contains("CircuitBreakerOpenException")) {
|
||||
throw CircuitBreakerException.circuitBreakerOpen("KOS-BILL-INQUIRY");
|
||||
}
|
||||
|
||||
// 기타 오류의 경우 실패 처리로 전환
|
||||
BillInquiryResponse fallbackResponse = BillInquiryResponse.builder()
|
||||
.status(BillInquiryResponse.ProcessStatus.FAILED)
|
||||
.build();
|
||||
|
||||
log.info("KOS 요금조회 동기 처리 fallback 응답 - 실패 처리로 전환");
|
||||
return fallbackResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* KOS 요금조회 Circuit Breaker Fallback 메소드
|
||||
*/
|
||||
@@ -203,29 +363,147 @@ public class KosClientService {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* KOS Mock 응답을 내부 응답 모델로 변환
|
||||
*/
|
||||
private BillInquiryResponse convertKosMockResponseToBillResponse(Map<String, Object> kosCommonResponse) {
|
||||
try {
|
||||
// KosCommonResponse에서 success와 data 추출
|
||||
Boolean success = (Boolean) kosCommonResponse.get("success");
|
||||
String resultCode = (String) kosCommonResponse.get("resultCode");
|
||||
Map<String, Object> data = (Map<String, Object>) kosCommonResponse.get("data");
|
||||
|
||||
if (!Boolean.TRUE.equals(success) || !"0000".equals(resultCode) || data == null) {
|
||||
log.warn("KOS Mock 요금조회 실패 - success: {}, resultCode: {}", success, resultCode);
|
||||
return BillInquiryResponse.builder()
|
||||
.requestId(data != null ? (String) data.get("requestId") : null)
|
||||
.status(BillInquiryResponse.ProcessStatus.FAILED)
|
||||
.build();
|
||||
}
|
||||
|
||||
// data에서 실제 요금 정보 추출
|
||||
String procStatus = (String) data.get("procStatus");
|
||||
Map<String, Object> billInfo = (Map<String, Object>) data.get("billInfo");
|
||||
|
||||
// 상태 변환
|
||||
BillInquiryResponse.ProcessStatus status = BillInquiryResponse.ProcessStatus.COMPLETED;
|
||||
if ("SUCCESS".equalsIgnoreCase(procStatus)) {
|
||||
status = BillInquiryResponse.ProcessStatus.COMPLETED;
|
||||
} else if ("PROCESSING".equalsIgnoreCase(procStatus)) {
|
||||
status = BillInquiryResponse.ProcessStatus.PROCESSING;
|
||||
} else if ("FAILED".equalsIgnoreCase(procStatus)) {
|
||||
status = BillInquiryResponse.ProcessStatus.FAILED;
|
||||
}
|
||||
|
||||
BillInquiryResponse.BillInfo convertedBillInfo = null;
|
||||
if (billInfo != null && status == BillInquiryResponse.ProcessStatus.COMPLETED) {
|
||||
// 할인 정보 처리
|
||||
List<BillInquiryResponse.DiscountInfo> discounts = new ArrayList<>();
|
||||
Object discountAmount = billInfo.get("discountAmount");
|
||||
if (discountAmount != null && convertToInteger(discountAmount) > 0) {
|
||||
discounts.add(BillInquiryResponse.DiscountInfo.builder()
|
||||
.name("기본 할인")
|
||||
.amount(convertToInteger(discountAmount))
|
||||
.build());
|
||||
}
|
||||
|
||||
convertedBillInfo = BillInquiryResponse.BillInfo.builder()
|
||||
.productName((String) billInfo.get("productName"))
|
||||
.contractInfo((String) billInfo.get("lineNumber"))
|
||||
.billingMonth((String) billInfo.get("billingMonth"))
|
||||
.totalAmount(convertToInteger(billInfo.get("totalFee")))
|
||||
.discountInfo(discounts)
|
||||
.usage(BillInquiryResponse.UsageInfo.builder()
|
||||
.voice((String) billInfo.get("voiceUsage"))
|
||||
.sms((String) billInfo.get("smsUsage"))
|
||||
.data((String) billInfo.get("dataUsage"))
|
||||
.build())
|
||||
.terminationFee(0) // KOS Mock에서 기본값
|
||||
.deviceInstallment(0) // KOS Mock에서 기본값
|
||||
.paymentInfo(BillInquiryResponse.PaymentInfo.builder()
|
||||
.billingDate((String) billInfo.get("dueDate"))
|
||||
.paymentStatus(getBillPaymentStatus((String) billInfo.get("billStatus")))
|
||||
.paymentMethod("자동이체")
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
return BillInquiryResponse.builder()
|
||||
.requestId((String) data.get("requestId"))
|
||||
.status(status)
|
||||
.billInfo(convertedBillInfo)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("KOS Mock 응답 변환 오류: {}", e.getMessage(), e);
|
||||
throw KosConnectionException.dataConversionError("KOS-BILL-INQUIRY", "BillInquiryResponse", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BigDecimal이나 다른 타입을 Integer로 변환
|
||||
*/
|
||||
private Integer convertToInteger(Object value) {
|
||||
if (value == null) return null;
|
||||
if (value instanceof Integer) return (Integer) value;
|
||||
if (value instanceof Number) return ((Number) value).intValue();
|
||||
try {
|
||||
return Integer.valueOf(value.toString().split("\\.")[0]); // 소수점 제거
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("숫자 변환 실패: {}", value);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KOS Mock의 billStatus를 PaymentStatus로 변환
|
||||
*/
|
||||
private BillInquiryResponse.PaymentStatus getBillPaymentStatus(String billStatus) {
|
||||
if (billStatus == null) return BillInquiryResponse.PaymentStatus.UNPAID;
|
||||
|
||||
switch (billStatus.toUpperCase()) {
|
||||
case "PAID":
|
||||
case "CONFIRMED":
|
||||
return BillInquiryResponse.PaymentStatus.PAID;
|
||||
case "UNPAID":
|
||||
return BillInquiryResponse.PaymentStatus.UNPAID;
|
||||
case "OVERDUE":
|
||||
return BillInquiryResponse.PaymentStatus.OVERDUE;
|
||||
default:
|
||||
return BillInquiryResponse.PaymentStatus.UNPAID;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KOS 응답을 내부 응답 모델로 변환
|
||||
*/
|
||||
private BillInquiryResponse convertKosResponseToBillResponse(KosResponse kosResponse) {
|
||||
try {
|
||||
// 상태 변환
|
||||
// 상태 변환 - null 체크 추가
|
||||
BillInquiryResponse.ProcessStatus status;
|
||||
switch (kosResponse.getStatus().toUpperCase()) {
|
||||
case "SUCCESS":
|
||||
case "COMPLETED":
|
||||
status = BillInquiryResponse.ProcessStatus.COMPLETED;
|
||||
break;
|
||||
case "PROCESSING":
|
||||
case "PENDING":
|
||||
status = BillInquiryResponse.ProcessStatus.PROCESSING;
|
||||
break;
|
||||
case "FAILED":
|
||||
case "ERROR":
|
||||
status = BillInquiryResponse.ProcessStatus.FAILED;
|
||||
break;
|
||||
default:
|
||||
status = BillInquiryResponse.ProcessStatus.PROCESSING;
|
||||
break;
|
||||
String kosStatus = kosResponse.getStatus();
|
||||
if (kosStatus == null || kosStatus.trim().isEmpty()) {
|
||||
log.warn("KOS 응답 상태가 null이거나 빈 문자열입니다. 기본값(PROCESSING)으로 설정합니다.");
|
||||
status = BillInquiryResponse.ProcessStatus.PROCESSING;
|
||||
} else {
|
||||
switch (kosStatus.toUpperCase()) {
|
||||
case "SUCCESS":
|
||||
case "COMPLETED":
|
||||
status = BillInquiryResponse.ProcessStatus.COMPLETED;
|
||||
break;
|
||||
case "PROCESSING":
|
||||
case "PENDING":
|
||||
status = BillInquiryResponse.ProcessStatus.PROCESSING;
|
||||
break;
|
||||
case "FAILED":
|
||||
case "ERROR":
|
||||
status = BillInquiryResponse.ProcessStatus.FAILED;
|
||||
break;
|
||||
default:
|
||||
log.warn("알 수 없는 KOS 상태: {}. 기본값(PROCESSING)으로 설정합니다.", kosStatus);
|
||||
status = BillInquiryResponse.ProcessStatus.PROCESSING;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
BillInquiryResponse.BillInfo billInfo = null;
|
||||
@@ -304,6 +582,165 @@ public class KosClientService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KOS Mock 직접 호출 Circuit Breaker Fallback 메소드
|
||||
*/
|
||||
public KosBillInquiryResponse inquireBillDirectFallback(String lineNumber, String inquiryMonth, Exception ex) {
|
||||
log.warn("KOS Mock 직접 호출 Circuit Breaker 작동 - 회선: {}, 조회월: {}, 오류: {}",
|
||||
lineNumber, inquiryMonth, ex.getMessage());
|
||||
|
||||
// 기본 실패 응답 생성
|
||||
return KosBillInquiryResponse.builder()
|
||||
.requestId(generateRequestId())
|
||||
.procStatus("FAILED")
|
||||
.resultCode("9999")
|
||||
.resultMessage("시스템 오류로 인한 조회 실패")
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map을 KosBillInquiryResponse로 변환
|
||||
*/
|
||||
private KosBillInquiryResponse convertMapToKosBillInquiryResponse(Map<String, Object> data) {
|
||||
try {
|
||||
// billInfo 데이터 처리
|
||||
KosBillInquiryResponse.BillInfo billInfo = null;
|
||||
Map<String, Object> billInfoMap = (Map<String, Object>) data.get("billInfo");
|
||||
if (billInfoMap != null) {
|
||||
billInfo = KosBillInquiryResponse.BillInfo.builder()
|
||||
.lineNumber((String) billInfoMap.get("lineNumber"))
|
||||
.billingMonth((String) billInfoMap.get("billingMonth"))
|
||||
.productCode((String) billInfoMap.get("productCode"))
|
||||
.productName((String) billInfoMap.get("productName"))
|
||||
.monthlyFee(convertToBigDecimal(billInfoMap.get("monthlyFee")))
|
||||
.usageFee(convertToBigDecimal(billInfoMap.get("usageFee")))
|
||||
.discountAmount(convertToBigDecimal(billInfoMap.get("discountAmount")))
|
||||
.totalFee(convertToBigDecimal(billInfoMap.get("totalFee")))
|
||||
.dataUsage((String) billInfoMap.get("dataUsage"))
|
||||
.voiceUsage((String) billInfoMap.get("voiceUsage"))
|
||||
.smsUsage((String) billInfoMap.get("smsUsage"))
|
||||
.billStatus((String) billInfoMap.get("billStatus"))
|
||||
.dueDate((String) billInfoMap.get("dueDate"))
|
||||
.build();
|
||||
}
|
||||
|
||||
// customerInfo 데이터 처리
|
||||
KosBillInquiryResponse.CustomerInfo customerInfo = null;
|
||||
Map<String, Object> customerInfoMap = (Map<String, Object>) data.get("customerInfo");
|
||||
if (customerInfoMap != null) {
|
||||
customerInfo = KosBillInquiryResponse.CustomerInfo.builder()
|
||||
.customerName((String) customerInfoMap.get("customerName"))
|
||||
.customerId((String) customerInfoMap.get("customerId"))
|
||||
.operatorCode((String) customerInfoMap.get("operatorCode"))
|
||||
.lineStatus((String) customerInfoMap.get("lineStatus"))
|
||||
.build();
|
||||
}
|
||||
|
||||
return KosBillInquiryResponse.builder()
|
||||
.requestId((String) data.get("requestId"))
|
||||
.procStatus((String) data.get("procStatus"))
|
||||
.resultCode((String) data.get("resultCode"))
|
||||
.resultMessage((String) data.get("resultMessage"))
|
||||
.billInfo(billInfo)
|
||||
.customerInfo(customerInfo)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Map을 KosBillInquiryResponse로 변환 실패: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("응답 데이터 변환 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Object를 BigDecimal로 변환
|
||||
*/
|
||||
private java.math.BigDecimal convertToBigDecimal(Object value) {
|
||||
if (value == null) return null;
|
||||
if (value instanceof java.math.BigDecimal) return (java.math.BigDecimal) value;
|
||||
if (value instanceof Number) return java.math.BigDecimal.valueOf(((Number) value).doubleValue());
|
||||
try {
|
||||
return new java.math.BigDecimal(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("BigDecimal 변환 실패: {}", value);
|
||||
return java.math.BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청 ID 생성
|
||||
*/
|
||||
private String generateRequestId() {
|
||||
String currentDate = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"));
|
||||
String uuid = java.util.UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
||||
return String.format("REQ_%s_%s", currentDate, uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회선번호의 실제 요금 데이터가 있는 월 목록 조회
|
||||
*
|
||||
* @param lineNumber 회선번호
|
||||
* @return 데이터가 있는 월 목록 (yyyy-MM 형식)
|
||||
*/
|
||||
@CircuitBreaker(name = "kos-available-months", fallbackMethod = "getAvailableMonthsFallback")
|
||||
@Retry(name = "kos-available-months")
|
||||
public List<String> getAvailableMonths(String lineNumber) {
|
||||
log.info("KOS에서 회선 {}의 데이터 보유 월 조회", lineNumber);
|
||||
|
||||
try {
|
||||
// 회선번호 형식 변환 (010-1234-5678 → 01012345678)
|
||||
String formattedLineNumber = lineNumber.replaceAll("-", "");
|
||||
|
||||
// HTTP 헤더 설정
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Content-Type", "application/json");
|
||||
headers.set("X-Service-Name", "MVNO-BILL-INQUIRY");
|
||||
headers.set("X-Request-ID", java.util.UUID.randomUUID().toString());
|
||||
|
||||
HttpEntity<String> requestEntity = new HttpEntity<>(headers);
|
||||
|
||||
// KOS Mock API 호출 - 월 목록 조회
|
||||
String kosUrl = kosProperties.getBaseUrl() + "/api/v1/kos/bill/available-months/" + formattedLineNumber;
|
||||
ResponseEntity<Map> responseEntity = restTemplate.exchange(
|
||||
kosUrl, HttpMethod.GET, requestEntity, Map.class
|
||||
);
|
||||
|
||||
Map<String, Object> response = responseEntity.getBody();
|
||||
|
||||
if (response == null) {
|
||||
log.warn("KOS에서 월 목록 응답이 없음");
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
// KosCommonResponse의 data 부분에서 월 목록 추출
|
||||
Map<String, Object> data = (Map<String, Object>) response.get("data");
|
||||
if (data == null) {
|
||||
log.warn("KOS 응답에서 data를 찾을 수 없음");
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
List<String> availableMonths = (List<String>) data.get("availableMonths");
|
||||
if (availableMonths == null) {
|
||||
availableMonths = new ArrayList<>();
|
||||
}
|
||||
|
||||
log.info("KOS에서 조회된 데이터 보유 월: {} (총 {}개월)", availableMonths, availableMonths.size());
|
||||
return availableMonths;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("KOS에서 데이터 보유 월 조회 오류 - 회선: {}, 오류: {}", lineNumber, e.getMessage(), e);
|
||||
throw new KosConnectionException("KOS-AVAILABLE-MONTHS",
|
||||
"KOS 시스템에서 데이터 보유 월 조회 중 오류가 발생했습니다", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 보유 월 조회 Circuit Breaker Fallback 메소드
|
||||
*/
|
||||
public List<String> getAvailableMonthsFallback(String lineNumber, Exception ex) {
|
||||
log.warn("KOS 데이터 보유 월 조회 Circuit Breaker 작동 - 회선: {}, 오류: {}", lineNumber, ex.getMessage());
|
||||
return new ArrayList<>(); // 빈 목록 반환
|
||||
}
|
||||
|
||||
/**
|
||||
* KOS 시스템 연결 상태 확인
|
||||
*
|
||||
|
||||
@@ -1,169 +1,6 @@
|
||||
# 통신요금 관리 서비스 - Bill Service 개발환경 설정
|
||||
# 개발자 편의성과 디버깅을 위한 설정
|
||||
#
|
||||
# @author 이개발(백엔더)
|
||||
# @version 1.0.0
|
||||
# @since 2025-09-08
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:${DB_KIND:postgresql}://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:bill_inquiry_db}
|
||||
username: ${DB_USERNAME:bill_inquiry_user}
|
||||
password: ${DB_PASSWORD:BillUser2025!}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
hikari:
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
leak-detection-threshold: 60000
|
||||
# JPA 설정
|
||||
jpa:
|
||||
show-sql: ${SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
use_sql_comments: true
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:update}
|
||||
|
||||
# Redis 설정
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:Redis2025Dev!}
|
||||
timeout: 2000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
max-wait: -1ms
|
||||
database: ${REDIS_DATABASE:1}
|
||||
|
||||
# 캐시 설정 (개발환경 - 짧은 TTL)
|
||||
cache:
|
||||
redis:
|
||||
time-to-live: 300000 # 5분
|
||||
|
||||
# 서버 설정 (개발환경)
|
||||
server:
|
||||
port: ${SERVER_PORT:8082}
|
||||
error:
|
||||
include-message: always
|
||||
include-binding-errors: always
|
||||
include-stacktrace: always # 개발환경에서는 스택트레이스 포함
|
||||
include-exception: true # 예외 정보 포함
|
||||
|
||||
# 액추에이터 설정 (개발환경)
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "*" # 개발환경에서는 모든 엔드포인트 노출
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
show-components: always
|
||||
security:
|
||||
enabled: false # 개발환경에서는 액추에이터 보안 비활성화
|
||||
|
||||
# KOS 시스템 연동 설정 (개발환경)
|
||||
kos:
|
||||
base-url: http://localhost:9090 # 로컬 KOS Mock 서버
|
||||
connect-timeout: 3000
|
||||
read-timeout: 10000
|
||||
max-retries: 2
|
||||
retry-delay: 500
|
||||
|
||||
# Circuit Breaker 설정 (개발환경 - 관대한 설정)
|
||||
circuit-breaker:
|
||||
failure-rate-threshold: 0.7 # 70% 실패율
|
||||
slow-call-duration-threshold: 15000 # 15초
|
||||
slow-call-rate-threshold: 0.7 # 70% 느린 호출
|
||||
sliding-window-size: 5 # 작은 윈도우
|
||||
minimum-number-of-calls: 3 # 적은 최소 호출
|
||||
permitted-number-of-calls-in-half-open-state: 2
|
||||
wait-duration-in-open-state: 30000 # 30초
|
||||
|
||||
# 인증 설정 (개발환경)
|
||||
authentication:
|
||||
enabled: false # 개발환경에서는 인증 비활성화
|
||||
api-key: dev-api-key
|
||||
secret-key: dev-secret-key
|
||||
token-expiration-seconds: 7200 # 2시간
|
||||
|
||||
# 모니터링 설정 (개발환경)
|
||||
monitoring:
|
||||
performance-logging-enabled: true
|
||||
slow-request-threshold: 1000 # 1초 (더 민감한 감지)
|
||||
metrics-enabled: true
|
||||
health-check-interval: 10000 # 10초
|
||||
|
||||
# 로깅 설정 (개발환경)
|
||||
# 로깅 설정
|
||||
logging:
|
||||
level:
|
||||
root: ${LOG_LEVEL_ROOT:INFO}
|
||||
com.phonebill: ${LOG_LEVEL_APP:DEBUG} # 애플리케이션 로그 디버그 레벨
|
||||
com.phonebill: DEBUG
|
||||
com.phonebill.bill.service: DEBUG
|
||||
com.phonebill.bill.repository: DEBUG
|
||||
org.springframework.cache: DEBUG
|
||||
org.springframework.web: DEBUG
|
||||
org.springframework.security: DEBUG
|
||||
org.hibernate.SQL: DEBUG # SQL 쿼리 로그
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: TRACE # SQL 파라미터 로그
|
||||
io.github.resilience4j: DEBUG
|
||||
redis.clients.jedis: DEBUG
|
||||
org.springframework.web.client.RestTemplate: DEBUG
|
||||
pattern:
|
||||
console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}"
|
||||
file:
|
||||
name: ${LOG_FILE_NAME:logs/bill-service.log}
|
||||
max-size: 50MB
|
||||
max-history: 7 # 개발환경에서는 7일만 보관
|
||||
|
||||
# Swagger 설정 (개발환경)
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: true
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
tags-sorter: alpha
|
||||
operations-sorter: alpha
|
||||
display-request-duration: true
|
||||
default-models-expand-depth: 2
|
||||
default-model-expand-depth: 2
|
||||
try-it-out-enabled: true
|
||||
filter: true
|
||||
doc-expansion: list
|
||||
show-actuator: true
|
||||
|
||||
# 개발환경 전용 설정
|
||||
debug: false # Spring Boot 디버그 모드
|
||||
|
||||
# 개발편의를 위한 프로파일 정보
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
|
||||
# 개발환경 정보
|
||||
info:
|
||||
environment: development
|
||||
debug:
|
||||
enabled: true
|
||||
database:
|
||||
name: bill_service_dev
|
||||
host: localhost
|
||||
port: 3306
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 1
|
||||
kos:
|
||||
host: localhost
|
||||
port: 9090
|
||||
mock: true
|
||||
@@ -1,237 +1,6 @@
|
||||
# 통신요금 관리 서비스 - Bill Service 운영환경 설정
|
||||
# 운영환경 안정성과 보안을 위한 설정
|
||||
#
|
||||
# @author 이개발(백엔더)
|
||||
# @version 1.0.0
|
||||
# @since 2025-09-08
|
||||
|
||||
spring:
|
||||
# 데이터베이스 설정 (운영환경)
|
||||
datasource:
|
||||
url: ${DB_URL:jdbc:mysql://prod-db-host:3306/bill_service_prod?useUnicode=true&characterEncoding=utf8&useSSL=true&requireSSL=true&serverTimezone=Asia/Seoul}
|
||||
username: ${DB_USERNAME}
|
||||
password: ${DB_PASSWORD}
|
||||
hikari:
|
||||
minimum-idle: 10
|
||||
maximum-pool-size: 50
|
||||
idle-timeout: 600000 # 10분
|
||||
max-lifetime: 1800000 # 30분
|
||||
connection-timeout: 30000 # 30초
|
||||
validation-timeout: 5000 # 5초
|
||||
leak-detection-threshold: 60000 # 1분
|
||||
|
||||
# JPA 설정 (운영환경)
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate # 운영환경에서는 스키마 검증만
|
||||
show-sql: false # 운영환경에서는 SQL 로그 비활성화
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: false
|
||||
use_sql_comments: false
|
||||
default_batch_fetch_size: 100
|
||||
jdbc:
|
||||
batch_size: 50
|
||||
connection:
|
||||
provider_disables_autocommit: true
|
||||
|
||||
# Redis 설정 (운영환경)
|
||||
redis:
|
||||
host: ${REDIS_HOST:prod-redis-host}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD}
|
||||
database: ${REDIS_DATABASE:0}
|
||||
timeout: 5000
|
||||
ssl: ${REDIS_SSL:true}
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 50
|
||||
max-idle: 20
|
||||
min-idle: 5
|
||||
max-wait: 5000
|
||||
cluster:
|
||||
refresh:
|
||||
adaptive: true
|
||||
period: 30s
|
||||
|
||||
# 캐시 설정 (운영환경)
|
||||
cache:
|
||||
redis:
|
||||
time-to-live: 3600000 # 1시간
|
||||
|
||||
# 서버 설정 (운영환경)
|
||||
server:
|
||||
port: ${SERVER_PORT:8081}
|
||||
error:
|
||||
include-message: never
|
||||
include-binding-errors: never
|
||||
include-stacktrace: never # 운영환경에서는 스택트레이스 숨김
|
||||
include-exception: false # 예외 정보 숨김
|
||||
tomcat:
|
||||
max-connections: 10000
|
||||
accept-count: 200
|
||||
threads:
|
||||
max: 300
|
||||
min-spare: 20
|
||||
connection-timeout: 20000
|
||||
compression:
|
||||
enabled: true
|
||||
mime-types: application/json,application/xml,text/html,text/xml,text/plain,application/javascript,text/css
|
||||
min-response-size: 1024
|
||||
|
||||
# 액추에이터 설정 (운영환경)
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus # 제한적 노출
|
||||
base-path: /actuator
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when_authorized # 인증된 사용자에게만 상세 정보 제공
|
||||
show-components: when_authorized
|
||||
probes:
|
||||
enabled: true
|
||||
info:
|
||||
enabled: true
|
||||
metrics:
|
||||
enabled: true
|
||||
prometheus:
|
||||
enabled: true
|
||||
security:
|
||||
enabled: true
|
||||
health:
|
||||
redis:
|
||||
enabled: true
|
||||
db:
|
||||
enabled: true
|
||||
diskspace:
|
||||
enabled: true
|
||||
threshold: 500MB
|
||||
metrics:
|
||||
export:
|
||||
prometheus:
|
||||
enabled: true
|
||||
descriptions: false
|
||||
distribution:
|
||||
percentiles-histogram:
|
||||
http.server.requests: true
|
||||
percentiles:
|
||||
http.server.requests: 0.95, 0.99
|
||||
sla:
|
||||
http.server.requests: 100ms, 500ms, 1s, 2s
|
||||
|
||||
# KOS 시스템 연동 설정 (운영환경)
|
||||
kos:
|
||||
base-url: ${KOS_BASE_URL}
|
||||
connect-timeout: 5000
|
||||
read-timeout: 30000
|
||||
max-retries: 3
|
||||
retry-delay: 1000
|
||||
|
||||
# Circuit Breaker 설정 (운영환경 - 엄격한 설정)
|
||||
circuit-breaker:
|
||||
failure-rate-threshold: 0.5 # 50% 실패율
|
||||
slow-call-duration-threshold: 10000 # 10초
|
||||
slow-call-rate-threshold: 0.5 # 50% 느린 호출
|
||||
sliding-window-size: 20 # 큰 윈도우로 정확한 측정
|
||||
minimum-number-of-calls: 10 # 충분한 샘플
|
||||
permitted-number-of-calls-in-half-open-state: 5
|
||||
wait-duration-in-open-state: 60000 # 60초
|
||||
|
||||
# 인증 설정 (운영환경)
|
||||
authentication:
|
||||
enabled: true
|
||||
api-key: ${KOS_API_KEY}
|
||||
secret-key: ${KOS_SECRET_KEY}
|
||||
token-expiration-seconds: 3600 # 1시간
|
||||
token-refresh-threshold-seconds: 300 # 5분
|
||||
|
||||
# 모니터링 설정 (운영환경)
|
||||
monitoring:
|
||||
performance-logging-enabled: true
|
||||
slow-request-threshold: 3000 # 3초
|
||||
metrics-enabled: true
|
||||
health-check-interval: 30000 # 30초
|
||||
|
||||
# 로깅 설정 (운영환경)
|
||||
# 로깅 설정
|
||||
logging:
|
||||
level:
|
||||
root: WARN
|
||||
com.phonebill: INFO # 애플리케이션 로그는 INFO 레벨
|
||||
com.phonebill: INFO
|
||||
com.phonebill.bill.service: INFO
|
||||
com.phonebill.bill.repository: WARN
|
||||
org.springframework.cache: WARN
|
||||
org.springframework.web: WARN
|
||||
org.springframework.security: WARN
|
||||
org.hibernate.SQL: WARN # SQL 로그 비활성화
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: WARN
|
||||
io.github.resilience4j: INFO
|
||||
redis.clients.jedis: WARN
|
||||
org.springframework.web.client.RestTemplate: WARN
|
||||
pattern:
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} ${LOG_LEVEL_PATTERN:%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}"
|
||||
file:
|
||||
name: ${LOG_FILE:/app/logs/bill-service.log}
|
||||
max-size: 200MB
|
||||
max-history: 30 # 30일 보관
|
||||
logback:
|
||||
rollingpolicy:
|
||||
total-size-cap: 5GB
|
||||
appender:
|
||||
console:
|
||||
enabled: false # 운영환경에서는 콘솔 로그 비활성화
|
||||
|
||||
# Swagger 설정 (운영환경 - 보안상 비활성화)
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: false
|
||||
swagger-ui:
|
||||
enabled: false
|
||||
show-actuator: false
|
||||
|
||||
# 운영환경 보안 설정
|
||||
security:
|
||||
require-ssl: true
|
||||
headers:
|
||||
frame:
|
||||
deny: true
|
||||
content-type:
|
||||
nosniff: true
|
||||
xss-protection:
|
||||
and-block: true
|
||||
|
||||
# 운영환경 전용 설정
|
||||
debug: false
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: prod
|
||||
|
||||
# 운영환경 정보
|
||||
info:
|
||||
environment: production
|
||||
debug:
|
||||
enabled: false
|
||||
security:
|
||||
ssl-enabled: true
|
||||
database:
|
||||
name: bill_service_prod
|
||||
ssl-enabled: true
|
||||
redis:
|
||||
ssl-enabled: true
|
||||
cluster-enabled: true
|
||||
kos:
|
||||
ssl-enabled: true
|
||||
authentication-enabled: true
|
||||
|
||||
# 운영환경 JVM 옵션 권장사항
|
||||
# -Xms2g -Xmx4g
|
||||
# -XX:+UseG1GC
|
||||
# -XX:MaxGCPauseMillis=200
|
||||
# -XX:+HeapDumpOnOutOfMemoryError
|
||||
# -XX:HeapDumpPath=/app/logs/heap-dump.hprof
|
||||
# -Djava.security.egd=file:/dev/./urandom
|
||||
# -Dspring.profiles.active=prod
|
||||
com.phonebill.bill.repository: INFO
|
||||
|
||||
@@ -11,59 +11,50 @@ spring:
|
||||
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||
include:
|
||||
- common
|
||||
|
||||
# 데이터베이스 설정
|
||||
datasource:
|
||||
url: ${DB_URL:jdbc:postgresql://localhost:5432/bill_inquiry_db}
|
||||
username: ${DB_USERNAME:bill_user}
|
||||
password: ${DB_PASSWORD:bill_pass}
|
||||
url: jdbc:postgresql://${DB_HOST:20.249.107.185}:${DB_PORT:5432}/${DB_NAME:product_change}
|
||||
username: ${DB_USERNAME:product_user}
|
||||
password: ${DB_PASSWORD:product_pass}
|
||||
driver-class-name: org.postgresql.Driver
|
||||
hikari:
|
||||
minimum-idle: ${DB_MIN_IDLE:5}
|
||||
maximum-pool-size: ${DB_MAX_POOL:20}
|
||||
idle-timeout: ${DB_IDLE_TIMEOUT:300000}
|
||||
max-lifetime: ${DB_MAX_LIFETIME:1800000}
|
||||
connection-timeout: ${DB_CONNECTION_TIMEOUT:30000}
|
||||
validation-timeout: ${DB_VALIDATION_TIMEOUT:5000}
|
||||
leak-detection-threshold: ${DB_LEAK_DETECTION:60000}
|
||||
|
||||
maximum-pool-size: 20
|
||||
minimum-idle: 5
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
leak-detection-threshold: 60000
|
||||
# JPA 설정
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: ${JPA_DDL_AUTO:validate}
|
||||
naming:
|
||||
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
|
||||
show-sql: ${JPA_SHOW_SQL:false}
|
||||
show-sql: ${SHOW_SQL:true}
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
format_sql: ${JPA_FORMAT_SQL:false}
|
||||
use_sql_comments: ${JPA_SQL_COMMENTS:false}
|
||||
default_batch_fetch_size: ${JPA_BATCH_SIZE:100}
|
||||
jdbc:
|
||||
batch_size: ${JPA_JDBC_BATCH_SIZE:20}
|
||||
order_inserts: true
|
||||
order_updates: true
|
||||
format_sql: true
|
||||
use_sql_comments: true
|
||||
connection:
|
||||
provider_disables_autocommit: true
|
||||
open-in-view: false
|
||||
|
||||
provider_disables_autocommit: false
|
||||
hibernate:
|
||||
ddl-auto: ${DDL_AUTO:update}
|
||||
|
||||
# Redis 설정
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
database: ${REDIS_DATABASE:0}
|
||||
timeout: ${REDIS_TIMEOUT:5000}
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: ${REDIS_MAX_ACTIVE:20}
|
||||
max-idle: ${REDIS_MAX_IDLE:8}
|
||||
min-idle: ${REDIS_MIN_IDLE:0}
|
||||
max-wait: ${REDIS_MAX_WAIT:-1}
|
||||
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:localhost}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:}
|
||||
timeout: 2000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
max-wait: -1ms
|
||||
database: ${REDIS_DATABASE:2}
|
||||
|
||||
# Cache 개발 설정 (TTL 단축)
|
||||
cache:
|
||||
redis:
|
||||
time-to-live: 3600000 # 1시간 (개발환경에서 단축)
|
||||
|
||||
# Jackson 설정
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
@@ -75,22 +66,6 @@ spring:
|
||||
accept-single-value-as-array: true
|
||||
time-zone: Asia/Seoul
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
|
||||
# Servlet 설정
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: ${SERVLET_MAX_FILE_SIZE:10MB}
|
||||
max-request-size: ${SERVLET_MAX_REQUEST_SIZE:100MB}
|
||||
|
||||
# 비동기 처리 설정
|
||||
task:
|
||||
execution:
|
||||
pool:
|
||||
core-size: ${ASYNC_CORE_SIZE:5}
|
||||
max-size: ${ASYNC_MAX_SIZE:20}
|
||||
queue-capacity: ${ASYNC_QUEUE_CAPACITY:100}
|
||||
keep-alive: ${ASYNC_KEEP_ALIVE:60s}
|
||||
thread-name-prefix: "bill-async-"
|
||||
|
||||
# 서버 설정
|
||||
server:
|
||||
@@ -105,13 +80,6 @@ server:
|
||||
include-binding-errors: always
|
||||
include-stacktrace: on_param
|
||||
include-exception: false
|
||||
tomcat:
|
||||
uri-encoding: UTF-8
|
||||
max-connections: ${TOMCAT_MAX_CONNECTIONS:8192}
|
||||
accept-count: ${TOMCAT_ACCEPT_COUNT:100}
|
||||
threads:
|
||||
max: ${TOMCAT_MAX_THREADS:200}
|
||||
min-spare: ${TOMCAT_MIN_THREADS:10}
|
||||
|
||||
# 액추에이터 설정 (모니터링)
|
||||
management:
|
||||
@@ -126,7 +94,7 @@ management:
|
||||
endpoint:
|
||||
health:
|
||||
enabled: true
|
||||
show-details: ${ACTUATOR_HEALTH_DETAILS:when_authorized}
|
||||
show-details: always
|
||||
show-components: always
|
||||
probes:
|
||||
enabled: true
|
||||
@@ -159,7 +127,7 @@ management:
|
||||
|
||||
# KOS 시스템 연동 설정
|
||||
kos:
|
||||
base-url: ${KOS_BASE_URL:http://localhost:9090}
|
||||
base-url: ${KOS_BASE_URL:http://localhost:8084}
|
||||
connect-timeout: ${KOS_CONNECT_TIMEOUT:5000}
|
||||
read-timeout: ${KOS_READ_TIMEOUT:30000}
|
||||
max-retries: ${KOS_MAX_RETRIES:3}
|
||||
@@ -174,82 +142,40 @@ kos:
|
||||
minimum-number-of-calls: ${KOS_CB_MIN_CALLS:5}
|
||||
permitted-number-of-calls-in-half-open-state: ${KOS_CB_HALF_OPEN_CALLS:3}
|
||||
wait-duration-in-open-state: ${KOS_CB_OPEN_DURATION:60000}
|
||||
|
||||
# 인증 설정
|
||||
authentication:
|
||||
enabled: ${KOS_AUTH_ENABLED:true}
|
||||
api-key: ${KOS_API_KEY:}
|
||||
secret-key: ${KOS_SECRET_KEY:}
|
||||
token-expiration-seconds: ${KOS_TOKEN_EXPIRATION:3600}
|
||||
token-refresh-threshold-seconds: ${KOS_TOKEN_REFRESH_THRESHOLD:300}
|
||||
|
||||
# 모니터링 설정
|
||||
monitoring:
|
||||
performance-logging-enabled: ${KOS_PERF_LOGGING:true}
|
||||
slow-request-threshold: ${KOS_SLOW_THRESHOLD:3000}
|
||||
metrics-enabled: ${KOS_METRICS_ENABLED:true}
|
||||
health-check-interval: ${KOS_HEALTH_INTERVAL:30000}
|
||||
|
||||
# 로깅 설정
|
||||
logging:
|
||||
level:
|
||||
root: ${LOG_LEVEL_ROOT:INFO}
|
||||
com.phonebill: ${LOG_LEVEL_APP:INFO}
|
||||
com.phonebill.bill.service: ${LOG_LEVEL_SERVICE:INFO}
|
||||
com.phonebill.bill.repository: ${LOG_LEVEL_REPOSITORY:INFO}
|
||||
org.springframework.cache: ${LOG_LEVEL_CACHE:INFO}
|
||||
org.springframework.web: ${LOG_LEVEL_WEB:INFO}
|
||||
org.springframework.security: ${LOG_LEVEL_SECURITY:INFO}
|
||||
org.hibernate.SQL: ${LOG_LEVEL_SQL:WARN}
|
||||
org.hibernate.type.descriptor.sql.BasicBinder: ${LOG_LEVEL_SQL_PARAM:WARN}
|
||||
io.github.resilience4j: ${LOG_LEVEL_RESILIENCE4J:INFO}
|
||||
redis.clients.jedis: ${LOG_LEVEL_REDIS:INFO}
|
||||
pattern:
|
||||
console: "${LOG_PATTERN_CONSOLE:%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}}"
|
||||
file: "${LOG_PATTERN_FILE:%d{yyyy-MM-dd HH:mm:ss.SSS} ${LOG_LEVEL_PATTERN:%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}}"
|
||||
file:
|
||||
name: ${LOG_FILE_NAME:logs/bill-service.log}
|
||||
max-size: ${LOG_FILE_MAX_SIZE:100MB}
|
||||
max-history: ${LOG_FILE_MAX_HISTORY:30}
|
||||
|
||||
# Swagger/OpenAPI 설정
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: ${SWAGGER_ENABLED:true}
|
||||
enabled: true
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
enabled: ${SWAGGER_UI_ENABLED:true}
|
||||
enabled: true
|
||||
path: /swagger-ui.html
|
||||
tags-sorter: alpha
|
||||
operations-sorter: alpha
|
||||
display-request-duration: true
|
||||
default-models-expand-depth: 1
|
||||
default-model-expand-depth: 1
|
||||
show-actuator: ${SWAGGER_SHOW_ACTUATOR:false}
|
||||
show-actuator: false
|
||||
writer-with-default-pretty-printer: true
|
||||
paths-to-exclude: /actuator/**
|
||||
|
||||
# CORS
|
||||
cors:
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000}
|
||||
|
||||
# JWT 보안 설정
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:dev-jwt-secret-key-for-development-only}
|
||||
expiration: ${JWT_EXPIRATION:86400000} # 24시간 (밀리초)
|
||||
refresh-expiration: ${JWT_REFRESH_EXPIRATION:604800000} # 7일 (밀리초)
|
||||
header: ${JWT_HEADER:Authorization}
|
||||
prefix: ${JWT_PREFIX:Bearer }
|
||||
secret: ${JWT_SECRET:}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:1800}
|
||||
refresh-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:86400}
|
||||
|
||||
# 애플리케이션 정보
|
||||
info:
|
||||
app:
|
||||
name: ${spring.application.name}
|
||||
description: 통신요금 조회 및 관리 서비스
|
||||
version: ${BUILD_VERSION:1.0.0}
|
||||
author: 이개발(백엔더)
|
||||
contact: dev@phonebill.com
|
||||
build:
|
||||
time: ${BUILD_TIME:@project.build.time@}
|
||||
artifact: ${BUILD_ARTIFACT:@project.artifactId@}
|
||||
group: ${BUILD_GROUP:@project.groupId@}
|
||||
java:
|
||||
version: ${java.version}
|
||||
git:
|
||||
branch: ${GIT_BRANCH:unknown}
|
||||
commit: ${GIT_COMMIT:unknown}
|
||||
# 로깅 설정
|
||||
logging:
|
||||
pattern:
|
||||
console: "${LOG_PATTERN_CONSOLE:%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}}"
|
||||
file: "${LOG_PATTERN_FILE:%d{yyyy-MM-dd HH:mm:ss.SSS} ${LOG_LEVEL_PATTERN:%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}}"
|
||||
file:
|
||||
name: logs/bill-service.log
|
||||
max-size: ${LOG_FILE_MAX_SIZE:100MB}
|
||||
max-history: ${LOG_FILE_MAX_HISTORY:30}
|
||||
|
||||
Reference in New Issue
Block a user