diff --git a/src/main/resources/static/js/comment-cards.js b/src/main/resources/static/js/comment-cards.js index 8643dd6..ffa887e 100644 --- a/src/main/resources/static/js/comment-cards.js +++ b/src/main/resources/static/js/comment-cards.js @@ -6,6 +6,8 @@ let roundedOn = false; // 모서리 둥글게 토글(기본: 각진 모서리) let wordMaskOn = true; // 단어 가리기 기본 ON(단어 클릭=가림). 끄면 카드 클릭=복사. let dirHandle = null; // 저장 폴더(File System Access API). null이면 브라우저 다운로드 폴백. + let dirOptOut = false; // 사용자가 폴더 지정을 직접 해제 → '전체 저장' 시 다시 묻지 않음 + const savedIds = new Set(); // 이미 저장한 댓글 id(재렌더·필터 변경에도 표시 유지) const DISPLAY_CAP = 2000; // 카드 DOM 렌더 상한(브라우저 보호). 분석은 전체 기준. @@ -117,8 +119,10 @@ const card = document.createElement('div'); card.className = 'comment-card' + (mosaicOn ? ' mosaic' : '') - + (roundedOn ? ' rounded' : ''); + + (roundedOn ? ' rounded' : '') + + (savedIds.has(c.__id) ? ' saved' : ''); // 이미 저장한 카드는 회색 톤 유지 card.dataset.index = i; + card.dataset.cid = c.__id; const avatarSrc = c.profileImageUrl ? '/api/comment-cards/avatar?url=' + encodeURIComponent(c.profileImageUrl) @@ -155,10 +159,8 @@ const saveBtn = document.createElement('button'); saveBtn.type = 'button'; saveBtn.className = 'cc-iconbtn'; - saveBtn.innerHTML = ' 저장'; - saveBtn.setAttribute('aria-label', 'PNG 파일로 저장'); - saveBtn.title = 'PNG 파일로 저장 (캡컷에서 불러오기)'; saveBtn.addEventListener('click', (e) => { e.stopPropagation(); window.downloadCard(card, c); }); + setSaveBtn(saveBtn, savedIds.has(c.__id)); actions.appendChild(saveBtn); card.appendChild(head); @@ -369,14 +371,41 @@ /* ------------------------- 캡처/복사/저장 ------------------------- */ + // 카드 저장 버튼의 저장 전/후 모습 + function setSaveBtn(btn, saved) { + btn.classList.toggle('saved', saved); + btn.innerHTML = saved + ? ' 저장됨' + : ' 저장'; + btn.setAttribute('aria-label', saved ? '저장 완료 (다시 저장하려면 클릭)' : 'PNG 파일로 저장'); + btn.title = saved ? '이미 저장한 카드 · 다시 저장하려면 클릭' : 'PNG 파일로 저장 (캡컷에서 불러오기)'; + if (window.lucide) window.lucide.createIcons(); + } + + // 저장 완료 표시: 카드 회색 톤(.saved) + 버튼 '저장됨'. 캡처 이미지엔 반영 안 됨(captureBlob에서 제외). + function markSaved(cardEl) { + const cid = cardEl.dataset.cid; + if (cid !== undefined) savedIds.add(Number(cid)); + cardEl.classList.add('saved'); + const btn = cardEl.querySelector('.cc-actions .cc-iconbtn'); + if (btn) setSaveBtn(btn, true); + } + async function captureBlob(cardEl) { const ms = window.modernScreenshot; if (!ms || !ms.domToBlob) throw new Error('캡처 라이브러리 로드 실패'); - return await ms.domToBlob(cardEl, { - backgroundColor: null, - scale: 2, - filter: (node) => !(node instanceof Element && node.classList.contains('cc-noexport')), - }); + // 저장 표시(회색 톤)는 화면 전용 → 캡처 동안만 벗겨서 원본 그대로 찍는다 + const wasSaved = cardEl.classList.contains('saved'); + if (wasSaved) cardEl.classList.remove('saved'); + try { + return await ms.domToBlob(cardEl, { + backgroundColor: null, + scale: 2, + filter: (node) => !(node instanceof Element && node.classList.contains('cc-noexport')), + }); + } finally { + if (wasSaved) cardEl.classList.add('saved'); + } } function downloadBlob(blob) { @@ -426,14 +455,26 @@ return false; } - // '저장 폴더 선택' — 세션 동안 이 폴더로 저장(이 사이트에만 적용, 크롬 전역 설정 무관) + // 폴더 지정 해제 → 이후 저장은 브라우저 기본 다운로드 폴더로 + function clearDir() { + dirHandle = null; + dirOptOut = true; + $('ccPickDirName').textContent = '저장 폴더'; + setToggle($('ccPickDir'), false); + showToast('폴더 지정 해제 → 다운로드 폴더로 저장'); + } + + // '저장 폴더' 버튼 — 지정돼 있으면 해제(다운로드 폴더), 아니면 폴더 선택 + // 선택한 폴더는 세션 동안 이 사이트에만 적용(크롬 전역 다운로드 설정과 무관) async function pickDir() { + if (dirHandle) { clearDir(); return; } if (!window.showDirectoryPicker) { showToast('이 브라우저는 폴더 저장 미지원 (크롬/엣지 권장)'); return; } try { dirHandle = await window.showDirectoryPicker({ id: 'cc-save', mode: 'readwrite' }); + dirOptOut = false; $('ccPickDirName').textContent = dirHandle.name; setToggle($('ccPickDir'), true); showToast('저장 폴더: ' + dirHandle.name); @@ -444,8 +485,8 @@ async function saveAllVisible() { const cards = Array.from(document.querySelectorAll('#cardGrid .comment-card')); if (!cards.length) { showToast('저장할 카드가 없어요'); return; } - // 폴더 미선택 상태에서 폴더 저장 지원 브라우저면 먼저 폴더 고르게 - if (!dirHandle && window.showDirectoryPicker) { + // 폴더 미선택 상태면 먼저 폴더를 고르게 한다. 단, 사용자가 직접 해제했으면 묻지 않고 다운로드 폴더로. + if (!dirHandle && !dirOptOut && window.showDirectoryPicker) { await pickDir(); if (!dirHandle) return; // 취소 } @@ -461,6 +502,7 @@ try { const blob = await captureBlob(cards[i]); await saveBlob(blob, 'comment-' + String(i + 1).padStart(pad, '0') + '.png'); + markSaved(cards[i]); ok++; } catch (e) { fail++; } } @@ -476,6 +518,7 @@ try { const blob = await captureBlob(cardEl); const toDir = await saveBlob(blob, buildFileName(c)); + markSaved(cardEl); showToast(toDir ? '저장됨 → ' + dirHandle.name : '저장됨 (다운로드 폴더)'); } catch (e) { showToast('저장 실패'); @@ -527,6 +570,9 @@ const json = await res.json(); if (!json.success) throw new Error(json.message || '가져오기 실패'); window.__all = json.data || []; + // 재렌더(필터·정렬)에도 유지되는 고유 id 부여 → 저장 표시가 카드에 붙어 다님 + window.__all.forEach((c, i) => { c.__id = i; }); + savedIds.clear(); // 새로 수집한 영상이므로 저장 표시 초기화 window.__cards = window.__all; // 딥링크: 공유·새로고침 시 결과 유지되도록 URL에 videoId 반영 if (window.__videoId) history.replaceState(null, '', '?v=' + encodeURIComponent(window.__videoId)); diff --git a/src/main/resources/templates/comment-cards.html b/src/main/resources/templates/comment-cards.html index 7098d81..00f6625 100644 --- a/src/main/resources/templates/comment-cards.html +++ b/src/main/resources/templates/comment-cards.html @@ -50,6 +50,10 @@ border-radius:6px; font-size:12px; font-weight:600; padding:.4rem .6rem; cursor:pointer; line-height:1; min-height:32px; display:inline-flex; align-items:center; gap:.25rem; } .cc-iconbtn:hover { color:var(--text); border-color:var(--primary, #6366f1); } + /* 저장 완료 표시 — 화면 전용(캡처 시 .saved를 벗겨서 찍으므로 PNG엔 안 남음) */ + .comment-card.saved { background:var(--surface-2); border-color:var(--border); + box-shadow:inset 3px 0 0 var(--text-3); opacity:.9; } + .cc-iconbtn.saved { background:var(--surface); color:var(--text-3); } /* 복사 완료 토스트 — 카드 밖(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; @@ -130,7 +134,8 @@ - +