package com.hlab.yanalyst.domain.upload; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.*; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Path; import java.time.Duration; import java.time.Instant; @Component public class YoutubeResumableUploadClient { private static final String UPLOAD_URL = "https://www.googleapis.com/upload/youtube/v3/videos"; private final RestTemplate restTemplate; private final ObjectMapper objectMapper; private final HttpClient httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build(); public YoutubeResumableUploadClient(@Qualifier("restTemplate") RestTemplate restTemplate, ObjectMapper objectMapper) { this.restTemplate = restTemplate; this.objectMapper = objectMapper; } public String upload(String accessToken, Path file, long size, String contentType, String title, String description, String privacyStatus, Instant publishAt) throws Exception { ObjectNode metadata = objectMapper.createObjectNode(); metadata.putObject("snippet").put("title", title).put("description", description).put("categoryId", "22"); ObjectNode status = metadata.putObject("status"); status.put("privacyStatus", publishAt == null ? privacyStatus : "private"); status.put("selfDeclaredMadeForKids", false); if (publishAt != null) status.put("publishAt", publishAt.toString()); URI initiateUri = UriComponentsBuilder.fromUriString(UPLOAD_URL) .queryParam("uploadType", "resumable").queryParam("part", "snippet,status").build().toUri(); HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(accessToken); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("X-Upload-Content-Type", contentType); headers.set("X-Upload-Content-Length", Long.toString(size)); ResponseEntity initiated = restTemplate.exchange(initiateUri, HttpMethod.POST, new HttpEntity<>(objectMapper.writeValueAsString(metadata), headers), String.class); URI sessionUri = initiated.getHeaders().getLocation(); if (sessionUri == null) throw new IllegalStateException("YouTube 업로드 세션 주소를 받지 못했습니다."); HttpRequest request = HttpRequest.newBuilder(sessionUri).timeout(Duration.ofHours(2)) .header("Authorization", "Bearer " + accessToken) .header("Content-Type", contentType) .header("Content-Length", Long.toString(size)) .PUT(HttpRequest.BodyPublishers.ofFile(file)).build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() < 200 || response.statusCode() >= 300) { throw new IllegalStateException("YouTube 업로드 실패 (HTTP " + response.statusCode() + "): " + response.body().substring(0, Math.min(500, response.body().length()))); } JsonNode result = objectMapper.readTree(response.body()); String videoId = result.path("id").asText(""); if (videoId.isBlank()) throw new IllegalStateException("YouTube 영상 ID를 받지 못했습니다."); return videoId; } }