From 0b45f42528619851b861263a4ff576d5b22cd27e Mon Sep 17 00:00:00 2001 From: teddy Date: Mon, 12 Jan 2026 13:42:25 +0900 Subject: [PATCH] =?UTF-8?q?[KC-108]=20=EB=B6=84=EC=84=9D=EB=8F=84=EC=97=BD?= =?UTF-8?q?=20ai=20api=ED=98=B8=EC=B6=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 - .../cd/kamcoback/config/WebClientConfig.java | 24 ------- .../resttemplate/ExternalHttpClient.java | 30 +++++++++ .../resttemplate/RestTemplateConfig.java | 21 ++++++ .../config/resttemplate/RetryInterceptor.java | 49 ++++++++++++++ .../inference/dto/InferenceResultDto.java | 14 +++- .../service/InferenceResultService.java | 66 ++++++++----------- .../core/InferenceResultCoreService.java | 16 ++++- .../scene/MapInkx5kRepositoryImpl.java | 3 +- .../cd/kamcoback/scene/dto/MapInkxMngDto.java | 2 - 10 files changed, 155 insertions(+), 71 deletions(-) delete mode 100644 src/main/java/com/kamco/cd/kamcoback/config/WebClientConfig.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/config/resttemplate/ExternalHttpClient.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/config/resttemplate/RestTemplateConfig.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/config/resttemplate/RetryInterceptor.java diff --git a/build.gradle b/build.gradle index be11e839..7546b58a 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,6 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-webflux' compileOnly 'org.projectlombok:lombok' runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/com/kamco/cd/kamcoback/config/WebClientConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/WebClientConfig.java deleted file mode 100644 index 3270a4ec..00000000 --- a/src/main/java/com/kamco/cd/kamcoback/config/WebClientConfig.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.kamco.cd.kamcoback.config; - -import io.netty.channel.ChannelOption; -import java.time.Duration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.netty.http.client.HttpClient; - -@Configuration -public class WebClientConfig { - - @Bean - public WebClient webClient() { - - HttpClient httpClient = - HttpClient.create() - .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000) - .responseTimeout(Duration.ofSeconds(5)); - - return WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).build(); - } -} diff --git a/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/ExternalHttpClient.java b/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/ExternalHttpClient.java new file mode 100644 index 00000000..3f418dda --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/ExternalHttpClient.java @@ -0,0 +1,30 @@ +package com.kamco.cd.kamcoback.config.resttemplate; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +@RequiredArgsConstructor +public class ExternalHttpClient { + + private final RestTemplate restTemplate; + + public ResponseEntity exchange( + String url, HttpMethod method, Object body, HttpHeaders headers) { + HttpEntity entity = new HttpEntity<>(body, headers); + return restTemplate.exchange(url, method, entity, String.class); + } + + public ExternalCallResult call(String url, HttpMethod method, Object body, HttpHeaders headers) { + ResponseEntity res = exchange(url, method, body, headers); + int code = res.getStatusCodeValue(); + return new ExternalCallResult(code, code >= 200 && code < 300, res.getBody()); + } + + public record ExternalCallResult(int statusCode, boolean success, String body) {} +} diff --git a/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/RestTemplateConfig.java b/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/RestTemplateConfig.java new file mode 100644 index 00000000..d1a4995e --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/RestTemplateConfig.java @@ -0,0 +1,21 @@ +package com.kamco.cd.kamcoback.config.resttemplate; + +import java.time.Duration; +import java.util.List; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate(RestTemplateBuilder builder) { + return builder + .connectTimeout(Duration.ofSeconds(2)) + .readTimeout(Duration.ofSeconds(3)) + .additionalInterceptors(List.of(new RetryInterceptor())) + .build(); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/RetryInterceptor.java b/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/RetryInterceptor.java new file mode 100644 index 00000000..e8e74ba7 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/config/resttemplate/RetryInterceptor.java @@ -0,0 +1,49 @@ +package com.kamco.cd.kamcoback.config.resttemplate; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; + +public class RetryInterceptor implements ClientHttpRequestInterceptor { + + private static final int MAX_RETRY = 3; + private static final long WAIT_MILLIS = 3000; + + @Override + public ClientHttpResponse intercept( + HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { + + IOException lastException = null; + + for (int attempt = 1; attempt <= MAX_RETRY; attempt++) { + try { + // HTTP 응답을 받으면(2xx/4xx/5xx 포함) 그대로 반환 + return execution.execute(request, body); + + } catch (IOException e) { + // 네트워크/타임아웃 등 I/O 예외만 재시도 + lastException = e; + } + + // 마지막 시도가 아니면 대기 + if (attempt < MAX_RETRY) { + sleep(); + } + } + + // 마지막 예외를 그대로 던져서 원인이 로그에 남게 함 + throw lastException; + } + + private void sleep() throws IOException { + try { + TimeUnit.MILLISECONDS.sleep(WAIT_MILLIS); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new IOException("Retry interrupted", ie); + } + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/inference/dto/InferenceResultDto.java b/src/main/java/com/kamco/cd/kamcoback/inference/dto/InferenceResultDto.java index c5c50915..bad92295 100644 --- a/src/main/java/com/kamco/cd/kamcoback/inference/dto/InferenceResultDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/inference/dto/InferenceResultDto.java @@ -170,7 +170,7 @@ public class InferenceResultDto { @NotNull private Integer targetYyyy; - @Schema(description = "분석대상 도엽 - 전체(ALL), 부분(PART)", example = "PART") + @Schema(description = "분석대상 도엽 - 전체(ALL), 부분(PART)", example = "ALL") @NotBlank @EnumValid(enumClass = MapSheetScope.class, message = "분석대상 도엽 옵션은 '전체', '부분' 만 사용 가능합니다.") private String mapSheetScope; @@ -184,8 +184,16 @@ public class InferenceResultDto { @Schema( description = "5k 도협 번호 목록", - example = "[\"37914034\",\"37914024\",\"37914023\",\"37914014\",\"37914005\",\"37914004\"]") + example = "[{\"mapSheetNum\":37914034,\"mapSheetName\":\"죽변\"}]") @NotNull - private List mapSheetNum; + private List mapSheetNum; + } + + @Getter + @Setter + public static class MapSheetNumDto { + + private String mapSheetNum; + private String mapSheetName; } } diff --git a/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java b/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java index bc9aac3a..9f01f9a7 100644 --- a/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java +++ b/src/main/java/com/kamco/cd/kamcoback/inference/service/InferenceResultService.java @@ -1,11 +1,14 @@ package com.kamco.cd.kamcoback.inference.service; import com.kamco.cd.kamcoback.common.exception.CustomApiException; +import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient; +import com.kamco.cd.kamcoback.config.resttemplate.ExternalHttpClient.ExternalCallResult; import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto; import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.Dashboard; import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.Detail; import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.MapSheet; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetNumDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetScope; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.ResultList; import com.kamco.cd.kamcoback.inference.dto.InferenceSendDto; @@ -22,12 +25,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; @Service @Log4j2 @@ -38,7 +41,7 @@ public class InferenceResultService { private final InferenceResultCoreService inferenceResultCoreService; private final MapSheetMngCoreService mapSheetMngCoreService; private final ModelMngCoreService modelMngCoreService; - private final WebClient webClient; + private final ExternalHttpClient externalHttpClient; @Value("${inference.url}") private String inferenceUrl; @@ -60,34 +63,41 @@ public class InferenceResultService { */ @Transactional public UUID saveInferenceInfo(InferenceResultDto.RegReq req) { - + // TODO tif 없으면 전년도 파일 조회 쿼리 추가해야함 // 전체일때 5k도협 가져오기 + List mapSheetNum = new ArrayList<>(); + if (MapSheetScope.ALL.getId().equals(req.getMapSheetScope())) { Search5kReq mapReq = new Search5kReq(); mapReq.setUseInference("USE"); - List mapSheetIds = new ArrayList<>(); + MapSheetNumDto mapSheetNumDto = new MapSheetNumDto(); inferenceResultCoreService .findByMapidList(mapReq) .forEach( mapInkx5kEntity -> { - mapSheetIds.add(mapInkx5kEntity.getMapidcdNo()); + mapSheetNumDto.setMapSheetNum(mapInkx5kEntity.getMapidcdNo()); + mapSheetNumDto.setMapSheetName(mapInkx5kEntity.getMapidNm()); + mapSheetNum.add(mapSheetNumDto); }); - req.setMapSheetNum(mapSheetIds); } + // 추론 테이블 저장 UUID uuid = inferenceResultCoreService.saveInferenceInfo(req); - // TODO tif 없으면 전년도 파일 조회 쿼리 추가해야함 - // TODO 도엽 개수를 target 기준으로 맞춰야함 - this.getSceneInference(String.valueOf(req.getCompareYyyy()), req.getMapSheetNum()); - this.getSceneInference(String.valueOf(req.getTargetYyyy()), req.getMapSheetNum()); + List mapSheetNumList = new ArrayList<>(); + for (MapSheetNumDto mapSheetDto : mapSheetNum) { + mapSheetNumList.add(mapSheetDto.getMapSheetNum()); + } + + this.getSceneInference(String.valueOf(req.getCompareYyyy()), mapSheetNumList); + this.getSceneInference(String.valueOf(req.getTargetYyyy()), mapSheetNumList); InferenceSendDto m1 = this.getModelInfo(req.getModel1Uuid()); InferenceSendDto m2 = this.getModelInfo(req.getModel2Uuid()); InferenceSendDto m3 = this.getModelInfo(req.getModel3Uuid()); - // - // ensureAccepted(m1); + + ensureAccepted(m1); // ensureAccepted(m2); // ensureAccepted(m3); @@ -99,33 +109,15 @@ public class InferenceResultService { * * @param dto */ - private Mono inferenceSend(InferenceSendDto dto) { - return webClient - .post() - .uri("http://localhost:8080/test") // inferenceUrl - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(dto) - .retrieve() - .toBodilessEntity() - .map(res -> res.getStatusCode().is2xxSuccessful()) - .doOnNext(ok -> log.info("발송결과={}", ok)) - .onErrorReturn(false); - } - - /** - * api 호출 실패시 예외처리 - * - * @param dto - */ private void ensureAccepted(InferenceSendDto dto) { - Boolean ok = - this.inferenceSend(dto) - .timeout(java.time.Duration.ofSeconds(3)) - .onErrorReturn(false) // 타임아웃/통신오류도 실패 처리 - .block(java.time.Duration.ofSeconds(4)); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); - if (!Boolean.TRUE.equals(ok)) { + ExternalCallResult result = + externalHttpClient.call(inferenceUrl, HttpMethod.POST, dto, headers); + + if (!result.success()) { throw new CustomApiException("BAD_GATEWAY", HttpStatus.BAD_GATEWAY); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java b/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java index 52339a4e..276f10d1 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/core/InferenceResultCoreService.java @@ -6,6 +6,7 @@ import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto; import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.Dashboard; import com.kamco.cd.kamcoback.inference.dto.InferenceDetailDto.MapSheet; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto; +import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.MapSheetNumDto; import com.kamco.cd.kamcoback.inference.dto.InferenceResultDto.ResultList; import com.kamco.cd.kamcoback.postgres.entity.MapInkx5kEntity; import com.kamco.cd.kamcoback.postgres.entity.MapSheetAnalDataInferenceEntity; @@ -59,6 +60,14 @@ public class InferenceResultCoreService { * @param req */ public UUID saveInferenceInfo(InferenceResultDto.RegReq req) { + String mapSheetName = + req.getMapSheetNum().get(0).getMapSheetName() + " 외 " + req.getMapSheetNum().size() + "건"; + + if (req.getMapSheetNum().size() == 1) { + mapSheetName = + req.getMapSheetNum().get(0).getMapSheetName() + " " + req.getMapSheetNum().size() + "건"; + } + MapSheetLearnEntity mapSheetLearnEntity = new MapSheetLearnEntity(); mapSheetLearnEntity.setTitle(req.getTitle()); mapSheetLearnEntity.setM1ModelUuid(req.getModel1Uuid()); @@ -69,19 +78,20 @@ public class InferenceResultCoreService { mapSheetLearnEntity.setMapSheetScope(req.getMapSheetScope()); mapSheetLearnEntity.setDetectOption(req.getDetectOption()); mapSheetLearnEntity.setCreatedUid(userUtil.getId()); + mapSheetLearnEntity.setMapSheetCnt(mapSheetName); + mapSheetLearnEntity.setDetectingCnt((long) req.getMapSheetNum().size()); // learn 테이블 저장 MapSheetLearnEntity savedLearn = mapSheetLearnRepository.save(mapSheetLearnEntity); final int CHUNK = 1000; List buffer = new ArrayList<>(CHUNK); - List mapSheetNumList = req.getMapSheetNum(); // learn 도엽 저장 - for (String mapSheetNum : mapSheetNumList) { + for (MapSheetNumDto mapSheetNum : req.getMapSheetNum()) { MapSheetLearn5kEntity e = new MapSheetLearn5kEntity(); e.setLearn(savedLearn); - e.setMapSheetNum(Long.parseLong(mapSheetNum)); + e.setMapSheetNum(Long.parseLong(mapSheetNum.getMapSheetNum())); e.setCreatedUid(userUtil.getId()); buffer.add(e); diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryImpl.java b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryImpl.java index c7ad0018..6bc1906b 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryImpl.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/repository/scene/MapInkx5kRepositoryImpl.java @@ -166,10 +166,11 @@ public class MapInkx5kRepositoryImpl extends QuerydslRepositorySupport mapInkx5kEntity.fid, mapInkx5kEntity.mapidcdNo, mapInkx5kEntity.mapidNm, - mapInkx5kEntity.geom, mapInkx5kEntity.useInference)) .from(mapInkx5kEntity) .where(builder) + // TODO limit 제거 필요 + .limit(100) .fetch(); } diff --git a/src/main/java/com/kamco/cd/kamcoback/scene/dto/MapInkxMngDto.java b/src/main/java/com/kamco/cd/kamcoback/scene/dto/MapInkxMngDto.java index 188acbc0..7dd84b69 100644 --- a/src/main/java/com/kamco/cd/kamcoback/scene/dto/MapInkxMngDto.java +++ b/src/main/java/com/kamco/cd/kamcoback/scene/dto/MapInkxMngDto.java @@ -15,7 +15,6 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.locationtech.jts.geom.Geometry; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -69,7 +68,6 @@ public class MapInkxMngDto { private Integer fid; private String mapidcdNo; private String mapidNm; - private Geometry geom; private CommonUseStatus useInference; }