UCO 목표량 google chat webhook 보내는 로직 추가

This commit is contained in:
Hyojin Ahn 2026-06-29 10:45:32 -04:00
parent eb71dd535c
commit 751880cc4a
8 changed files with 266 additions and 0 deletions

View File

@ -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);
}
}

View File

@ -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, <URL|라벨>).
*/
@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();
}
}

View File

@ -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<String, String> driverWebhooks = new HashMap<>();
}

View File

@ -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<DriverChatMessage> messages;
}

View File

@ -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<Detail> details;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class Detail {
private Long driverId;
private String status; // SENT / FAILED / SKIPPED
private String error; // FAILED 때만
}
}

View File

@ -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;
}

View File

@ -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<DriverChatMessage> 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<Detail> 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();
}
}

View File

@ -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 로