From 418242f47d85c160e0ac3fdf8fadf936a486831f Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Mon, 29 Jun 2026 17:06:53 +0900 Subject: [PATCH] =?UTF-8?q?feat(comment-cards):=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=ED=8E=98=EC=9D=B4=EC=A7=80=C2=B7=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=EB=B0=94=C2=B7=EB=A0=8C=EB=8D=94/=EC=A0=95?= =?UTF-8?q?=EB=A0=AC/=ED=95=84=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /comment-cards 라우트, 유튜브 스타일 카드 렌더, 정렬/임계값 필터, 전체 모자이크 토글 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../com/hlab/yanalyst/web/WebController.java | 6 + src/main/resources/static/js/comment-cards.js | 116 ++++++++++++++++++ .../resources/templates/comment-cards.html | 70 +++++++++++ .../resources/templates/layout/sidebar.html | 3 + 4 files changed, 195 insertions(+) create mode 100644 src/main/resources/static/js/comment-cards.js create mode 100644 src/main/resources/templates/comment-cards.html diff --git a/src/main/java/com/hlab/yanalyst/web/WebController.java b/src/main/java/com/hlab/yanalyst/web/WebController.java index fa05177..9e505ce 100644 --- a/src/main/java/com/hlab/yanalyst/web/WebController.java +++ b/src/main/java/com/hlab/yanalyst/web/WebController.java @@ -71,6 +71,12 @@ public class WebController { return "rework"; } + @GetMapping("/comment-cards") + public String commentCards(Model model) { + model.addAttribute("currentPage", "comment-cards"); + return "comment-cards"; + } + @GetMapping("/production") public String production(Model model) { model.addAttribute("currentPage", "production"); diff --git a/src/main/resources/static/js/comment-cards.js b/src/main/resources/static/js/comment-cards.js new file mode 100644 index 0000000..ecd934c --- /dev/null +++ b/src/main/resources/static/js/comment-cards.js @@ -0,0 +1,116 @@ +(function () { + window.__cards = []; + let mosaicOn = false; + + const $ = (id) => document.getElementById(id); + + function timeAgo(iso) { + if (!iso) return ''; + const then = new Date(iso).getTime(); + if (isNaN(then)) return ''; + const sec = Math.floor((Date.now() - then) / 1000); + const units = [['년', 31536000], ['개월', 2592000], ['일', 86400], ['시간', 3600], ['분', 60]]; + for (const [label, s] of units) { + const v = Math.floor(sec / s); + if (v >= 1) return v + label + ' 전'; + } + return '방금 전'; + } + + // YouTube textDisplay(HTML) → 안전한 평문 (태그 제거,
→줄바꿈) + function toPlainText(html) { + const tmp = document.createElement('div'); + tmp.innerHTML = String(html).replace(//gi, '\n'); + return tmp.textContent || ''; + } + + function applyFilterSort(cards) { + const sort = $('ccSort').value; + const minLikes = parseInt($('ccMinLikes').value, 10) || 0; + const repliesOnly = $('ccRepliesOnly').checked; + let list = cards.filter(c => (c.likeCount || 0) >= minLikes && (!repliesOnly || (c.replyCount || 0) > 0)); + if (sort === 'likes') list.sort((a, b) => (b.likeCount || 0) - (a.likeCount || 0)); + else if (sort === 'replies') list.sort((a, b) => (b.replyCount || 0) - (a.replyCount || 0)); + else if (sort === 'latest') list.sort((a, b) => new Date(b.publishedAt) - new Date(a.publishedAt)); + return list; + } + + window.renderCards = function () { + const grid = $('cardGrid'); + const list = applyFilterSort(window.__cards); + $('ccCount').textContent = list.length + '개'; + grid.innerHTML = ''; + list.forEach((c, i) => { + const card = document.createElement('div'); + card.className = 'comment-card' + (mosaicOn ? ' mosaic' : ''); + card.dataset.index = i; + + const avatarSrc = c.profileImageUrl + ? '/api/comment-cards/avatar?url=' + encodeURIComponent(c.profileImageUrl) + : ''; + + const head = document.createElement('div'); + head.className = 'cc-head'; + head.innerHTML = + '' + + '
' + + '
' + + '
' + + '
👍 ' + (c.likeCount || 0).toLocaleString() + '' + + '💬 ' + (c.replyCount || 0).toLocaleString() + '
' + + '
'; + head.querySelector('.cc-author').textContent = c.authorName || ''; + head.querySelector('.cc-time').textContent = timeAgo(c.publishedAt); + head.querySelector('.cc-text').textContent = toPlainText(c.text); + + const copyBtn = document.createElement('button'); + copyBtn.className = 'cc-btn secondary cc-copy'; + copyBtn.textContent = '복사'; + copyBtn.addEventListener('click', () => window.copyCard(card, copyBtn)); // Task 5에서 정의 + + card.appendChild(head); + card.appendChild(copyBtn); + grid.appendChild(card); + }); + if (window.lucide) window.lucide.createIcons(); + }; + + async function fetchComments() { + const url = $('ccUrl').value.trim(); + if (!url) return; + const btn = $('ccFetch'); + btn.disabled = true; btn.textContent = '가져오는 중…'; + $('ccEmpty').textContent = ''; + try { + const res = await fetch('/api/comment-cards/fetch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url }) + }); + const json = await res.json(); + if (!json.success) throw new Error(json.message || '가져오기 실패'); + window.__cards = json.data || []; + $('ccFilters').style.display = window.__cards.length ? 'flex' : 'none'; + $('ccEmpty').textContent = window.__cards.length ? '' : '댓글이 없습니다.'; + window.renderCards(); + } catch (e) { + $('ccFilters').style.display = 'none'; + $('cardGrid').innerHTML = ''; + $('ccEmpty').textContent = '⚠️ ' + e.message; + } finally { + btn.disabled = false; btn.textContent = '가져오기'; + } + } + + document.addEventListener('DOMContentLoaded', () => { + $('ccFetch').addEventListener('click', fetchComments); + $('ccUrl').addEventListener('keydown', (e) => { if (e.key === 'Enter') fetchComments(); }); + ['ccSort', 'ccMinLikes', 'ccRepliesOnly'].forEach(id => + $(id).addEventListener('input', () => window.renderCards())); + $('ccMosaic').addEventListener('click', () => { + mosaicOn = !mosaicOn; + $('ccMosaic').textContent = mosaicOn ? '모자이크 해제' : '전체 모자이크'; + document.querySelectorAll('.comment-card').forEach(el => el.classList.toggle('mosaic', mosaicOn)); + }); + }); +})(); diff --git a/src/main/resources/templates/comment-cards.html b/src/main/resources/templates/comment-cards.html new file mode 100644 index 0000000..174de44 --- /dev/null +++ b/src/main/resources/templates/comment-cards.html @@ -0,0 +1,70 @@ + + + + h-lab - 댓글 카드 + + + +
+

댓글 카드

+

+ 유튜브 링크의 댓글을 가져와 카드로 만들고, 프로필·아이디를 모자이크해 영상 소스로 복사하세요. +

+ +
+ + +
+ + + +
+
+
+ + + + + + + + diff --git a/src/main/resources/templates/layout/sidebar.html b/src/main/resources/templates/layout/sidebar.html index f32f3dc..9909dbb 100644 --- a/src/main/resources/templates/layout/sidebar.html +++ b/src/main/resources/templates/layout/sidebar.html @@ -49,6 +49,9 @@ 프로덕션 + + 댓글 카드 +