실태조사 적합여부 log 추가

This commit is contained in:
2026-03-11 22:03:01 +09:00
parent c32c11496f
commit 141b735ccc
17 changed files with 272 additions and 96 deletions

View File

@@ -1,5 +1,6 @@
package com.kamco.cd.kamcoback.config;
import java.time.Duration;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -10,6 +11,10 @@ public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
return builder
.connectTimeout(Duration.ofSeconds(5))
.readTimeout(Duration.ofSeconds(120)) // read timeout
.build();
}
}

View File

@@ -25,6 +25,11 @@ public class ExternalHttpClient {
public <T> ExternalCallResult<T> call(
String url, HttpMethod method, Object body, HttpHeaders headers, Class<T> responseType) {
long start = System.currentTimeMillis();
log.info("[API-REQ] {} {}", method, url);
log.debug("[API-REQ] headers={}", headers);
// responseType 기반으로 Accept 동적 세팅
HttpHeaders resolvedHeaders = resolveHeaders(headers, responseType);
logRequestBody(body);
@@ -32,39 +37,67 @@ public class ExternalHttpClient {
HttpEntity<Object> entity = new HttpEntity<>(body, resolvedHeaders);
try {
// String: raw bytes -> UTF-8 string
if (responseType == String.class) {
ResponseEntity<byte[]> res = restTemplate.exchange(url, method, entity, byte[].class);
long elapsed = System.currentTimeMillis() - start;
log.info("[API-RES] status={} elapsed={}ms", res.getStatusCodeValue(), elapsed);
String raw =
(res.getBody() == null) ? null : new String(res.getBody(), StandardCharsets.UTF_8);
log.debug("[API-RES] body={}", raw);
@SuppressWarnings("unchecked")
T casted = (T) raw;
return new ExternalCallResult<>(res.getStatusCodeValue(), true, casted, null);
}
// byte[]: raw bytes로 받고, JSON이면 에러로 처리
// byte[]
if (responseType == byte[].class) {
ResponseEntity<byte[]> res = restTemplate.exchange(url, method, entity, byte[].class);
long elapsed = System.currentTimeMillis() - start;
log.info("[API-RES] status={} elapsed={}ms", res.getStatusCodeValue(), elapsed);
MediaType ct = res.getHeaders().getContentType();
byte[] bytes = res.getBody();
if (isJsonLike(ct)) {
String err = (bytes == null) ? null : new String(bytes, StandardCharsets.UTF_8);
log.warn("[API-RES] JSON error body={}", err);
return new ExternalCallResult<>(res.getStatusCodeValue(), false, null, err);
}
@SuppressWarnings("unchecked")
T casted = (T) bytes;
return new ExternalCallResult<>(res.getStatusCodeValue(), true, casted, null);
}
// DTO 등: 일반 역직렬화
// DTO
ResponseEntity<T> res = restTemplate.exchange(url, method, entity, responseType);
long elapsed = System.currentTimeMillis() - start;
log.info("[API-RES] status={} elapsed={}ms", res.getStatusCodeValue(), elapsed);
log.debug("[API-RES] body={}", res.getBody());
return new ExternalCallResult<>(res.getStatusCodeValue(), true, res.getBody(), null);
} catch (HttpStatusCodeException e) {
long elapsed = System.currentTimeMillis() - start;
log.error(
"[API-ERROR] status={} elapsed={}ms body={}",
e.getStatusCode().value(),
elapsed,
e.getResponseBodyAsString());
return new ExternalCallResult<>(
e.getStatusCode().value(), false, null, e.getResponseBodyAsString());
}

View File

@@ -1,5 +1,6 @@
package com.kamco.cd.kamcoback.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kamco.cd.kamcoback.gukyuin.dto.ChngDetectContDto.StbltResult;
import com.kamco.cd.kamcoback.gukyuin.dto.ChngDetectMastDto.LearnKeyDto;
import com.kamco.cd.kamcoback.gukyuin.dto.ChngDetectMastDto.RlbDtctDto;
@@ -11,6 +12,8 @@ import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@@ -24,88 +27,163 @@ public class GukYuinApiStbltJobService {
private final GukYuinStbltJobCoreService gukYuinStbltJobCoreService;
private final GukYuinApiService gukYuinApiService;
private final ObjectMapper objectMapper;
@Value("${spring.profiles.active}")
private String profile;
/**
* 실행중인 profile
*
* @return
*/
private boolean isLocalProfile() {
return "local".equalsIgnoreCase(profile);
}
public void runTask() {
findGukYuinEligibleForSurvey(null);
}
/** 국유인 연동 후, 실태조사 적합여부 확인하여 update */
public void findGukYuinEligibleForSurvey(LocalDate baseDate) {
long jobStart = System.currentTimeMillis();
String jobId = UUID.randomUUID().toString().substring(0, 8);
log.info("[Step 1-1] 국유인 연동 PNU 완료된 추론 회차 정보 가져오기 ");
log.info(" learn 테이블의 apply_status : {}", GukYuinStatus.PNU_COMPLETED.getId());
int totalTargetCount = 0;
int successCount = 0;
int failCount = 0;
int emptyCount = 0;
log.info("=== Jenkins Job : AM00-PNU-UPDATE-DATA 를 수행한 이후 진행해야 함 ===");
log.info("=== AM00-PNU-UPDATE-DATA 수행 내용");
log.info("=== 1) 추론결과 폴리곤 tb_map_sheet_anal_data_inference_geom 테이블에 pnu 갯수 업데이트");
log.info("=== 2) 추론 tb_map_sheet_learn 테이블의 apply_status = PNU_COMPLETED 로 업데이트");
log.info("* 이 과정이 수행되어야 아래 쿼리에서 조건을 조회할 수 있음");
log.info("[JOB-START][{}] GukYuinApiStbltJob 시작", jobId);
log.info("[JOB-START][{}] profile={}, BATCH_DATE={}", jobId, profile, baseDate);
try {
log.info("[Step 1-1][{}] 국유인 연동 PNU 완료된 추론 회차 정보 조회 시작", jobId);
log.info("[Step 1-1] 조회 대상 추론 회차 조건 : ");
log.info(
"===== 추론 tb_map_sheet_learn 테이블의 apply_status = {}",
GukYuinStatus.PNU_COMPLETED.getId());
log.info("===== 추론결과 폴리곤 tb_map_sheet_anal_data_inference_geom 테이블의 pnu > 0");
log.info("===== 추론결과 폴리곤 tb_map_sheet_anal_data_inference_geom 테이블의 fit_state is null");
List<LearnKeyDto> list =
gukYuinStbltJobCoreService.findGukYuinEligibleForSurveyList(
GukYuinStatus.PNU_COMPLETED.getId());
log.info("[Step 1-2] 국유인 연동 PNU 완료된 추론 회차 갯수 : {}", list == null ? 0 : list.size());
if (list.isEmpty()) {
log.info("[Step 1-3] 국유인 연동 PNU 완료된 추론 회차 갯수 없어서 return");
totalTargetCount = (list == null) ? 0 : list.size();
log.info("[Step 1-2][{}] 조회 대상 추론 회차 수={}", jobId, totalTargetCount);
if (list == null || list.isEmpty()) {
log.info("[Step 1-3][{}] 처리 대상 없음. job 종료", jobId);
return;
}
log.info("[Step 2-1] 추론 회차 list 로 for문 실행하기 ");
log.info("[Step 2-1][{}] 대상 회차 for문 처리 시작", jobId);
int index = 0;
for (LearnKeyDto dto : list) {
index++;
long itemStart = System.currentTimeMillis();
String uid = dto.getUid();
try {
log.info("[ITEM-START][{}][{}/{}] uid={} 처리 시작", jobId, index, totalTargetCount, uid);
String targetDate =
LocalDate.now(ZoneId.of("Asia/Seoul"))
.minusDays(1)
.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
log.info("[Step 2-2] 실태조사 적합여부 조회 날짜 확인 : {}", targetDate);
if (baseDate != null) { // 파라미터가 있으면
log.info("[Step 2-2][{}] 기본은 어제 날짜 targetDate={}", jobId, targetDate);
if (baseDate != null) {
targetDate = baseDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
log.info("[Step 2-3] 수동호출 baseDate 가 있을 경우, 실태조사 적합여부 조회 날짜 확인 : {}", targetDate);
log.info("[Step 2-3][{}] baseDate 파라미터가 있으면 targetDate={}", jobId, targetDate);
}
log.info("[Step 3-1] 국유인 실태조사 적합여부 API 호출 시작 ");
log.info(" === 값 확인 - uid : {}", dto.getUid());
log.info(" === 값 확인 - targetDate : {}", targetDate);
RlbDtctDto result = gukYuinApiService.findRlbDtctList(dto.getUid(), targetDate, "Y");
log.info(
"[Step 3-1][{}] 국유인 실태조사 적합여부 API 호출 시작 uid={}, targetDate={}",
jobId,
uid,
targetDate);
if (result == null || result.getResult() == null || result.getResult().isEmpty()) {
log.info("[GUKYUIN] empty result chnDtctId={}", dto.getUid());
log.info("=== 국유인 API 조회 결과 없어서 continue");
long apiStart = System.currentTimeMillis();
RlbDtctDto result = gukYuinApiService.findRlbDtctList(uid, targetDate, "Y");
long apiEnd = System.currentTimeMillis();
try {
log.info("[Step 3-2] API result JSON : {}", objectMapper.writeValueAsString(result));
} catch (Exception e) {
log.error("result json convert error", e);
}
int resultSize =
(result == null || result.getResult() == null) ? 0 : result.getResult().size();
log.info(
"[Step 3-2][{}] API 호출 종료 uid={}, resultSize={}, elapsed={}ms",
jobId,
uid,
resultSize,
(apiEnd - apiStart));
if (result == null) {
log.warn("[ITEM-WARN][{}] API result 가 null 입니다. uid={}", jobId, uid);
emptyCount++;
continue;
}
log.info("[Step 4-1] 국유인 실태조사 적합여부 result 값으로 데이터 업데이트");
log.info(" === 데이터 갯수 : {}", result.getResult() == null ? 0 : result.getResult().size());
for (RlbDtctMastDto stbltDto : result.getResult()) {
log.info("[Step 4-2] 국유인 실태조사 적합여부 결과 가져오기");
String resultUid = stbltDto.getChnDtctObjtId();
log.info(" == 테이블 tb_pnu 에 적합여부 리턴 결과를 upsert 진행, 객체 uid : {}", resultUid);
gukYuinStbltJobCoreService.updateGukYuinEligibleForSurvey(resultUid, stbltDto);
if (result.getResult() == null || result.getResult().isEmpty()) {
log.info("[ITEM-EMPTY][{}] API 조회 결과 없음 uid={}", jobId, uid);
emptyCount++;
continue;
}
log.info(
"[Step 4-1][{}] API 결과 기반 tb_pnu upsert 시작 uid={}, count={}",
jobId,
uid,
result.getResult().size());
int upsertCount = 0;
for (RlbDtctMastDto stbltDto : result.getResult()) {
String resultUid = stbltDto.getChnDtctObjtId();
log.debug(
"[Step 4-2][{}] tb_pnu upsert 대상 resultUid={}, stbltYn={}, incyCd={}",
jobId,
resultUid,
stbltDto.getStbltYn(),
stbltDto.getIncyCd());
gukYuinStbltJobCoreService.updateGukYuinEligibleForSurvey(resultUid, stbltDto);
upsertCount++;
}
log.info(
"[Step 4-2][{}] tb_pnu upsert 완료 uid={}, upsertCount={}", jobId, uid, upsertCount);
log.info("[Step 4-3] 1개 폴리곤 객체 objtId 에 여러 pnu가 존재할 수 있음.");
log.info("1개 폴리곤 객체 objtId에 여러 pnu의 적합여부가 들어온다면 ");
log.info("폴리곤 객체 objtId 에 해당하는 pnu의 적합여부가 하나라도 Y가 있다면 Y (적합)");
log.info("Y가 없다면 N (부적합) 으로 판단한다.");
Map<String, StbltResult> resultMap =
result.getResult().stream()
.collect(Collectors.groupingBy(RlbDtctMastDto::getChnDtctObjtId))
.entrySet()
.stream()
.collect(
Collectors.toMap(
Map.Entry::getKey,
.map(
e -> {
String resultUid = e.getKey();
List<RlbDtctMastDto> pnuList = e.getValue();
log.info("[Step 4-3] 국유인 실태조사 적합여부 업데이트 값을 객체 uid 기준으로 DTO 생성");
boolean hasY = pnuList.stream().anyMatch(v -> "Y".equals(v.getStbltYn()));
log.debug(
"[Step 4-3][{}] 객체별 결과 집계 resultUid={}, rowCount={}",
jobId,
resultUid,
pnuList == null ? 0 : pnuList.size());
boolean hasY = pnuList.stream().anyMatch(v -> "Y".equals(v.getStbltYn()));
String fitYn = hasY ? "Y" : "N";
log.info("=== 적합여부 fitYn={}", fitYn);
RlbDtctMastDto selected =
hasY
@@ -118,22 +196,82 @@ public class GukYuinApiStbltJobService {
.findFirst()
.orElse(null);
log.info(" === selected DTO : {}", selected);
log.info(
"객체 objtId 에 해당하는 pnuList 중에서 적합여부: {}에 해당하는 첫번째 값을 조회한다. => selected",
fitYn);
if (selected == null) {
log.info(" === selected NULL");
return null; // 방어 코드
log.warn("[Step 4-3][{}] selected null resultUid={}", jobId, resultUid);
return null;
}
return new StbltResult(
fitYn, selected.getIncyCd(), selected.getIncyRsnCont());
}));
log.debug(
"[Step 4-3][{}] resultUid={}, fitYn={}, incyCd={}, incyRsnCont={}",
jobId,
resultUid,
fitYn,
selected.getIncyCd(),
selected.getIncyRsnCont());
log.info("[Step 4-4] 국유인 실태조사 적합여부, 사유, 내용을 inference_geom 테이블에 update");
resultMap.forEach(gukYuinStbltJobCoreService::updateGukYuinObjectStbltYn);
return Map.entry(
resultUid,
new StbltResult(
fitYn, selected.getIncyCd(), selected.getIncyRsnCont()));
})
.filter(Objects::nonNull)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
log.info(
"[Step 4-4][{}] inference_geom 에 적합여부 update 시작 uid={}, objectCount={}",
jobId,
uid,
resultMap.size());
int objectUpdateCount = 0;
for (Map.Entry<String, StbltResult> entry : resultMap.entrySet()) {
log.info("result_uid: {}", entry.getKey());
log.info("StbltResult: {}", entry.getValue());
gukYuinStbltJobCoreService.updateGukYuinObjectStbltYn(entry.getKey(), entry.getValue());
objectUpdateCount++;
}
log.info(
"[Step 4-5][{}] inference_geom update 완료 uid={}, objectUpdateCount={}",
jobId,
uid,
objectUpdateCount);
successCount++;
log.info(
"[ITEM-END][{}][{}/{}] uid={} 처리 완료 elapsed={}ms",
jobId,
index,
totalTargetCount,
uid,
(System.currentTimeMillis() - itemStart));
} catch (Exception e) {
log.error("[GUKYUIN] failed uid={}", dto.getUid(), e);
failCount++;
log.error(
"[ITEM-ERROR][{}][{}/{}] uid={} 처리 실패 elapsed={}ms",
jobId,
index,
totalTargetCount,
uid,
(System.currentTimeMillis() - itemStart),
e);
}
}
} finally {
log.info(
"[JOB-END][{}] totalTargetCount={}, successCount={}, failCount={}, emptyCount={}, totalElapsed={}ms",
jobId,
totalTargetCount,
successCount,
failCount,
emptyCount,
(System.currentTimeMillis() - jobStart));
}
}
}

View File

@@ -1,8 +1,8 @@
spring:
datasource:
url: jdbc:postgresql://localhost:5432/kamco_cds
url: jdbc:postgresql://192.168.2.127:15432/kamco_cds
username: kamco_cds
password: kamco_cds
password: kamco_cds_Q!W@E#R$
batch:
job:
name: exportGeoJsonJob # 기본 실행 Job 지정