# 댓글 카드(Comment Cards) 기능 설계 작성일: 2026-06-29 ## 1. 목적 유튜브 영상 링크를 입력하면 해당 영상의 댓글을 가져와, 댓글마다 **유튜브 댓글 UI를 그대로 재현한 카드**로 보여준다. 사용자가 카드의 프로필 사진과 아이디를 **모자이크(블러)** 처리한 뒤, 각 카드를 **PNG 이미지로 클립보드에 복사**해 영상 편집툴(캡컷 등)에 바로 붙여넣어 영상 하단 소스로 쓰는 것이 목표다. 콘텐츠 제작용 보조 도구이며, DB 저장 없이 일회성 조회/생성으로 동작하는 독립 페이지다. ## 2. 사용 흐름 1. 사이드바 "제작" 그룹의 **댓글 카드** 메뉴 진입 (`/comment-cards`) 2. 유튜브 링크(또는 영상 ID) 입력 → **[가져오기]** 3. 서버가 YouTube Data API로 댓글을 여러 페이지에 걸쳐 수집해 한 번에 전달 4. 프론트에서 정렬/필터: - 정렬: 좋아요순 / 답글순 / 최신순 - 필터: 좋아요 N개 이상, 답글 있는 것만 5. **[전체 모자이크]** 토글 → 모든 카드의 프로필 + 아이디 블러 처리 6. 각 카드의 **[복사]** 버튼 → 그 카드를 PNG 이미지로 만들어 클립보드에 복사 7. 캡컷 등에서 Ctrl+V로 붙여넣기 ## 3. 아키텍처 기존 `web/` + `service/` 레이어 컨벤션을 따른다 (YoutubeSearchService / YoutubeSearchApiController와 동일 패턴). DB·엔티티는 추가하지 않는다. ### 3.1 백엔드 **`service/YoutubeCommentService`** (신규) - `List fetchComments(String videoIdOrUrl)` - 입력에서 videoId 추출(정규식: `v=`, `youtu.be/`, `/shorts/`, 또는 11자리 ID 그대로) - YouTube Data API `commentThreads.list` 호출: - `part=snippet` - `videoId={id}` - `order=relevance` - `maxResults=100` - `key={youtube.api.key}` - `pageToken`으로 페이지네이션 (최대 N페이지까지만 — 기본 5페이지 = 약 500개, 쿼터/응답크기 보호) - 각 thread의 `snippet.topLevelComment.snippet`에서 추출: - `authorDisplayName` → authorName - `authorProfileImageUrl` → profileImageUrl - `textDisplay` → text (HTML 포함 가능, 프론트에서 안전 렌더링) - `likeCount` → likeCount - `publishedAt` → publishedAt - thread의 `snippet.totalReplyCount` → replyCount - 댓글이 비활성화된 영상(403 commentsDisabled) 등은 빈 목록 + 사유 메시지로 처리 **`web/CommentCardApiController`** (신규, `/api/comment-cards`) - `POST /api/comment-cards/fetch` — body `{ "url": "..." }`, 응답 `ApiResponse>` - `GET /api/comment-cards/avatar?url=...` — 프로필 이미지 **프록시**. - 이유: 카드를 PNG로 캡처할 때 외부 도메인(yt3.ggpht.com 등) 이미지는 canvas를 오염(taint)시켜 클립보드 복사가 막힘. 동일 출처로 프록시해 회피. - 화이트리스트: `*.ggpht.com`, `*.googleusercontent.com` 만 허용(SSRF 방지). 그 외 URL은 거부. - 응답: 원본 바이트 + 적절한 Content-Type, 캐시 헤더. **`web/dto/CommentCardDto`** (신규) - `authorName, profileImageUrl, text, likeCount, replyCount, publishedAt` **쿼터**: `commentThreads.list`는 호출당 1유닛. 기존 `YoutubeQuotaGuard.tryConsume(pageCount)` 적용 — 예산 초과 시 수집 중단하고 그때까지 모은 댓글 반환 + 안내. ### 3.2 프론트엔드 **페이지 라우트**: `WebController`에 `@GetMapping("/comment-cards")` 추가, `currentPage="comment-cards"`, 템플릿 `comment-cards.html` 반환. **사이드바**: `layout/sidebar.html`의 "제작" 그룹에 메뉴 항목 추가 (아이콘 예: `message-square-quote`). **템플릿 `templates/comment-cards.html`**: `layout/base.html` 사용, 다크모드 디자인시스템(variables.css) 준수. - 상단 입력바: URL input + [가져오기] - 필터바: 정렬 셀렉트 + 좋아요 임계값 input + "답글만" 체크 + [전체 모자이크] 토글 버튼 - 카드 그리드: 유튜브 댓글 스타일 카드 - 좌측 원형 프로필(프록시 경유 `/api/comment-cards/avatar?url=...`) - 상단 아이디 + 작성시간(상대표기) - 본문 댓글 텍스트 - 하단 좋아요수(👍) + 답글수 - 우상단/하단 [복사] 버튼 - 모자이크 상태일 때 프로필 + 아이디에 블러 적용 **정적 JS `static/js/comment-cards.js`**: - fetch 호출 → 카드 렌더 → 클라이언트 정렬/필터 - 전체 모자이크 토글 → 카드에 `.mosaic` 클래스 토글 - 복사 → 카드 DOM을 PNG로 캡처 → 클립보드(`navigator.clipboard.write([new ClipboardItem({'image/png': blob})])`) **DOM → 이미지 캡처 라이브러리**: - `modern-screenshot`(또는 `snapdom`) 사용 — CSS `filter: blur()`를 SVG foreignObject로 충실히 렌더하므로 모자이크가 캡처 결과에 그대로 반영됨. (html2canvas는 blur 필터 미지원이라 부적합.) - CDN `