feat(comment-cards): 카드 PNG 파일 저장(다운로드) 버튼 추가

캡컷은 클립보드 이미지 붙여넣기를 못 받아 파일 불러오기가 필요 → 카드별 '⬇ 저장' 버튼으로
PNG 다운로드 제공. 저장 버튼은 cc-noexport+modern-screenshot filter로 캡처 이미지에서 제외.
카드 클릭=클립보드 복사는 유지.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-06-30 12:45:46 +09:00
parent 7de8f954ad
commit ba3776eb7e
2 changed files with 50 additions and 5 deletions

View File

@ -63,11 +63,23 @@
head.querySelector('.cc-time').textContent = timeAgo(c.publishedAt);
head.querySelector('.cc-text').textContent = toPlainText(c.text);
// 카드 자체를 클릭하면 복사 (버튼이 캡처 이미지에 찍히지 않도록 버튼 제거)
card.title = '클릭하면 이미지 복사';
// 카드 자체를 클릭하면 복사 (클립보드)
card.title = '클릭하면 이미지 복사 · 저장 버튼은 PNG 파일로 내려받기';
card.addEventListener('click', () => window.copyCard(card));
// 저장(다운로드) 버튼 — cc-noexport라 캡처 이미지엔 안 찍힘. 캡컷엔 이 파일을 불러오기.
const actions = document.createElement('div');
actions.className = 'cc-actions cc-noexport';
const saveBtn = document.createElement('button');
saveBtn.type = 'button';
saveBtn.className = 'cc-iconbtn';
saveBtn.textContent = '⬇ 저장';
saveBtn.title = 'PNG 파일로 저장 (캡컷에서 불러오기)';
saveBtn.addEventListener('click', (e) => { e.stopPropagation(); window.downloadCard(card, c); });
actions.appendChild(saveBtn);
card.appendChild(head);
card.appendChild(actions);
grid.appendChild(card);
});
if (window.lucide) window.lucide.createIcons();
@ -78,7 +90,12 @@
const ms = window.modernScreenshot;
if (!ms || !ms.domToBlob) throw new Error('캡처 라이브러리 로드 실패');
// backgroundColor:null → 카드 바깥 투명. scale 2 → 또렷하게.
return await ms.domToBlob(cardEl, { backgroundColor: null, scale: 2 });
// filter: .cc-noexport(저장/액션 버튼)는 캡처 이미지에서 제외
return await ms.domToBlob(cardEl, {
backgroundColor: null,
scale: 2,
filter: (node) => !(node instanceof Element && node.classList.contains('cc-noexport')),
});
}
function downloadBlob(blob) {
@ -104,6 +121,29 @@
toastTimer = setTimeout(() => t.classList.remove('show'), 1500);
}
function buildFileName(c) {
var who = (c && c.authorName) ? c.authorName.replace(/[^\w가-힣]+/g, '').slice(0, 20) : 'card';
return 'comment-' + (who || 'card') + '.png';
}
window.downloadCard = async function (cardEl, c) {
if (cardEl.dataset.busy === '1') return;
cardEl.dataset.busy = '1';
try {
const blob = await captureBlob(cardEl);
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = buildFileName(c);
a.click();
URL.revokeObjectURL(a.href);
showToast('저장됨 ⬇ (캡컷에서 불러오기)');
} catch (e) {
showToast('저장 실패');
} finally {
setTimeout(() => { delete cardEl.dataset.busy; }, 600);
}
};
window.copyCard = async function (cardEl) {
if (cardEl.dataset.copying === '1') return; // 중복 클릭 방지
cardEl.dataset.copying = '1';

View File

@ -27,6 +27,11 @@
.cc-text { font-size:14px; color:var(--text); margin-top:.35rem; white-space:pre-wrap; word-break:break-word; line-height:1.45; }
.cc-stats { font-size:12px; color:var(--text-3); margin-top:.5rem; display:flex; gap:1rem; }
.comment-card { cursor:pointer; }
/* 저장 버튼 — cc-noexport라 캡처 이미지에서 제외됨 */
.cc-actions { position:absolute; top:.5rem; right:.5rem; display:flex; gap:.3rem; }
.cc-iconbtn { background:var(--surface-2); color:var(--text-2); border:1px solid var(--border);
border-radius:6px; font-size:11.5px; font-weight:600; padding:.25rem .5rem; cursor:pointer; line-height:1; }
.cc-iconbtn:hover { color:var(--text); border-color:var(--primary, #6366f1); }
/* 복사 완료 토스트 — 카드 밖(body)에 떠서 캡처 이미지에 안 찍힘 */
.cc-toast { position:fixed; left:50%; bottom:2rem; transform:translateX(-50%) translateY(1rem);
background:var(--text); color:var(--surface); padding:.6rem 1.1rem; border-radius:999px;
@ -42,7 +47,7 @@
<div layout:fragment="content">
<h1 style="font-size:1.5rem;font-weight:700;margin-bottom:.3rem;">댓글 카드</h1>
<p style="color:var(--text-3);margin-bottom:1.2rem;font-size:14px;">
유튜브 링크의 댓글을 가져와 카드로 만들고, 프로필·아이디를 모자이크하세요. <b>카드를 클릭하면 이미지가 복사</b>됩니다(캡컷 등에 Ctrl+V).
유튜브 링크의 댓글을 가져와 카드로 만들고, 프로필·아이디를 모자이크하세요. <b>카드 클릭=이미지 복사</b>, <b>⬇ 저장=PNG 파일 다운로드</b>(캡컷은 클립보드 붙여넣기가 안 되니 저장한 파일을 "불러오기" 하세요).
</p>
<div class="cc-toolbar">
@ -71,7 +76,7 @@
<th:block layout:fragment="script">
<!-- DOM→이미지 캡처 (Task 5에서 사용). blur 필터 지원 위해 modern-screenshot 사용. -->
<script src="https://cdn.jsdelivr.net/npm/modern-screenshot@4/dist/index.js"></script>
<script th:src="@{/js/comment-cards.js(v=20260629c)}"></script>
<script th:src="@{/js/comment-cards.js(v=20260630)}"></script>
</th:block>
</body>
</html>