From 0e84f5de08f62c434ce9291e9ed34e0b58d14407 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Mon, 29 Jun 2026 17:05:27 +0900 Subject: [PATCH] =?UTF-8?q?feat(comment-cards):=20=EB=8C=93=EA=B8=80=20fet?= =?UTF-8?q?ch=20+=20=ED=94=84=EB=A1=9C=ED=95=84=20=ED=94=84=EB=A1=9D?= =?UTF-8?q?=EC=8B=9C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST /api/comment-cards/fetch, GET /api/comment-cards/avatar(구글 도메인 화이트리스트) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../web/CommentCardApiController.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/main/java/com/hlab/yanalyst/web/CommentCardApiController.java diff --git a/src/main/java/com/hlab/yanalyst/web/CommentCardApiController.java b/src/main/java/com/hlab/yanalyst/web/CommentCardApiController.java new file mode 100644 index 0000000..444c1ea --- /dev/null +++ b/src/main/java/com/hlab/yanalyst/web/CommentCardApiController.java @@ -0,0 +1,65 @@ +package com.hlab.yanalyst.web; + +import com.hlab.yanalyst.global.common.ApiResponse; +import com.hlab.yanalyst.service.YoutubeCommentService; +import com.hlab.yanalyst.web.dto.CommentCardDto; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.springframework.http.CacheControl; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; +import java.time.Duration; +import java.util.List; + +@RestController +@RequestMapping("/api/comment-cards") +@RequiredArgsConstructor +public class CommentCardApiController { + + private final YoutubeCommentService commentService; + private final RestTemplate restTemplate; + + @PostMapping("/fetch") + public ApiResponse> fetch(@RequestBody FetchRequest req) { + return ApiResponse.ok(commentService.fetchComments(req.getUrl())); + } + + /** 프로필 이미지를 동일 출처로 프록시(canvas taint 회피). 구글 도메인만 허용(SSRF 방지). */ + @GetMapping("/avatar") + public ResponseEntity avatar(@RequestParam("url") String url) { + if (!isAllowed(url)) { + return ResponseEntity.badRequest().build(); + } + ResponseEntity resp = restTemplate.getForEntity(URI.create(url), byte[].class); + MediaType ct = resp.getHeaders().getContentType(); + return ResponseEntity.ok() + .contentType(ct != null ? ct : MediaType.IMAGE_JPEG) + .cacheControl(CacheControl.maxAge(Duration.ofHours(6)).cachePublic()) + .body(resp.getBody()); + } + + private boolean isAllowed(String url) { + try { + String host = URI.create(url).getHost(); + if (host == null) return false; + host = host.toLowerCase(); + return host.endsWith("ggpht.com") || host.endsWith("googleusercontent.com"); + } catch (Exception e) { + return false; + } + } + + @Data + public static class FetchRequest { + private String url; + } +}