diff --git a/src/main/java/com/kamco/cd/kamcoback/common/download/RangeDownloadResponder.java b/src/main/java/com/kamco/cd/kamcoback/common/download/RangeDownloadResponder.java index a5a6cb5a..01cd0d24 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/download/RangeDownloadResponder.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/download/RangeDownloadResponder.java @@ -5,7 +5,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; -import java.util.Map; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.ResourceRegion; @@ -15,43 +14,11 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; -/** - * Range(이어받기) 지원 파일 다운로드 응답 생성기. - * - *

- Range 헤더 없으면 200 + Resource - Range 헤더 있으면 206 + ResourceRegion - 잘못된 Range면 416 + - * Content-Range: bytes - * - *

주의: - 다중 Range는 첫 번째 Range만 처리(안정성 목적). - 파일이 이미 생성되어 있는 대용량(zip 등) 다운로드에 적합. - */ @Component public class RangeDownloadResponder { - /** 기본(zip) 다운로드 응답 생성. */ public ResponseEntity buildZipResponse( Path filePath, String downloadFileName, HttpServletRequest request) throws IOException { - return buildResponse( - filePath, downloadFileName, MediaType.APPLICATION_OCTET_STREAM, request, Map.of()); - } - - /** 추가 헤더가 필요한 경우(예: X-Accel-Buffering) 사용. */ - public ResponseEntity buildZipResponse( - Path filePath, - String downloadFileName, - HttpServletRequest request, - Map extraHeaders) - throws IOException { - return buildResponse( - filePath, downloadFileName, MediaType.APPLICATION_OCTET_STREAM, request, extraHeaders); - } - - /** 콘텐츠 타입까지 지정하고 싶을 때 사용. */ - public ResponseEntity buildResponse( - Path filePath, - String downloadFileName, - MediaType contentType, - HttpServletRequest request, - Map extraHeaders) - throws IOException { if (!Files.isRegularFile(filePath)) { return ResponseEntity.notFound().build(); @@ -60,73 +27,53 @@ public class RangeDownloadResponder { long totalSize = Files.size(filePath); Resource resource = new FileSystemResource(filePath); - // 공통 헤더(200/206 공통) String disposition = "attachment; filename=\"" + downloadFileName + "\""; String rangeHeader = request.getHeader(HttpHeaders.RANGE); - if (rangeHeader == null || rangeHeader.isBlank()) { - // 200 OK (전체 다운로드) - ResponseEntity.BodyBuilder ok = - ResponseEntity.ok() - .contentType(contentType) - .header(HttpHeaders.CONTENT_DISPOSITION, disposition) - .header(HttpHeaders.ACCEPT_RANGES, "bytes") - .contentLength(totalSize); + // 🔥 공통 헤더 (여기 고정) + ResponseEntity.BodyBuilder base = + ResponseEntity.ok() + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .header(HttpHeaders.CONTENT_DISPOSITION, disposition) + .header(HttpHeaders.ACCEPT_RANGES, "bytes") + .header("X-Accel-Buffering", "no"); - applyExtraHeaders(ok, extraHeaders); - return ok.body(resource); + if (rangeHeader == null || rangeHeader.isBlank()) { + return base.contentLength(totalSize).body(resource); } - // Range 파싱 List ranges; try { ranges = HttpRange.parseRanges(rangeHeader); } catch (IllegalArgumentException ex) { - // 416 Range Not Satisfiable - ResponseEntity.BodyBuilder badRange = - ResponseEntity.status(416).header(HttpHeaders.CONTENT_RANGE, "bytes */" + totalSize); - applyExtraHeaders(badRange, extraHeaders); - return badRange.build(); + return ResponseEntity.status(416) + .header(HttpHeaders.CONTENT_RANGE, "bytes */" + totalSize) + .header("X-Accel-Buffering", "no") + .build(); } - // 실무에선 단일 Range만 처리하는 게 안전합니다. HttpRange range = ranges.get(0); long start = range.getRangeStart(totalSize); long end = range.getRangeEnd(totalSize); if (start >= totalSize) { - ResponseEntity.BodyBuilder badRange = - ResponseEntity.status(416).header(HttpHeaders.CONTENT_RANGE, "bytes */" + totalSize); - applyExtraHeaders(badRange, extraHeaders); - return badRange.build(); + return ResponseEntity.status(416) + .header(HttpHeaders.CONTENT_RANGE, "bytes */" + totalSize) + .header("X-Accel-Buffering", "no") + .build(); } long regionLength = end - start + 1; ResourceRegion region = new ResourceRegion(resource, start, regionLength); - // 206 Partial Content - ResponseEntity.BodyBuilder partial = - ResponseEntity.status(206) - .contentType(contentType) - .header(HttpHeaders.CONTENT_DISPOSITION, disposition) - .header(HttpHeaders.ACCEPT_RANGES, "bytes") - .header(HttpHeaders.CONTENT_RANGE, "bytes " + start + "-" + end + "/" + totalSize) - .contentLength(regionLength); - - applyExtraHeaders(partial, extraHeaders); - return partial.body(region); - } - - private void applyExtraHeaders( - ResponseEntity.HeadersBuilder builder, Map extraHeaders) { - if (extraHeaders == null || extraHeaders.isEmpty()) return; - - extraHeaders.forEach( - (k, v) -> { - if (k != null && !k.isBlank() && v != null) { - builder.header(k, v); - } - }); + return ResponseEntity.status(206) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .header(HttpHeaders.CONTENT_DISPOSITION, disposition) + .header(HttpHeaders.ACCEPT_RANGES, "bytes") + .header("X-Accel-Buffering", "no") + .header(HttpHeaders.CONTENT_RANGE, "bytes " + start + "-" + end + "/" + totalSize) + .contentLength(regionLength) + .body(region); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java b/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java index 3fc6a081..91ab9525 100644 --- a/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/inference/InferenceResultApiController.java @@ -374,8 +374,7 @@ public class InferenceResultApiController { Path zipPath = Path.of(path); // Range + 200/206/416 공통 처리 (추가 헤더 포함) - return rangeDownloadResponder.buildZipResponse( - zipPath, uid + ".zip", request, Map.of("X-Accel-Buffering", "no")); + return rangeDownloadResponder.buildZipResponse(zipPath, uid + ".zip", request); } @Operation(summary = "shp 파일 다운로드 이력 조회", description = "추론관리 분석결과 shp 파일 다운로드 이력 조회")