This commit is contained in:
hiondal
2025-09-09 01:12:14 +09:00
parent 7ec8a682c6
commit b489c73201
276 changed files with 43859 additions and 98 deletions
@@ -0,0 +1,40 @@
package com.phonebill.kosmock;
import com.phonebill.kosmock.data.MockDataService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
/**
* KOS Mock Service 메인 애플리케이션 클래스
*/
@SpringBootApplication(exclude = {
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration.class,
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration.class
})
@EnableCaching
@RequiredArgsConstructor
@Slf4j
public class KosMockApplication implements CommandLineRunner {
private final MockDataService mockDataService;
public static void main(String[] args) {
SpringApplication.run(KosMockApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
log.info("=== KOS Mock Service 시작 ===");
log.info("Mock 데이터 초기화를 시작합니다...");
mockDataService.initializeMockData();
log.info("KOS Mock Service가 성공적으로 시작되었습니다.");
log.info("Swagger UI: http://localhost:8080/kos-mock/swagger-ui.html");
log.info("Health Check: http://localhost:8080/kos-mock/actuator/health");
}
}
@@ -0,0 +1,39 @@
package com.phonebill.kosmock.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* KOS Mock 설정
*/
@Configuration
@ConfigurationProperties(prefix = "kos.mock")
@Data
public class MockConfig {
/**
* Mock 응답 지연 시간 (밀리초)
*/
private long responseDelay = 500;
/**
* Mock 실패율 (0.0 ~ 1.0)
*/
private double failureRate = 0.0;
/**
* 최대 재시도 횟수
*/
private int maxRetryCount = 3;
/**
* 타임아웃 시간 (밀리초)
*/
private long timeoutMs = 30000;
/**
* 디버그 모드 활성화 여부
*/
private boolean debugMode = false;
}
@@ -0,0 +1,40 @@
package com.phonebill.kosmock.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
/**
* 보안 설정
* Mock 서비스이므로 간단한 설정만 적용합니다.
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
/**
* 보안 필터 체인 설정
* 내부 시스템용 Mock 서비스이므로 모든 요청을 허용합니다.
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// CSRF 보호 비활성화 (Mock 서비스)
.csrf(AbstractHttpConfigurer::disable)
// 프레임 옵션 비활성화 (Swagger UI 사용)
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions.disable())
)
// 모든 요청 허용
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll()
);
return http.build();
}
}
@@ -0,0 +1,45 @@
package com.phonebill.kosmock.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* Swagger/OpenAPI 설정
*/
@Configuration
public class SwaggerConfig {
@Value("${server.servlet.context-path:/}")
private String contextPath;
@Bean
public OpenAPI kosMockOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("KOS Mock Service API")
.description("KT 통신사 시스템(KOS-Order)을 모방한 Mock 서비스 API")
.version("v1.0.0")
.contact(new Contact()
.name("개발팀")
.email("dev@phonebill.com"))
.license(new License()
.name("Internal Use Only")
.url("http://www.phonebill.com/license")))
.servers(List.of(
new Server()
.url("http://localhost:8080" + contextPath)
.description("개발 환경"),
new Server()
.url("https://kos-mock.phonebill.com" + contextPath)
.description("운영 환경")
));
}
}
@@ -0,0 +1,171 @@
package com.phonebill.kosmock.controller;
import com.phonebill.kosmock.dto.*;
import com.phonebill.kosmock.service.KosMockService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/**
* KOS Mock API 컨트롤러
* KT 통신사 시스템(KOS-Order)의 API를 모방합니다.
*/
@RestController
@RequestMapping("/api/v1/kos")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "KOS Mock API", description = "KT 통신사 시스템 Mock API")
public class KosMockController {
private final KosMockService kosMockService;
/**
* 요금 조회 API
*/
@PostMapping("/bill/inquiry")
@Operation(summary = "요금 조회", description = "고객의 통신요금 정보를 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "조회 성공",
content = @Content(schema = @Schema(implementation = KosCommonResponse.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
public ResponseEntity<KosCommonResponse<KosBillInquiryResponse>> inquireBill(
@Valid @RequestBody KosBillInquiryRequest request) {
log.info("요금 조회 요청 수신 - RequestId: {}, LineNumber: {}",
request.getRequestId(), request.getLineNumber());
try {
KosBillInquiryResponse response = kosMockService.processBillInquiry(request);
if ("0000".equals(response.getResultCode())) {
return ResponseEntity.ok(KosCommonResponse.success(response, "요금 조회가 완료되었습니다"));
} else {
return ResponseEntity.ok(KosCommonResponse.failure(
response.getResultCode(), response.getResultMessage()));
}
} catch (Exception e) {
log.error("요금 조회 처리 중 오류 발생 - RequestId: {}", request.getRequestId(), e);
return ResponseEntity.ok(KosCommonResponse.systemError());
}
}
/**
* 상품 변경 API
*/
@PostMapping("/product/change")
@Operation(summary = "상품 변경", description = "고객의 통신상품을 변경합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "변경 처리 성공",
content = @Content(schema = @Schema(implementation = KosCommonResponse.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
public ResponseEntity<KosCommonResponse<KosProductChangeResponse>> changeProduct(
@Valid @RequestBody KosProductChangeRequest request) {
log.info("상품 변경 요청 수신 - RequestId: {}, LineNumber: {}, Target: {}",
request.getRequestId(), request.getLineNumber(), request.getTargetProductCode());
try {
KosProductChangeResponse response = kosMockService.processProductChange(request);
if ("0000".equals(response.getResultCode())) {
return ResponseEntity.ok(KosCommonResponse.success(response, "상품 변경이 완료되었습니다"));
} else {
return ResponseEntity.ok(KosCommonResponse.failure(
response.getResultCode(), response.getResultMessage()));
}
} catch (Exception e) {
log.error("상품 변경 처리 중 오류 발생 - RequestId: {}", request.getRequestId(), e);
return ResponseEntity.ok(KosCommonResponse.systemError());
}
}
/**
* 처리 상태 조회 API
*/
@GetMapping("/status/{requestId}")
@Operation(summary = "처리 상태 조회", description = "요청의 처리 상태를 조회합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "조회 성공"),
@ApiResponse(responseCode = "404", description = "요청 ID를 찾을 수 없음"),
@ApiResponse(responseCode = "500", description = "서버 오류")
})
public ResponseEntity<KosCommonResponse<Object>> getProcessingStatus(
@Parameter(description = "요청 ID", example = "REQ_20250108_001")
@PathVariable String requestId) {
log.info("처리 상태 조회 요청 - RequestId: {}", requestId);
try {
// Mock 데이터에서 처리 결과 조회 로직은 간단하게 구현
// 실제로는 mockDataService.getProcessingResult(requestId) 사용
return ResponseEntity.ok(KosCommonResponse.success(
"PROCESSING 상태 - 처리 중입니다.",
"처리 상태 조회가 완료되었습니다"));
} catch (Exception e) {
log.error("처리 상태 조회 중 오류 발생 - RequestId: {}", requestId, e);
return ResponseEntity.ok(KosCommonResponse.systemError());
}
}
/**
* 서비스 상태 체크 API
*/
@GetMapping("/health")
@Operation(summary = "서비스 상태 체크", description = "KOS Mock 서비스의 상태를 확인합니다.")
public ResponseEntity<KosCommonResponse<Object>> healthCheck() {
log.debug("KOS Mock 서비스 상태 체크 요청");
try {
return ResponseEntity.ok(KosCommonResponse.success(
"KOS Mock Service is running normally",
"서비스가 정상 동작 중입니다"));
} catch (Exception e) {
log.error("서비스 상태 체크 중 오류 발생", e);
return ResponseEntity.ok(KosCommonResponse.systemError());
}
}
/**
* Mock 설정 조회 API (개발/테스트용)
*/
@GetMapping("/mock/config")
@Operation(summary = "Mock 설정 조회", description = "현재 Mock 서비스의 설정을 조회합니다. (개발/테스트용)")
public ResponseEntity<KosCommonResponse<Object>> getMockConfig() {
log.info("Mock 설정 조회 요청");
try {
// Mock 설정 정보를 간단히 반환
String configInfo = String.format(
"Response Delay: %dms, Failure Rate: %.2f%%, Service Status: ACTIVE",
500, 1.0); // 하드코딩된 값 (실제로는 MockConfig에서 가져올 수 있음)
return ResponseEntity.ok(KosCommonResponse.success(
configInfo,
"Mock 설정 조회가 완료되었습니다"));
} catch (Exception e) {
log.error("Mock 설정 조회 중 오류 발생", e);
return ResponseEntity.ok(KosCommonResponse.systemError());
}
}
}
@@ -0,0 +1,93 @@
package com.phonebill.kosmock.data;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
/**
* Mock 요금 데이터 모델
* KOS 시스템의 요금 정보를 모방합니다.
*/
@Data
@Builder
public class MockBillData {
/**
* 회선번호
*/
private String lineNumber;
/**
* 청구월 (YYYYMM)
*/
private String billingMonth;
/**
* 상품 코드
*/
private String productCode;
/**
* 상품명
*/
private String productName;
/**
* 월 기본료
*/
private BigDecimal monthlyFee;
/**
* 사용료
*/
private BigDecimal usageFee;
/**
* 총 요금
*/
private BigDecimal totalFee;
/**
* 데이터 사용량
*/
private String dataUsage;
/**
* 음성 사용량
*/
private String voiceUsage;
/**
* SMS 사용량
*/
private String smsUsage;
/**
* 청구 상태 (PENDING, CONFIRMED, PAID)
*/
private String billStatus;
/**
* 납부 기한 (YYYYMMDD)
*/
private String dueDate;
/**
* 할인 금액
*/
@Builder.Default
private BigDecimal discountAmount = BigDecimal.ZERO;
/**
* 부가세
*/
@Builder.Default
private BigDecimal vat = BigDecimal.ZERO;
/**
* 미납 금액
*/
@Builder.Default
private BigDecimal unpaidAmount = BigDecimal.ZERO;
}
@@ -0,0 +1,67 @@
package com.phonebill.kosmock.data;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
/**
* Mock 고객 데이터 모델
* KOS 시스템의 고객 정보를 모방합니다.
*/
@Data
@Builder
public class MockCustomerData {
/**
* 회선번호 (Primary Key)
*/
private String lineNumber;
/**
* 고객명
*/
private String customerName;
/**
* 고객 ID
*/
private String customerId;
/**
* 통신사업자 코드 (KT, SKT, LGU+ 등)
*/
private String operatorCode;
/**
* 현재 상품 코드
*/
private String currentProductCode;
/**
* 회선 상태 (ACTIVE, SUSPENDED, TERMINATED)
*/
private String lineStatus;
/**
* 계약일시
*/
private LocalDateTime contractDate;
/**
* 최종 수정일시
*/
private LocalDateTime lastModified;
/**
* 고객 등급 (VIP, GOLD, SILVER, BRONZE)
*/
@Builder.Default
private String customerGrade = "SILVER";
/**
* 가입 유형 (INDIVIDUAL, CORPORATE)
*/
@Builder.Default
private String subscriptionType = "INDIVIDUAL";
}
@@ -0,0 +1,265 @@
package com.phonebill.kosmock.data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* KOS Mock 데이터 서비스
* 통신요금 조회 및 상품변경에 필요한 Mock 데이터를 제공합니다.
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class MockDataService {
// Mock 사용자 데이터 (회선번호 기반)
private final Map<String, MockCustomerData> mockCustomers = new ConcurrentHashMap<>();
// Mock 상품 데이터
private final Map<String, MockProductData> mockProducts = new ConcurrentHashMap<>();
// Mock 요금 데이터
private final Map<String, MockBillData> mockBills = new ConcurrentHashMap<>();
// 요청 처리 이력
private final Map<String, MockProcessingResult> processingResults = new ConcurrentHashMap<>();
/**
* 초기 Mock 데이터 생성
*/
public void initializeMockData() {
log.info("KOS Mock 데이터 초기화 시작");
initializeMockProducts();
initializeMockCustomers();
initializeMockBills();
log.info("KOS Mock 데이터 초기화 완료 - 고객: {}, 상품: {}, 요금: {}",
mockCustomers.size(), mockProducts.size(), mockBills.size());
}
/**
* Mock 상품 데이터 초기화
*/
private void initializeMockProducts() {
// 5G 상품
mockProducts.put("5G-PREMIUM-001", MockProductData.builder()
.productCode("5G-PREMIUM-001")
.productName("5G 프리미엄 플랜")
.monthlyFee(new BigDecimal("89000"))
.dataAllowance("무제한")
.voiceAllowance("무제한")
.smsAllowance("무제한")
.operatorCode("KT")
.networkType("5G")
.status("ACTIVE")
.description("5G 네트워크 무제한 프리미엄 요금제")
.build());
mockProducts.put("5G-STANDARD-001", MockProductData.builder()
.productCode("5G-STANDARD-001")
.productName("5G 스탠다드 플랜")
.monthlyFee(new BigDecimal("69000"))
.dataAllowance("100GB")
.voiceAllowance("무제한")
.smsAllowance("무제한")
.operatorCode("KT")
.networkType("5G")
.status("ACTIVE")
.description("5G 네트워크 스탠다드 요금제")
.build());
// LTE 상품
mockProducts.put("LTE-PREMIUM-001", MockProductData.builder()
.productCode("LTE-PREMIUM-001")
.productName("LTE 프리미엄 플랜")
.monthlyFee(new BigDecimal("59000"))
.dataAllowance("50GB")
.voiceAllowance("무제한")
.smsAllowance("무제한")
.operatorCode("KT")
.networkType("LTE")
.status("ACTIVE")
.description("LTE 네트워크 프리미엄 요금제")
.build());
mockProducts.put("LTE-BASIC-001", MockProductData.builder()
.productCode("LTE-BASIC-001")
.productName("LTE 베이직 플랜")
.monthlyFee(new BigDecimal("39000"))
.dataAllowance("20GB")
.voiceAllowance("무제한")
.smsAllowance("기본 제공")
.operatorCode("KT")
.networkType("LTE")
.status("ACTIVE")
.description("LTE 네트워크 베이직 요금제")
.build());
// 종료된 상품 (변경 불가)
mockProducts.put("3G-OLD-001", MockProductData.builder()
.productCode("3G-OLD-001")
.productName("3G 레거시 플랜")
.monthlyFee(new BigDecimal("29000"))
.dataAllowance("5GB")
.voiceAllowance("500분")
.smsAllowance("100건")
.operatorCode("KT")
.networkType("3G")
.status("DISCONTINUED")
.description("3G 네트워크 레거시 요금제 (신규 가입 불가)")
.build());
}
/**
* Mock 고객 데이터 초기화
*/
private void initializeMockCustomers() {
// 테스트용 고객 데이터
String[] testNumbers = {
"01012345678", "01087654321", "01055554444",
"01099998888", "01077776666", "01033332222"
};
String[] testNames = {
"김테스트", "이샘플", "박데모", "최모의", "정시험", "한실험"
};
String[] currentProducts = {
"5G-PREMIUM-001", "5G-STANDARD-001", "LTE-PREMIUM-001",
"LTE-BASIC-001", "3G-OLD-001", "5G-PREMIUM-001"
};
for (int i = 0; i < testNumbers.length; i++) {
mockCustomers.put(testNumbers[i], MockCustomerData.builder()
.lineNumber(testNumbers[i])
.customerName(testNames[i])
.customerId("CUST" + String.format("%06d", i + 1))
.operatorCode("KT")
.currentProductCode(currentProducts[i])
.lineStatus("ACTIVE")
.contractDate(LocalDateTime.now().minusMonths(12 + i))
.lastModified(LocalDateTime.now().minusDays(i))
.build());
}
// 비활성 회선 테스트용
mockCustomers.put("01000000000", MockCustomerData.builder()
.lineNumber("01000000000")
.customerName("비활성사용자")
.customerId("CUST999999")
.operatorCode("KT")
.currentProductCode("LTE-BASIC-001")
.lineStatus("SUSPENDED")
.contractDate(LocalDateTime.now().minusMonths(6))
.lastModified(LocalDateTime.now().minusDays(30))
.build());
}
/**
* Mock 요금 데이터 초기화
*/
private void initializeMockBills() {
for (MockCustomerData customer : mockCustomers.values()) {
MockProductData product = mockProducts.get(customer.getCurrentProductCode());
if (product != null) {
// 최근 3개월 요금 데이터 생성
for (int month = 0; month < 3; month++) {
LocalDateTime billDate = LocalDateTime.now().minusMonths(month);
String billKey = customer.getLineNumber() + "_" + billDate.format(DateTimeFormatter.ofPattern("yyyyMM"));
BigDecimal usageFee = calculateUsageFee(product, month);
BigDecimal totalFee = product.getMonthlyFee().add(usageFee);
mockBills.put(billKey, MockBillData.builder()
.lineNumber(customer.getLineNumber())
.billingMonth(billDate.format(DateTimeFormatter.ofPattern("yyyyMM")))
.productCode(product.getProductCode())
.productName(product.getProductName())
.monthlyFee(product.getMonthlyFee())
.usageFee(usageFee)
.totalFee(totalFee)
.dataUsage(generateRandomDataUsage(product))
.voiceUsage(generateRandomVoiceUsage(product))
.smsUsage(generateRandomSmsUsage())
.billStatus("CONFIRMED")
.dueDate(billDate.plusDays(25).format(DateTimeFormatter.ofPattern("yyyyMMdd")))
.build());
}
}
}
}
private BigDecimal calculateUsageFee(MockProductData product, int month) {
// 간단한 사용료 계산 로직 (랜덤하게 0~30000원)
Random random = new Random();
return new BigDecimal(random.nextInt(30000));
}
private String generateRandomDataUsage(MockProductData product) {
Random random = new Random();
if ("무제한".equals(product.getDataAllowance())) {
return random.nextInt(200) + "GB";
} else {
int allowance = Integer.parseInt(product.getDataAllowance().replace("GB", ""));
return random.nextInt(allowance) + "GB";
}
}
private String generateRandomVoiceUsage(MockProductData product) {
Random random = new Random();
if ("무제한".equals(product.getVoiceAllowance())) {
return random.nextInt(500) + "";
} else {
int allowance = Integer.parseInt(product.getVoiceAllowance().replace("", ""));
return random.nextInt(allowance) + "";
}
}
private String generateRandomSmsUsage() {
Random random = new Random();
return random.nextInt(100) + "";
}
// Getter methods
public MockCustomerData getCustomerData(String lineNumber) {
return mockCustomers.get(lineNumber);
}
public MockProductData getProductData(String productCode) {
return mockProducts.get(productCode);
}
public MockBillData getBillData(String lineNumber, String billingMonth) {
return mockBills.get(lineNumber + "_" + billingMonth);
}
public List<MockProductData> getAllAvailableProducts() {
return mockProducts.values().stream()
.filter(product -> "ACTIVE".equals(product.getStatus()))
.sorted(Comparator.comparing(MockProductData::getMonthlyFee).reversed())
.toList();
}
public void saveProcessingResult(String requestId, MockProcessingResult result) {
processingResults.put(requestId, result);
}
public MockProcessingResult getProcessingResult(String requestId) {
return processingResults.get(requestId);
}
public List<MockBillData> getBillHistory(String lineNumber) {
return mockBills.values().stream()
.filter(bill -> lineNumber.equals(bill.getLineNumber()))
.sorted(Comparator.comparing(MockBillData::getBillingMonth).reversed())
.toList();
}
}
@@ -0,0 +1,71 @@
package com.phonebill.kosmock.data;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
/**
* Mock 처리 결과 데이터 모델
* KOS 시스템의 비동기 처리 결과를 모방합니다.
*/
@Data
@Builder
public class MockProcessingResult {
/**
* 요청 ID
*/
private String requestId;
/**
* 처리 유형 (BILL_INQUIRY, PRODUCT_CHANGE)
*/
private String processingType;
/**
* 처리 상태 (PROCESSING, SUCCESS, FAILURE)
*/
private String status;
/**
* 처리 결과 메시지
*/
private String message;
/**
* 처리 결과 데이터 (JSON String)
*/
private String resultData;
/**
* 요청 일시
*/
private LocalDateTime requestedAt;
/**
* 처리 완료 일시
*/
private LocalDateTime completedAt;
/**
* 오류 코드 (실패 시)
*/
private String errorCode;
/**
* 오류 상세 메시지 (실패 시)
*/
private String errorDetails;
/**
* 재시도 횟수
*/
@Builder.Default
private Integer retryCount = 0;
/**
* 처리 소요 시간 (밀리초)
*/
private Long processingTimeMs;
}
@@ -0,0 +1,83 @@
package com.phonebill.kosmock.data;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
/**
* Mock 상품 데이터 모델
* KOS 시스템의 상품 정보를 모방합니다.
*/
@Data
@Builder
public class MockProductData {
/**
* 상품 코드 (Primary Key)
*/
private String productCode;
/**
* 상품명
*/
private String productName;
/**
* 월 기본료
*/
private BigDecimal monthlyFee;
/**
* 데이터 제공량 (예: "100GB", "무제한")
*/
private String dataAllowance;
/**
* 음성 제공량 (예: "300분", "무제한")
*/
private String voiceAllowance;
/**
* SMS 제공량 (예: "100건", "기본 제공")
*/
private String smsAllowance;
/**
* 통신사업자 코드 (KT, SKT, LGU+ 등)
*/
private String operatorCode;
/**
* 네트워크 타입 (5G, LTE, 3G)
*/
private String networkType;
/**
* 상품 상태 (ACTIVE, DISCONTINUED)
*/
private String status;
/**
* 상품 설명
*/
private String description;
/**
* 최소 이용기간 (개월)
*/
@Builder.Default
private Integer minimumUsagePeriod = 12;
/**
* 약정 할인 가능 여부
*/
@Builder.Default
private Boolean discountAvailable = true;
/**
* 요금제 유형 (POSTPAID, PREPAID)
*/
@Builder.Default
private String planType = "POSTPAID";
}
@@ -0,0 +1,30 @@
package com.phonebill.kosmock.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
/**
* KOS 요금 조회 요청 DTO
*/
@Data
@Schema(description = "KOS 요금 조회 요청")
public class KosBillInquiryRequest {
@Schema(description = "회선번호", example = "01012345678", required = true)
@NotBlank(message = "회선번호는 필수입니다")
@Pattern(regexp = "^010\\d{8}$", message = "올바른 회선번호 형식이 아닙니다")
private String lineNumber;
@Schema(description = "청구월 (YYYYMM)", example = "202501")
@Pattern(regexp = "^\\d{6}$", message = "청구월은 YYYYMM 형식이어야 합니다")
private String billingMonth;
@Schema(description = "요청 ID", example = "REQ_20250108_001", required = true)
@NotBlank(message = "요청 ID는 필수입니다")
private String requestId;
@Schema(description = "요청자 ID", example = "BILL_SERVICE")
private String requestorId;
}
@@ -0,0 +1,94 @@
package com.phonebill.kosmock.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
/**
* KOS 요금 조회 응답 DTO
*/
@Data
@Builder
@Schema(description = "KOS 요금 조회 응답")
public class KosBillInquiryResponse {
@Schema(description = "요청 ID", example = "REQ_20250108_001")
private String requestId;
@Schema(description = "처리 결과 코드", example = "0000")
private String resultCode;
@Schema(description = "처리 결과 메시지", example = "정상 처리되었습니다")
private String resultMessage;
@Schema(description = "요금 정보")
private BillInfo billInfo;
@Schema(description = "고객 정보")
private CustomerInfo customerInfo;
@Data
@Builder
@Schema(description = "요금 정보")
public static class BillInfo {
@Schema(description = "회선번호", example = "01012345678")
private String lineNumber;
@Schema(description = "청구월", example = "202501")
private String billingMonth;
@Schema(description = "상품 코드", example = "5G-PREMIUM-001")
private String productCode;
@Schema(description = "상품명", example = "5G 프리미엄 플랜")
private String productName;
@Schema(description = "월 기본료", example = "89000")
private BigDecimal monthlyFee;
@Schema(description = "사용료", example = "15000")
private BigDecimal usageFee;
@Schema(description = "할인 금액", example = "5000")
private BigDecimal discountAmount;
@Schema(description = "총 요금", example = "99000")
private BigDecimal totalFee;
@Schema(description = "데이터 사용량", example = "150GB")
private String dataUsage;
@Schema(description = "음성 사용량", example = "250분")
private String voiceUsage;
@Schema(description = "SMS 사용량", example = "50건")
private String smsUsage;
@Schema(description = "청구 상태", example = "CONFIRMED")
private String billStatus;
@Schema(description = "납부 기한", example = "20250125")
private String dueDate;
}
@Data
@Builder
@Schema(description = "고객 정보")
public static class CustomerInfo {
@Schema(description = "고객명", example = "김테스트")
private String customerName;
@Schema(description = "고객 ID", example = "CUST000001")
private String customerId;
@Schema(description = "통신사업자 코드", example = "KT")
private String operatorCode;
@Schema(description = "회선 상태", example = "ACTIVE")
private String lineStatus;
}
}
@@ -0,0 +1,84 @@
package com.phonebill.kosmock.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
/**
* KOS 공통 응답 DTO
*/
@Data
@Builder
@Schema(description = "KOS 공통 응답")
public class KosCommonResponse<T> {
@Schema(description = "성공 여부", example = "true")
private Boolean success;
@Schema(description = "처리 결과 코드", example = "0000")
private String resultCode;
@Schema(description = "처리 결과 메시지", example = "정상 처리되었습니다")
private String resultMessage;
@Schema(description = "응답 데이터")
private T data;
@Schema(description = "처리 시간", example = "2025-01-08T14:30:00")
private LocalDateTime timestamp;
@Schema(description = "요청 추적 ID", example = "TRACE_20250108_001")
private String traceId;
/**
* 성공 응답 생성
*/
public static <T> KosCommonResponse<T> success(T data) {
return KosCommonResponse.<T>builder()
.success(true)
.resultCode("0000")
.resultMessage("정상 처리되었습니다")
.data(data)
.timestamp(LocalDateTime.now())
.build();
}
/**
* 성공 응답 생성 (메시지 포함)
*/
public static <T> KosCommonResponse<T> success(T data, String message) {
return KosCommonResponse.<T>builder()
.success(true)
.resultCode("0000")
.resultMessage(message)
.data(data)
.timestamp(LocalDateTime.now())
.build();
}
/**
* 실패 응답 생성
*/
public static <T> KosCommonResponse<T> failure(String errorCode, String errorMessage) {
return KosCommonResponse.<T>builder()
.success(false)
.resultCode(errorCode)
.resultMessage(errorMessage)
.timestamp(LocalDateTime.now())
.build();
}
/**
* 시스템 오류 응답 생성
*/
public static <T> KosCommonResponse<T> systemError() {
return KosCommonResponse.<T>builder()
.success(false)
.resultCode("9999")
.resultMessage("시스템 오류가 발생했습니다")
.timestamp(LocalDateTime.now())
.build();
}
}
@@ -0,0 +1,41 @@
package com.phonebill.kosmock.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
/**
* KOS 상품 변경 요청 DTO
*/
@Data
@Schema(description = "KOS 상품 변경 요청")
public class KosProductChangeRequest {
@Schema(description = "회선번호", example = "01012345678", required = true)
@NotBlank(message = "회선번호는 필수입니다")
@Pattern(regexp = "^010\\d{8}$", message = "올바른 회선번호 형식이 아닙니다")
private String lineNumber;
@Schema(description = "현재 상품 코드", example = "LTE-BASIC-001", required = true)
@NotBlank(message = "현재 상품 코드는 필수입니다")
private String currentProductCode;
@Schema(description = "변경할 상품 코드", example = "5G-PREMIUM-001", required = true)
@NotBlank(message = "변경할 상품 코드는 필수입니다")
private String targetProductCode;
@Schema(description = "요청 ID", example = "REQ_20250108_002", required = true)
@NotBlank(message = "요청 ID는 필수입니다")
private String requestId;
@Schema(description = "요청자 ID", example = "PRODUCT_SERVICE")
private String requestorId;
@Schema(description = "변경 사유", example = "고객 요청에 의한 상품 변경")
private String changeReason;
@Schema(description = "적용 일자 (YYYYMMDD)", example = "20250115")
@Pattern(regexp = "^\\d{8}$", message = "적용 일자는 YYYYMMDD 형식이어야 합니다")
private String effectiveDate;
}
@@ -0,0 +1,59 @@
package com.phonebill.kosmock.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
/**
* KOS 상품 변경 응답 DTO
*/
@Data
@Builder
@Schema(description = "KOS 상품 변경 응답")
public class KosProductChangeResponse {
@Schema(description = "요청 ID", example = "REQ_20250108_002")
private String requestId;
@Schema(description = "처리 결과 코드", example = "0000")
private String resultCode;
@Schema(description = "처리 결과 메시지", example = "정상 처리되었습니다")
private String resultMessage;
@Schema(description = "변경 처리 정보")
private ChangeInfo changeInfo;
@Data
@Builder
@Schema(description = "변경 처리 정보")
public static class ChangeInfo {
@Schema(description = "회선번호", example = "01012345678")
private String lineNumber;
@Schema(description = "이전 상품 코드", example = "LTE-BASIC-001")
private String previousProductCode;
@Schema(description = "이전 상품명", example = "LTE 베이직 플랜")
private String previousProductName;
@Schema(description = "새로운 상품 코드", example = "5G-PREMIUM-001")
private String newProductCode;
@Schema(description = "새로운 상품명", example = "5G 프리미엄 플랜")
private String newProductName;
@Schema(description = "변경 적용 일자", example = "20250115")
private String effectiveDate;
@Schema(description = "변경 처리 상태", example = "SUCCESS")
private String changeStatus;
@Schema(description = "KOS 주문 번호", example = "KOS20250108001")
private String kosOrderNumber;
@Schema(description = "예상 처리 완료 시간", example = "2025-01-08T15:30:00")
private String estimatedCompletionTime;
}
}
@@ -0,0 +1,138 @@
package com.phonebill.kosmock.exception;
import com.phonebill.kosmock.dto.KosCommonResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import java.util.stream.Collectors;
/**
* 전역 예외 처리 핸들러
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* Bean Validation 실패 처리
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<KosCommonResponse<Object>> handleValidationException(MethodArgumentNotValidException e) {
String errorMessage = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
log.warn("입력값 검증 실패: {}", errorMessage);
return ResponseEntity.badRequest()
.body(KosCommonResponse.failure("9001", "입력값이 올바르지 않습니다: " + errorMessage));
}
/**
* Bean Binding 실패 처리
*/
@ExceptionHandler(BindException.class)
public ResponseEntity<KosCommonResponse<Object>> handleBindException(BindException e) {
String errorMessage = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
log.warn("데이터 바인딩 실패: {}", errorMessage);
return ResponseEntity.badRequest()
.body(KosCommonResponse.failure("9002", "데이터 바인딩에 실패했습니다: " + errorMessage));
}
/**
* HTTP 메시지 읽기 실패 처리
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<KosCommonResponse<Object>> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
log.warn("HTTP 메시지 읽기 실패", e);
return ResponseEntity.badRequest()
.body(KosCommonResponse.failure("9003", "요청 데이터 형식이 올바르지 않습니다"));
}
/**
* 메서드 인자 타입 불일치 처리
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<KosCommonResponse<Object>> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
log.warn("메서드 인자 타입 불일치: {}", e.getMessage());
return ResponseEntity.badRequest()
.body(KosCommonResponse.failure("9004", "요청 파라미터 타입이 올바르지 않습니다"));
}
/**
* 지원하지 않는 HTTP 메서드 처리
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<KosCommonResponse<Object>> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
log.warn("지원하지 않는 HTTP 메서드: {}", e.getMethod());
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(KosCommonResponse.failure("9005", "지원하지 않는 HTTP 메서드입니다"));
}
/**
* 핸들러를 찾을 수 없음 처리
*/
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<KosCommonResponse<Object>> handleNoHandlerFoundException(NoHandlerFoundException e) {
log.warn("핸들러를 찾을 수 없음: {}", e.getRequestURL());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(KosCommonResponse.failure("9006", "요청한 API를 찾을 수 없습니다"));
}
/**
* KOS Mock 특화 예외 처리
*/
@ExceptionHandler(KosMockException.class)
public ResponseEntity<KosCommonResponse<Object>> handleKosMockException(KosMockException e) {
log.warn("KOS Mock 예외 발생: {}", e.getMessage());
return ResponseEntity.ok()
.body(KosCommonResponse.failure(e.getErrorCode(), e.getMessage()));
}
/**
* 런타임 예외 처리
*/
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<KosCommonResponse<Object>> handleRuntimeException(RuntimeException e) {
log.error("런타임 예외 발생", e);
// Mock 환경에서는 특정 에러 메시지들을 그대로 반환
if (e.getMessage() != null && e.getMessage().contains("KOS 시스템")) {
return ResponseEntity.ok()
.body(KosCommonResponse.failure("8888", e.getMessage()));
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(KosCommonResponse.failure("9998", "처리 중 오류가 발생했습니다"));
}
/**
* 모든 예외 처리 (최종 catch)
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<KosCommonResponse<Object>> handleException(Exception e) {
log.error("예상하지 못한 예외 발생", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(KosCommonResponse.failure("9999", "시스템 오류가 발생했습니다"));
}
}
@@ -0,0 +1,23 @@
package com.phonebill.kosmock.exception;
/**
* KOS Mock 서비스 전용 예외
*/
public class KosMockException extends RuntimeException {
private final String errorCode;
public KosMockException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public KosMockException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
@@ -0,0 +1,253 @@
package com.phonebill.kosmock.service;
import com.phonebill.kosmock.config.MockConfig;
import com.phonebill.kosmock.data.*;
import com.phonebill.kosmock.dto.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Random;
import java.util.UUID;
/**
* KOS Mock 서비스
* 실제 KOS 시스템의 동작을 모방합니다.
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class KosMockService {
private final MockDataService mockDataService;
private final MockConfig mockConfig;
private final Random random = new Random();
/**
* 요금 조회 처리 (Mock)
*/
public KosBillInquiryResponse processBillInquiry(KosBillInquiryRequest request) {
log.info("KOS Mock 요금 조회 요청 처리 시작 - RequestId: {}, LineNumber: {}",
request.getRequestId(), request.getLineNumber());
// Mock 응답 지연 시뮬레이션
simulateProcessingDelay();
// Mock 실패 시뮬레이션
if (shouldSimulateFailure()) {
log.warn("KOS Mock 요금 조회 실패 시뮬레이션 - RequestId: {}", request.getRequestId());
throw new RuntimeException("KOS 시스템 일시적 오류");
}
// 고객 데이터 조회
MockCustomerData customerData = mockDataService.getCustomerData(request.getLineNumber());
if (customerData == null) {
log.warn("존재하지 않는 회선번호 - LineNumber: {}", request.getLineNumber());
return createBillInquiryErrorResponse(request.getRequestId(), "1001", "존재하지 않는 회선번호입니다");
}
// 회선 상태 확인
if (!"ACTIVE".equals(customerData.getLineStatus())) {
log.warn("비활성 회선 - LineNumber: {}, Status: {}",
request.getLineNumber(), customerData.getLineStatus());
return createBillInquiryErrorResponse(request.getRequestId(), "1002", "비활성 상태의 회선입니다");
}
// 청구월 설정 (없으면 현재월 사용)
String billingMonth = request.getBillingMonth();
if (billingMonth == null || billingMonth.isEmpty()) {
billingMonth = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
}
// 요금 데이터 조회
MockBillData billData = mockDataService.getBillData(request.getLineNumber(), billingMonth);
if (billData == null) {
log.warn("해당 청구월 요금 정보 없음 - LineNumber: {}, BillingMonth: {}",
request.getLineNumber(), billingMonth);
return createBillInquiryErrorResponse(request.getRequestId(), "1003", "해당 월 요금 정보가 없습니다");
}
// 성공 응답 생성
KosBillInquiryResponse response = KosBillInquiryResponse.builder()
.requestId(request.getRequestId())
.resultCode("0000")
.resultMessage("정상 처리되었습니다")
.billInfo(KosBillInquiryResponse.BillInfo.builder()
.lineNumber(billData.getLineNumber())
.billingMonth(billData.getBillingMonth())
.productCode(billData.getProductCode())
.productName(billData.getProductName())
.monthlyFee(billData.getMonthlyFee())
.usageFee(billData.getUsageFee())
.discountAmount(billData.getDiscountAmount())
.totalFee(billData.getTotalFee())
.dataUsage(billData.getDataUsage())
.voiceUsage(billData.getVoiceUsage())
.smsUsage(billData.getSmsUsage())
.billStatus(billData.getBillStatus())
.dueDate(billData.getDueDate())
.build())
.customerInfo(KosBillInquiryResponse.CustomerInfo.builder()
.customerName(customerData.getCustomerName())
.customerId(customerData.getCustomerId())
.operatorCode(customerData.getOperatorCode())
.lineStatus(customerData.getLineStatus())
.build())
.build();
log.info("KOS Mock 요금 조회 처리 완료 - RequestId: {}", request.getRequestId());
return response;
}
/**
* 상품 변경 처리 (Mock)
*/
public KosProductChangeResponse processProductChange(KosProductChangeRequest request) {
log.info("KOS Mock 상품 변경 요청 처리 시작 - RequestId: {}, LineNumber: {}, Target: {}",
request.getRequestId(), request.getLineNumber(), request.getTargetProductCode());
// Mock 응답 지연 시뮬레이션
simulateProcessingDelay();
// Mock 실패 시뮬레이션
if (shouldSimulateFailure()) {
log.warn("KOS Mock 상품 변경 실패 시뮬레이션 - RequestId: {}", request.getRequestId());
throw new RuntimeException("KOS 시스템 일시적 오류");
}
// 고객 데이터 조회
MockCustomerData customerData = mockDataService.getCustomerData(request.getLineNumber());
if (customerData == null) {
log.warn("존재하지 않는 회선번호 - LineNumber: {}", request.getLineNumber());
return createProductChangeErrorResponse(request.getRequestId(), "2001", "존재하지 않는 회선번호입니다");
}
// 회선 상태 확인
if (!"ACTIVE".equals(customerData.getLineStatus())) {
log.warn("비활성 회선 - LineNumber: {}, Status: {}",
request.getLineNumber(), customerData.getLineStatus());
return createProductChangeErrorResponse(request.getRequestId(), "2002", "비활성 상태의 회선입니다");
}
// 현재 상품과 타겟 상품 조회
MockProductData currentProduct = mockDataService.getProductData(request.getCurrentProductCode());
MockProductData targetProduct = mockDataService.getProductData(request.getTargetProductCode());
if (currentProduct == null || targetProduct == null) {
log.warn("존재하지 않는 상품 코드 - Current: {}, Target: {}",
request.getCurrentProductCode(), request.getTargetProductCode());
return createProductChangeErrorResponse(request.getRequestId(), "2003", "존재하지 않는 상품 코드입니다");
}
// 타겟 상품 판매 상태 확인
if (!"ACTIVE".equals(targetProduct.getStatus())) {
log.warn("판매 중단된 상품 - ProductCode: {}, Status: {}",
request.getTargetProductCode(), targetProduct.getStatus());
return createProductChangeErrorResponse(request.getRequestId(), "2004", "판매가 중단된 상품입니다");
}
// 통신사업자 일치 확인
if (!currentProduct.getOperatorCode().equals(targetProduct.getOperatorCode())) {
log.warn("다른 통신사업자 상품으로 변경 시도 - Current: {}, Target: {}",
currentProduct.getOperatorCode(), targetProduct.getOperatorCode());
return createProductChangeErrorResponse(request.getRequestId(), "2005", "다른 통신사업자 상품으로는 변경할 수 없습니다");
}
// KOS 주문 번호 생성
String kosOrderNumber = generateKosOrderNumber();
// 적용 일자 설정 (없으면 내일 사용)
String effectiveDate = request.getEffectiveDate();
if (effectiveDate == null || effectiveDate.isEmpty()) {
effectiveDate = LocalDateTime.now().plusDays(1).format(DateTimeFormatter.ofPattern("yyyyMMdd"));
}
// 성공 응답 생성
KosProductChangeResponse response = KosProductChangeResponse.builder()
.requestId(request.getRequestId())
.resultCode("0000")
.resultMessage("정상 처리되었습니다")
.changeInfo(KosProductChangeResponse.ChangeInfo.builder()
.lineNumber(request.getLineNumber())
.previousProductCode(currentProduct.getProductCode())
.previousProductName(currentProduct.getProductName())
.newProductCode(targetProduct.getProductCode())
.newProductName(targetProduct.getProductName())
.effectiveDate(effectiveDate)
.changeStatus("SUCCESS")
.kosOrderNumber(kosOrderNumber)
.estimatedCompletionTime(LocalDateTime.now().plusMinutes(30)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")))
.build())
.build();
// 처리 결과 저장
MockProcessingResult processingResult = MockProcessingResult.builder()
.requestId(request.getRequestId())
.processingType("PRODUCT_CHANGE")
.status("SUCCESS")
.message("상품 변경이 성공적으로 처리되었습니다")
.requestedAt(LocalDateTime.now())
.completedAt(LocalDateTime.now())
.processingTimeMs(mockConfig.getResponseDelay())
.build();
mockDataService.saveProcessingResult(request.getRequestId(), processingResult);
log.info("KOS Mock 상품 변경 처리 완료 - RequestId: {}, KosOrderNumber: {}",
request.getRequestId(), kosOrderNumber);
return response;
}
/**
* 처리 지연 시뮬레이션
*/
private void simulateProcessingDelay() {
try {
Thread.sleep(mockConfig.getResponseDelay());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("처리 지연 시뮬레이션 중단", e);
}
}
/**
* 실패 시뮬레이션 여부 결정
*/
private boolean shouldSimulateFailure() {
return random.nextDouble() < mockConfig.getFailureRate();
}
/**
* KOS 주문 번호 생성
*/
private String generateKosOrderNumber() {
return "KOS" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"))
+ String.format("%03d", random.nextInt(1000));
}
/**
* 요금 조회 오류 응답 생성
*/
private KosBillInquiryResponse createBillInquiryErrorResponse(String requestId, String errorCode, String errorMessage) {
return KosBillInquiryResponse.builder()
.requestId(requestId)
.resultCode(errorCode)
.resultMessage(errorMessage)
.build();
}
/**
* 상품 변경 오류 응답 생성
*/
private KosProductChangeResponse createProductChangeErrorResponse(String requestId, String errorCode, String errorMessage) {
return KosProductChangeResponse.builder()
.requestId(requestId)
.resultCode(errorCode)
.resultMessage(errorMessage)
.build();
}
}
@@ -0,0 +1,51 @@
spring:
# H2 데이터베이스 설정 (Mock 서비스용)
datasource:
url: jdbc:h2:mem:kosmock;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
driver-class-name: org.h2.Driver
# JPA 설정
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
# H2 Console (개발환경에서만)
h2:
console:
enabled: true
path: /h2-console
# Redis 설정
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:4}
# Mock 응답 시간 (개발 환경에서는 빠른 응답)
kos:
mock:
response-delay: 100 # milliseconds
failure-rate: 0.01 # 1% 실패율
# 로깅 레벨 (개발환경)
logging:
level:
com.phonebill.kosmock: DEBUG
org.springframework.web: DEBUG
org.springframework.data.redis: DEBUG
@@ -0,0 +1,27 @@
spring:
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
timeout: 2000ms
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
# Mock 응답 시간 (실제 KOS 시스템을 모방)
kos:
mock:
response-delay: 1000 # milliseconds (1초)
failure-rate: 0.05 # 5% 실패율
# 로깅 레벨 (운영환경)
logging:
level:
com.phonebill.kosmock: INFO
org.springframework.web: WARN
org.springframework.data.redis: WARN
file:
name: /var/log/kos-mock-service.log
@@ -0,0 +1,43 @@
spring:
application:
name: kos-mock-service
profiles:
active: dev
server:
port: ${SERVER_PORT:8080}
servlet:
context-path: /kos-mock
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when-authorized
metrics:
export:
prometheus:
enabled: true
logging:
level:
com.phonebill.kosmock: INFO
org.springframework.web: INFO
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n'
file: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n'
file:
name: logs/kos-mock-service.log
# Swagger/OpenAPI
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
show-actuator: true