dto다운로드
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 22s

This commit is contained in:
Boram
2026-09-03 10:46:10 +09:00
parent 693ddea18d
commit 82c087e2da

View File

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