feat(comment-cards): 댓글 fetch + 프로필 프록시 API 추가

POST /api/comment-cards/fetch, GET /api/comment-cards/avatar(구글 도메인 화이트리스트)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-06-29 17:05:27 +09:00
parent 15a655d7f9
commit 0e84f5de08

View File

@ -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<List<CommentCardDto>> fetch(@RequestBody FetchRequest req) {
return ApiResponse.ok(commentService.fetchComments(req.getUrl()));
}
/** 프로필 이미지를 동일 출처로 프록시(canvas taint 회피). 구글 도메인만 허용(SSRF 방지). */
@GetMapping("/avatar")
public ResponseEntity<byte[]> avatar(@RequestParam("url") String url) {
if (!isAllowed(url)) {
return ResponseEntity.badRequest().build();
}
ResponseEntity<byte[]> 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;
}
}