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 @@
-
+