From a45e749331cbbdf6f4daebfb795ce485b7ad9306 Mon Sep 17 00:00:00 2001 From: eastargh Date: Wed, 23 Sep 2026 10:33:08 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9B=B9=ED=9B=85=20=EB=B0=9C=EC=86=A1?= =?UTF-8?q?=EC=8B=9C=20=EB=A6=AC=EB=B2=84=EC=8A=A4=ED=94=84=EB=A1=9D?= =?UTF-8?q?=EC=8B=9C=20=EC=A0=9C=EA=B1=B0,=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=EB=B0=9C=EC=86=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../onl/apim/portalorg/portalOrgManDetail.jsp | 10 --- .../onl/apim/webhook/webhookSendLogMan.jsp | 3 +- .../apim/webhook/webhookSendLogManDetail.jsp | 5 +- readme.md | 24 +++++- .../rms/common/context/MonitoringContext.java | 1 - .../onl/djb/webhook/WebhookSendLog.java | 3 - .../ApiIncidentPrenoticeService.java | 18 ++--- .../ext/djb/webhook/dto/WebhookPayload.java | 16 ++++ .../ext/djb/webhook/dto/WebhookSendLogUI.java | 1 - .../djb/webhook/dto/WebhookSendRequest.java | 6 +- .../service/WebhookSendLogManService.java | 1 - .../djb/webhook/service/WebhookService.java | 75 ++++--------------- 12 files changed, 64 insertions(+), 99 deletions(-) diff --git a/WebContent/jsp/onl/apim/portalorg/portalOrgManDetail.jsp b/WebContent/jsp/onl/apim/portalorg/portalOrgManDetail.jsp index b4ccc1d..908012a 100644 --- a/WebContent/jsp/onl/apim/portalorg/portalOrgManDetail.jsp +++ b/WebContent/jsp/onl/apim/portalorg/portalOrgManDetail.jsp @@ -730,16 +730,6 @@ - - <%= localeMessage.getString("portalOrg.reverseProxyPath") %> - - <%=reverseProxyUrl %>/ - - - - - - <%--사업자등록증--%> <%= localeMessage.getString("portalOrg.compRegFile") %> diff --git a/WebContent/jsp/onl/apim/webhook/webhookSendLogMan.jsp b/WebContent/jsp/onl/apim/webhook/webhookSendLogMan.jsp index 0c88f88..8b3d821 100644 --- a/WebContent/jsp/onl/apim/webhook/webhookSendLogMan.jsp +++ b/WebContent/jsp/onl/apim/webhook/webhookSendLogMan.jsp @@ -89,14 +89,13 @@ mtype: 'POST', url: url, postData: getSearchForJqgrid("cmd", "LIST"), - colNames: ['id', 'No.', '기관명', '이벤트유형', 'Target URL', 'Proxy URL', 'Status', '성공여부', '재시도', '발송서버', '발송일시'], + colNames: ['id', 'No.', '기관명', '이벤트유형', 'Target URL', 'Status', '성공여부', '재시도', '발송서버', '발송일시'], colModel: [ {name: 'id', align: 'center', key: true, hidden: true}, {name: 'rowNum', align: 'center', width: 45, sortable: false}, {name: 'orgName', align: 'left', width: 140}, {name: 'eventType', align: 'center', width: 120, formatter: formatEventType}, {name: 'targetUrl', align: 'left', width: 300, formatter: formatTargetUrl}, - {name: 'proxyUrl', align: 'left', width: 300, formatter: formatTargetUrl}, {name: 'statusCode', align: 'center', width: 60}, {name: 'success', align: 'center', width: 70, formatter: formatSuccess}, {name: 'retryCount', align: 'center', width: 60}, diff --git a/WebContent/jsp/onl/apim/webhook/webhookSendLogManDetail.jsp b/WebContent/jsp/onl/apim/webhook/webhookSendLogManDetail.jsp index f1e9291..e03545a 100644 --- a/WebContent/jsp/onl/apim/webhook/webhookSendLogManDetail.jsp +++ b/WebContent/jsp/onl/apim/webhook/webhookSendLogManDetail.jsp @@ -37,7 +37,6 @@ $('#eventType').text(data.eventTypeName || data.eventType || '') .attr('title', data.eventType || ''); $('#targetUrl').text(data.targetUrl || ''); - $('#proxyUrl').text(data.proxyUrl || ''); $('#statusCode').text(data.statusCode != null ? data.statusCode : ''); $('#retryCount').text(data.retryCount != null ? data.retryCount : ''); $('#sendBy').text(data.sendBy || ''); @@ -99,9 +98,7 @@ Target URL - - Proxy URL - + 성공여부 diff --git a/readme.md b/readme.md index f800ae2..4c3503b 100644 --- a/readme.md +++ b/readme.md @@ -272,7 +272,6 @@ DB 테이블 `PTL_PROPERTY` 에 그룹명 `'Portal'` 로 저장된다. - `rms.webhook.enabled` - 웹훅 발송 여부 - `rms.webhook.retry_count` - 웹훅 재발송 횟수 - `rms.webhook.retry_time` - 웹훅 재발송 시간간격 -- `rms.webhook.reverse_proxy.url` - 리버스 프록시 경로 > Portal 프라퍼티 (파트너포탈 > 포탈관리 > Property관리) @@ -296,6 +295,12 @@ DB 테이블 `PTL_PROPERTY` 에 그룹명 `'Portal'` 로 저장된다. - `api.status.delay.range_minute` - API 지연 상태 판단을 위한 로그 구간(분) - `api.status.delay.avg_resp_time` - API 지연 상태 판단을 위한 평균 응답속도 (ex: 10분동안 평균 응답속도가 10초이상이면 지연으로 판단) +**API 점검 사전 공지 웹훅 발송** +- `ApiIncidentPrenoticeJob` - API 점검이 시작되기 전에 사전 공지를 웹훅으로 발송한다 +- 실행간격(10분) CLUSTERED로 구성한다 +- 공지사항에 점검 게시물을 등록할때 사전발송일에 일시가 등록된 경우 발송된다. +- 웹훅 신청시 `점검시작(CHECK_NOTICE)` 이벤트유형을 선택한 제휴사에게 웹훅을 발송한다 + **유량제어토큰실패감시** - `InflowTokenMonitorJob` - 유량제어 토큰 획득 실패(TSEAIFR11) 발생시 직원에게 메신저를 발송한다 - `api.inflow.fail.range_minute` - 최근 몇분동안 감시하는지 설정 @@ -320,7 +325,7 @@ DB 테이블 `PTL_PROPERTY` 에 그룹명 `'Portal'` 로 저장된다. **토큰발급이력삭제** - `TOKENISSUANCELOGCLEANUPJOB` - 7일이 지난 토큰 발급내역을 삭제한다 -## IIM 연동 - 제주은행 커스텀 +## 레이아웃 연동 - IIM(제주은행 커스텀 버전) **인터페이스** - 최초 연동시 APIM에 등록되지 않은 경우에만 인터페이스를 생성한다. @@ -336,9 +341,20 @@ DB 테이블 `PTL_PROPERTY` 에 그룹명 `'Portal'` 로 저장된다. - 전문이 등록된 후 API 관리화면에서 필요한 정보를 입력하고 **수정버튼을 클릭하여 G/W에 반영된다** **연동이력** -- 연동 이력은 `환경정보 > EMS관리 > 연동 배포 이력` 메뉴에서 조회한다. +- 연동 이력은 `환경정보 > EMS관리 > 연동 배포 이력` 메뉴에서 조회한다.(서비스명 : MessageSync) -## 배포 +## 인터페이스 운영 배포 + +**eCams** +- 제주은행 형상관리 시스템으로 개발서버의 인터페이스를 운영서버로 배포한다 +- eCams에서 실행시 아래 폴더에 json을 생성한다. (스펙정보 포함) +- `iomap.download.path` - 인터페이스.json 파일 다운로드 개발서버 경로 +- `iomap.upload.path` - 인터페이스.json 파일 업로드 운영서버 경로 + +**배포이력** +- 배포 이력은 `환경정보 > EMS관리 > 연동 배포 이력` 메뉴에서 조회한다.(서비스명 : interfaceDeploy) + +## 소스배포 | 대상 | 비고 | |---|---| diff --git a/src/main/java/com/eactive/eai/rms/common/context/MonitoringContext.java b/src/main/java/com/eactive/eai/rms/common/context/MonitoringContext.java index 513c824..4d0e452 100644 --- a/src/main/java/com/eactive/eai/rms/common/context/MonitoringContext.java +++ b/src/main/java/com/eactive/eai/rms/common/context/MonitoringContext.java @@ -260,7 +260,6 @@ public interface MonitoringContext { // 웹훅 public static final String RMS_WEBHOOK_ENABLED = "rms.webhook.enabled"; - public static final String RMS_WEBHOOK_REVERSE_PROXY_URL = "rms.webhook.reverse_proxy.url"; public static final String RMS_WEBHOOK_RETRY_COUNT = "rms.webhook.retry_count"; public static final String RMS_WEBHOOK_RETRY_TIME = "rms.webhook.retry_time"; diff --git a/src/main/java/com/eactive/eai/rms/data/entity/onl/djb/webhook/WebhookSendLog.java b/src/main/java/com/eactive/eai/rms/data/entity/onl/djb/webhook/WebhookSendLog.java index 5b1cd5a..1600d0d 100644 --- a/src/main/java/com/eactive/eai/rms/data/entity/onl/djb/webhook/WebhookSendLog.java +++ b/src/main/java/com/eactive/eai/rms/data/entity/onl/djb/webhook/WebhookSendLog.java @@ -29,9 +29,6 @@ public class WebhookSendLog { @Column(name = "TARGET_URL", length = 500) private String targetUrl; - @Column(name = "PROXY_URL", length = 500) - private String proxyUrl; - @Column(name = "EVENT_TYPE", length = 100) private String eventType; diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiIncidentPrenoticeService.java b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiIncidentPrenoticeService.java index ef26b34..d398572 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiIncidentPrenoticeService.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/apistatus/ApiIncidentPrenoticeService.java @@ -147,7 +147,8 @@ public class ApiIncidentPrenoticeService { List failures = new ArrayList<>(); for (WebhookSendRequest req : sendList) { req.setEventType(NOTICE_EVENT); - req.setData(buildNoticeData(incident, req.getData())); + req.setExtra(buildNoticeExtra(incident)); + // data(apiIds 배열)는 findSendList 가 이미 채워둔 값을 그대로 사용한다 - 장애/지연 웹훅과 동일한 형식 WebhookSendLog result = webhookService.send(req); if (result != null && Boolean.TRUE.equals(result.getSuccess())) { @@ -168,15 +169,12 @@ public class ApiIncidentPrenoticeService { incidentId, targetCnt, sentCnt, result); } - /** 원래 org 기준 apiId 목록(req.getData())은 유지하고, 점검 예정 일정을 덧붙인다. */ - private Map buildNoticeData(DjbApistatusIncident incident, Object apiIds) { - Map data = new LinkedHashMap<>(); - data.put("incidentId", incident.getIncidentId()); - data.put("title", incident.getTitle()); - data.put("scheduledStartAt", formatNoticeDate(incident.getStartedAt())); - data.put("scheduledEndAt", formatNoticeDate(incident.getEndAt())); - data.put("apiIds", apiIds); - return data; + /** 점검 예정 시작/종료일시 - payload 최상위(data와 형제)에 실린다. */ + private Map buildNoticeExtra(DjbApistatusIncident incident) { + Map extra = new LinkedHashMap<>(); + extra.put("scheduledStartAt", formatNoticeDate(incident.getStartedAt())); + extra.put("scheduledEndAt", formatNoticeDate(incident.getEndAt())); + return extra; } private String formatNoticeDate(LocalDateTime dateTime) { diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookPayload.java b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookPayload.java index c5cd3ad..94488e6 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookPayload.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookPayload.java @@ -1,11 +1,14 @@ package com.eactive.eai.rms.ext.djb.webhook.dto; +import com.fasterxml.jackson.annotation.JsonAnyGetter; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.Map; import java.util.UUID; @Getter @@ -21,13 +24,26 @@ public class WebhookPayload { private long timestamp; private Object data; + /** 이벤트별 추가 정보(예: 점검 예정 시작/종료일시) - JSON 최상위(data와 형제)로 펼쳐진다. */ + private Map extra; + + @JsonAnyGetter + public Map getExtra() { + return extra; + } + public static WebhookPayload of(String eventType, String message, Object data) { + return of(eventType, message, data, null); + } + + public static WebhookPayload of(String eventType, String message, Object data, Map extra) { return WebhookPayload.builder() .eventType(eventType) .eventId(UUID.randomUUID().toString()) .message(message) .timestamp(System.currentTimeMillis()) .data(data) + .extra(extra) .build(); } } diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookSendLogUI.java b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookSendLogUI.java index ab7f0e3..852bea3 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookSendLogUI.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookSendLogUI.java @@ -20,7 +20,6 @@ public class WebhookSendLogUI { /** 모니터링 공통코드(EVENT_TYPE) 코드명. 상세 조회에서만 채워진다(목록은 화면에서 콤보로 변환). */ private String eventTypeName; private String targetUrl; - private String proxyUrl; private String payload; private String signature; diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookSendRequest.java b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookSendRequest.java index b8be05d..b31276a 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookSendRequest.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/dto/WebhookSendRequest.java @@ -1,5 +1,7 @@ package com.eactive.eai.rms.ext.djb.webhook.dto; +import java.util.Map; + import lombok.Getter; import lombok.Setter; @@ -8,10 +10,10 @@ import lombok.Setter; public class WebhookSendRequest { private String orgId; private String targetUrl; - private String proxyUrl; private String eventType; private String secret; - private String reverseProxyPath; private String userSecret; private Object data; + /** 이벤트별 웹훅 payload 최상위에 실을 추가 정보 (예: CHECK_NOTICE의 점검 예정 시작/종료일시) */ + private Map extra; } \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookSendLogManService.java b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookSendLogManService.java index 6901cc5..ab70082 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookSendLogManService.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookSendLogManService.java @@ -154,7 +154,6 @@ public class WebhookSendLogManService extends BaseService { ui.setEventType(e.getEventType()); ui.setEventTypeName(eventTypeName); ui.setTargetUrl(e.getTargetUrl()); - ui.setProxyUrl(e.getProxyUrl()); ui.setPayload(e.getPayload()); ui.setSignature(e.getSignature()); ui.setUserSecret(e.getUserSecret()); diff --git a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookService.java b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookService.java index 511ff70..44932b2 100644 --- a/src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookService.java +++ b/src/main/java/com/eactive/eai/rms/ext/djb/webhook/service/WebhookService.java @@ -21,7 +21,6 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.RestTemplate; -import com.eactive.apim.portal.portalorg.entity.QPortalOrg; import com.eactive.eai.rms.common.context.MonitoringContext; import com.eactive.eai.rms.common.util.ContainerUtil; import com.eactive.eai.rms.data.entity.onl.djb.webhook.QWebhookReq; @@ -61,16 +60,12 @@ public class WebhookService { QWebhookReq req = QWebhookReq.webhookReq; QWebhookReqEvent evt = QWebhookReqEvent.webhookReqEvent; QWebhookReqApi api = QWebhookReqApi.webhookReqApi; - QPortalOrg org = QPortalOrg.portalOrg; - - String reverseProxyUrl = monitoringContext.getProperty(MonitoringContext.RMS_WEBHOOK_REVERSE_PROXY_URL); List tuples = new JPAQueryFactory(entityManager) - .select(req.orgId, req.targetUrl, req.secret, req.userSecret, org.reverseProxyPath, api.id.apiId) + .select(req.orgId, req.targetUrl, req.secret, req.userSecret, api.id.apiId) .from(req) .join(evt).on(req.id.eq(evt.id.webhookReqId)) .join(api).on(req.id.eq(api.id.webhookReqId)) - .join(org).on(req.orgId.eq(org.id)) .where(evt.id.eventType.eq(event) .and(api.id.apiId.in(apiIds))) .fetch(); @@ -83,14 +78,13 @@ public class WebhookService { WebhookSendRequest wr = new WebhookSendRequest(); wr.setOrgId(t.get(req.orgId)); wr.setTargetUrl(t.get(req.targetUrl)); - wr.setProxyUrl(this.getProxyUrl(reverseProxyUrl, t.get(org.reverseProxyPath))); wr.setSecret(t.get(req.secret)); wr.setUserSecret(t.get(req.userSecret)); wr.setEventType(event); wr.setData(new ArrayList()); return wr; }); - + WebhookSendRequest wsr = resultMap.get(key); if (wsr != null) { List list = (List) wsr.getData(); @@ -102,46 +96,6 @@ public class WebhookService { return new ArrayList<>(resultMap.values()); } - - //reverse proxy 서버로 발송하기 위한 url 만든다 - private String getProxyUrl(String proxyUrl, String path) { - String base = stripTrailingSlash(proxyUrl); - String normalizedPath = stripSlashes(path); -// // scheme://host[:port] 부분만 제거하고 이후 path/query/fragment는 그대로 유지 -// String targetPath = targetUrl.replaceFirst("^[a-zA-Z][a-zA-Z0-9+.-]*://[^/]+", ""); - - StringBuilder url = new StringBuilder(base); - if (!normalizedPath.isEmpty()) { - url.append('/').append(normalizedPath); - } -// url.append(targetPath); - return url.toString(); - } - - private String stripTrailingSlash(String value) { - if (value == null) { - return ""; - } - int end = value.length(); - while (end > 0 && value.charAt(end - 1) == '/') { - end--; - } - return value.substring(0, end); - } - - private String stripSlashes(String value) { - if (value == null) { - return ""; - } - String result = value.trim(); - while (result.startsWith("/")) { - result = result.substring(1); - } - while (result.endsWith("/")) { - result = result.substring(0, result.length() - 1); - } - return result; - } /** * 웹훅 발송 메인 메서드 @@ -156,24 +110,23 @@ public class WebhookService { public WebhookSendLog send(WebhookSendRequest req) { String targetUrl = req.getTargetUrl(); - String proxyUrl = req.getProxyUrl(); String secret = req.getSecret(); String userSecret = req.getUserSecret(); String eventType = req.getEventType(); String message = getMessage(eventType); Object data = req.getData(); - + Map extra = req.getExtra(); + WebhookSendLog sendLog = new WebhookSendLog(); sendLog.setOrgId(req.getOrgId()); sendLog.setTargetUrl(targetUrl); - sendLog.setProxyUrl(proxyUrl); sendLog.setEventType(eventType); sendLog.setUserSecret(userSecret); sendLog.setSendBy(ContainerUtil.getInstanceName()); try { // 1. Payload 생성 - WebhookPayload webhookPayload = WebhookPayload.of(eventType, message, data); + WebhookPayload webhookPayload = WebhookPayload.of(eventType, message, data, extra); String payloadJson = objectMapper.writeValueAsString(webhookPayload); sendLog.setPayload(payloadJson); @@ -194,7 +147,7 @@ public class WebhookService { int retryTime = monitoringContext.getIntProperty(MonitoringContext.RMS_WEBHOOK_RETRY_TIME, DEFAULT_RETRY_TIME); sendLog.setSentAt(LocalDateTime.now()); HttpEntity httpRequest = new HttpEntity<>(payloadJson, headers); - ResponseEntity response = sendWithRetry(proxyUrl, httpRequest, sendLog, retryCount, retryTime); + ResponseEntity response = sendWithRetry(targetUrl, httpRequest, sendLog, retryCount, retryTime); // 5. 성공 로그 sendLog.setStatusCode(response.getStatusCode().value()); @@ -202,7 +155,7 @@ public class WebhookService { sendLog.setSuccess(response.getStatusCode().is2xxSuccessful()); log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}, retryCount: {}", - eventType, proxyUrl, response.getStatusCode(), sendLog.getRetryCount()); + eventType, targetUrl, response.getStatusCode(), sendLog.getRetryCount()); } catch (HttpClientErrorException | HttpServerErrorException e) { sendLog.setStatusCode(e.getStatusCode().value()); @@ -210,13 +163,13 @@ public class WebhookService { sendLog.setSuccess(false); sendLog.setErrorMessage(e.getMessage()); log.error("[Webhook] HTTP 오류 - eventType: {}, url: {}, status: {}", - eventType, proxyUrl, e.getStatusCode()); + eventType, targetUrl, e.getStatusCode()); } catch (Exception e) { sendLog.setSuccess(false); sendLog.setErrorMessage(e.getMessage()); log.error("[Webhook] 발송 실패 - eventType: {}, url: {}, error: {}", - eventType, proxyUrl, e.getMessage()); + eventType, targetUrl, e.getMessage()); } return sendLogRepository.save(sendLog); @@ -243,29 +196,29 @@ public class WebhookService { * - 실패할 때마다 즉시 로그를 저장한다 (최초 실패는 INSERT, 이후 재시도 실패는 같은 row를 UPDATE) - * 재시도 도중 앱이 죽어도 마지막으로 저장된 실패 기록이 남도록 하기 위함 */ - private ResponseEntity sendWithRetry(String proxyUrl, HttpEntity request, WebhookSendLog sendLog, int retryCount, int retryTime) throws Exception { + private ResponseEntity sendWithRetry(String targetUrl, HttpEntity request, WebhookSendLog sendLog, int retryCount, int retryTime) throws Exception { Exception lastException = null; for (int attempt = 0; attempt <= retryCount; attempt++) { try { if (attempt == 0) { // 배포 반영 확인용: HttpComponentsClientHttpRequestFactory 여야 SSL 우회 RestTemplate 사용 중. // SimpleClientHttpRequestFactory 로 찍히면 기본 new RestTemplate() 이 주입된 것 → PKIX 원인. - log.info("[Webhook] POST 시도 - url: {}, requestFactory: {}", proxyUrl, + log.info("[Webhook] POST 시도 - url: {}, requestFactory: {}", targetUrl, restTemplate.getRequestFactory().getClass().getSimpleName()); } if (attempt > 0) { long delayMs = (long) retryTime * attempt; - log.info("[Webhook] 재시도 {}/{} - url: {}, delay: {}ms", attempt, retryCount, proxyUrl, delayMs); + log.info("[Webhook] 재시도 {}/{} - url: {}, delay: {}ms", attempt, retryCount, targetUrl, delayMs); Thread.sleep(delayMs); } - return restTemplate.postForEntity(proxyUrl, request, String.class); + return restTemplate.postForEntity(targetUrl, request, String.class); } catch (HttpClientErrorException e) { //throw e; // 4xx는 재시도 불필요 lastException = e; //TODO: 재시도 테스트 recordAttemptFailure(sendLog, attempt, e.getStatusCode().value(), e.getResponseBodyAsString(), e.getMessage()); } catch (Exception e) { lastException = e; - log.warn("[Webhook] 발송 실패 (attempt {}/{}) - url: {}, error: {}", attempt, retryCount, proxyUrl, e.getMessage()); + log.warn("[Webhook] 발송 실패 (attempt {}/{}) - url: {}, error: {}", attempt, retryCount, targetUrl, e.getMessage()); recordAttemptFailure(sendLog, attempt, null, null, e.getMessage()); } }