h-lab/src/main/resources/templates/comment-cards.html
hehihoho3@gmail.com fcfa4084e4 fix(comment-cards): 영상에서 확대 시 글자 깨지던 문제(저장 화질 상향)
캡처 배율이 2배 고정이라 카드(~320px)가 ~640px PNG로 저장돼 영상에서 확대하면
뭉개졌음. 화질 선택(2x/4x/6x/8x)을 추가하고 기본을 4배로 올림.
검정 배경 카드는 그레이스케일 안티에일리어싱을 적용해 색 번짐 제거.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 09:09:45 +09:00

189 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/base}">
<head>
<title>h-lab - 댓글 카드</title>
<style>
.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: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; 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); }
/* 키보드 포커스 링(모든 상호작용 요소) */
.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 배경. */
/* 기본은 각진 모서리, '모서리 둥글게' 토글 시 .rounded 로 살짝 라운드 */
.comment-card { background:var(--surface); border:1px solid var(--border); border-radius:4px;
padding:1rem; position:relative; break-inside:avoid; -webkit-column-break-inside:avoid;
margin-bottom:1rem; }
.comment-card.rounded { border-radius:16px; }
.cc-head { display:flex; gap:.6rem; align-items:flex-start; }
.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-2); margin-left:.4rem; }
/* 내용 글자 크기: 카드 레이아웃은 그대로 두고 텍스트만 확대/축소(--cc-text-scale, 카드 인라인에 지정) */
.cc-text { font-size:calc(14px * var(--cc-text-scale, 1)); color:var(--text); margin-top:.35rem; white-space:pre-wrap; word-break:break-word; line-height:1.5; }
/* 배경색 옵션: 흰색/검정. 검정이면 글자는 흰색 계열로 대비 확보 */
.comment-card.bg-white { background:#ffffff; border-color:#e5e5e5; }
.comment-card.bg-white .cc-author, .comment-card.bg-white .cc-text { color:#0f0f0f; }
.comment-card.bg-white .cc-time, .comment-card.bg-white .cc-stats { color:#606060; }
/* 어두운 배경 위 밝은 글자는 서브픽셀 AA에서 색 번짐이 생김 → 그레이스케일 AA로 또렷하게 */
.comment-card.bg-black { background:#000000; border-color:#333333;
-webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale; }
.comment-card.bg-black .cc-author, .comment-card.bg-black .cc-text { color:#ffffff; }
.comment-card.bg-black .cc-time, .comment-card.bg-black .cc-stats { color:#aaaaaa; }
/* 글자 크기 스테퍼 */
.cc-fontsize { display:inline-flex; align-items:center; gap:.35rem; }
.cc-fontsize .cc-btn { min-width:38px; padding:.55rem .6rem; }
.cc-fontsize #ccFontLabel { font-size:12px; color:var(--text-2); min-width:3.2ch; text-align:center; font-variant-numeric:tabular-nums; }
.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: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엔 안 남음)
surface-2는 다크 테마에서 차이가 거의 없어, 텍스트 색을 섞어 두 테마 모두에서 확실히 회색이 되게 함 */
.comment-card.saved { background:var(--surface-2);
background:color-mix(in srgb, var(--text) 14%, var(--surface));
border-color:var(--text-3); box-shadow:inset 4px 0 0 var(--primary); opacity:.72; }
.comment-card.saved:hover { opacity:.9; }
.cc-iconbtn.saved { background:var(--surface); color:var(--text-3); border-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;
font-size:14px; font-weight:600; opacity:0; pointer-events:none; transition:opacity .15s, transform .15s; z-index:9999; }
.cc-toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
/* 모자이크 */
.comment-card.mosaic .cc-avatar { filter: blur(6px); }
.comment-card.mosaic .cc-author { filter: blur(5px); }
/* 단어 가리기: '단어 가리기 켜짐'일 때 단어에 포인터, 클릭하면 .masked로 블러 */
.cc-word { cursor: default; }
.cc-grid.wordmask .cc-word { cursor: pointer; }
.cc-grid.wordmask .comment-card { cursor: default; }
.cc-word.masked { filter: blur(4px); }
.cc-empty { color:var(--text-3); padding:2rem 0; }
/* ---- 분석 패널 ---- */
.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-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; 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:.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-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: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:11px; color:var(--text-2); white-space:nowrap; }
</style>
</head>
<body>
<div layout:fragment="content">
<h1 style="font-size:1.5rem;font-weight:700;margin-bottom:.3rem;">댓글 카드</h1>
<p style="color:var(--text-3);margin-bottom:1.2rem;font-size:14px;">
유튜브 링크의 <b>전체 댓글</b>을 가져와 분석(많이 언급된 타임라인·자주 나온 단어·요약 통계)하고 카드로 만듭니다.
모자이크는 기본 켜짐, <b>모서리 둥글게</b>로 각진/라운드 전환. 기본은 <b>단어 클릭=가리기</b>(블러), <b>단어 가리기</b> 토글을 끄면 <b>카드 클릭=이미지 복사</b>. <b>저장 버튼=PNG 파일 다운로드</b>(캡컷은 붙여넣기가 안 되니 저장한 파일을 "불러오기" 하세요). <b>저장 폴더</b>를 고르면 저장·<b>전체 저장</b>이 그 폴더로 바로 갑니다(크롬/엣지).
영상에서 카드를 크게 확대해 쓸 거면 <b>화질</b>을 4x 이상으로 두세요(배율이 높을수록 저장이 느리고 파일이 큽니다).
</p>
<div class="cc-toolbar">
<input type="text" id="ccUrl" class="cc-url" placeholder="유튜브 링크 또는 영상 ID 붙여넣기"
aria-label="유튜브 링크 또는 영상 ID" />
<button class="cc-btn" id="ccFetch">가져오기</button>
</div>
<div class="cc-toolbar" id="ccFilters" style="display:none;">
<input type="text" id="ccSearch" placeholder="댓글 내용 검색" aria-label="댓글 내용 검색" style="min-width:180px;" />
<input type="text" id="ccExclude" placeholder="제외 단어 (쉼표로 구분)" aria-label="제외 단어 (쉼표로 구분)" style="min-width:180px;" />
<select id="ccSort" aria-label="정렬 기준">
<option value="likes">좋아요순</option>
<option value="replies">답글순</option>
<option value="latest">최신순</option>
</select>
<label style="font-size:13px;color:var(--text-2);">좋아요
<input type="number" id="ccMinLikes" value="0" min="0" style="width:90px;" aria-label="최소 좋아요 수" /> 이상</label>
<label style="font-size:13px;color:var(--text-2);display:flex;align-items:center;gap:.3rem;">
<input type="checkbox" id="ccRepliesOnly" /> 답글 있는 것만</label>
<button class="cc-btn secondary active" id="ccMosaic" aria-pressed="true">모자이크 해제</button>
<button class="cc-btn secondary active" id="ccRounded" aria-pressed="true">모서리 각지게</button>
<button class="cc-btn secondary active" id="ccWordMask" aria-pressed="true">단어 가리기: 켜짐</button>
<span class="cc-fontsize" role="group" aria-label="내용 글자 크기">
<button class="cc-btn secondary" id="ccFontDown" aria-label="글자 작게" title="내용 글자 작게">A</button>
<span id="ccFontLabel">100%</span>
<button class="cc-btn secondary" id="ccFontUp" aria-label="글자 크게" title="내용 글자 크게">A+</button>
</span>
<button class="cc-btn secondary active" id="ccBg" aria-pressed="true" title="배경색: 기본 → 흰색 → 검정 순환">배경: 검정</button>
<label style="font-size:13px;color:var(--text-2);display:flex;align-items:center;gap:.3rem;"
title="저장·복사되는 PNG의 배율. 영상에서 크게 확대할수록 높게">화질
<select id="ccScale" aria-label="저장 화질(배율)">
<option value="2">2x (가벼움)</option>
<option value="4" selected>4x (권장)</option>
<option value="6">6x</option>
<option value="8">8x (최고)</option>
</select>
</label>
<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>
<div class="cc-analytics" id="ccAnalytics" style="display:none;"></div>
<div class="cc-grid" id="cardGrid"></div>
<div class="cc-empty" id="ccEmpty"></div>
</div>
<th:block layout:fragment="script">
<!-- DOM→이미지 캡처 (Task 5에서 사용). blur 필터 지원 위해 modern-screenshot 사용. -->
<script src="https://cdn.jsdelivr.net/npm/modern-screenshot@4/dist/index.js"></script>
<script th:src="@{/js/comment-cards.js(v=20260722)}"></script>
</th:block>
</body>
</html>