feat(comment-cards): 저장한 카드 표시 + 저장 폴더 토글 해제
- 저장 완료 카드에 회색 톤(.saved)과 '저장됨 ✓' 버튼 상태 부여 - 저장 표시는 화면 전용: 캡처 직전 .saved를 벗겨 PNG엔 남지 않음 - 댓글에 고유 id를 붙여 필터·정렬 재렌더 후에도 저장 표시 유지 - '저장 폴더' 버튼 재클릭 시 지정 해제 → 브라우저 다운로드 폴더로 저장 (해제 이후 '전체 저장'은 폴더를 다시 묻지 않음) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5ad597c97a
commit
73aebf82d3
@ -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 = '<i data-lucide="download" style="width:13px;height:13px;"></i> 저장';
|
||||
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
|
||||
? '<i data-lucide="check" style="width:13px;height:13px;"></i> 저장됨'
|
||||
: '<i data-lucide="download" style="width:13px;height:13px;"></i> 저장';
|
||||
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('캡처 라이브러리 로드 실패');
|
||||
// 저장 표시(회색 톤)는 화면 전용 → 캡처 동안만 벗겨서 원본 그대로 찍는다
|
||||
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));
|
||||
|
||||
@ -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 @@
|
||||
<button class="cc-btn secondary active" id="ccMosaic" aria-pressed="true">모자이크 해제</button>
|
||||
<button class="cc-btn secondary" id="ccRounded" aria-pressed="false">모서리 둥글게</button>
|
||||
<button class="cc-btn secondary active" id="ccWordMask" aria-pressed="true">단어 가리기: 켜짐</button>
|
||||
<button class="cc-btn secondary" id="ccPickDir" aria-pressed="false"><i data-lucide="folder"></i> <span id="ccPickDirName">저장 폴더</span></button>
|
||||
<button class="cc-btn secondary" id="ccPickDir" aria-pressed="false"
|
||||
title="클릭: 저장 폴더 지정 · 다시 클릭: 해제(브라우저 다운로드 폴더로 저장)"><i data-lucide="folder"></i> <span id="ccPickDirName">저장 폴더</span></button>
|
||||
<button class="cc-btn secondary" id="ccSaveAll"><i data-lucide="download"></i> 전체 저장</button>
|
||||
<span id="ccCount" style="font-size:12px;color:var(--text-2);"></span>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user