This commit is contained in:
@@ -1,8 +1,11 @@
|
|||||||
package io.shinhanlife.dat.mcg.presentation;
|
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 java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.core.ParameterizedTypeReference;
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.http.ContentDisposition;
|
import org.springframework.http.ContentDisposition;
|
||||||
import org.springframework.http.HttpHeaders;
|
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.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.client.RestClient;
|
import org.springframework.web.client.RestClient;
|
||||||
|
import org.springframework.web.client.RestClientException;
|
||||||
import org.springframework.web.client.RestClientResponseException;
|
import org.springframework.web.client.RestClientResponseException;
|
||||||
|
|
||||||
/**
|
/** Aggregates DTO downloads from the CUS, SAL, PRO, and SYS Tool Pods. */
|
||||||
* Gateway 화면에서 들어온 DTO 엑셀 요청을 실제 DTO 클래스가 있는 Tool Pod로 전달한다.
|
|
||||||
* 브라우저가 동일 출처(8081)만 호출하도록 하여 CORS와 배포 주소 차이를 숨긴다.
|
|
||||||
*/
|
|
||||||
@RestController
|
@RestController
|
||||||
public class DtoDownloadProxyController {
|
public class DtoDownloadProxyController {
|
||||||
|
|
||||||
private static final MediaType XLSX_MEDIA_TYPE = MediaType.parseMediaType(
|
private static final MediaType XLSX_MEDIA_TYPE = MediaType.parseMediaType(
|
||||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
private static final List<String> DTO_ROUTE_KEYS = List.of("cus", "sal", "pro", "sys");
|
||||||
|
|
||||||
private final RestClient restClient;
|
private final RestClient restClient;
|
||||||
private final String toolPodUrl;
|
private final List<String> toolPodUrls;
|
||||||
|
|
||||||
public DtoDownloadProxyController(
|
public DtoDownloadProxyController(
|
||||||
RestClient.Builder restClientBuilder,
|
RestClient.Builder restClientBuilder,
|
||||||
@Value("${mcp.gateway.fallback.default-url:http://localhost:8084}") String toolPodUrl) {
|
GatewayFallbackProperties fallbackProperties) {
|
||||||
this.restClient = restClientBuilder.build();
|
this.restClient = restClientBuilder.build();
|
||||||
this.toolPodUrl = toolPodUrl.replaceAll("/+$", "");
|
this.toolPodUrls = resolveToolPodUrls(fallbackProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/dto-download/options")
|
@GetMapping("/dto-download/options")
|
||||||
public List<String> options() {
|
public List<String> options() {
|
||||||
// 셀렉트박스에 표시할 DTO 이름 목록을 Tool Pod에서 조회한다.
|
Set<String> mergedOptions = new LinkedHashSet<>();
|
||||||
List<String> options = restClient.get()
|
for (String toolPodUrl : toolPodUrls) {
|
||||||
.uri(toolPodUrl + "/dto-download/options")
|
try {
|
||||||
.retrieve()
|
List<String> podOptions = restClient.get()
|
||||||
.body(new ParameterizedTypeReference<>() {});
|
.uri(toolPodUrl + "/dto-download/options")
|
||||||
return options == null ? List.of() : 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}")
|
@GetMapping("/dto-download/{dtoName}")
|
||||||
public ResponseEntity<byte[]> download(@PathVariable("dtoName") String dtoName) {
|
public ResponseEntity<byte[]> download(@PathVariable("dtoName") String dtoName) {
|
||||||
byte[] workbook;
|
for (String toolPodUrl : toolPodUrls) {
|
||||||
try {
|
try {
|
||||||
// 생성된 엑셀 바이트를 그대로 브라우저에 전달한다.
|
byte[] workbook = restClient.get()
|
||||||
workbook = restClient.get()
|
.uri(toolPodUrl + "/dto-download/{dtoName}", dtoName)
|
||||||
.uri(toolPodUrl + "/dto-download/{dtoName}", dtoName)
|
.retrieve()
|
||||||
.retrieve()
|
.body(byte[].class);
|
||||||
.body(byte[].class);
|
return workbookResponse(dtoName, workbook);
|
||||||
} catch (RestClientResponseException error) {
|
} catch (RestClientResponseException error) {
|
||||||
// 형식 불일치 등의 상태 코드와 오류 메시지도 변경 없이 전달한다.
|
if (error.getStatusCode().value() == 404) {
|
||||||
return ResponseEntity.status(error.getStatusCode())
|
continue;
|
||||||
.contentType(error.getResponseHeaders() != null
|
}
|
||||||
&& error.getResponseHeaders().getContentType() != null
|
return downstreamError(error);
|
||||||
? error.getResponseHeaders().getContentType() : MediaType.TEXT_PLAIN)
|
} catch (RestClientException ignored) {
|
||||||
.body(error.getResponseBodyAsByteArray());
|
// 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<String> resolveToolPodUrls(GatewayFallbackProperties properties) {
|
||||||
|
List<String> 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<byte[]> workbookResponse(String dtoName, byte[] workbook) {
|
||||||
|
byte[] body = workbook == null ? new byte[0] : workbook;
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.contentType(XLSX_MEDIA_TYPE)
|
.contentType(XLSX_MEDIA_TYPE)
|
||||||
.contentLength(workbook == null ? 0 : workbook.length)
|
.contentLength(body.length)
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
ContentDisposition.attachment().filename(fileName).build().toString())
|
ContentDisposition.attachment().filename(dtoName + ".xlsx").build().toString())
|
||||||
.body(workbook == null ? new byte[0] : workbook);
|
.body(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResponseEntity<byte[]> 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user