';
@@ -330,9 +338,10 @@
// 월별 댓글량
if (stats.months.length > 1) {
const mx = Math.max(...stats.months.map(m => m[1]));
- html += '
📅 월별 댓글량 '
+ html += '
월별 댓글량'
+ stats.months.slice(-18).map(([mo, n]) =>
'
'
+ + '' + n.toLocaleString() + ' ' // 값 상시 노출
+ ' '
+ '' + esc(mo.slice(2)) + '
').join('')
+ '
';
@@ -341,6 +350,7 @@
html += '
'; // #ccAnBody
box.innerHTML = html;
box.style.display = 'block';
+ if (window.lucide) window.lucide.createIcons(); // 섹션 헤더 아이콘(이모지 대체) 렌더
const tgl = $('ccAnToggle');
if (tgl) tgl.addEventListener('click', () => {
@@ -348,6 +358,7 @@
const hidden = b.style.display === 'none';
b.style.display = hidden ? '' : 'none';
tgl.textContent = hidden ? '접기' : '펼치기';
+ tgl.setAttribute('aria-expanded', String(hidden));
});
const wt = $('ccWeighted');
if (wt) wt.addEventListener('change', () => renderAnalytics(window.__all));
@@ -445,7 +456,8 @@
window.__videoId = parseVideoId(url);
const btn = $('ccFetch');
btn.disabled = true; btn.textContent = '수집 중…';
- $('ccEmpty').textContent = '전체 댓글을 모으는 중입니다. 댓글이 많으면 시간이 걸릴 수 있어요…';
+ $('ccEmpty').innerHTML = '
'
+ + '전체 댓글을 모으는 중입니다. 댓글이 많으면 시간이 걸릴 수 있어요…';
try {
const res = await fetch('/api/comment-cards/fetch', {
method: 'POST',
@@ -456,6 +468,8 @@
if (!json.success) throw new Error(json.message || '가져오기 실패');
window.__all = json.data || [];
window.__cards = window.__all;
+ // 딥링크: 공유·새로고침 시 결과 유지되도록 URL에 videoId 반영
+ if (window.__videoId) history.replaceState(null, '', '?v=' + encodeURIComponent(window.__videoId));
$('ccFilters').style.display = window.__all.length ? 'flex' : 'none';
$('ccEmpty').textContent = window.__all.length ? '' : '댓글이 없습니다.';
renderAnalytics(window.__all);
@@ -470,12 +484,22 @@
}
}
+ // 토글 버튼 공통: 상태 클래스(.active)·aria-pressed 갱신
+ function setToggle(btn, on) {
+ btn.classList.toggle('active', on);
+ btn.setAttribute('aria-pressed', String(on));
+ }
+
document.addEventListener('DOMContentLoaded', () => {
$('ccFetch').addEventListener('click', fetchComments);
$('ccUrl').addEventListener('keydown', (e) => { if (e.key === 'Enter') fetchComments(); });
- ['ccSearch', 'ccSort', 'ccMinLikes', 'ccRepliesOnly'].forEach(id =>
+ // 즉시 반영해도 가벼운 것(정렬·체크박스)
+ ['ccSort', 'ccRepliesOnly'].forEach(id =>
$(id).addEventListener('input', () => window.renderCards()));
- // 제외 단어는 카드+분석 둘 다 갱신. 대량 댓글 대비 디바운스.
+ // 대량 카드(최대 2,000장) 대비 검색·좋아요 필터는 디바운스
+ const debouncedRender = debounce(() => window.renderCards(), 200);
+ ['ccSearch', 'ccMinLikes'].forEach(id => $(id).addEventListener('input', debouncedRender));
+ // 제외 단어는 카드+분석 둘 다 갱신.
$('ccExclude').addEventListener('input', debounce(() => {
window.renderCards();
renderAnalytics(window.__all);
@@ -483,17 +507,25 @@
$('ccMosaic').addEventListener('click', () => {
mosaicOn = !mosaicOn;
$('ccMosaic').textContent = mosaicOn ? '모자이크 해제' : '전체 모자이크';
+ setToggle($('ccMosaic'), mosaicOn);
document.querySelectorAll('.comment-card').forEach(el => el.classList.toggle('mosaic', mosaicOn));
});
$('ccRounded').addEventListener('click', () => {
roundedOn = !roundedOn;
$('ccRounded').textContent = roundedOn ? '모서리 각지게' : '모서리 둥글게';
+ setToggle($('ccRounded'), roundedOn);
document.querySelectorAll('.comment-card').forEach(el => el.classList.toggle('rounded', roundedOn));
});
$('ccWordMask').addEventListener('click', () => {
wordMaskOn = !wordMaskOn;
- $('ccWordMask').textContent = wordMaskOn ? '🖍 단어 가리기: 켜짐' : '🖍 단어 가리기: 꺼짐(복사)';
+ $('ccWordMask').textContent = wordMaskOn ? '단어 가리기: 켜짐' : '단어 가리기: 꺼짐(복사)';
+ setToggle($('ccWordMask'), wordMaskOn);
$('cardGrid').classList.toggle('wordmask', wordMaskOn);
});
+
+ // 딥링크: URL ?v=
가 있으면 자동 조회(공유·새로고침 시 결과 유지)
+ const params = new URLSearchParams(location.search);
+ const v = params.get('v');
+ if (v) { $('ccUrl').value = v; fetchComments(); }
});
})();
diff --git a/src/main/resources/templates/comment-cards.html b/src/main/resources/templates/comment-cards.html
index 69bc37b..5aa2252 100644
--- a/src/main/resources/templates/comment-cards.html
+++ b/src/main/resources/templates/comment-cards.html
@@ -7,12 +7,26 @@
.cc-toolbar { display:flex; flex-wrap:wrap; gap:.6rem; align-items:center; margin-bottom:1rem; }
.cc-toolbar input[type=text], .cc-toolbar select, .cc-toolbar input[type=number] {
background:var(--surface-2); color:var(--text); border:1px solid var(--border);
- border-radius:8px; padding:.5rem .7rem; font-size:14px;
+ border-radius:8px; padding:.5rem .7rem; font-size:16px; /* 16px: iOS 포커스 자동확대 방지 */
}
.cc-url { flex:1; min-width:240px; }
.cc-btn { background:var(--primary-gradient); color:#fff; border:none; border-radius:8px;
- padding:.55rem 1rem; font-weight:600; cursor:pointer; font-size:14px; }
+ 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); }
+ /* 토글 ON 상태: accent로 구분 */
+ .cc-btn.secondary.active { background:var(--accent-soft); border-color:var(--primary); color:var(--primary); }
+ /* 키보드 포커스 링(모든 상호작용 요소) */
+ .cc-btn:focus-visible, .cc-iconbtn:focus-visible, .cc-mini-select:focus-visible,
+ .cc-toolbar input:focus-visible, .comment-card:focus-visible, .cc-tl-link:focus-visible {
+ outline:2px solid var(--primary); outline-offset:2px; }
+ /* 수집 중 스피너 */
+ .cc-spinner { display:inline-block; width:14px; height:14px; border:2px solid var(--border);
+ border-top-color:var(--primary); border-radius:50%; animation:cc-spin .7s linear infinite;
+ vertical-align:-2px; margin-right:.45rem; }
+ @keyframes cc-spin { to { transform:rotate(360deg); } }
+ @media (prefers-reduced-motion: reduce) {
+ .cc-spinner { animation:none; } .cc-toast { transition:none; }
+ }
/* 메이슨리(다단): 카드가 내용 높이에 딱 맞게 쌓여 짧은 글 아래 흰 여백이 안 생김 */
.cc-grid { column-width:320px; column-gap:1rem; }
/* 카드: 영상 위에 얹을 소스 → 투명 캡처 기준. 미리보기는 surface 배경. */
@@ -25,14 +39,15 @@
.cc-avatar { width:40px; height:40px; border-radius:50%; flex-shrink:0; object-fit:cover; background:var(--surface-2); }
.cc-meta { flex:1; min-width:0; }
.cc-author { font-weight:600; font-size:13.5px; color:var(--text); }
- .cc-time { font-size:11.5px; color:var(--text-3); margin-left:.4rem; }
- .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; }
+ .cc-time { font-size:11.5px; color:var(--text-2); margin-left:.4rem; }
+ .cc-text { font-size:14px; color:var(--text); margin-top:.35rem; white-space:pre-wrap; word-break:break-word; line-height:1.5; }
+ .cc-stats { font-size:12px; color:var(--text-2); margin-top:.5rem; display:flex; gap:1rem; font-variant-numeric:tabular-nums; }
.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; }
+ 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); }
/* 복사 완료 토스트 — 카드 밖(body)에 떠서 캡처 이미지에 안 찍힘 */
.cc-toast { position:fixed; left:50%; bottom:2rem; transform:translateX(-50%) translateY(1rem);
@@ -53,34 +68,36 @@
.cc-analytics { background:var(--surface); border:1px solid var(--border); border-radius:12px;
padding:1rem 1.1rem; margin-bottom:1.2rem; }
.cc-an-head { display:flex; align-items:center; gap:.6rem; margin-bottom:.9rem; font-size:15px; }
- .cc-an-sub { color:var(--text-3); font-size:12.5px; font-weight:400; }
+ .cc-an-sub { color:var(--text-2); font-size:12.5px; font-weight:400; }
.cc-an-head .cc-iconbtn { margin-left:auto; }
+ .cc-an-head [data-lucide], .cc-an-sec h4 [data-lucide] { width:16px; height:16px; vertical-align:-3px; }
.cc-tiles { display:grid; grid-template-columns:repeat(auto-fit,minmax(120px,1fr)); gap:.6rem; margin-bottom:1rem; }
.cc-tile { background:var(--surface-2); border:1px solid var(--border); border-radius:10px; padding:.6rem .8rem; }
- .cc-tile-v { font-size:1.25rem; font-weight:700; color:var(--text); line-height:1.2; }
- .cc-tile-l { font-size:11.5px; color:var(--text-3); margin-top:.15rem; }
+ .cc-tile-v { font-size:1.25rem; font-weight:700; color:var(--text); line-height:1.2; font-variant-numeric:tabular-nums; }
+ .cc-tile-l { font-size:12px; color:var(--text-2); margin-top:.15rem; }
.cc-an-sec { margin-top:1rem; }
.cc-an-sec h4 { font-size:13.5px; font-weight:600; color:var(--text-2); margin:0 0 .5rem; }
.cc-an-sechead { display:flex; align-items:center; justify-content:space-between; gap:.6rem; }
.cc-wlabel { font-size:12px; color:var(--text-2); display:flex; align-items:center; gap:.3rem; cursor:pointer; }
.cc-mini-select { background:var(--surface-2); color:var(--text); border:1px solid var(--border);
- border-radius:8px; padding:.25rem .5rem; font-size:12px; cursor:pointer; }
+ border-radius:8px; padding:.35rem .6rem; font-size:13px; cursor:pointer; min-height:32px; }
.cc-an-empty { color:var(--text-3); font-size:13px; margin:.2rem 0; }
.cc-bars { display:flex; flex-direction:column; gap:.35rem; }
.cc-bar-row { display:grid; grid-template-columns:76px 1fr 52px; align-items:center; gap:.5rem; font-size:12.5px; }
.cc-bar-label { color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.cc-bar-track { background:var(--surface-2); border-radius:999px; height:9px; overflow:hidden; }
.cc-bar-fill { display:block; height:100%; background:var(--primary-gradient); border-radius:999px; }
- .cc-bar-val { color:var(--text-3); text-align:right; }
+ .cc-bar-val { color:var(--text-2); text-align:right; font-variant-numeric:tabular-nums; }
a.cc-tl-link { text-decoration:none; }
a.cc-tl-link:hover .cc-bar-label { text-decoration:underline; color:var(--primary,#6366f1); }
.cc-top-comment { background:var(--surface-2); border:1px solid var(--border); border-radius:10px;
padding:.6rem .8rem; font-size:12.5px; color:var(--text-2); }
.cc-top-text { color:var(--text); margin-top:.3rem; white-space:pre-wrap; word-break:break-word; }
- .cc-months { display:flex; align-items:flex-end; gap:.35rem; height:92px; overflow-x:auto; padding-top:.3rem; }
- .cc-mo { display:flex; flex-direction:column; align-items:center; justify-content:flex-end; gap:.25rem; min-width:26px; }
+ .cc-months { display:flex; align-items:flex-end; gap:.35rem; height:108px; overflow-x:auto; padding-top:.3rem; }
+ .cc-mo { display:flex; flex-direction:column; align-items:center; justify-content:flex-end; gap:.2rem; min-width:28px; }
+ .cc-mo-val { font-size:10px; color:var(--text-2); font-variant-numeric:tabular-nums; } /* 값 상시 노출(hover 의존 X) */
.cc-mo-bar { display:block; width:16px; background:var(--primary-gradient); border-radius:4px 4px 0 0; }
- .cc-mo-lb { font-size:10px; color:var(--text-3); white-space:nowrap; }
+ .cc-mo-lb { font-size:11px; color:var(--text-2); white-space:nowrap; }
@@ -88,30 +105,31 @@
댓글 카드
유튜브 링크의 전체 댓글 을 가져와 분석(많이 언급된 타임라인·자주 나온 단어·요약 통계)하고 카드로 만듭니다.
- 모자이크는 기본 켜짐, 모서리 둥글게 로 각진/라운드 전환. 기본은 단어 클릭=가리기 (블러), 🖍 단어 가리기 를 끄면 카드 클릭=이미지 복사 . ⬇ 저장=PNG 파일 다운로드 (캡컷은 붙여넣기가 안 되니 저장한 파일을 "불러오기" 하세요).
+ 모자이크는 기본 켜짐, 모서리 둥글게 로 각진/라운드 전환. 기본은 단어 클릭=가리기 (블러), 단어 가리기 토글을 끄면 카드 클릭=이미지 복사 . 저장 버튼=PNG 파일 다운로드 (캡컷은 붙여넣기가 안 되니 저장한 파일을 "불러오기" 하세요).
-
+
가져오기
-
-
-
+
+
+
좋아요순
답글순
최신순
좋아요
- 이상
+ 이상
답글 있는 것만
- 모자이크 해제
- 모서리 둥글게
- 🖍 단어 가리기: 켜짐
-
+ 모자이크 해제
+ 모서리 둥글게
+ 단어 가리기: 켜짐
+
@@ -123,7 +141,7 @@
-
+