scheduler 가 UCO 목표량을 전송하는 /integration-service 호출하는 것 추가.
This commit is contained in:
parent
b777217d1f
commit
7b33e83c55
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.goi.erp.client;
|
||||||
|
|
||||||
|
import com.goi.erp.dto.gchat.ChatBroadcastRequestDto;
|
||||||
|
import com.goi.erp.dto.gchat.ChatBroadcastResultDto;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestClientResponseException;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* crm-rest-api → integration-service 호출.
|
||||||
|
* POST {integration.api.base-url}/google/chat/broadcast
|
||||||
|
* (integration 은 driverId 로 env webhook 을 해석해 기사별 방으로 fan-out 한다.)
|
||||||
|
*
|
||||||
|
* sys HcmEmployeeClient / opr SystemTokenProvider 와 동일하게 RestTemplate 사용.
|
||||||
|
* (crm 은 RestTemplateConfig 의 RestTemplate 빈을 그대로 주입받음 — webflux 불필요)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GoogleChatNotifyClient {
|
||||||
|
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
|
@Value("${integration.api.base-url}")
|
||||||
|
private String integrationBaseUrl;
|
||||||
|
|
||||||
|
public ChatBroadcastResultDto broadcast(ChatBroadcastRequestDto request) {
|
||||||
|
String url = integrationBaseUrl + "/google/chat/broadcast";
|
||||||
|
try {
|
||||||
|
return restTemplate.postForObject(url, request, ChatBroadcastResultDto.class);
|
||||||
|
} catch (RestClientResponseException e) {
|
||||||
|
// 4xx/5xx 응답 — 본문 포함해 상위(Service)로 던짐
|
||||||
|
throw new RuntimeException(
|
||||||
|
"INTEGRATION_GCHAT_ERROR: " + e.getStatusCode()
|
||||||
|
+ " body=" + e.getResponseBodyAsString(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,19 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* permitAll 경로(스케줄러 워커, swagger 등)는 JWT 필터를 아예 타지 않게 한다.
|
||||||
|
* sys 스케줄러는 토큰 없이 워커를 호출하므로, 떠도는 만료 쿠키(AUTH_TOKEN)가
|
||||||
|
* 섞여 들어와도 "Session has expired" 로 막히면 안 된다.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilter(@NonNull HttpServletRequest request) {
|
||||||
|
String uri = request.getRequestURI(); // context-path 포함
|
||||||
|
return uri.contains("/scheduler/")
|
||||||
|
|| uri.contains("/swagger-ui")
|
||||||
|
|| uri.contains("/v3/api-docs");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(
|
protected void doFilterInternal(
|
||||||
@NonNull HttpServletRequest request,
|
@NonNull HttpServletRequest request,
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ public class SecurityConfig {
|
||||||
.dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()
|
.dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()
|
||||||
.requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll()
|
.requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll()
|
||||||
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||||
|
.requestMatchers("/scheduler/**").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
.addFilterBefore(jwtAuthFilter,
|
.addFilterBefore(jwtAuthFilter,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.goi.erp.controller;
|
||||||
|
|
||||||
|
import com.goi.erp.dto.ScheduleWorkerRequestDto;
|
||||||
|
import com.goi.erp.dto.ScheduleWorkerResponseDto;
|
||||||
|
import com.goi.erp.service.UcoTargetNotifyService;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sys-rest-api 스케줄러가 호출하는 crm 워커 엔드포인트 모음.
|
||||||
|
* opr-rest-api 와 동일하게 모든 워커를 이 컨트롤러 한 곳에 모은다.
|
||||||
|
* 새 스케줄러가 생기면 여기에 @PostMapping 메서드만 추가하면 된다.
|
||||||
|
*
|
||||||
|
* ⚠ crm-rest-api 에 이미 SchedulerWorkerController 가 있다면,
|
||||||
|
* 이 클래스를 새로 만들지 말고 아래 필드/메서드만 기존 컨트롤러에 추가하세요.
|
||||||
|
* ⚠ SecurityConfig 에서 "/scheduler/**" 가 permitAll 인지 확인 (opr 와 동일).
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/scheduler/worker")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SchedulerWorkerController {
|
||||||
|
|
||||||
|
private final UcoTargetNotifyService ucoTargetNotifyService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CRM_UCO_TARGET_NOTIFY
|
||||||
|
* 오늘자 기사별 UCO 목표를 각 기사 Google Chat 방으로 전송.
|
||||||
|
*/
|
||||||
|
@PostMapping("/uco/target/notify")
|
||||||
|
public ScheduleWorkerResponseDto notifyUcoTarget(
|
||||||
|
@RequestBody ScheduleWorkerRequestDto request
|
||||||
|
) {
|
||||||
|
return ucoTargetNotifyService.notifyDailyTargets(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.goi.erp.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sys-rest-api 스케줄러가 워커로 보내는 요청.
|
||||||
|
* from/to 는 UTC LocalDateTime 이다.
|
||||||
|
* ⚠ crm-rest-api 에 이미 동일 DTO 가 있으면 이 파일은 추가하지 말 것.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class ScheduleWorkerRequestDto {
|
||||||
|
private String jobCode;
|
||||||
|
private LocalDateTime from;
|
||||||
|
private LocalDateTime to;
|
||||||
|
private Integer maxRecords;
|
||||||
|
private Map<String, Map<String, String>> config;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.goi.erp.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 워커 → sys 스케줄러 응답. (opr-rest-api 와 동일 계약)
|
||||||
|
* ⚠ crm-rest-api 에 이미 동일 DTO 가 있으면 이 파일은 추가하지 말 것.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class ScheduleWorkerResponseDto {
|
||||||
|
private boolean success;
|
||||||
|
private int processedCount;
|
||||||
|
private int successCount;
|
||||||
|
private int failCount;
|
||||||
|
private String errorCode;
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
public static ScheduleWorkerResponseDto successEmpty() {
|
||||||
|
return ScheduleWorkerResponseDto.builder()
|
||||||
|
.success(true).processedCount(0).successCount(0).failCount(0).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ScheduleWorkerResponseDto success(int processed, int successCount, int failCount) {
|
||||||
|
return ScheduleWorkerResponseDto.builder()
|
||||||
|
.success(true).processedCount(processed).successCount(successCount).failCount(failCount).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ScheduleWorkerResponseDto failure(String errorCode, String errorMessage) {
|
||||||
|
return ScheduleWorkerResponseDto.builder()
|
||||||
|
.success(false).errorCode(errorCode).errorMessage(errorMessage).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.goi.erp.dto.gchat;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** integration-service /google/chat/broadcast 요청 바디. */
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class ChatBroadcastRequestDto {
|
||||||
|
private LocalDate date;
|
||||||
|
private List<DriverChatMessageDto> messages;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.goi.erp.dto.gchat;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/** integration-service 응답. details 는 생략하고 집계 수치만 받는다. */
|
||||||
|
@Data
|
||||||
|
public class ChatBroadcastResultDto {
|
||||||
|
private int requested;
|
||||||
|
private int sent;
|
||||||
|
private int failed;
|
||||||
|
private int skipped;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.goi.erp.dto.gchat;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/** integration-service 로 보낼 기사 1명분 메시지. webhook URL 은 보내지 않는다(integration env 에서 해석). */
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class DriverChatMessageDto {
|
||||||
|
private Long driverId;
|
||||||
|
private String text;
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,9 @@ public interface CustomerDailyUcoRepository extends JpaRepository<CustomerDailyU
|
||||||
List<CustomerDailyUco> findByCduDriverIdAndCduDateBetween(
|
List<CustomerDailyUco> findByCduDriverIdAndCduDateBetween(
|
||||||
Long cduDriverId, LocalDate from, LocalDate to);
|
Long cduDriverId, LocalDate from, LocalDate to);
|
||||||
|
|
||||||
|
// 해당 영업일자에 목표(qty)가 설정된 행만 기사ID 순으로 조회
|
||||||
|
List<CustomerDailyUco> findByCduDateAndCduTargetQtyNotNullOrderByCduDriverId(LocalDate cduDate);
|
||||||
|
|
||||||
// ── MIS 08:00 목표 upsert (목표 컬럼만 갱신, 실측값 보존) ──
|
// ── MIS 08:00 목표 upsert (목표 컬럼만 갱신, 실측값 보존) ──
|
||||||
@Modifying
|
@Modifying
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
package com.goi.erp.service;
|
||||||
|
|
||||||
|
import com.goi.erp.client.GoogleChatNotifyClient;
|
||||||
|
import com.goi.erp.dto.ScheduleWorkerRequestDto;
|
||||||
|
import com.goi.erp.dto.ScheduleWorkerResponseDto;
|
||||||
|
import com.goi.erp.dto.gchat.ChatBroadcastRequestDto;
|
||||||
|
import com.goi.erp.dto.gchat.ChatBroadcastResultDto;
|
||||||
|
import com.goi.erp.dto.gchat.DriverChatMessageDto;
|
||||||
|
import com.goi.erp.entity.CustomerDailyUco;
|
||||||
|
import com.goi.erp.repository.CustomerDailyUcoRepository;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 오늘자 기사별 UCO 목표를 조회해 integration-service 로 일괄 전송 요청한다.
|
||||||
|
* sys 스케줄러가 보낸 from/to(UTC) 기준으로 영업일(운영 TZ)을 산정한다.
|
||||||
|
* 별도 repository/프로젝션 없이 기존 CustomerDailyUcoRepository + CustomerDailyUco 엔티티를 그대로 사용.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UcoTargetNotifyService {
|
||||||
|
|
||||||
|
// ⚠ cdu_date 가 어느 TZ 기준으로 저장되는지에 맞춰야 함.
|
||||||
|
// MIS 8am 스냅샷이 토론토 영업일 기준이므로 America/Toronto 로 둠.
|
||||||
|
private static final ZoneId OP_ZONE = ZoneId.of("America/Toronto");
|
||||||
|
|
||||||
|
private final CustomerDailyUcoRepository customerDailyUcoRepository;
|
||||||
|
private final GoogleChatNotifyClient googleChatNotifyClient;
|
||||||
|
|
||||||
|
public ScheduleWorkerResponseDto notifyDailyTargets(ScheduleWorkerRequestDto request) {
|
||||||
|
|
||||||
|
LocalDate targetDate = resolveTargetDate(request);
|
||||||
|
|
||||||
|
List<CustomerDailyUco> rows =
|
||||||
|
customerDailyUcoRepository
|
||||||
|
.findByCduDateAndCduTargetQtyNotNullOrderByCduDriverId(targetDate);
|
||||||
|
|
||||||
|
log.info("[UCO_NOTIFY][REQUEST] jobCode={}, date={}, drivers={}",
|
||||||
|
request.getJobCode(), targetDate, rows.size());
|
||||||
|
|
||||||
|
if (rows.isEmpty()) {
|
||||||
|
return ScheduleWorkerResponseDto.successEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
int month = targetDate.getMonthValue();
|
||||||
|
int day = targetDate.getDayOfMonth();
|
||||||
|
|
||||||
|
List<DriverChatMessageDto> messages = rows.stream()
|
||||||
|
.map(r -> DriverChatMessageDto.builder()
|
||||||
|
.driverId(r.getCduDriverId())
|
||||||
|
.text(buildText(month, day, r))
|
||||||
|
.build())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
ChatBroadcastRequestDto body = ChatBroadcastRequestDto.builder()
|
||||||
|
.date(targetDate)
|
||||||
|
.messages(messages)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try {
|
||||||
|
ChatBroadcastResultDto result = googleChatNotifyClient.broadcast(body);
|
||||||
|
|
||||||
|
int requested = result != null ? result.getRequested() : messages.size();
|
||||||
|
int sent = result != null ? result.getSent() : 0;
|
||||||
|
// 전송 실패 + 웹훅 미설정(skipped) 을 모두 failCount 로 집계
|
||||||
|
int fail = result != null ? (result.getFailed() + result.getSkipped()) : messages.size();
|
||||||
|
|
||||||
|
log.info("[UCO_NOTIFY][DONE] requested={}, sent={}, fail(incl.skipped)={}",
|
||||||
|
requested, sent, fail);
|
||||||
|
|
||||||
|
return ScheduleWorkerResponseDto.success(requested, sent, fail);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[UCO_NOTIFY][FAILED] date={}, error={}", targetDate, e.getMessage(), e);
|
||||||
|
return ScheduleWorkerResponseDto.failure("UCO_NOTIFY_ERROR", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** sys 가 보낸 to(UTC) → 운영 TZ 영업일. 없으면 운영 TZ 의 오늘. */
|
||||||
|
private LocalDate resolveTargetDate(ScheduleWorkerRequestDto request) {
|
||||||
|
if (request != null && request.getTo() != null) {
|
||||||
|
return request.getTo()
|
||||||
|
.atZone(ZoneOffset.UTC)
|
||||||
|
.withZoneSameInstant(OP_ZONE)
|
||||||
|
.toLocalDate();
|
||||||
|
}
|
||||||
|
return LocalDate.now(OP_ZONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Google Chat 포맷: *굵게*, \n */
|
||||||
|
private String buildText(int month, int day, CustomerDailyUco r) {
|
||||||
|
BigDecimal qty = r.getCduTargetQty();
|
||||||
|
String qtyStr = (qty == null) ? "-" : String.format("%,.0f", qty);
|
||||||
|
|
||||||
|
// 예) 6/29
|
||||||
|
// 오늘의 목표량은 *4,331 L* 입니다. (4,331 L 가 굵게)
|
||||||
|
return month + "/" + day + "\n"
|
||||||
|
+ "오늘의 목표량은 *" + qtyStr + " L* 입니다.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,3 +37,7 @@ server:
|
||||||
hcm:
|
hcm:
|
||||||
api:
|
api:
|
||||||
base-url: http://hcm-rest-api:8081/hcm-rest-api
|
base-url: http://hcm-rest-api:8081/hcm-rest-api
|
||||||
|
|
||||||
|
integration:
|
||||||
|
api:
|
||||||
|
base-url: http://integration-service:8091/integration-service
|
||||||
Loading…
Reference in New Issue