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 @@ 프로덕션 + + 댓글 카드 +