Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/eapim-admin.git
eapim-admin CI / build (push) Has been cancelled
eapim-admin CI / build (push) Has been cancelled
This commit is contained in:
@@ -189,7 +189,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<input type="text" id="keywordList" style="width:100%" placeholder="쉼표(,)로 구분하여 입력">
|
<input type="text" id="keywordList" style="width:100%" placeholder="쉼표(,)로 구분하여 입력">
|
||||||
<div class="help-text" style="color:#666; font-size:12px; margin-top:5px;">
|
<div class="help-text" style="color:#666; font-size:12px; margin-top:5px;">
|
||||||
예시: #인증, #대출, #예금
|
예시: 인증, 대출, 예금
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -327,14 +327,61 @@
|
|||||||
}, 'json').fail(ajaxFail);
|
}, 'json').fail(ajaxFail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 캐시 Reload ──
|
||||||
|
var reloadModal;
|
||||||
|
var reloadBtnHtml;
|
||||||
|
|
||||||
function reloadPortalCache() {
|
function reloadPortalCache() {
|
||||||
|
var $btn = $('#btnReloadCache');
|
||||||
|
if ($btn.prop('disabled')) return;
|
||||||
|
if (reloadBtnHtml === undefined) reloadBtnHtml = $btn.html();
|
||||||
|
$btn.prop('disabled', true)
|
||||||
|
.html('<span class="spinner-border spinner-border-sm"></span> 리로드 중...');
|
||||||
|
|
||||||
$.post(url, { cmd: 'TRANSACTION_RELOAD' }, function (result) {
|
$.post(url, { cmd: 'TRANSACTION_RELOAD' }, function (result) {
|
||||||
if (result && result.success) {
|
showReloadResult(result);
|
||||||
alert('포탈 메뉴 캐시 리로드 완료 (항목 ' + result.itemCount + '건, ' + result.reloadedAt + ')');
|
}, 'json').fail(ajaxFail).always(function () {
|
||||||
} else {
|
$btn.prop('disabled', false).html(reloadBtnHtml);
|
||||||
alert('캐시 리로드 실패: ' + (result ? result.message : '알 수 없는 오류'));
|
});
|
||||||
}
|
}
|
||||||
}, 'json').fail(ajaxFail);
|
|
||||||
|
function showReloadResult(result) {
|
||||||
|
if (!result) {
|
||||||
|
alert('캐시 리로드 실패: 알 수 없는 오류');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var servers = result.servers || [];
|
||||||
|
var $summary = $('#reloadSummary');
|
||||||
|
$summary.removeClass('alert-success alert-danger alert-warning')
|
||||||
|
.addClass(result.success ? 'alert-success'
|
||||||
|
: (result.successCount > 0 ? 'alert-warning' : 'alert-danger'))
|
||||||
|
.text(result.message || '');
|
||||||
|
|
||||||
|
var rows = '';
|
||||||
|
servers.forEach(function (s) {
|
||||||
|
var badge = s.success
|
||||||
|
? '<span class="badge text-bg-success">성공</span>'
|
||||||
|
: '<span class="badge text-bg-danger">실패</span>';
|
||||||
|
var detail = s.success
|
||||||
|
? ('항목 ' + (s.itemCount == null ? '-' : s.itemCount) + '건'
|
||||||
|
+ (s.reloadedAt ? ' / ' + escapeHtml(String(s.reloadedAt)) : ''))
|
||||||
|
: escapeHtml(String(s.message || '-'));
|
||||||
|
rows += '<tr>'
|
||||||
|
+ '<td class="text-center">' + badge + '</td>'
|
||||||
|
+ '<td style="word-break:break-all;">' + escapeHtml(String(s.url || '')) + '</td>'
|
||||||
|
+ '<td>' + detail + '</td>'
|
||||||
|
+ '<td class="text-end">' + (s.elapsedMs == null ? '-' : s.elapsedMs + 'ms') + '</td>'
|
||||||
|
+ '</tr>';
|
||||||
|
});
|
||||||
|
if (rows === '') {
|
||||||
|
rows = '<tr><td colspan="4" class="text-center text-muted">'
|
||||||
|
+ '호출 대상 서버가 없습니다. 아래 설정 안내를 확인하세요.</td></tr>';
|
||||||
|
}
|
||||||
|
$('#reloadResultBody').html(rows);
|
||||||
|
|
||||||
|
if (!reloadModal) reloadModal = new bootstrap.Modal(document.getElementById('reloadResultModal'));
|
||||||
|
reloadModal.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
function ajaxFail(xhr) {
|
function ajaxFail(xhr) {
|
||||||
@@ -453,7 +500,7 @@
|
|||||||
<button type="button" class="btn btn-sm btn-outline-danger" level="W" onclick="initializeMenus()">
|
<button type="button" class="btn btn-sm btn-outline-danger" level="W" onclick="initializeMenus()">
|
||||||
<i class="bi bi-arrow-counterclockwise"></i> 메뉴 초기화</button>
|
<i class="bi bi-arrow-counterclockwise"></i> 메뉴 초기화</button>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" level="W" onclick="reloadPortalCache()">
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnReloadCache" level="W" onclick="reloadPortalCache()">
|
||||||
<i class="bi bi-arrow-repeat"></i> 캐시 Reload</button>
|
<i class="bi bi-arrow-repeat"></i> 캐시 Reload</button>
|
||||||
<button type="button" class="btn btn-sm btn-success" level="W" onclick="savePlacements()">
|
<button type="button" class="btn btn-sm btn-success" level="W" onclick="savePlacements()">
|
||||||
<i class="bi bi-save"></i> 저장</button>
|
<i class="bi bi-save"></i> 저장</button>
|
||||||
@@ -463,6 +510,12 @@
|
|||||||
드래그로 위치를 조절한 뒤 <strong>저장</strong> 버튼으로 일괄 반영합니다.
|
드래그로 위치를 조절한 뒤 <strong>저장</strong> 버튼으로 일괄 반영합니다.
|
||||||
기본 항목([기본] 배지)은 [-] 시 <strong>미배치</strong>로만 이동하고, 커스텀 항목은 미배치 시 <strong>삭제</strong>됩니다.
|
기본 항목([기본] 배지)은 [-] 시 <strong>미배치</strong>로만 이동하고, 커스텀 항목은 미배치 시 <strong>삭제</strong>됩니다.
|
||||||
저장 후 <strong>캐시 Reload</strong> 를 실행해야 포탈에 즉시 반영됩니다(미실행 시 TTL 경과 후 반영).
|
저장 후 <strong>캐시 Reload</strong> 를 실행해야 포탈에 즉시 반영됩니다(미실행 시 TTL 경과 후 반영).
|
||||||
|
<br>
|
||||||
|
캐시 Reload 대상 서버는 포탈 프로퍼티 <code>Portal / portal.internal.menu-reload-url</code> 로 설정합니다.
|
||||||
|
포탈이 <strong>다중 서버(이중화)</strong> 로 기동된 경우 모든 서버의 리로드 URL 을
|
||||||
|
<strong>콤마(,) 또는 줄바꿈</strong> 으로 나열해야 전체 서버에 반영됩니다
|
||||||
|
(예: <code>http://10.0.0.11:39130/internal/menu/reload, http://10.0.0.12:39130/internal/menu/reload</code>).
|
||||||
|
서버별 요청 타임아웃은 <strong>3초</strong> 이며, 실행 결과는 서버별로 표시됩니다.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="menu-board">
|
<div class="menu-board">
|
||||||
@@ -544,6 +597,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 캐시 Reload 결과 -->
|
||||||
|
<div class="modal fade" id="reloadResultModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">포탈 메뉴 캐시 Reload 결과</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert py-2 px-3" id="reloadSummary" style="font-size:13px;"></div>
|
||||||
|
<table class="table table-sm table-bordered align-middle" style="font-size:12px;">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width:70px;" class="text-center">결과</th>
|
||||||
|
<th>서버 URL</th>
|
||||||
|
<th style="width:38%;">상세</th>
|
||||||
|
<th style="width:80px;" class="text-end">소요</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="reloadResultBody"></tbody>
|
||||||
|
</table>
|
||||||
|
<div class="text-muted" style="font-size:12px;">
|
||||||
|
대상 서버는 포탈 프로퍼티 <code>Portal / portal.internal.menu-reload-url</code> 에
|
||||||
|
콤마(,) 또는 줄바꿈으로 나열합니다. 서버별 타임아웃 3초, 전 서버 병렬 호출입니다.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">닫기</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div><!-- end content_middle -->
|
</div><!-- end content_middle -->
|
||||||
</div><!-- end right_box -->
|
</div><!-- end right_box -->
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,22 +1,40 @@
|
|||||||
package com.eactive.eai.rms.onl.apim.portalmenu;
|
package com.eactive.eai.rms.onl.apim.portalmenu;
|
||||||
|
|
||||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.apache.http.client.config.RequestConfig;
|
||||||
|
import org.apache.http.impl.client.HttpClientBuilder;
|
||||||
import org.apache.log4j.Logger;
|
import org.apache.log4j.Logger;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* eapim-portal 메뉴 캐시 리로드 클라이언트 (admin → portal 내부 호출).
|
* eapim-portal 메뉴 캐시 리로드 클라이언트 (admin → portal 내부 호출).
|
||||||
*
|
*
|
||||||
* <p>호출 URL 은 PTL_PROPERTY {@code Portal / portal.internal.menu-reload-url} 로 관리한다
|
* <p>호출 URL 은 PTL_PROPERTY {@code Portal / portal.internal.menu-reload-url} 로 관리한다
|
||||||
* (기본 {@code http://127.0.0.1:39130/internal/menu/reload}).
|
* (기본 {@code http://127.0.0.1:39130/internal/menu/reload}).
|
||||||
|
* 포탈이 다중 인스턴스로 기동되는 환경을 위해 <b>콤마/세미콜론/줄바꿈</b> 으로 여러 URL 을 나열할 수 있으며,
|
||||||
|
* 나열된 모든 서버로 병렬 호출한다. 각 호출의 connect/read 타임아웃은 {@value #TIMEOUT_MS}ms 이다.
|
||||||
* portal 측은 {@code menu.internal.allow-ips} 허용 IP 목록으로 요청을 검증한다.
|
* portal 측은 {@code menu.internal.allow-ips} 허용 IP 목록으로 요청을 검증한다.
|
||||||
* 오류는 결과 Map 으로 감싸 화면에 사유를 표시한다 — 예외를 전파하지 않는다.</p>
|
* 오류는 결과 Map 으로 감싸 화면에 서버별 사유를 표시한다 — 예외를 전파하지 않는다.</p>
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class PortalMenuCacheClient {
|
public class PortalMenuCacheClient {
|
||||||
@@ -26,50 +44,230 @@ public class PortalMenuCacheClient {
|
|||||||
static final String PROP_GROUP = "Portal";
|
static final String PROP_GROUP = "Portal";
|
||||||
static final String PROP_RELOAD_URL = "portal.internal.menu-reload-url";
|
static final String PROP_RELOAD_URL = "portal.internal.menu-reload-url";
|
||||||
static final String DEFAULT_RELOAD_URL = "http://127.0.0.1:39130/internal/menu/reload";
|
static final String DEFAULT_RELOAD_URL = "http://127.0.0.1:39130/internal/menu/reload";
|
||||||
|
static final String PROP_DESCRIPTION =
|
||||||
|
"eapim-portal 메뉴 캐시 리로드 내부 URL. 다중 서버는 콤마(,)/세미콜론(;)/줄바꿈으로 구분";
|
||||||
|
|
||||||
|
/** 서버 1대당 connect/read 타임아웃(ms) */
|
||||||
|
static final int TIMEOUT_MS = 3000;
|
||||||
|
/** 병렬 호출 전체 대기 여유분(ms) — 스레드 시작/응답 파싱 지연 흡수 */
|
||||||
|
private static final int OVERALL_SLACK_MS = 2000;
|
||||||
|
/** 동시 호출 스레드 상한 */
|
||||||
|
private static final int MAX_PARALLELISM = 10;
|
||||||
|
|
||||||
private final PortalPropertyService portalPropertyService;
|
private final PortalPropertyService portalPropertyService;
|
||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public PortalMenuCacheClient(PortalPropertyService portalPropertyService, RestTemplate restTemplate) {
|
public PortalMenuCacheClient(PortalPropertyService portalPropertyService) {
|
||||||
|
this(portalPropertyService, createRestTemplateWithTimeout(TIMEOUT_MS));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 테스트용 — 타임아웃이 설정된 RestTemplate 을 직접 주입한다. */
|
||||||
|
PortalMenuCacheClient(PortalPropertyService portalPropertyService, RestTemplate restTemplate) {
|
||||||
this.portalPropertyService = portalPropertyService;
|
this.portalPropertyService = portalPropertyService;
|
||||||
this.restTemplate = restTemplate;
|
this.restTemplate = restTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static RestTemplate createRestTemplateWithTimeout(int timeoutMs) {
|
||||||
|
RequestConfig requestConfig = RequestConfig.custom()
|
||||||
|
.setConnectionRequestTimeout(timeoutMs)
|
||||||
|
.setConnectTimeout(timeoutMs)
|
||||||
|
.setSocketTimeout(timeoutMs)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpClient httpClient = HttpClientBuilder.create()
|
||||||
|
.setMaxConnTotal(MAX_PARALLELISM * 2)
|
||||||
|
.setMaxConnPerRoute(MAX_PARALLELISM)
|
||||||
|
.setDefaultRequestConfig(requestConfig)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
|
||||||
|
factory.setHttpClient(httpClient);
|
||||||
|
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
restTemplate.setRequestFactory(factory);
|
||||||
|
return restTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {@code {success, message, itemCount?, reloadedAt?}}
|
* 설정된 모든 포탈 서버의 메뉴 캐시를 리로드한다.
|
||||||
|
*
|
||||||
|
* @return {@code {success, message, total, successCount, failCount, itemCount?, reloadedAt?,
|
||||||
|
* servers:[{url, success, message, itemCount?, reloadedAt?, elapsedMs}]}}
|
||||||
|
* — {@code success} 는 <b>모든</b> 서버가 성공한 경우에만 true
|
||||||
*/
|
*/
|
||||||
public Map<String, Object> reload() {
|
public Map<String, Object> reload() {
|
||||||
Map<String, Object> result = new LinkedHashMap<>();
|
List<String> urls = resolveUrls();
|
||||||
String url = resolveUrl();
|
List<Map<String, Object>> servers = callAll(urls);
|
||||||
try {
|
|
||||||
@SuppressWarnings("unchecked")
|
int successCount = 0;
|
||||||
ResponseEntity<Map> response = restTemplate.postForEntity(url, null, Map.class);
|
for (Map<String, Object> server : servers) {
|
||||||
Map<String, Object> body = response.getBody();
|
if (Boolean.TRUE.equals(server.get("success"))) {
|
||||||
if (response.getStatusCode().is2xxSuccessful() && body != null && "OK".equals(body.get("result"))) {
|
successCount++;
|
||||||
result.put("success", true);
|
}
|
||||||
result.put("message", "포탈 메뉴 캐시 리로드 완료");
|
}
|
||||||
result.put("itemCount", body.get("itemCount"));
|
int failCount = servers.size() - successCount;
|
||||||
result.put("reloadedAt", body.get("reloadedAt"));
|
|
||||||
} else {
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
result.put("success", false);
|
result.put("success", failCount == 0);
|
||||||
result.put("message", "포탈 응답 오류: " + (body == null ? response.getStatusCode() : body));
|
result.put("message", summarize(servers.size(), successCount, failCount));
|
||||||
|
result.put("total", servers.size());
|
||||||
|
result.put("successCount", successCount);
|
||||||
|
result.put("failCount", failCount);
|
||||||
|
result.put("servers", servers);
|
||||||
|
|
||||||
|
// 단일 서버 시절 응답과의 호환 — 첫 성공 서버의 값을 최상위에도 노출
|
||||||
|
for (Map<String, Object> server : servers) {
|
||||||
|
if (Boolean.TRUE.equals(server.get("success"))) {
|
||||||
|
result.put("itemCount", server.get("itemCount"));
|
||||||
|
result.put("reloadedAt", server.get("reloadedAt"));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("포탈 메뉴 캐시 리로드 실패 - url: " + url, e);
|
|
||||||
result.put("success", false);
|
|
||||||
result.put("message", "포탈 호출 실패(" + url + "): " + e.getMessage());
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolveUrl() {
|
private String summarize(int total, int successCount, int failCount) {
|
||||||
|
if (total == 0) {
|
||||||
|
return "리로드 대상 URL 이 설정되지 않았습니다 (" + PROP_GROUP + " / " + PROP_RELOAD_URL + ")";
|
||||||
|
}
|
||||||
|
if (failCount == 0) {
|
||||||
|
return "포탈 메뉴 캐시 리로드 완료 (" + successCount + "/" + total + " 서버)";
|
||||||
|
}
|
||||||
|
if (successCount == 0) {
|
||||||
|
return "포탈 메뉴 캐시 리로드 실패 (0/" + total + " 서버)";
|
||||||
|
}
|
||||||
|
return "포탈 메뉴 캐시 리로드 일부 실패 (" + successCount + "/" + total + " 서버 성공)";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 모든 URL 로 병렬 호출. 반환 순서는 {@code urls} 순서와 동일하다. */
|
||||||
|
private List<Map<String, Object>> callAll(List<String> urls) {
|
||||||
|
if (urls.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
if (urls.size() == 1) {
|
||||||
|
return Collections.singletonList(callOne(urls.get(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Callable<Map<String, Object>>> tasks = new ArrayList<>(urls.size());
|
||||||
|
for (final String url : urls) {
|
||||||
|
tasks.add(new Callable<Map<String, Object>>() {
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> call() {
|
||||||
|
return callOne(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ExecutorService executor = Executors.newFixedThreadPool(Math.min(urls.size(), MAX_PARALLELISM));
|
||||||
try {
|
try {
|
||||||
return portalPropertyService.getOrCreateProperty(PROP_GROUP, PROP_RELOAD_URL,
|
List<Future<Map<String, Object>>> futures =
|
||||||
DEFAULT_RELOAD_URL, "eapim-portal 메뉴 캐시 리로드 내부 URL");
|
executor.invokeAll(tasks, TIMEOUT_MS + OVERALL_SLACK_MS, TimeUnit.MILLISECONDS);
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("리로드 URL 조회 실패 - 기본값 사용: " + DEFAULT_RELOAD_URL, e);
|
List<Map<String, Object>> results = new ArrayList<>(urls.size());
|
||||||
return DEFAULT_RELOAD_URL;
|
for (int i = 0; i < futures.size(); i++) {
|
||||||
|
String url = urls.get(i);
|
||||||
|
Future<Map<String, Object>> future = futures.get(i);
|
||||||
|
try {
|
||||||
|
results.add(future.get());
|
||||||
|
} catch (CancellationException e) {
|
||||||
|
logger.warn("포탈 메뉴 캐시 리로드 타임아웃 - url: " + url);
|
||||||
|
results.add(failure(url, "응답 시간 초과(" + TIMEOUT_MS + "ms)", TIMEOUT_MS));
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
logger.error("포탈 메뉴 캐시 리로드 실패 - url: " + url, e.getCause());
|
||||||
|
results.add(failure(url, message(e.getCause()), 0L));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
logger.error("포탈 메뉴 캐시 리로드 중단됨", e);
|
||||||
|
List<Map<String, Object>> results = new ArrayList<>(urls.size());
|
||||||
|
for (String url : urls) {
|
||||||
|
results.add(failure(url, "호출이 중단되었습니다", 0L));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
} finally {
|
||||||
|
executor.shutdownNow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 서버 1대 호출 — 예외를 전파하지 않고 실패 Map 으로 감싼다. */
|
||||||
|
private Map<String, Object> callOne(String url) {
|
||||||
|
long startedAt = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ResponseEntity<Map> response = restTemplate.postForEntity(url, null, Map.class);
|
||||||
|
long elapsed = System.currentTimeMillis() - startedAt;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> body = response.getBody();
|
||||||
|
if (response.getStatusCode().is2xxSuccessful() && body != null && "OK".equals(body.get("result"))) {
|
||||||
|
Map<String, Object> server = new LinkedHashMap<>();
|
||||||
|
server.put("url", url);
|
||||||
|
server.put("success", true);
|
||||||
|
server.put("message", "리로드 완료");
|
||||||
|
server.put("itemCount", body.get("itemCount"));
|
||||||
|
server.put("reloadedAt", body.get("reloadedAt"));
|
||||||
|
server.put("elapsedMs", elapsed);
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
logger.warn("포탈 메뉴 캐시 리로드 응답 오류 - url: " + url + ", status: " + response.getStatusCode()
|
||||||
|
+ ", body: " + body);
|
||||||
|
return failure(url, "포탈 응답 오류: " + (body == null ? response.getStatusCode() : body), elapsed);
|
||||||
|
} catch (Exception e) {
|
||||||
|
long elapsed = System.currentTimeMillis() - startedAt;
|
||||||
|
logger.error("포탈 메뉴 캐시 리로드 실패 - url: " + url, e);
|
||||||
|
return failure(url, message(e), elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> failure(String url, String message, long elapsedMs) {
|
||||||
|
Map<String, Object> server = new LinkedHashMap<>();
|
||||||
|
server.put("url", url);
|
||||||
|
server.put("success", false);
|
||||||
|
server.put("message", message);
|
||||||
|
server.put("elapsedMs", elapsedMs);
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String message(Throwable t) {
|
||||||
|
if (t == null) {
|
||||||
|
return "알 수 없는 오류";
|
||||||
|
}
|
||||||
|
String msg = t.getMessage();
|
||||||
|
return (msg == null || msg.trim().isEmpty()) ? t.getClass().getSimpleName() : msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> resolveUrls() {
|
||||||
|
String raw;
|
||||||
|
try {
|
||||||
|
raw = portalPropertyService.getOrCreateProperty(PROP_GROUP, PROP_RELOAD_URL,
|
||||||
|
DEFAULT_RELOAD_URL, PROP_DESCRIPTION);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("리로드 URL 조회 실패 - 기본값 사용: " + DEFAULT_RELOAD_URL, e);
|
||||||
|
raw = DEFAULT_RELOAD_URL;
|
||||||
|
}
|
||||||
|
List<String> urls = parseUrls(raw);
|
||||||
|
return urls.isEmpty() ? parseUrls(DEFAULT_RELOAD_URL) : urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 콤마/세미콜론/줄바꿈으로 구분된 URL 목록을 파싱한다.
|
||||||
|
* {@code #} 로 시작하는 줄은 주석으로 무시하고, 중복 URL 은 최초 순서만 남긴다.
|
||||||
|
*/
|
||||||
|
static List<String> parseUrls(String raw) {
|
||||||
|
if (raw == null || raw.trim().isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
Set<String> urls = new LinkedHashSet<>();
|
||||||
|
for (String token : raw.split("[,;\\r\\n]")) {
|
||||||
|
String url = token.trim();
|
||||||
|
if (url.isEmpty() || url.startsWith("#")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
urls.add(url);
|
||||||
|
}
|
||||||
|
return new ArrayList<>(urls);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user