From 82c087e2da0ce1aec01fa8d58e52d482c51eace6 Mon Sep 17 00:00:00 2001 From: Boram Date: Thu, 3 Sep 2026 10:46:10 +0900 Subject: [PATCH] =?UTF-8?q?dto=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DtoDownloadProxyController.java | 112 ++++++++++++------ 1 file changed, 79 insertions(+), 33 deletions(-) diff --git a/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/DtoDownloadProxyController.java b/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/DtoDownloadProxyController.java index c93255cd..416a9f30 100644 --- a/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/DtoDownloadProxyController.java +++ b/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/DtoDownloadProxyController.java @@ -1,8 +1,11 @@ package io.shinhanlife.dat.mcg.presentation; +import io.shinhanlife.dat.mcg.config.GatewayFallbackProperties; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; - -import org.springframework.beans.factory.annotation.Value; +import java.util.Set; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; @@ -12,61 +15,104 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestClientResponseException; -/** - * Gateway 화면에서 들어온 DTO 엑셀 요청을 실제 DTO 클래스가 있는 Tool Pod로 전달한다. - * 브라우저가 동일 출처(8081)만 호출하도록 하여 CORS와 배포 주소 차이를 숨긴다. - */ +/** Aggregates DTO downloads from the CUS, SAL, PRO, and SYS Tool Pods. */ @RestController public class DtoDownloadProxyController { private static final MediaType XLSX_MEDIA_TYPE = MediaType.parseMediaType( "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + private static final List DTO_ROUTE_KEYS = List.of("cus", "sal", "pro", "sys"); private final RestClient restClient; - private final String toolPodUrl; + private final List toolPodUrls; public DtoDownloadProxyController( RestClient.Builder restClientBuilder, - @Value("${mcp.gateway.fallback.default-url:http://localhost:8084}") String toolPodUrl) { + GatewayFallbackProperties fallbackProperties) { this.restClient = restClientBuilder.build(); - this.toolPodUrl = toolPodUrl.replaceAll("/+$", ""); + this.toolPodUrls = resolveToolPodUrls(fallbackProperties); } @GetMapping("/dto-download/options") public List options() { - // 셀렉트박스에 표시할 DTO 이름 목록을 Tool Pod에서 조회한다. - List options = restClient.get() - .uri(toolPodUrl + "/dto-download/options") - .retrieve() - .body(new ParameterizedTypeReference<>() {}); - return options == null ? List.of() : options; + Set mergedOptions = new LinkedHashSet<>(); + for (String toolPodUrl : toolPodUrls) { + try { + List podOptions = restClient.get() + .uri(toolPodUrl + "/dto-download/options") + .retrieve() + .body(new ParameterizedTypeReference<>() {}); + if (podOptions != null) { + mergedOptions.addAll(podOptions); + } + } catch (RestClientException ignored) { + // An unavailable Pod must not hide DTOs returned by the other running Pods. + } + } + return mergedOptions.stream().sorted().toList(); } @GetMapping("/dto-download/{dtoName}") public ResponseEntity download(@PathVariable("dtoName") String dtoName) { - byte[] workbook; - try { - // 생성된 엑셀 바이트를 그대로 브라우저에 전달한다. - workbook = restClient.get() - .uri(toolPodUrl + "/dto-download/{dtoName}", dtoName) - .retrieve() - .body(byte[].class); - } catch (RestClientResponseException error) { - // 형식 불일치 등의 상태 코드와 오류 메시지도 변경 없이 전달한다. - return ResponseEntity.status(error.getStatusCode()) - .contentType(error.getResponseHeaders() != null - && error.getResponseHeaders().getContentType() != null - ? error.getResponseHeaders().getContentType() : MediaType.TEXT_PLAIN) - .body(error.getResponseBodyAsByteArray()); + for (String toolPodUrl : toolPodUrls) { + try { + byte[] workbook = restClient.get() + .uri(toolPodUrl + "/dto-download/{dtoName}", dtoName) + .retrieve() + .body(byte[].class); + return workbookResponse(dtoName, workbook); + } catch (RestClientResponseException error) { + if (error.getStatusCode().value() == 404) { + continue; + } + return downstreamError(error); + } catch (RestClientException ignored) { + // Try the next configured Tool Pod when this Pod cannot be reached. + } } - String fileName = dtoName + ".xlsx"; + + return ResponseEntity.status(404) + .contentType(new MediaType("text", "plain", StandardCharsets.UTF_8)) + .body(("DTO not found in CUS/SAL/PRO/SYS: " + dtoName) + .getBytes(StandardCharsets.UTF_8)); + } + + private static List resolveToolPodUrls(GatewayFallbackProperties properties) { + List urls = new ArrayList<>(); + for (String routeKey : DTO_ROUTE_KEYS) { + String url = properties.getRoutes().get(routeKey); + if (url != null && !url.isBlank()) { + String normalized = url.replaceAll("/+$", ""); + if (!urls.contains(normalized)) { + urls.add(normalized); + } + } + } + if (urls.isEmpty() && properties.getDefaultUrl() != null + && !properties.getDefaultUrl().isBlank()) { + urls.add(properties.getDefaultUrl().replaceAll("/+$", "")); + } + return List.copyOf(urls); + } + + private static ResponseEntity workbookResponse(String dtoName, byte[] workbook) { + byte[] body = workbook == null ? new byte[0] : workbook; return ResponseEntity.ok() .contentType(XLSX_MEDIA_TYPE) - .contentLength(workbook == null ? 0 : workbook.length) + .contentLength(body.length) .header(HttpHeaders.CONTENT_DISPOSITION, - ContentDisposition.attachment().filename(fileName).build().toString()) - .body(workbook == null ? new byte[0] : workbook); + ContentDisposition.attachment().filename(dtoName + ".xlsx").build().toString()) + .body(body); + } + + private static ResponseEntity downstreamError(RestClientResponseException error) { + return ResponseEntity.status(error.getStatusCode()) + .contentType(error.getResponseHeaders() != null + && error.getResponseHeaders().getContentType() != null + ? error.getResponseHeaders().getContentType() : MediaType.TEXT_PLAIN) + .body(error.getResponseBodyAsByteArray()); } }