mirror of
https://github.com/cna-bootcamp/phonebill.git
synced 2026-06-12 19:49:10 +00:00
release
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="kos-mock" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="env">
|
||||
<map>
|
||||
<!-- Server Configuration -->
|
||||
<entry key="SERVER_PORT" value="8084" />
|
||||
<entry key="SPRING_PROFILES_ACTIVE" value="dev" />
|
||||
|
||||
<!-- Logging Configuration -->
|
||||
<entry key="LOG_FILE" value="logs/kos-mock.log" />
|
||||
<entry key="LOG_LEVEL_ROOT" value="INFO" />
|
||||
<entry key="LOG_LEVEL_APP" value="DEBUG" />
|
||||
|
||||
<!-- Redis Configuration (Optional - KOS Mock에서는 캐시 기능 비활성화 상태) -->
|
||||
<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="4" />
|
||||
|
||||
<!-- KOS Mock Specific Settings -->
|
||||
<entry key="KOS_MOCK_DELAY_MIN" value="100" />
|
||||
<entry key="KOS_MOCK_DELAY_MAX" value="500" />
|
||||
<entry key="KOS_MOCK_ERROR_RATE" value="0.05" />
|
||||
|
||||
<!-- Development Settings -->
|
||||
<entry key="CORS_ALLOWED_ORIGINS" value="*" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="kos-mock:bootRun" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" value="-Dspring.profiles.active=dev -Dfile.encoding=UTF-8 -Duser.timezone=Asia/Seoul" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -0,0 +1,165 @@
|
||||
# KOS Mock Service
|
||||
|
||||
KT 통신사 시스템(KOS-Order)을 모방한 Mock 서비스입니다.
|
||||
|
||||
## 개요
|
||||
|
||||
KOS Mock Service는 통신요금 관리 서비스의 다른 마이크로서비스들이 외부 시스템과의 연동을 테스트할 수 있도록 하는 내부 Mock 서비스입니다.
|
||||
|
||||
## 주요 기능
|
||||
|
||||
### 1. 요금 조회 Mock API
|
||||
- 고객의 통신요금 정보 조회
|
||||
- 회선번호 기반 요금 데이터 제공
|
||||
- 다양한 오류 상황 시뮬레이션
|
||||
|
||||
### 2. 상품 변경 Mock API
|
||||
- 고객의 통신상품 변경 처리
|
||||
- 상품 변경 가능성 검증
|
||||
- KOS 주문 번호 생성
|
||||
|
||||
### 3. Mock 데이터 관리
|
||||
- 테스트용 고객 데이터 제공
|
||||
- 요금제별 Mock 상품 데이터
|
||||
- 청구월별 요금 이력 데이터
|
||||
|
||||
## 기술 스택
|
||||
|
||||
- **Framework**: Spring Boot 3.2
|
||||
- **Language**: Java 17
|
||||
- **Documentation**: Swagger/OpenAPI 3.0
|
||||
- **Cache**: Redis (선택적)
|
||||
- **Test**: JUnit 5, MockMvc
|
||||
|
||||
## API 엔드포인트
|
||||
|
||||
### 기본 정보
|
||||
- **Base URL**: `http://localhost:8080/kos-mock`
|
||||
- **API Version**: v1
|
||||
- **Content-Type**: `application/json`
|
||||
|
||||
### 주요 API
|
||||
|
||||
#### 1. 요금 조회 API
|
||||
```http
|
||||
POST /api/v1/kos/bill/inquiry
|
||||
```
|
||||
|
||||
**요청 예시:**
|
||||
```json
|
||||
{
|
||||
"lineNumber": "01012345678",
|
||||
"billingMonth": "202501",
|
||||
"requestId": "REQ_20250108_001",
|
||||
"requestorId": "BILL_SERVICE"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 상품 변경 API
|
||||
```http
|
||||
POST /api/v1/kos/product/change
|
||||
```
|
||||
|
||||
**요청 예시:**
|
||||
```json
|
||||
{
|
||||
"lineNumber": "01012345678",
|
||||
"currentProductCode": "LTE-BASIC-001",
|
||||
"targetProductCode": "5G-PREMIUM-001",
|
||||
"requestId": "REQ_20250108_002",
|
||||
"requestorId": "PRODUCT_SERVICE",
|
||||
"changeReason": "고객 요청에 의한 상품 변경"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 서비스 상태 체크 API
|
||||
```http
|
||||
GET /api/v1/kos/health
|
||||
```
|
||||
|
||||
## Mock 데이터
|
||||
|
||||
### 테스트용 회선번호
|
||||
- `01012345678` - 김테스트 (5G 프리미엄)
|
||||
- `01087654321` - 이샘플 (5G 스탠다드)
|
||||
- `01055554444` - 박데모 (LTE 프리미엄)
|
||||
- `01099998888` - 최모의 (LTE 베이직)
|
||||
- `01000000000` - 비활성사용자 (정지 상태)
|
||||
|
||||
### 상품 코드
|
||||
- `5G-PREMIUM-001` - 5G 프리미엄 플랜 (89,000원)
|
||||
- `5G-STANDARD-001` - 5G 스탠다드 플랜 (69,000원)
|
||||
- `LTE-PREMIUM-001` - LTE 프리미엄 플랜 (59,000원)
|
||||
- `LTE-BASIC-001` - LTE 베이직 플랜 (39,000원)
|
||||
- `3G-OLD-001` - 3G 레거시 플랜 (판매 중단)
|
||||
|
||||
## 실행 방법
|
||||
|
||||
### 1. 개발 환경에서 실행
|
||||
```bash
|
||||
./gradlew bootRun
|
||||
```
|
||||
|
||||
### 2. JAR 파일로 실행
|
||||
```bash
|
||||
./gradlew build
|
||||
java -jar build/libs/kos-mock-service-1.0.0.jar
|
||||
```
|
||||
|
||||
### 3. 특정 프로파일로 실행
|
||||
```bash
|
||||
java -jar kos-mock-service-1.0.0.jar --spring.profiles.active=prod
|
||||
```
|
||||
|
||||
## 설정
|
||||
|
||||
### Mock 응답 지연 설정
|
||||
```yaml
|
||||
kos:
|
||||
mock:
|
||||
response-delay: 1000 # 밀리초
|
||||
failure-rate: 0.05 # 5% 실패율
|
||||
```
|
||||
|
||||
### Redis 설정 (선택적)
|
||||
```yaml
|
||||
spring:
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
```
|
||||
|
||||
## 테스트
|
||||
|
||||
### 단위 테스트 실행
|
||||
```bash
|
||||
./gradlew test
|
||||
```
|
||||
|
||||
### API 테스트
|
||||
Swagger UI를 통해 API를 직접 테스트할 수 있습니다:
|
||||
- URL: http://localhost:8080/kos-mock/swagger-ui.html
|
||||
|
||||
## 모니터링
|
||||
|
||||
### Health Check
|
||||
- URL: http://localhost:8080/kos-mock/actuator/health
|
||||
|
||||
### Metrics
|
||||
- URL: http://localhost:8080/kos-mock/actuator/metrics
|
||||
|
||||
## 주의사항
|
||||
|
||||
1. **내부 시스템 전용**: 이 서비스는 내부 테스트 목적으로만 사용하세요.
|
||||
2. **보안 설정 간소화**: Mock 서비스이므로 보안 설정이 간소화되어 있습니다.
|
||||
3. **데이터 지속성**: Mock 데이터는 메모리에만 저장되며, 재시작 시 초기화됩니다.
|
||||
4. **성능 제한**: 실제 부하 테스트 용도로는 적합하지 않습니다.
|
||||
|
||||
## 문의
|
||||
|
||||
KOS Mock Service 관련 문의사항이 있으시면 개발팀으로 연락해 주세요.
|
||||
|
||||
- 개발팀: dev@phonebill.com
|
||||
- 문서 버전: v1.0.0
|
||||
- 최종 업데이트: 2025-01-08
|
||||
@@ -0,0 +1,54 @@
|
||||
// kos-mock 모듈
|
||||
// 루트 build.gradle의 subprojects 블록에서 공통 설정 적용됨
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom annotationProcessor
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Spring Boot
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-cache'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
|
||||
// Database (Mock 서비스용 H2)
|
||||
runtimeOnly 'com.h2database:h2'
|
||||
|
||||
// Swagger/OpenAPI
|
||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
|
||||
|
||||
// JSON Processing
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
|
||||
|
||||
// Commons
|
||||
implementation project(':common')
|
||||
|
||||
// Lombok
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
|
||||
// Test
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
|
||||
testImplementation 'org.testcontainers:junit-jupiter'
|
||||
testImplementation 'org.testcontainers:testcontainers:1.19.3'
|
||||
|
||||
// Configuration Processor
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
// JAR 파일 이름 설정
|
||||
jar {
|
||||
archiveBaseName = 'kos-mock-service'
|
||||
archiveVersion = version
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.phonebill.kosmock;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
/**
|
||||
* KOS Mock Application 통합 테스트
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
class KosMockApplicationTest {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
// Spring Context가 정상적으로 로드되는지 확인
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.phonebill.kosmock.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.phonebill.kosmock.dto.KosBillInquiryRequest;
|
||||
import com.phonebill.kosmock.dto.KosProductChangeRequest;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* KOS Mock Controller 테스트
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class KosMockControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Test
|
||||
@DisplayName("서비스 상태 체크 API 테스트")
|
||||
void healthCheck() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/kos/health"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.resultCode").value("0000"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("요금 조회 API 성공 테스트")
|
||||
void inquireBill_Success() throws Exception {
|
||||
KosBillInquiryRequest request = new KosBillInquiryRequest();
|
||||
request.setLineNumber("01012345678");
|
||||
request.setBillingMonth("202501");
|
||||
request.setRequestId("TEST_REQ_001");
|
||||
request.setRequestorId("TEST_SERVICE");
|
||||
|
||||
mockMvc.perform(post("/api/v1/kos/bill/inquiry")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("요금 조회 API 입력값 검증 실패 테스트")
|
||||
void inquireBill_ValidationFailure() throws Exception {
|
||||
KosBillInquiryRequest request = new KosBillInquiryRequest();
|
||||
// 필수값 누락
|
||||
request.setBillingMonth("202501");
|
||||
|
||||
mockMvc.perform(post("/api/v1/kos/bill/inquiry")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.success").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("상품 변경 API 성공 테스트")
|
||||
void changeProduct_Success() throws Exception {
|
||||
KosProductChangeRequest request = new KosProductChangeRequest();
|
||||
request.setLineNumber("01012345678");
|
||||
request.setCurrentProductCode("LTE-BASIC-001");
|
||||
request.setTargetProductCode("5G-PREMIUM-001");
|
||||
request.setRequestId("TEST_REQ_002");
|
||||
request.setRequestorId("TEST_SERVICE");
|
||||
request.setChangeReason("테스트 상품 변경");
|
||||
|
||||
mockMvc.perform(post("/api/v1/kos/product/change")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Mock 설정 조회 API 테스트")
|
||||
void getMockConfig() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/kos/mock/config"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.resultCode").value("0000"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
spring:
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
timeout: 1000ms
|
||||
|
||||
# 테스트용 Mock 설정
|
||||
kos:
|
||||
mock:
|
||||
response-delay: 0 # 테스트에서는 지연 없음
|
||||
failure-rate: 0.0 # 테스트에서는 실패 시뮬레이션 없음
|
||||
debug-mode: true
|
||||
|
||||
# 로깅 레벨 (테스트환경)
|
||||
logging:
|
||||
level:
|
||||
com.phonebill.kosmock: DEBUG
|
||||
org.springframework.web: INFO
|
||||
org.springframework.test: INFO
|
||||
Reference in New Issue
Block a user