From 751880cc4a87cf2d17b6717488122a204317fac3 Mon Sep 17 00:00:00 2001 From: Hyojin Ahn Date: Mon, 29 Jun 2026 10:45:32 -0400 Subject: [PATCH] =?UTF-8?q?UCO=20=EB=AA=A9=ED=91=9C=EB=9F=89=20google=20ch?= =?UTF-8?q?at=20webhook=20=EB=B3=B4=EB=82=B4=EB=8A=94=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../google/api/GoogleChatController.java | 29 ++++++ .../google/client/GoogleChatClient.java | 36 ++++++++ .../google/config/GoogleChatProperties.java | 22 +++++ .../google/dto/ChatBroadcastRequest.java | 22 +++++ .../google/dto/ChatBroadcastResult.java | 34 +++++++ .../google/dto/DriverChatMessage.java | 19 ++++ .../service/GoogleChatBroadcastService.java | 89 +++++++++++++++++++ src/main/resources/application.yaml | 15 ++++ 8 files changed, 266 insertions(+) create mode 100644 src/main/java/com/goi/integration/google/api/GoogleChatController.java create mode 100644 src/main/java/com/goi/integration/google/client/GoogleChatClient.java create mode 100644 src/main/java/com/goi/integration/google/config/GoogleChatProperties.java create mode 100644 src/main/java/com/goi/integration/google/dto/ChatBroadcastRequest.java create mode 100644 src/main/java/com/goi/integration/google/dto/ChatBroadcastResult.java create mode 100644 src/main/java/com/goi/integration/google/dto/DriverChatMessage.java create mode 100644 src/main/java/com/goi/integration/google/service/GoogleChatBroadcastService.java diff --git a/src/main/java/com/goi/integration/google/api/GoogleChatController.java b/src/main/java/com/goi/integration/google/api/GoogleChatController.java new file mode 100644 index 0000000..056775e --- /dev/null +++ b/src/main/java/com/goi/integration/google/api/GoogleChatController.java @@ -0,0 +1,29 @@ +package com.goi.integration.google.api; + +import com.goi.integration.google.dto.ChatBroadcastRequest; +import com.goi.integration.google.dto.ChatBroadcastResult; +import com.goi.integration.google.service.GoogleChatBroadcastService; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * crm-rest-api 가 호출하는 내부 엔드포인트. + * 전체 경로: POST {context-path}/google/chat/broadcast + * = /integration-service/google/chat/broadcast + */ +@RestController +@RequestMapping("/google/chat") +@RequiredArgsConstructor +public class GoogleChatController { + + private final GoogleChatBroadcastService broadcastService; + + @PostMapping("/broadcast") + public ChatBroadcastResult broadcast(@RequestBody ChatBroadcastRequest request) { + return broadcastService.broadcast(request); + } +} \ No newline at end of file diff --git a/src/main/java/com/goi/integration/google/client/GoogleChatClient.java b/src/main/java/com/goi/integration/google/client/GoogleChatClient.java new file mode 100644 index 0000000..a02027d --- /dev/null +++ b/src/main/java/com/goi/integration/google/client/GoogleChatClient.java @@ -0,0 +1,36 @@ +package com.goi.integration.google.client; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.Map; + +/** + * Google Chat incoming webhook 전송 클라이언트. + * base-url 을 고정하지 않고, 호출 시 전달받은 webhook URL(절대주소) 로 POST 한다. + * payload 는 단순 text 메시지: {"text": "..."} (Google Chat 포맷: *굵게*, \n, ). + */ +@Slf4j +@Component +public class GoogleChatClient { + + private final WebClient webClient = WebClient.builder().build(); + + public void sendText(String webhookUrl, String text) { + webClient.post() + .uri(webhookUrl) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(Map.of("text", text)) + .retrieve() + .onStatus( + status -> status.is4xxClientError() || status.is5xxServerError(), + resp -> resp.bodyToMono(String.class) + .map(body -> new RuntimeException( + "GCHAT_HTTP_ERROR: " + resp.statusCode() + " body=" + body)) + ) + .bodyToMono(String.class) + .block(); + } +} \ No newline at end of file diff --git a/src/main/java/com/goi/integration/google/config/GoogleChatProperties.java b/src/main/java/com/goi/integration/google/config/GoogleChatProperties.java new file mode 100644 index 0000000..861d3eb --- /dev/null +++ b/src/main/java/com/goi/integration/google/config/GoogleChatProperties.java @@ -0,0 +1,22 @@ +package com.goi.integration.google.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * Google Chat 연동 설정. + * driverId(String) -> 해당 기사 전용 Google Chat incoming webhook URL. + * 실제 URL 은 env 로만 주입한다 (application.yaml 의 ext.gchat.driver-webhooks 참고). + */ +@Data +@Component +@ConfigurationProperties(prefix = "ext.gchat") +public class GoogleChatProperties { + + /** key = driverId(문자열), value = webhook URL */ + private Map driverWebhooks = new HashMap<>(); +} \ No newline at end of file diff --git a/src/main/java/com/goi/integration/google/dto/ChatBroadcastRequest.java b/src/main/java/com/goi/integration/google/dto/ChatBroadcastRequest.java new file mode 100644 index 0000000..8c9f199 --- /dev/null +++ b/src/main/java/com/goi/integration/google/dto/ChatBroadcastRequest.java @@ -0,0 +1,22 @@ +package com.goi.integration.google.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.util.List; + +/** + * crm-rest-api 가 보내는 일괄 전송 요청. + * date 는 로깅/추적용. messages 의 각 항목이 기사별 방으로 전송된다. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ChatBroadcastRequest { + private LocalDate date; + private List messages; +} \ No newline at end of file diff --git a/src/main/java/com/goi/integration/google/dto/ChatBroadcastResult.java b/src/main/java/com/goi/integration/google/dto/ChatBroadcastResult.java new file mode 100644 index 0000000..1ac3f83 --- /dev/null +++ b/src/main/java/com/goi/integration/google/dto/ChatBroadcastResult.java @@ -0,0 +1,34 @@ +package com.goi.integration.google.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 일괄 전송 결과 요약. + * skipped = 해당 driverId 의 webhook 이 env 에 설정되지 않아 건너뜀. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ChatBroadcastResult { + private int requested; + private int sent; + private int failed; + private int skipped; + private List details; + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class Detail { + private Long driverId; + private String status; // SENT / FAILED / SKIPPED + private String error; // FAILED 일 때만 + } +} \ No newline at end of file diff --git a/src/main/java/com/goi/integration/google/dto/DriverChatMessage.java b/src/main/java/com/goi/integration/google/dto/DriverChatMessage.java new file mode 100644 index 0000000..0fe029e --- /dev/null +++ b/src/main/java/com/goi/integration/google/dto/DriverChatMessage.java @@ -0,0 +1,19 @@ +package com.goi.integration.google.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 기사 1명에게 보낼 메시지 1건. + * webhook URL 은 crm 이 넘기지 않는다 — integration 이 driverId 로 env 에서 해석. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DriverChatMessage { + private Long driverId; + private String text; +} \ No newline at end of file diff --git a/src/main/java/com/goi/integration/google/service/GoogleChatBroadcastService.java b/src/main/java/com/goi/integration/google/service/GoogleChatBroadcastService.java new file mode 100644 index 0000000..3305add --- /dev/null +++ b/src/main/java/com/goi/integration/google/service/GoogleChatBroadcastService.java @@ -0,0 +1,89 @@ +package com.goi.integration.google.service; + +import com.goi.integration.google.client.GoogleChatClient; +import com.goi.integration.google.config.GoogleChatProperties; +import com.goi.integration.google.dto.ChatBroadcastRequest; +import com.goi.integration.google.dto.ChatBroadcastResult; +import com.goi.integration.google.dto.ChatBroadcastResult.Detail; +import com.goi.integration.google.dto.DriverChatMessage; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +/** + * driverId -> env webhook 해석 후 기사별 방으로 메시지를 fan-out 한다. + * 한 기사 실패가 전체를 막지 않도록 건별 try/catch 로 격리하고 결과를 집계한다. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class GoogleChatBroadcastService { + + private final GoogleChatProperties props; + private final GoogleChatClient client; + + public ChatBroadcastResult broadcast(ChatBroadcastRequest request) { + + List messages = + request.getMessages() != null ? request.getMessages() : List.of(); + + log.info("[GCHAT][BROADCAST][REQUEST] date={}, drivers={}", + request.getDate(), messages.size()); + + int sent = 0, failed = 0, skipped = 0; + List details = new ArrayList<>(); + + for (DriverChatMessage m : messages) { + + String webhook = (m.getDriverId() == null) + ? null + : props.getDriverWebhooks().get(String.valueOf(m.getDriverId())); + + // 1) 웹훅 미설정 → 건너뜀 + if (webhook == null || webhook.isBlank()) { + skipped++; + details.add(Detail.builder() + .driverId(m.getDriverId()) + .status("SKIPPED") + .error("no webhook configured") + .build()); + log.warn("[GCHAT][BROADCAST][SKIP] driverId={} (웹훅 미설정)", m.getDriverId()); + continue; + } + + // 2) 전송 + try { + client.sendText(webhook, m.getText()); + sent++; + details.add(Detail.builder() + .driverId(m.getDriverId()) + .status("SENT") + .build()); + } catch (Exception e) { + failed++; + details.add(Detail.builder() + .driverId(m.getDriverId()) + .status("FAILED") + .error(e.getMessage()) + .build()); + log.error("[GCHAT][BROADCAST][FAIL] driverId={}, error={}", + m.getDriverId(), e.getMessage()); + } + } + + log.info("[GCHAT][BROADCAST][DONE] requested={}, sent={}, failed={}, skipped={}", + messages.size(), sent, failed, skipped); + + return ChatBroadcastResult.builder() + .requested(messages.size()) + .sent(sent) + .failed(failed) + .skipped(skipped) + .details(details) + .build(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 8244fa5..6719a04 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -31,6 +31,21 @@ ext: acc: base-url: http://acc-rest-api:8084/acc-rest-api internal-token: ${ACC_INTERNAL_TOKEN} + gchat: + # driverId -> 해당 기사 전용 Google Chat incoming webhook URL + # 값은 env 로 주입 + driver-webhooks: + "169": ${GCHAT_WEBHOOK_DRIVER_169:} # H.K + "195": ${GCHAT_WEBHOOK_DRIVER_195:} # J.S + "199": ${GCHAT_WEBHOOK_DRIVER_199:} # J.O + "178": ${GCHAT_WEBHOOK_DRIVER_178:} # T.K + "173": ${GCHAT_WEBHOOK_DRIVER_173:} # S.K + "194": ${GCHAT_WEBHOOK_DRIVER_194:} # Y.S + "192": ${GCHAT_WEBHOOK_DRIVER_192:} # S.S + "206": ${GCHAT_WEBHOOK_DRIVER_206:} # D.L + "171": ${GCHAT_WEBHOOK_DRIVER_171:} # M.K + "243": ${GCHAT_WEBHOOK_DRIVER_243:} # S.L + "240": ${GCHAT_WEBHOOK_DRIVER_240:} # T.Y samsara: base-url: https://api.samsara.com api-token: ${SAMSARA_API_TOKEN} # 반드시 env 로