From 5ad597c97a2964b2666e261f557e0e39ead5b1d1 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Fri, 10 Jul 2026 12:05:12 +0900 Subject: [PATCH] =?UTF-8?q?feat(comment-cards):=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EC=84=A0=ED=83=9D=20+=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=EB=90=9C=20=EC=B9=B4=EB=93=9C=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - '저장 폴더' 버튼(File System Access API)으로 폴더를 한 번 고르면 ⬇ 저장·전체 저장이 그 폴더로 바로 저장(이 사이트 세션에만 적용) - '전체 저장' 버튼: 현재 표시된 카드를 순서대로 일괄 저장(진행 표시) - 미지원 브라우저(파이어폭스/사파리)는 폴더 버튼 숨김·기존 다운로드 폴백 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/resources/static/js/comment-cards.js | 77 +++++++++++++++++-- .../resources/templates/comment-cards.html | 7 +- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/src/main/resources/static/js/comment-cards.js b/src/main/resources/static/js/comment-cards.js index 4858ab3..8643dd6 100644 --- a/src/main/resources/static/js/comment-cards.js +++ b/src/main/resources/static/js/comment-cards.js @@ -5,6 +5,7 @@ let mosaicOn = true; // 프로필·아이디 모자이크 기본 ON let roundedOn = false; // 모서리 둥글게 토글(기본: 각진 모서리) let wordMaskOn = true; // 단어 가리기 기본 ON(단어 클릭=가림). 끄면 카드 클릭=복사. + let dirHandle = null; // 저장 폴더(File System Access API). null이면 브라우저 다운로드 폴백. const DISPLAY_CAP = 2000; // 카드 DOM 렌더 상한(브라우저 보호). 분석은 전체 기준. @@ -406,17 +407,76 @@ return 'comment-' + (who || 'card') + '.png'; } + // 선택한 폴더에 blob을 파일로 씀(File System Access API) + async function writeToDir(blob, filename) { + const fh = await dirHandle.getFileHandle(filename, { create: true }); + const w = await fh.createWritable(); + await w.write(blob); + await w.close(); + } + + // 폴더가 선택돼 있으면 그 폴더로, 아니면 브라우저 다운로드 폴더로 저장 + async function saveBlob(blob, filename) { + if (dirHandle) { await writeToDir(blob, filename); return true; } + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = filename; + a.click(); + URL.revokeObjectURL(a.href); + return false; + } + + // '저장 폴더 선택' — 세션 동안 이 폴더로 저장(이 사이트에만 적용, 크롬 전역 설정 무관) + async function pickDir() { + if (!window.showDirectoryPicker) { + showToast('이 브라우저는 폴더 저장 미지원 (크롬/엣지 권장)'); + return; + } + try { + dirHandle = await window.showDirectoryPicker({ id: 'cc-save', mode: 'readwrite' }); + $('ccPickDirName').textContent = dirHandle.name; + setToggle($('ccPickDir'), true); + showToast('저장 폴더: ' + dirHandle.name); + } catch (e) { /* 사용자가 취소 → 무시 */ } + } + + // '전체 저장' — 현재 표시된 카드를 순서대로 저장(폴더 선택 시 그 폴더로 일괄) + async function saveAllVisible() { + const cards = Array.from(document.querySelectorAll('#cardGrid .comment-card')); + if (!cards.length) { showToast('저장할 카드가 없어요'); return; } + // 폴더 미선택 상태에서 폴더 저장 지원 브라우저면 먼저 폴더 고르게 + if (!dirHandle && window.showDirectoryPicker) { + await pickDir(); + if (!dirHandle) return; // 취소 + } + if (!dirHandle && !confirm(cards.length + '개 파일이 각각 다운로드됩니다. 계속할까요?')) return; + if (cards.length > 50 && !confirm(cards.length + '개를 저장할까요? 시간이 걸릴 수 있어요.')) return; + + const btn = $('ccSaveAll'); + btn.disabled = true; + const pad = String(cards.length).length; + let ok = 0, fail = 0; + for (let i = 0; i < cards.length; i++) { + btn.textContent = '저장 중… ' + (i + 1) + '/' + cards.length; + try { + const blob = await captureBlob(cards[i]); + await saveBlob(blob, 'comment-' + String(i + 1).padStart(pad, '0') + '.png'); + ok++; + } catch (e) { fail++; } + } + btn.disabled = false; + btn.textContent = '전체 저장'; + showToast('완료: ' + ok + '개 저장' + (fail ? ', ' + fail + ' 실패' : '') + + (dirHandle ? ' → ' + dirHandle.name : ' (다운로드 폴더)')); + } + 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('저장됨 ⬇ (캡컷에서 불러오기)'); + const toDir = await saveBlob(blob, buildFileName(c)); + showToast(toDir ? '저장됨 → ' + dirHandle.name : '저장됨 (다운로드 폴더)'); } catch (e) { showToast('저장 실패'); } finally { @@ -523,6 +583,11 @@ $('cardGrid').classList.toggle('wordmask', wordMaskOn); }); + $('ccPickDir').addEventListener('click', pickDir); + $('ccSaveAll').addEventListener('click', saveAllVisible); + // 폴더 저장 미지원 브라우저(파이어폭스/사파리)면 폴더 버튼 숨김 → 전체 저장은 다운로드 폴백 + if (!window.showDirectoryPicker) $('ccPickDir').style.display = 'none'; + // 딥링크: URL ?v=가 있으면 자동 조회(공유·새로고침 시 결과 유지) const params = new URLSearchParams(location.search); const v = params.get('v'); diff --git a/src/main/resources/templates/comment-cards.html b/src/main/resources/templates/comment-cards.html index 5aa2252..7098d81 100644 --- a/src/main/resources/templates/comment-cards.html +++ b/src/main/resources/templates/comment-cards.html @@ -13,6 +13,7 @@ .cc-btn { background:var(--primary-gradient); color:#fff; border:none; border-radius:8px; padding:.55rem 1rem; font-weight:600; cursor:pointer; font-size:14px; min-height:40px; } .cc-btn.secondary { background:var(--surface-2); color:var(--text); border:1px solid var(--border); } + .cc-btn [data-lucide] { width:15px; height:15px; vertical-align:-3px; } /* 토글 ON 상태: accent로 구분 */ .cc-btn.secondary.active { background:var(--accent-soft); border-color:var(--primary); color:var(--primary); } /* 키보드 포커스 링(모든 상호작용 요소) */ @@ -105,7 +106,7 @@

댓글 카드

유튜브 링크의 전체 댓글을 가져와 분석(많이 언급된 타임라인·자주 나온 단어·요약 통계)하고 카드로 만듭니다. - 모자이크는 기본 켜짐, 모서리 둥글게로 각진/라운드 전환. 기본은 단어 클릭=가리기(블러), 단어 가리기 토글을 끄면 카드 클릭=이미지 복사. 저장 버튼=PNG 파일 다운로드(캡컷은 붙여넣기가 안 되니 저장한 파일을 "불러오기" 하세요). + 모자이크는 기본 켜짐, 모서리 둥글게로 각진/라운드 전환. 기본은 단어 클릭=가리기(블러), 단어 가리기 토글을 끄면 카드 클릭=이미지 복사. 저장 버튼=PNG 파일 다운로드(캡컷은 붙여넣기가 안 되니 저장한 파일을 "불러오기" 하세요). 저장 폴더를 고르면 저장·전체 저장이 그 폴더로 바로 갑니다(크롬/엣지).

@@ -129,6 +130,8 @@ + +
@@ -141,7 +144,7 @@ - +