캡처 배율이 2배 고정이라 카드(~320px)가 ~640px PNG로 저장돼 영상에서 확대하면 뭉개졌음. 화질 선택(2x/4x/6x/8x)을 추가하고 기본을 4배로 올림. 검정 배경 카드는 그레이스케일 안티에일리어싱을 적용해 색 번짐 제거. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
189 lines
15 KiB
HTML
189 lines
15 KiB
HTML
<!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>
|