@ -15,6 +15,7 @@ import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl ;
import org.springframework.stereotype.Service ;
import org.springframework.transaction.annotation.Transactional ;
import org.springframework.transaction.support.TransactionTemplate ;
import org.springframework.web.multipart.MultipartFile ;
import java.io.BufferedReader ;
@ -26,6 +27,11 @@ import java.util.ArrayList;
import java.util.HashMap ;
import java.util.List ;
import java.util.Map ;
import java.util.concurrent.CompletableFuture ;
import java.util.concurrent.ExecutorService ;
import java.util.concurrent.Executors ;
import java.util.concurrent.TimeUnit ;
import java.util.stream.Collectors ;
/ * *
* 자 동 차 과 태 료 대 상 미 필 Service 구 현 체
@ -38,6 +44,7 @@ public class CarFfnlgTrgtIncmpServiceImpl extends EgovAbstractServiceImpl implem
private final CarFfnlgTrgtIncmpMapper mapper ;
private final CarFfnlgPrnParseConfig parseConfig ;
private final ComparisonOmService comparisonOmService ;
private final TransactionTemplate transactionTemplate ;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter . ofPattern ( "yyyyMMdd" ) ;
@ -396,127 +403,202 @@ public class CarFfnlgTrgtIncmpServiceImpl extends EgovAbstractServiceImpl implem
}
/ * *
* 선 택 된 목 록 에 대 해 API 호 출 및 기 본 정 보 / 등 록 원 부 비 교
* 선 택 된 목 록 에 대 해 API 호 출 및 기 본 정 보 / 등 록 원 부 비 교 ( 병 렬 처 리 )
* 미 필 의 경 우 부 과 일 자 = 검 사 유 효 기 간 종 료 일 + OM_DAY_CD 의 D 코 드 값 ( 146 일 )
* 변 경 사 항 : 각 건 을 병 렬 로 처 리 하 며 , 개 별 트 랜 잭 션 으 로 관 리 됩 니 다 .
* 실 패 한 건 만 실 패 처 리 되 고 , 성 공 한 건 은 정 상 처 리 됩 니 다 .
*
* @param targetList 선 택 된 과 태 료 대 상 목 록
* @return 비 교 결 과
* /
@Override
@Transactional
public Map < String , Object > compareWithApi ( List < Map < String , String > > targetList ) {
log . info ( "========== 미필 API 호출 및 비교 시작 ==========" ) ;
log . info ( "========== 미필 API 호출 및 비교 시작 (병렬처리) ==========") ;
log . info ( "선택된 데이터 건수: {}" , targetList ! = null ? targetList . size ( ) : 0 ) ;
if ( targetList = = null | | targetList . isEmpty ( ) ) {
throw new IllegalArgumentException ( "선택된 데이터가 없습니다." ) ;
}
List < Map < String , Object > > compareResults = new ArrayList < > ( ) ;
int successCount = 0 ;
int productUseCount = 0 ;
int transferCount = 0 ;
int normalCount = 0 ;
// 가산일 조회 (OM_DAY_CD 코드의 D값)
// 가산일 조회 (OM_DAY_CD 코드의 D값) - 병렬처리 전에 미리 조회
String plusDayStr = mapper . selectOmDayPlusDay ( ) ;
int plusDay = 146 ; // 기본값
final int plusDay ;
if ( plusDayStr ! = null & & ! plusDayStr . isEmpty ( ) ) {
int parsed = 146 ;
try {
p lusDay = Integer . parseInt ( plusDayStr ) ;
parsed = Integer . parseInt ( plusDayStr ) ;
} catch ( NumberFormatException e ) {
log . warn ( "가산일 파싱 실패, 기본값 146 사용: {}" , plusDayStr ) ;
}
plusDay = parsed ;
} else {
plusDay = 146 ; // 기본값
}
log . info ( "부과일자 가산일: {}일" , plusDay ) ;
for ( Map < String , String > target : targetList ) {
String carFfnlgTrgtIncmpId = target . get ( "carFfnlgTrgtIncmpId" ) ;
String vhclno = target . get ( "vhclno" ) ;
String inspVldPrd = target . get ( "inspVldPrd" ) ;
log . info ( "처리 중 - 차량번호: {}, 검사유효기간: {}" , vhclno , inspVldPrd ) ;
Map < String , Object > compareResult = new HashMap < > ( ) ;
compareResult . put ( "carFfnlgTrgtIncmpId" , carFfnlgTrgtIncmpId ) ;
compareResult . put ( "vhclno" , vhclno ) ;
try {
// 1. 기존 데이터 조회
CarFfnlgTrgtIncmpVO existingData = new CarFfnlgTrgtIncmpVO ( ) ;
existingData . setCarFfnlgTrgtIncmpId ( carFfnlgTrgtIncmpId ) ;
existingData = mapper . selectOne ( existingData ) ;
if ( existingData = = null ) {
String errorMsg = String . format ( "기존 데이터를 찾을 수 없습니다. 차량번호: %s" , vhclno ) ;
log . error ( errorMsg ) ;
throw new MessageException ( errorMsg ) ;
}
// 2. 처리상태 검증 - 접수상태(01)가 아닌 경우 API 호출 불가
if ( ! TaskPrcsSttsConstants . TASK_PRCS_STTS_CD_01_RCPT . equals ( existingData . getTaskPrcsSttsCd ( ) ) ) {
String errorMsg = String . format ( "접수 상태(01)인 데이터만 API 호출이 가능합니다. 차량번호: %s, 현재 상태: %s" ,
vhclno , existingData . getTaskPrcsSttsCd ( ) ) ;
log . error ( errorMsg ) ;
throw new MessageException ( errorMsg ) ;
}
// 3. 검사유효기간에서 부과일자 계산 (종료일 + 가산일)
String levyCrtrYmd = calculateLevyCrtrYmdFromInspVldPrd ( inspVldPrd , plusDay ) ;
existingData . setLevyCrtrYmd ( levyCrtrYmd ) ;
log . info ( "부과일자 계산 완료 - 검사유효기간: {}, 부과일자: {}" , inspVldPrd , levyCrtrYmd ) ;
// I/O 작업이므로 CPU 코어 수의 2배로 스레드 풀 생성
int threadPoolSize = Runtime . getRuntime ( ) . availableProcessors ( ) * 2 ;
ExecutorService executor = Executors . newFixedThreadPool ( threadPoolSize ) ;
// 4. 비교 로직 실행
String statusCode = comparisonOmService . executeComparison ( existingData ) ;
log . info ( "병렬처리 스레드 풀 크기: {}" , threadPoolSize ) ;
// 결과 처리
if ( statusCode ! = null ) {
if ( TaskPrcsSttsConstants . TASK_PRCS_STTS_CD_02_PRODUCT_USE . equals ( statusCode ) ) {
try {
// 병렬로 각 건 처리
List < CompletableFuture < Map < String , Object > > > futures = targetList . stream ( )
. map ( target - > CompletableFuture . supplyAsync ( ( ) - > processOneTarget ( target , plusDay ) , executor ) )
. collect ( Collectors . toList ( ) ) ;
// 모든 작업 완료 대기
CompletableFuture . allOf ( futures . toArray ( new CompletableFuture [ 0 ] ) ) . join ( ) ;
// 결과 수집
List < Map < String , Object > > compareResults = futures . stream ( )
. map ( CompletableFuture : : join )
. collect ( Collectors . toList ( ) ) ;
// 통계 집계
int successCount = 0 ;
int failCount = 0 ;
int productUseCount = 0 ;
int transferCount = 0 ;
int normalCount = 0 ;
for ( Map < String , Object > result : compareResults ) {
Boolean success = ( Boolean ) result . get ( "success" ) ;
if ( Boolean . TRUE . equals ( success ) ) {
successCount + + ;
String processStatus = ( String ) result . get ( "processStatus" ) ;
if ( "상품용" . equals ( processStatus ) ) {
productUseCount + + ;
compareResult . put ( "processStatus" , "상품용" ) ;
compareResult . put ( "message" , "상품용으로 처리되었습니다." ) ;
} else if ( TaskPrcsSttsConstants . TASK_PRCS_STTS_CD_03_TRANSFER . equals ( statusCode ) ) {
} else if ( "이첩" . equals ( processStatus ) ) {
transferCount + + ;
compareResult . put ( "processStatus" , "이첩" ) ;
compareResult . put ( "message" , "이첩으로 처리되었습니다." ) ;
} else if ( TaskPrcsSttsConstants . TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED . equals ( statusCode ) ) {
} else if ( "내사종결" . equals ( processStatus ) | | "정상" . equals ( processStatus ) ) {
normalCount + + ;
compareResult . put ( "processStatus" , "내사종결" ) ;
compareResult . put ( "message" , "내사종결로 처리되었습니다." ) ;
} else {
normalCount + + ;
compareResult . put ( "processStatus" , "기타" ) ;
compareResult . put ( "message" , "기타 상태로 처리되었습니다." ) ;
}
compareResult . put ( "success" , true ) ;
successCount + + ;
} else {
normalCount + + ;
compareResult . put ( "success" , true ) ;
compareResult . put ( "message" , "정상 처리되었습니다." ) ;
compareResult . put ( "processStatus" , "정상" ) ;
successCount + + ;
failCount + + ;
}
} catch ( Exception e ) {
log . error ( "데이터 비교 중 오류 발생 - 차량번호: {}, 전체 롤백 처리" , vhclno , e ) ;
throw new MessageException ( e . getMessage ( ) , e ) ;
}
compareResults . add ( compareResult ) ;
Map < String , Object > resultData = new HashMap < > ( ) ;
resultData . put ( "compareResults" , compareResults ) ;
resultData . put ( "totalCount" , targetList . size ( ) ) ;
resultData . put ( "successCount" , successCount ) ;
resultData . put ( "failCount" , failCount ) ;
resultData . put ( "productUseCount" , productUseCount ) ;
resultData . put ( "transferCount" , transferCount ) ;
resultData . put ( "normalCount" , normalCount ) ;
log . info ( "========== 미필 API 호출 및 비교 완료 (병렬처리) ==========" ) ;
log . info ( "전체: {}건, 성공: {}건, 실패: {}건, 상품용: {}건, 이첩: {}건, 정상: {}건" ,
targetList . size ( ) , successCount , failCount , productUseCount , transferCount , normalCount ) ;
return resultData ;
} finally {
// ExecutorService 종료
executor . shutdown ( ) ;
try {
if ( ! executor . awaitTermination ( 120 , TimeUnit . SECONDS ) ) {
executor . shutdownNow ( ) ;
}
} catch ( InterruptedException e ) {
executor . shutdownNow ( ) ;
Thread . currentThread ( ) . interrupt ( ) ;
}
}
}
/ * *
* 개 별 건 을 처 리 ( 개 별 트 랜 잭 션 )
*
* @param target 처 리 할 대 상 데 이 터
* @param plusDay 부 과 일 자 가 산 일
* @return 처 리 결 과
* /
private Map < String , Object > processOneTarget ( Map < String , String > target , int plusDay ) {
String carFfnlgTrgtIncmpId = target . get ( "carFfnlgTrgtIncmpId" ) ;
String vhclno = target . get ( "vhclno" ) ;
String inspVldPrd = target . get ( "inspVldPrd" ) ;
log . info ( "처리 중 - 차량번호: {}, 검사유효기간: {}" , vhclno , inspVldPrd ) ;
Map < String , Object > compareResult = new HashMap < > ( ) ;
compareResult . put ( "carFfnlgTrgtIncmpId" , carFfnlgTrgtIncmpId ) ;
compareResult . put ( "vhclno" , vhclno ) ;
try {
// 개별 트랜잭션으로 실행
String statusCode = transactionTemplate . execute ( status - > {
try {
// 1. 기존 데이터 조회
CarFfnlgTrgtIncmpVO existingData = new CarFfnlgTrgtIncmpVO ( ) ;
existingData . setCarFfnlgTrgtIncmpId ( carFfnlgTrgtIncmpId ) ;
existingData = mapper . selectOne ( existingData ) ;
if ( existingData = = null ) {
String errorMsg = String . format ( "기존 데이터를 찾을 수 없습니다. 차량번호: %s" , vhclno ) ;
log . error ( errorMsg ) ;
throw new MessageException ( errorMsg ) ;
}
Map < String , Object > resultData = new HashMap < > ( ) ;
resultData . put ( "compareResults" , compareResults ) ;
resultData . put ( "totalCount" , targetList . size ( ) ) ;
resultData . put ( "successCount" , successCount ) ;
resultData . put ( "failCount" , 0 ) ;
resultData . put ( "productUseCount" , productUseCount ) ;
resultData . put ( "transferCount" , transferCount ) ;
resultData . put ( "normalCount" , normalCount ) ;
// 2. 처리상태 검증 - 접수상태(01)가 아닌 경우 API 호출 불가
if ( ! TaskPrcsSttsConstants . TASK_PRCS_STTS_CD_01_RCPT . equals ( existingData . getTaskPrcsSttsCd ( ) ) ) {
String errorMsg = String . format ( "접수 상태(01)인 데이터만 API 호출이 가능합니다. 차량번호: %s, 현재 상태: %s" ,
vhclno , existingData . getTaskPrcsSttsCd ( ) ) ;
log . error ( errorMsg ) ;
throw new MessageException ( errorMsg ) ;
}
// 3. 검사유효기간에서 부과일자 계산 (종료일 + 가산일)
String levyCrtrYmd = calculateLevyCrtrYmdFromInspVldPrd ( inspVldPrd , plusDay ) ;
existingData . setLevyCrtrYmd ( levyCrtrYmd ) ;
log . info ( "부과일자 계산 완료 - 검사유효기간: {}, 부과일자: {}" , inspVldPrd , levyCrtrYmd ) ;
// 4. 비교 로직 실행
return comparisonOmService . executeComparison ( existingData ) ;
log . info ( "========== 미필 API 호출 및 비교 완료 ==========" ) ;
log . info ( "성공: {}건, 상품용: {}건, 이첩: {}건, 정상: {}건" ,
successCount , productUseCount , transferCount , normalCount ) ;
} catch ( Exception e ) {
// 트랜잭션 롤백
status . setRollbackOnly ( ) ;
throw e ;
}
} ) ;
// 결과 처리
if ( statusCode ! = null ) {
// 비교 규칙이 적용됨
if ( TaskPrcsSttsConstants . TASK_PRCS_STTS_CD_02_PRODUCT_USE . equals ( statusCode ) ) {
compareResult . put ( "processStatus" , "상품용" ) ;
compareResult . put ( "message" , "상품용으로 처리되었습니다." ) ;
} else if ( TaskPrcsSttsConstants . TASK_PRCS_STTS_CD_03_TRANSFER . equals ( statusCode ) ) {
compareResult . put ( "processStatus" , "이첩" ) ;
compareResult . put ( "message" , "이첩으로 처리되었습니다." ) ;
} else if ( TaskPrcsSttsConstants . TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED . equals ( statusCode ) ) {
compareResult . put ( "processStatus" , "내사종결" ) ;
compareResult . put ( "message" , "내사종결로 처리되었습니다." ) ;
} else {
compareResult . put ( "processStatus" , "기타" ) ;
compareResult . put ( "message" , "기타 상태로 처리되었습니다." ) ;
}
compareResult . put ( "success" , true ) ;
} else {
// 정상 처리 (비교 로직에 해당 안됨)
compareResult . put ( "success" , true ) ;
compareResult . put ( "message" , "정상 처리되었습니다." ) ;
compareResult . put ( "processStatus" , "정상" ) ;
}
} catch ( Exception e ) {
log . error ( "데이터 비교 중 오류 발생 - 차량번호: {}" , vhclno , e ) ;
compareResult . put ( "success" , false ) ;
compareResult . put ( "message" , "처리 실패: " + e . getMessage ( ) ) ;
compareResult . put ( "processStatus" , "실패" ) ;
}
return resultData ;
return compa reRe sult;
}
/ * *