docs(comment-cards): 유튜브 댓글 카드 기능 설계서 추가
링크 입력→댓글 수집(commentThreads)→유튜브 스타일 카드 렌더→전체 모자이크→카드별 PNG 클립보드 복사 흐름 정의 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e2caf9a0ba
commit
f309db46b6
125
docs/superpowers/specs/2026-06-29-comment-cards-design.md
Normal file
125
docs/superpowers/specs/2026-06-29-comment-cards-design.md
Normal file
@ -0,0 +1,125 @@
|
||||
# 댓글 카드(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<CommentCardDto> 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<List<CommentCardDto>>`
|
||||
- `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 `<script>`로 로드(기존 정적 자산 방식과 일관). 빌드 파이프라인 없음.
|
||||
- 프로필 이미지는 프록시 동일출처라 taint 없이 캡처 가능.
|
||||
|
||||
**모자이크 방식**: 프로필 사진은 `filter: blur(6px)`(필요시 추가로 축소-확대 픽셀화), 아이디 텍스트는 `filter: blur(5px)`. 보내준 레퍼런스 이미지 수준의 식별 불가 처리.
|
||||
|
||||
## 4. 데이터 흐름
|
||||
|
||||
```
|
||||
[브라우저] URL 입력
|
||||
→ POST /api/comment-cards/fetch
|
||||
[서버] videoId 추출 → commentThreads.list (페이지네이션, QuotaGuard)
|
||||
→ List<CommentCardDto> (ApiResponse)
|
||||
[브라우저] 카드 렌더 (프로필은 /api/comment-cards/avatar 프록시로 로드)
|
||||
→ 정렬/필터 (클라이언트)
|
||||
→ 전체 모자이크 토글 (CSS 블러)
|
||||
→ [복사] → modern-screenshot로 카드 PNG 캡처 → 클립보드
|
||||
```
|
||||
|
||||
## 5. 에러 처리
|
||||
|
||||
- 잘못된 URL/videoId 추출 실패 → 400, "유효한 유튜브 링크가 아닙니다"
|
||||
- 댓글 비활성화 영상 → 빈 목록 + "이 영상은 댓글이 비활성화되어 있습니다"
|
||||
- API 키 오류/쿼터 초과 → `GlobalExceptionHandler` 경유 에러 응답 + 프론트 토스트
|
||||
- 프록시 화이트리스트 외 URL → 400 (SSRF 방지)
|
||||
- 클립보드 복사 미지원 브라우저 → PNG 다운로드로 폴백 + 안내
|
||||
|
||||
## 6. 범위 밖 (YAGNI)
|
||||
|
||||
- 댓글 DB 저장/이력
|
||||
- 영상 합성/편집 (사용자가 편집툴에서 직접)
|
||||
- 답글(대댓글) 펼치기 — 최상위 댓글만
|
||||
- 감정/키워드 분석
|
||||
- 카드 디자인 커스터마이징(폰트/색상 옵션)
|
||||
|
||||
## 7. 검증
|
||||
|
||||
- 인기 영상 링크로 댓글 100+ 수집 확인
|
||||
- 정렬(좋아요/답글/최신), 필터(임계값) 동작
|
||||
- 전체 모자이크 토글 → 프로필+아이디 블러
|
||||
- [복사] → 클립보드에 PNG 들어가고 캡컷/그림판에 붙여넣기 확인 (모자이크 반영 포함)
|
||||
- 댓글 비활성화 영상/잘못된 링크 에러 처리
|
||||
- 쿼터 가드 동작(페이지 상한)
|
||||
Loading…
Reference in New Issue
Block a user