feat(ui): collection — compact category, help modal, fit-to-width table
- Category management collapsed into a slim single-row strip - "사용법" button opens a reusable help modal (.modal-overlay/.modal-card + .help-item) explaining categories, filters, list view, and row actions - Merge thumbnail+title+channel into one "영상" column and tighten cells so the whole table fits the viewport — no horizontal scroll on page or table card - #mainContent min-width:0 to avoid flex overflow Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fc58a32a20
commit
616003479b
@ -256,7 +256,7 @@ ul { list-style: none; padding: 0; margin: 0; }
|
|||||||
.nav-icon { margin-right: 0.7rem; color: currentColor; min-width: 18px; width: 18px; height: 18px; }
|
.nav-icon { margin-right: 0.7rem; color: currentColor; min-width: 18px; width: 18px; height: 18px; }
|
||||||
|
|
||||||
/* Main content offset */
|
/* Main content offset */
|
||||||
#mainContent { transition: margin-left 0.3s ease; margin-left: var(--sidebar-width); }
|
#mainContent { transition: margin-left 0.3s ease; margin-left: var(--sidebar-width); min-width: 0; }
|
||||||
body.sidebar-collapsed #mainContent { margin-left: var(--sidebar-collapsed-width); }
|
body.sidebar-collapsed #mainContent { margin-left: var(--sidebar-collapsed-width); }
|
||||||
|
|
||||||
/* ===== Page header (shared) ===== */
|
/* ===== Page header (shared) ===== */
|
||||||
@ -401,3 +401,39 @@ thead th {
|
|||||||
tbody td { padding: 0.65rem 0.75rem; border-bottom: 1px solid var(--border); }
|
tbody td { padding: 0.65rem 0.75rem; border-bottom: 1px solid var(--border); }
|
||||||
tbody tr { transition: background 0.15s ease; }
|
tbody tr { transition: background 0.15s ease; }
|
||||||
tbody tr:hover { background: var(--hover); }
|
tbody tr:hover { background: var(--hover); }
|
||||||
|
|
||||||
|
/* ===== Modal (reusable) ===== */
|
||||||
|
.modal-overlay {
|
||||||
|
display: none; position: fixed; inset: 0; z-index: 1000;
|
||||||
|
background: rgba(10, 14, 22, 0.55);
|
||||||
|
-webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
|
||||||
|
align-items: center; justify-content: center; padding: 1rem;
|
||||||
|
}
|
||||||
|
.modal-overlay.open { display: flex; }
|
||||||
|
.modal-card {
|
||||||
|
background: var(--surface); border: 1px solid var(--border);
|
||||||
|
border-radius: var(--r); box-shadow: var(--shadow-lg);
|
||||||
|
width: 100%; max-width: 580px; max-height: 86vh; overflow: auto;
|
||||||
|
}
|
||||||
|
.modal-head {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
padding: 1.05rem 1.4rem; border-bottom: 1px solid var(--border);
|
||||||
|
position: sticky; top: 0; background: var(--surface); z-index: 1;
|
||||||
|
}
|
||||||
|
.modal-head h3 { font-size: 1.05rem; font-weight: 700; }
|
||||||
|
.modal-close { background: none; border: none; color: var(--text-3); cursor: pointer; font-size: 1.5rem; line-height: 1; padding: 0 0.2rem; }
|
||||||
|
.modal-close:hover { color: var(--text); }
|
||||||
|
.modal-body { padding: 0.6rem 1.4rem 1.2rem; }
|
||||||
|
|
||||||
|
/* Help guide items */
|
||||||
|
.help-item { display: flex; gap: 0.9rem; padding: 0.85rem 0; border-bottom: 1px solid var(--border); }
|
||||||
|
.help-item:last-child { border-bottom: none; }
|
||||||
|
.help-item .hi-ic {
|
||||||
|
width: 34px; height: 34px; border-radius: 9px; flex-shrink: 0;
|
||||||
|
background: var(--accent-soft); color: var(--accent);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.help-item .hi-ic svg { width: 17px; height: 17px; }
|
||||||
|
.help-item .hi-t { font-weight: 600; font-size: 0.9rem; margin-bottom: 3px; }
|
||||||
|
.help-item .hi-d { font-size: 0.82rem; color: var(--text-2); line-height: 1.6; }
|
||||||
|
.help-item .hi-d b { color: var(--text); font-weight: 600; }
|
||||||
|
|||||||
@ -13,31 +13,28 @@
|
|||||||
<h1>수집함</h1>
|
<h1>수집함</h1>
|
||||||
<p class="sub">수집한 영상을 분류·발굴하고 재가공 대상을 관리합니다.</p>
|
<p class="sub">수집한 영상을 분류·발굴하고 재가공 대상을 관리합니다.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="actions">
|
||||||
|
<button class="btn btn-secondary" onclick="openHelp()">
|
||||||
<!-- 카테고리 관리 -->
|
<i data-lucide="help-circle" style="width:15px;"></i> 사용법
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
|
||||||
<h3 class="text-lg font-bold">카테고리</h3>
|
|
||||||
<button class="btn btn-secondary p-2" onclick="document.getElementById('catForm').classList.toggle('hidden')">
|
|
||||||
<i data-lucide="plus" style="width:16px;"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="categoryChips" class="flex flex-wrap gap-2 mb-2">
|
</div>
|
||||||
|
|
||||||
|
<!-- 카테고리 관리 (컴팩트) -->
|
||||||
|
<div class="card mb-4" style="padding:0.75rem 1.1rem;">
|
||||||
|
<div class="flex items-center gap-3" style="flex-wrap:wrap;">
|
||||||
|
<span class="text-sm font-semibold" style="color:var(--text-2);">카테고리</span>
|
||||||
|
<div id="categoryChips" class="flex flex-wrap gap-2" style="flex:1; min-width:120px;">
|
||||||
<span class="text-muted text-sm">로딩...</span>
|
<span class="text-muted text-sm">로딩...</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="catForm" class="hidden flex gap-2 mt-4" style="flex-wrap: wrap; align-items:flex-end;">
|
<button class="btn btn-secondary" style="padding:0.4rem 0.7rem;" onclick="document.getElementById('catForm').classList.toggle('hidden')">
|
||||||
<div style="display:flex; flex-direction:column; gap:4px;">
|
<i data-lucide="plus" style="width:15px;"></i> 추가
|
||||||
<label class="text-sm text-muted">이름</label>
|
</button>
|
||||||
<input id="catName" type="text" placeholder="예: 동물 썰"
|
|
||||||
style="padding:8px; background:var(--surface-2); border:1px solid var(--glass-border); border-radius:var(--radius-md); color:var(--text); outline:none;">
|
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; flex-direction:column; gap:4px;">
|
<div id="catForm" class="hidden" style="display:flex; gap:0.6rem; flex-wrap:wrap; align-items:center; margin-top:0.7rem; padding-top:0.7rem; border-top:1px solid var(--border);">
|
||||||
<label class="text-sm text-muted">색상</label>
|
<input id="catName" type="text" placeholder="이름 (예: 동물 썰)" style="flex:1; min-width:140px;">
|
||||||
<input id="catColor" type="color" value="#7C3AED"
|
<input id="catColor" type="color" value="#7C3AED" title="색상" style="padding:2px; height:38px; width:48px;">
|
||||||
style="padding:2px; height:42px; width:56px; background:var(--surface-2); border:1px solid var(--glass-border); border-radius:var(--radius-md);">
|
<button class="btn btn-primary" onclick="addCategory()">추가</button>
|
||||||
</div>
|
|
||||||
<button class="btn btn-primary px-4 py-2" onclick="addCategory()">추가</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -89,27 +86,65 @@
|
|||||||
|
|
||||||
<!-- 결과 테이블 -->
|
<!-- 결과 테이블 -->
|
||||||
<div class="card p-0" style="overflow-x:auto;">
|
<div class="card p-0" style="overflow-x:auto;">
|
||||||
<table class="w-full" style="border-collapse:collapse; text-align:left;">
|
<table class="w-full coll-table" style="border-collapse:collapse; text-align:left;">
|
||||||
<thead style="background:var(--surface-2); border-bottom:1px solid var(--glass-border);">
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="p-3 text-sm font-bold text-muted">썸네일</th>
|
<th>영상</th>
|
||||||
<th class="p-3 text-sm font-bold text-muted">제목</th>
|
<th>구독자</th>
|
||||||
<th class="p-3 text-sm font-bold text-muted">채널</th>
|
<th>조회수</th>
|
||||||
<th class="p-3 text-sm font-bold text-muted">구독자</th>
|
<th>시간당</th>
|
||||||
<th class="p-3 text-sm font-bold text-muted">조회수</th>
|
<th>배율</th>
|
||||||
<th class="p-3 text-sm font-bold text-muted">시간당</th>
|
<th>상태</th>
|
||||||
<th class="p-3 text-sm font-bold text-muted">배율</th>
|
<th>카테고리</th>
|
||||||
<th class="p-3 text-sm font-bold text-muted">상태</th>
|
<th style="text-align:right;">관리</th>
|
||||||
<th class="p-3 text-sm font-bold text-muted">카테고리</th>
|
|
||||||
<th class="p-3 text-sm font-bold text-muted">관리</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="resultBody">
|
<tbody id="resultBody">
|
||||||
<tr><td colspan="10" class="p-8 text-center text-muted">로딩 중...</td></tr>
|
<tr><td colspan="8" class="p-8 text-center text-muted">로딩 중...</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 사용법 모달 -->
|
||||||
|
<div id="helpModal" class="modal-overlay" onclick="if(event.target===this) closeHelp()">
|
||||||
|
<div class="modal-card">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3>📖 수집함 사용법</h3>
|
||||||
|
<button class="modal-close" onclick="closeHelp()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="help-item">
|
||||||
|
<div class="hi-ic"><i data-lucide="tag"></i></div>
|
||||||
|
<div>
|
||||||
|
<div class="hi-t">카테고리</div>
|
||||||
|
<div class="hi-d">영상을 주제별로 분류하는 라벨입니다. 우측 <b>[+ 추가]</b>로 이름·색을 정해 만들고, 칩의 <b>×</b>로 삭제합니다. 표의 <b>‘카테고리’ 드롭다운</b>으로 각 영상에 지정하세요.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="help-item">
|
||||||
|
<div class="hi-ic"><i data-lucide="filter"></i></div>
|
||||||
|
<div>
|
||||||
|
<div class="hi-t">필터 · 정렬</div>
|
||||||
|
<div class="hi-d"><b>칩</b>(🚀 떡상 후보 · Shorts · ⭐ 북마크)을 눌러 켜고 끕니다. 드롭다운으로 <b>상태·카테고리·출처</b>를 좁히고 <b>정렬</b>(배율/시간당/조회수/최신순)을 바꿉니다. 우측 배지에 결과 수가 표시됩니다.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="help-item">
|
||||||
|
<div class="hi-ic"><i data-lucide="table-2"></i></div>
|
||||||
|
<div>
|
||||||
|
<div class="hi-t">목록 보기</div>
|
||||||
|
<div class="hi-d"><b>썸네일 클릭</b> = 팝업 미리보기, <b>제목 클릭</b> = YouTube 새 탭. <b>배율</b>은 조회수÷구독자로, 높을수록 ‘떡상’(10x↑ 빨강, 2x↑ 주황). 상태·카테고리 드롭다운은 바꾸면 <b>즉시 저장</b>됩니다.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="help-item">
|
||||||
|
<div class="hi-ic"><i data-lucide="mouse-pointer-click"></i></div>
|
||||||
|
<div>
|
||||||
|
<div class="hi-t">행 버튼 (관리)</div>
|
||||||
|
<div class="hi-d"><b>🪄 재가공</b> = 자막 추출·재작성 에디터로 이동 · <b>⭐ 북마크</b> = 즐겨찾기 토글 · <b>🗑 삭제</b> = 수집함에서 제거(스크립트도 함께 삭제).</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 영상 모달 -->
|
<!-- 영상 모달 -->
|
||||||
<div id="videoModal" style="display:none; position:fixed; z-index:1000; left:0; top:0; width:100%; height:100%; background:rgba(0,0,0,0.85); align-items:center; justify-content:center;" onclick="if(event.target===this) closeVideoModal()">
|
<div id="videoModal" style="display:none; position:fixed; z-index:1000; left:0; top:0; width:100%; height:100%; background:rgba(0,0,0,0.85); align-items:center; justify-content:center;" onclick="if(event.target===this) closeVideoModal()">
|
||||||
<div style="position:relative; width:95%; max-width:900px; background:var(--glass-bg,var(--surface)); border-radius:12px; padding:16px; border:1px solid var(--glass-border);">
|
<div style="position:relative; width:95%; max-width:900px; background:var(--glass-bg,var(--surface)); border-radius:12px; padding:16px; border:1px solid var(--glass-border);">
|
||||||
@ -123,9 +158,13 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.hidden { display:none !important; }
|
.hidden { display:none !important; }
|
||||||
.row-sel { padding:4px 6px; background:var(--surface-2); border:1px solid var(--glass-border); border-radius:6px; color:var(--text); font-size:0.8rem; outline:none; }
|
.row-sel { padding:4px 6px; background:var(--surface-2); border:1px solid var(--border-strong); border-radius:6px; color:var(--text); font-size:0.78rem; outline:none; max-width:110px; }
|
||||||
.row-sel option { background:var(--surface); color:var(--text); }
|
.row-sel option { background:var(--surface); color:var(--text); }
|
||||||
.cat-chip { padding:4px 10px; border-radius:999px; font-size:0.8rem; display:inline-flex; align-items:center; gap:6px; }
|
.cat-chip { padding:4px 10px; border-radius:999px; font-size:0.8rem; display:inline-flex; align-items:center; gap:6px; }
|
||||||
|
/* 컴팩트 표: 숫자/상태 칸은 줄바꿈 없이, 영상 칸만 넓게 */
|
||||||
|
.coll-table th, .coll-table td { white-space:nowrap; vertical-align:middle; }
|
||||||
|
.coll-table td:first-child, .coll-table th:first-child { white-space:normal; width:42%; min-width:300px; }
|
||||||
|
.coll-table th { background:var(--surface-2); }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
@ -233,7 +272,7 @@
|
|||||||
const body = document.getElementById('resultBody');
|
const body = document.getElementById('resultBody');
|
||||||
document.getElementById('resultCount').textContent = list.length + '건';
|
document.getElementById('resultCount').textContent = list.length + '건';
|
||||||
if(list.length===0){
|
if(list.length===0){
|
||||||
body.innerHTML = '<tr><td colspan="10" class="p-8 text-center text-muted">조건에 맞는 영상이 없습니다.</td></tr>';
|
body.innerHTML = '<tr><td colspan="8" class="p-8 text-center text-muted">조건에 맞는 영상이 없습니다.</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
body.innerHTML = list.map(v=>{
|
body.innerHTML = list.map(v=>{
|
||||||
@ -242,32 +281,31 @@
|
|||||||
const statusOpts = STATUS_KEYS.map(k=>
|
const statusOpts = STATUS_KEYS.map(k=>
|
||||||
`<option value="${k}" ${v.interestStatus===k?'selected':''}>${STATUS_LABEL[k]}</option>`).join('');
|
`<option value="${k}" ${v.interestStatus===k?'selected':''}>${STATUS_LABEL[k]}</option>`).join('');
|
||||||
const star = v.bookmarked ? '#f59e0b' : 'var(--text-muted)';
|
const star = v.bookmarked ? '#f59e0b' : 'var(--text-muted)';
|
||||||
const shortsBadge = v.isShorts ? '<span style="font-size:0.65rem; background:#7C3AED33; color:#a78bfa; padding:1px 5px; border-radius:4px; margin-left:4px;">Shorts</span>' : '';
|
const shortsBadge = v.isShorts ? '<span class="badge badge-primary" style="margin-left:5px;">Shorts</span>' : '';
|
||||||
return `<tr style="border-bottom:1px solid var(--glass-border);">
|
return `<tr>
|
||||||
<td class="p-3">
|
<td>
|
||||||
<div style="position:relative; cursor:pointer; width:96px;" data-vid="${v.videoId}" data-title="${esc(v.title)}" onclick="openVideoModal(this.dataset.vid, this.dataset.title)">
|
<div class="flex items-center gap-3" style="min-width:0;">
|
||||||
<img src="${esc(v.thumbnailUrl)}" style="width:96px; height:54px; object-fit:cover; border-radius:6px;">
|
<img src="${esc(v.thumbnailUrl)}" data-vid="${v.videoId}" data-title="${esc(v.title)}" onclick="openVideoModal(this.dataset.vid, this.dataset.title)"
|
||||||
</div>
|
style="width:84px; height:47px; object-fit:cover; border-radius:6px; cursor:pointer; flex-shrink:0;">
|
||||||
</td>
|
<div style="min-width:0;">
|
||||||
<td class="p-3" style="max-width:280px;">
|
<div class="font-semibold text-sm" style="display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; line-height:1.35;">
|
||||||
<div class="font-bold text-sm" style="display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;">
|
|
||||||
<a href="https://www.youtube.com/watch?v=${v.videoId}" target="_blank" class="hover:underline">${esc(v.title)}</a>${shortsBadge}
|
<a href="https://www.youtube.com/watch?v=${v.videoId}" target="_blank" class="hover:underline">${esc(v.title)}</a>${shortsBadge}
|
||||||
</div>
|
</div>
|
||||||
|
<a href="${v.ytChannelId?('https://www.youtube.com/channel/'+v.ytChannelId):'#'}" target="_blank" class="hover:underline truncate" style="color:var(--text-3); font-size:0.72rem; display:block; margin-top:2px;">${esc(v.channelTitle||'-')}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="p-3 text-sm text-muted" style="max-width:130px;">
|
<td class="text-sm">${fmt(v.subscriberCount)}</td>
|
||||||
<a href="${v.ytChannelId?('https://www.youtube.com/channel/'+v.ytChannelId):'#'}" target="_blank" class="hover:underline truncate" style="color:var(--text-2);">${esc(v.channelTitle||'-')}</a>
|
<td class="text-sm">${fmt(v.viewCount)}</td>
|
||||||
</td>
|
<td class="text-sm">${fmt(v.viewsPerHour)}</td>
|
||||||
<td class="p-3 text-sm">${fmt(v.subscriberCount)}</td>
|
<td class="text-sm">${ratioBadge(v.viewsPerSubRatio)}</td>
|
||||||
<td class="p-3 text-sm">${fmt(v.viewCount)}</td>
|
<td><select class="row-sel" onchange="setStatus(${v.id}, this.value)">${statusOpts}</select></td>
|
||||||
<td class="p-3 text-sm">${fmt(v.viewsPerHour)}</td>
|
<td><select class="row-sel" onchange="setCategory(${v.id}, this.value)">${catOpts}</select></td>
|
||||||
<td class="p-3 text-sm">${ratioBadge(v.viewsPerSubRatio)}</td>
|
<td>
|
||||||
<td class="p-3"><select class="row-sel" onchange="setStatus(${v.id}, this.value)">${statusOpts}</select></td>
|
<div class="flex items-center gap-1 justify-end">
|
||||||
<td class="p-3"><select class="row-sel" onchange="setCategory(${v.id}, this.value)">${catOpts}</select></td>
|
<a class="btn btn-primary" style="padding:0.4rem;" title="재가공" href="/rework/${v.id}"><i data-lucide="wand-2" style="width:15px;"></i></a>
|
||||||
<td class="p-3">
|
<button class="btn btn-secondary" style="padding:0.4rem;" title="북마크" onclick="toggleBookmark(${v.id}, ${!!v.bookmarked})"><i data-lucide="star" style="width:15px; color:${star};"></i></button>
|
||||||
<div class="flex items-center gap-1">
|
<button class="btn btn-secondary" style="padding:0.4rem;" title="삭제" onclick="deleteVideo(${v.id})"><i data-lucide="trash-2" style="width:15px;"></i></button>
|
||||||
<a class="btn btn-primary p-2" title="재가공" href="/rework/${v.id}"><i data-lucide="wand-2" style="width:15px;"></i></a>
|
|
||||||
<button class="btn btn-secondary p-2" title="북마크" onclick="toggleBookmark(${v.id}, ${!!v.bookmarked})"><i data-lucide="star" style="width:15px; color:${star};"></i></button>
|
|
||||||
<button class="btn btn-secondary p-2" title="삭제" onclick="deleteVideo(${v.id})"><i data-lucide="trash-2" style="width:15px;"></i></button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
@ -306,6 +344,11 @@
|
|||||||
document.getElementById('videoModal').style.display = 'none';
|
document.getElementById('videoModal').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----- 사용법 모달 -----
|
||||||
|
function openHelp(){ document.getElementById('helpModal').classList.add('open'); }
|
||||||
|
function closeHelp(){ document.getElementById('helpModal').classList.remove('open'); }
|
||||||
|
document.addEventListener('keydown', e => { if(e.key === 'Escape'){ closeHelp(); closeVideoModal(); } });
|
||||||
|
|
||||||
// ----- URL 파라미터 필터 적용 -----
|
// ----- URL 파라미터 필터 적용 -----
|
||||||
function applyUrlFilters(){
|
function applyUrlFilters(){
|
||||||
const q = new URLSearchParams(window.location.search);
|
const q = new URLSearchParams(window.location.search);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user