Compare commits
No commits in common. "dd42499f43c466f7c8b38f553ea5ec5cb64ee060" and "fa6342f97eb43c8a999d2f694b3d51430c72cd7d" have entirely different histories.
dd42499f43
...
fa6342f97e
@ -1,85 +0,0 @@
|
||||
# UI 리뉴얼 — 정제된 라이트/다크 SaaS 디자인 시스템
|
||||
|
||||
작성일: 2026-06-12
|
||||
대상: 전체 SSR(Thymeleaf) 화면의 시각 디자인 전면 개편
|
||||
|
||||
## 1. 목표
|
||||
|
||||
현재 UI(다크 글래스모피즘)가 "너무 어둡고 대비 낮아 뭉개진다". 정제된 SaaS 스타일(Linear/Notion/Vercel 계열)로 전면 리뉴얼하되, **라이트를 기본**으로 하고 **라이트/다크 토글**을 제공한다. 승인된 시안: 대시보드 라이트 mockup.
|
||||
|
||||
## 2. 핵심 결정
|
||||
|
||||
- **기본 테마 = 라이트.** 다크는 토글로 전환, 선택은 `localStorage`에 저장.
|
||||
- **무플래시 초기화**: `<head>` 인라인 스크립트가 페인트 전에 `localStorage.theme`(기본 light)를 `<html data-theme>`에 적용.
|
||||
- **테마 토글 버튼은 사이드바 하단(전역)** — 페이지마다 넣지 않는다.
|
||||
- **레이아웃/마크업 구조는 유지**(사이드바 + 메인). 시각 스타일과 일부 페이지의 그리드/컴포넌트만 교체.
|
||||
- **아이콘은 기존 Lucide 유지**(CDN). 폰트 Inter 유지.
|
||||
|
||||
## 3. 디자인 토큰 (`variables.css` 재작성)
|
||||
|
||||
CSS 변수로 라이트 기본 + `:root[data-theme="dark"]` 오버라이드. 의미 기반 토큰:
|
||||
|
||||
| 토큰 | 라이트 | 다크 |
|
||||
|---|---|---|
|
||||
| `--bg` | #f6f7f9 | #0a0b0e |
|
||||
| `--surface` | #ffffff | #131519 |
|
||||
| `--surface-2` | #f1f3f6 | #191c22 |
|
||||
| `--inset` | #eef0f3 | #0e1014 |
|
||||
| `--border` | #e6e8ec | rgba(255,255,255,.07) |
|
||||
| `--border-strong` | #d9dce1 | rgba(255,255,255,.12) |
|
||||
| `--text` | #15181d | #e8eaef |
|
||||
| `--text-2` | #5a626d | #9aa1ad |
|
||||
| `--text-3` | #9aa1ad | #646b78 |
|
||||
| `--shadow` | 부드러운 2단 그림자 | 어두운 그림자 |
|
||||
|
||||
공통: `--accent #4f7cff`, `--accent-2 #7c5cff`(그라데이션 포인트만 절제), `--accent-soft`, 상태색 `--green/--amber/--red/--purple/--sky`. radius `--r:12px / --r-sm:9px`. 기존 변수명(`--primary`, `--text-primary`, `--text-muted`, `--glass-border` 등)은 **별칭으로 매핑 유지**해 기존 마크업이 깨지지 않게 한다(점진 교체).
|
||||
|
||||
## 4. 컴포넌트 (`style.css` 재작성/확장)
|
||||
|
||||
- **사이드바**: 라벨 + 섹션 구분(분석 / 파이프라인 / 기타), 활성 상태(accent-soft 배경 + 좌측 accent 바), 하단 사용자 + **테마 토글**. 라벨 한국어 통일(대시보드/발굴/채널/수집함/칸반 보드/재가공/발행 큐/프로덕션/설정).
|
||||
- **페이지 헤더**: 타이틀 + 서브타이틀 + 우측 액션 영역.
|
||||
- **버튼**: `.btn`(secondary 기본) / `.btn-primary`(accent) / `.btn.icon`. 그림자·보더 정돈.
|
||||
- **카드**: `.card`(surface+border+shadow). 섹션 헤더 `.card .ch`.
|
||||
- **스탯**: `.stat-grid`(반응형 5열) + `.stat`(+`.accent` 강조). 빈공간 없는 균등 그리드.
|
||||
- **프로그레스바** `.bar`, **배지** `.badge`(비율/상태색), **랭크/리스트행** `.rank`/`.mini`.
|
||||
- **테이블**: 수집함/발행 목록용 — 헤더, zebra/hover, 셀 패딩 정돈.
|
||||
- **폼 입력**: `.input`/`select`/`textarea` 통일(재가공·발행 화면).
|
||||
- **칸반**: `.kb-col`/`.kb-card` 라이트/다크 대응.
|
||||
- **칩/태그**: 카테고리·상태.
|
||||
|
||||
## 5. 파일 변경
|
||||
|
||||
- `static/css/variables.css` — 토큰 재작성(라이트 기본 + 다크 오버라이드 + 기존 변수 별칭).
|
||||
- `static/css/style.css` — 컴포넌트 재작성/확장.
|
||||
- `static/js/common.js` — 테마 토글 함수 + 적용/저장.
|
||||
- `templates/layout/base.html` — `<head>`에 무플래시 테마 init 인라인 스크립트.
|
||||
- `templates/layout/sidebar.html` — 라벨/섹션/활성/사용자/테마토글 재구성.
|
||||
- 각 페이지 템플릿 — 페이지별 마크업을 새 컴포넌트로 조정(인라인 스타일 정리).
|
||||
|
||||
## 6. 롤아웃 (페이지)
|
||||
|
||||
라이브 라우트 12개: dashboard, collection, board, discover, channels, channel_detail, multi_channel_videos, rework, publish, production, production_detail, videos.
|
||||
|
||||
> `videos.html`은 레거시 가능성(Video 모델 제거됨) — 적용 전 라우트 실사용 확인, 미사용이면 nav에서 숨김.
|
||||
|
||||
**단계:**
|
||||
- **A. 디자인 시스템 토대** — variables.css + style.css 코어 + common.js 토글 + base.html init + sidebar.html. (적용 즉시 공통 컴포넌트 쓰는 화면 전반이 새 톤으로 전환)
|
||||
- **B. 페이지 폴리시** — dashboard(시안 일치)부터 → collection → board → rework → publish → discover → channels/channel_detail → production/production_detail. 페이지별 그리드·인라인스타일을 컴포넌트로 교체.
|
||||
|
||||
## 7. 검증
|
||||
|
||||
- 각 페이지를 라이트/다크 양쪽에서 Playwright 스크린샷으로 확인(주요 화면은 사용자에게 텔레그램 이미지로 공유).
|
||||
- 테마 토글 동작 + 새로고침 후 유지(localStorage) 확인.
|
||||
- 기존 JS(데이터 fetch, 드래그앤드롭 등) 회귀 없는지 스모크.
|
||||
|
||||
## 8. 범위 밖(YAGNI)
|
||||
|
||||
- 반응형 모바일 정밀 대응은 최소(데스크톱 우선, 기존 모바일 토글 유지).
|
||||
- 컴포넌트 라이브러리/빌드 도구 도입(순수 CSS 유지, SSR 그대로).
|
||||
- 기능/도메인 로직 변경 없음 — 순수 프레젠테이션.
|
||||
|
||||
## 9. 열린 항목
|
||||
|
||||
- 액센트 색: 현재 파랑(#4f7cff)→보라(#7c5cff) 그라데이션. 사용자 확정 대기(변경 시 토큰만 수정).
|
||||
- `videos.html` 레거시 여부 → 숨김/유지 결정.
|
||||
- 사이드바 사용자 영역 실제 값(현재 "Admin User/Pro Plan" 더미) — 개인용이라 단순화.
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,104 +1,60 @@
|
||||
/* ===========================================================================
|
||||
Design tokens — Refined SaaS, LIGHT default + DARK via [data-theme="dark"].
|
||||
Old variable names are aliased to theme tokens so existing markup adapts.
|
||||
=========================================================================== */
|
||||
:root {
|
||||
/* Surfaces (light) */
|
||||
--bg: #f6f7f9;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f1f3f6;
|
||||
--inset: #eef0f3;
|
||||
--border: #e6e8ec;
|
||||
--border-strong: #d9dce1;
|
||||
/* Premium Dark Theme Palette (Cyber/Space) */
|
||||
|
||||
/* Text (light) */
|
||||
--text: #15181d;
|
||||
--text-2: #5a626d;
|
||||
--text-3: #9aa1ad;
|
||||
/* Backgrounds */
|
||||
--bg-primary: #050508;
|
||||
/* Ultra dark, almost black */
|
||||
--bg-glass: rgba(15, 23, 42, 0.6);
|
||||
|
||||
/* On-surface translucent (hover/active) */
|
||||
--hover: rgba(16, 24, 40, .045);
|
||||
--hover-strong: rgba(16, 24, 40, .07);
|
||||
/* Glassmorphism */
|
||||
--glass-border: rgba(255, 255, 255, 0.08);
|
||||
--glass-highlight: rgba(255, 255, 255, 0.15);
|
||||
--backdrop-blur: 12px;
|
||||
|
||||
/* Elevation */
|
||||
--shadow: 0 1px 2px rgba(16, 24, 40, .04), 0 4px 12px rgba(16, 24, 40, .05);
|
||||
--shadow-lg: 0 10px 30px rgba(16, 24, 40, .10);
|
||||
--side-bg: #ffffff;
|
||||
/* Typography */
|
||||
--text-primary: #f1f5f9;
|
||||
--text-secondary: #94a3b8;
|
||||
--text-muted: #64748b;
|
||||
|
||||
/* Brand Colors (Neon/Vibrant) */
|
||||
--primary: #3b82f6;
|
||||
--primary-glow: rgba(59, 130, 246, 0.5);
|
||||
--primary-gradient: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||||
|
||||
/* Accent + status (shared across themes) */
|
||||
--accent: #4f7cff;
|
||||
--accent-2: #7c5cff;
|
||||
--accent-soft: rgba(79, 124, 255, .10);
|
||||
--primary: #4f7cff;
|
||||
--primary-gradient: linear-gradient(135deg, #4f7cff 0%, #7c5cff 100%);
|
||||
--primary-glow: rgba(79, 124, 255, .35);
|
||||
--success: #10b981;
|
||||
--danger: #f43f5e;
|
||||
--warning: #f59e0b;
|
||||
--green: #10b981;
|
||||
--amber: #f59e0b;
|
||||
--red: #f43f5e;
|
||||
--purple: #8b5cf6;
|
||||
--sky: #0ea5e9;
|
||||
--primary-rgb: 79, 124, 255;
|
||||
--success-rgb: 16, 185, 129;
|
||||
--danger-rgb: 244, 63, 94;
|
||||
--danger: #ef4444;
|
||||
|
||||
/* === Back-compat aliases (old names → theme tokens) === */
|
||||
--bg-primary: var(--bg);
|
||||
--bg-glass: var(--surface);
|
||||
--glass-border: var(--border);
|
||||
--glass-highlight: var(--border-strong);
|
||||
--text-primary: var(--text);
|
||||
--text-secondary: var(--text-2);
|
||||
--text-muted: var(--text-3);
|
||||
--bg-hover: var(--surface-2);
|
||||
--backdrop-blur: 0px;
|
||||
|
||||
/* Radius / spacing / font */
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 14px;
|
||||
/* UI Elements */
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 20px;
|
||||
--radius-full: 9999px;
|
||||
--r: 12px;
|
||||
--r-sm: 9px;
|
||||
--space-1: .25rem;
|
||||
--space-2: .5rem;
|
||||
--space-3: .75rem;
|
||||
|
||||
/* Semantic (added) */
|
||||
--warning: #f59e0b;
|
||||
--primary-rgb: 59, 130, 246; /* matches --primary #3b82f6; used by rgba(var(--primary-rgb), …) */
|
||||
--success-rgb: 16, 185, 129;
|
||||
--danger-rgb: 239, 68, 68;
|
||||
|
||||
/* Spacing scale */
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-6: 1.5rem;
|
||||
--space-8: 2rem;
|
||||
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* ===== Dark theme overrides ===== */
|
||||
:root[data-theme="dark"] {
|
||||
--bg: #0a0b0e;
|
||||
--surface: #131519;
|
||||
--surface-2: #191c22;
|
||||
--inset: #0e1014;
|
||||
--border: rgba(255, 255, 255, .08);
|
||||
--border-strong: rgba(255, 255, 255, .13);
|
||||
|
||||
--text: #e8eaef;
|
||||
--text-2: #9aa1ad;
|
||||
--text-3: #6b7280;
|
||||
|
||||
--hover: rgba(255, 255, 255, .05);
|
||||
--hover-strong: rgba(255, 255, 255, .09);
|
||||
|
||||
--shadow: 0 1px 2px rgba(0, 0, 0, .3);
|
||||
--shadow-lg: 0 12px 30px rgba(0, 0, 0, .45);
|
||||
--side-bg: linear-gradient(180deg, #0c0e12, #0a0b0e);
|
||||
--accent-soft: rgba(79, 124, 255, .16);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
background-color: var(--bg-primary);
|
||||
background-image: radial-gradient(circle at 15% 50%, rgba(56, 189, 248, 0.08), transparent 25%),
|
||||
radial-gradient(circle at 85% 30%, rgba(99, 102, 241, 0.08), transparent 25%);
|
||||
background-attachment: fixed;
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-sans);
|
||||
font-feature-settings: "tnum" 1, "cv01" 1;
|
||||
margin: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
min-height: 100vh;
|
||||
transition: background-color .2s ease, color .2s ease;
|
||||
}
|
||||
@ -1,12 +1,3 @@
|
||||
// Theme toggle (light/dark). State applied pre-paint in base.html <head>.
|
||||
function toggleTheme() {
|
||||
const root = document.documentElement;
|
||||
const next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||
root.setAttribute('data-theme', next);
|
||||
try { localStorage.setItem('theme', next); } catch (e) { /* ignore */ }
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
}
|
||||
|
||||
// Sidebar Logic
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
|
||||
@ -8,51 +8,29 @@
|
||||
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="page-header">
|
||||
<header class="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h1>칸반 보드</h1>
|
||||
<p class="sub">카드를 드래그해 단계를 옮기세요. 수집 → 검토 → 작업대상 → 완료.</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-secondary" onclick="openHelp()"><i data-lucide="help-circle" style="width:15px;"></i> 사용법</button>
|
||||
<button class="btn btn-secondary" onclick="loadBoard()"><i data-lucide="refresh-cw" style="width:15px;"></i> 새로고침</button>
|
||||
</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="columns-3"></i></div>
|
||||
<div><div class="hi-t">단계(컬럼)</div><div class="hi-d"><b>수집됨 → 검토중 → 작업대상 → 완료</b> 순으로 영상의 진행 상태를 나타냅니다. <b>제외</b>는 작업하지 않을 영상입니다.</div></div>
|
||||
</div>
|
||||
<div class="help-item">
|
||||
<div class="hi-ic"><i data-lucide="move"></i></div>
|
||||
<div><div class="hi-t">드래그로 단계 이동</div><div class="hi-d">카드를 <b>끌어다 다른 컬럼에 놓으면</b> 상태가 바로 바뀝니다(저장됨). 상단 숫자는 컬럼별 카드 수입니다.</div></div>
|
||||
</div>
|
||||
<div class="help-item">
|
||||
<div class="hi-ic"><i data-lucide="wand-2"></i></div>
|
||||
<div><div class="hi-t">카드</div><div class="hi-d">썸네일·제목·지표(조회·시간당·길이)와 배율을 보여줍니다. <b>🪄 아이콘</b>으로 재가공 에디터로 이동합니다.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold mb-1">작업 보드</h1>
|
||||
<p class="text-muted">카드를 드래그해 단계를 옮기세요. 수집 → 검토 → 작업대상 → 완료.</p>
|
||||
</div>
|
||||
<button class="btn btn-secondary px-4 py-2 flex items-center gap-1" onclick="loadBoard()">
|
||||
<i data-lucide="refresh-cw" style="width:16px;"></i> 새로고침
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div id="board" style="display:grid; grid-template-columns: repeat(5, minmax(220px, 1fr)); gap:1rem; align-items:start; overflow-x:auto;">
|
||||
<!-- columns injected -->
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.kb-col { background: var(--surface-2); border:1px solid var(--border); border-radius: var(--r); display:flex; flex-direction:column; min-height:200px; }
|
||||
.kb-col-head { padding:12px; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; background:var(--surface-2); border-radius:var(--r) var(--r) 0 0; }
|
||||
.kb-col { background: rgba(255,255,255,0.02); border:1px solid var(--glass-border); border-radius: var(--radius-md); display:flex; flex-direction:column; min-height:200px; }
|
||||
.kb-col-head { padding:12px; border-bottom:1px solid var(--glass-border); display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; }
|
||||
.kb-body { padding:10px; display:flex; flex-direction:column; gap:8px; min-height:120px; flex:1; transition: background 0.15s; }
|
||||
.kb-body.drag-over { background: var(--accent-soft); }
|
||||
.kb-card { background: var(--surface); border:1px solid var(--border); border-radius:9px; padding:9px; cursor:grab; box-shadow:var(--shadow); transition: transform 0.1s, border-color 0.15s; }
|
||||
.kb-card:hover { border-color: var(--accent); }
|
||||
.kb-body.drag-over { background: rgba(124,58,237,0.12); }
|
||||
.kb-card { background: var(--glass-bg,#1e1e2d); border:1px solid var(--glass-border); border-radius:8px; padding:8px; cursor:grab; transition: transform 0.1s, border-color 0.15s; }
|
||||
.kb-card:hover { border-color:#7C3AED88; }
|
||||
.kb-card.dragging { opacity:0.5; }
|
||||
.kb-count { font-size:0.75rem; font-weight:600; background:var(--surface); border:1px solid var(--border); color:var(--text-2); padding:2px 9px; border-radius:999px; }
|
||||
.kb-count { font-size:0.75rem; background:rgba(255,255,255,0.08); padding:2px 8px; border-radius:999px; }
|
||||
</style>
|
||||
|
||||
<script th:inline="javascript">
|
||||
@ -68,14 +46,6 @@
|
||||
let dragId = null;
|
||||
|
||||
function esc(s){ return (s==null?'':String(s)).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
function fmt(n){ return (n==null)?'-':Number(n).toLocaleString(); }
|
||||
function durBadge(v){
|
||||
if(v.isShorts) return 'Shorts';
|
||||
const s = v.durationSec;
|
||||
if(s==null || s<=0) return '';
|
||||
const m = Math.floor(s/60), sec = s%60;
|
||||
return m+':'+String(sec).padStart(2,'0');
|
||||
}
|
||||
async function api(url, opts){
|
||||
const res = await fetch(url, opts);
|
||||
const json = await res.json().catch(()=>({}));
|
||||
@ -86,12 +56,6 @@
|
||||
function cardHtml(v){
|
||||
const ratio = v.viewsPerSubRatio!=null ? Number(v.viewsPerSubRatio).toFixed(1)+'x' : '';
|
||||
const ratioColor = (v.viewsPerSubRatio>=10)?'#ef4444':((v.viewsPerSubRatio>=2)?'#f59e0b':'#10b981');
|
||||
const db = durBadge(v);
|
||||
const metrics = [
|
||||
v.viewCount!=null ? '👁 '+fmt(v.viewCount) : '',
|
||||
v.viewsPerHour!=null ? '⚡ '+fmt(Math.round(v.viewsPerHour))+'/h' : '',
|
||||
db ? '⏱ '+db : ''
|
||||
].filter(Boolean).join(' · ');
|
||||
return `<div class="kb-card" draggable="true" data-id="${v.id}">
|
||||
<div style="display:flex; gap:8px;">
|
||||
<img src="${esc(v.thumbnailUrl)}" style="width:64px; height:36px; object-fit:cover; border-radius:4px; flex-shrink:0;">
|
||||
@ -99,7 +63,6 @@
|
||||
<div class="text-sm font-bold" style="display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; line-height:1.3;">${esc(v.title)}</div>
|
||||
</div>
|
||||
</div>
|
||||
${metrics?`<div class="text-muted mt-2" style="font-size:0.68rem; line-height:1.4;">${metrics}</div>`:''}
|
||||
<div class="flex items-center justify-between mt-2">
|
||||
<span class="text-muted" style="font-size:0.7rem;">${esc(v.channelTitle||'')}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
@ -182,10 +145,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
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(); });
|
||||
|
||||
loadBoard();
|
||||
/*]]>*/
|
||||
</script>
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<!-- Video List Table -->
|
||||
<div class="card p-0" style="overflow-x: auto;">
|
||||
<table class="w-full" style="border-collapse: collapse; text-align: left;">
|
||||
<thead style="background: var(--surface-2); border-bottom: 1px solid var(--glass-border);">
|
||||
<thead style="background: rgba(255,255,255,0.02); border-bottom: 1px solid var(--glass-border);">
|
||||
<tr>
|
||||
<th class="p-4 text-sm font-bold text-muted">Video</th>
|
||||
<th class="p-4 text-sm font-bold text-muted text-center">Script</th>
|
||||
@ -239,10 +239,10 @@
|
||||
if (icon) {
|
||||
const colName = th.getAttribute('onclick').match(/'([^']+)'/)[1];
|
||||
if (colName === sortState.column) {
|
||||
th.style.color = 'var(--accent)';
|
||||
th.classList.add('text-white');
|
||||
icon.setAttribute('data-lucide', sortState.direction === 'asc' ? 'chevron-up' : 'chevron-down');
|
||||
} else {
|
||||
th.style.color = '';
|
||||
th.classList.remove('text-white');
|
||||
icon.setAttribute('data-lucide', 'arrow-up-down');
|
||||
}
|
||||
}
|
||||
@ -315,10 +315,10 @@
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
plugins: { legend: { labels: { color: 'var(--text-2)' } } },
|
||||
plugins: { legend: { labels: { color: '#cbd5e1' } } },
|
||||
scales: {
|
||||
x: { ticks: { color: '#94a3b8' }, grid: { color: 'var(--surface-2)' } },
|
||||
y: { position: 'left', ticks: { color: '#a78bfa' }, grid: { color: 'var(--surface-2)' }, title: { display: true, text: '구독자', color: '#a78bfa' } },
|
||||
x: { ticks: { color: '#94a3b8' }, grid: { color: 'rgba(255,255,255,0.05)' } },
|
||||
y: { position: 'left', ticks: { color: '#a78bfa' }, grid: { color: 'rgba(255,255,255,0.05)' }, title: { display: true, text: '구독자', color: '#a78bfa' } },
|
||||
y1: { position: 'right', ticks: { color: '#34d399' }, grid: { drawOnChartArea: false }, title: { display: true, text: '조회수', color: '#34d399' } }
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,19 +10,19 @@
|
||||
<div layout:fragment="content">
|
||||
<header class="flex justify-between items-center mb-4" style="margin-bottom: 2rem;">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold mb-4">채널</h1>
|
||||
<p class="text-muted">추적 중인 YouTube 채널을 관리합니다.</p>
|
||||
<h1 class="text-xl font-bold mb-4">Channels</h1>
|
||||
<p class="text-muted">Manage the YouTube channels you are tracking.</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer text-sm text-muted hover:text-white transition-colors">
|
||||
<input type="checkbox" id="selectAllCheckbox" onchange="toggleSelectAll()" style="width: 18px; height: 18px; cursor: pointer;">
|
||||
전체 선택
|
||||
Select All
|
||||
</label>
|
||||
<button id="viewSelectedBtn" class="btn btn-outline" onclick="viewSelectedVideos()" style="display: none;">
|
||||
<i data-lucide="play" style="width: 18px; margin-right: 8px;"></i> View Selected Videos (<span id="selectedCount">0</span>)
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="openAddModal()">
|
||||
<i data-lucide="plus" style="width: 18px; margin-right: 8px;"></i> 채널 추가
|
||||
<i data-lucide="plus" style="width: 18px; margin-right: 8px;"></i> Add Channel
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
@ -40,13 +40,13 @@
|
||||
onchange="updateSelectedCount(); event.stopPropagation();">
|
||||
<div class="custom-checkbox flex items-center justify-center" style="
|
||||
width: 24px; height: 24px;
|
||||
border: 2px solid var(--border-strong);
|
||||
border: 2px solid rgba(255,255,255,0.2);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-2);
|
||||
background: rgba(255,255,255,0.05);
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: none;
|
||||
">
|
||||
<i data-lucide="check" style="width: 16px; height: 16px; color: var(--text); display: none;"></i>
|
||||
<i data-lucide="check" style="width: 16px; height: 16px; color: white; display: none;"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -57,8 +57,8 @@
|
||||
<div>
|
||||
<h3 class="font-bold" th:text="${channel.title}">Tech Reviewer A</h3>
|
||||
<div class="text-muted text-sm"
|
||||
th:text="${#numbers.formatInteger(channel.subscriberCount ?: 0, 0, 'COMMA')} + ' 구독자'">
|
||||
1.2M 구독자</div>
|
||||
th:text="${#numbers.formatInteger(channel.subscriberCount ?: 0, 0, 'COMMA')} + ' Subscribers'">
|
||||
1.2M Subscribers</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -75,14 +75,14 @@
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.5rem;">
|
||||
<div class="p-4"
|
||||
style="background: var(--surface-2); border-radius: 8px; text-align: center;">
|
||||
<div class="text-muted mb-4" style="font-size: 0.75rem">영상 수</div>
|
||||
style="background: rgba(255,255,255,0.03); border-radius: 8px; text-align: center;">
|
||||
<div class="text-muted mb-4" style="font-size: 0.75rem">Videos</div>
|
||||
<div class="font-bold" th:text="${#numbers.formatInteger(channel.videoCount ?: 0, 0, 'COMMA')}">450
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4"
|
||||
style="background: var(--surface-2); border-radius: 8px; text-align: center;">
|
||||
<div class="text-muted mb-4" style="font-size: 0.75rem">총 조회수</div>
|
||||
style="background: rgba(255,255,255,0.03); border-radius: 8px; text-align: center;">
|
||||
<div class="text-muted mb-4" style="font-size: 0.75rem">Total Views</div>
|
||||
<div class="font-bold" th:text="${#numbers.formatInteger(channel.viewCount ?: 0, 0, 'COMMA')}">45K
|
||||
</div>
|
||||
</div>
|
||||
@ -91,15 +91,15 @@
|
||||
<div class="flex gap-2" onclick="event.stopPropagation()">
|
||||
<a th:href="@{/channels/{id}(id=${channel.id})}" class="btn btn-primary w-full"
|
||||
style="font-size: 0.8rem; text-decoration: none; display: flex; align-items: center; justify-content: center;">
|
||||
<i data-lucide="list" style="width: 14px; margin-right: 6px;"></i> 상세
|
||||
<i data-lucide="list" style="width: 14px; margin-right: 6px;"></i> Detail
|
||||
</a>
|
||||
<a th:href="'https://www.youtube.com/channel/' + ${channel.channelId}" target="_blank"
|
||||
class="btn btn-ghost w-full"
|
||||
style="font-size: 0.8rem; text-decoration: none; display: flex; align-items: center; justify-content: center;">
|
||||
<i data-lucide="external-link" style="width: 14px; margin-right: 6px;"></i> 방문
|
||||
<i data-lucide="external-link" style="width: 14px; margin-right: 6px;"></i> Visit
|
||||
</a>
|
||||
<button class="btn btn-ghost w-full" style="color: var(--danger); font-size: 0.8rem;" th:onclick="'deleteChannel(' + ${channel.id} + '); event.stopPropagation();'">
|
||||
<i data-lucide="trash-2" style="width: 14px; margin-right: 6px;"></i> 삭제
|
||||
<i data-lucide="trash-2" style="width: 14px; margin-right: 6px;"></i> Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -118,7 +118,7 @@
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-ghost" onclick="closeAddModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="submitAddChannel()">채널 추가</button>
|
||||
<button class="btn btn-primary" onclick="submitAddChannel()">Add Channel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -224,8 +224,8 @@
|
||||
} else {
|
||||
card.style.borderColor = 'var(--glass-border)';
|
||||
card.style.background = 'var(--glass-bg)';
|
||||
customCb.style.background = 'var(--surface-2)';
|
||||
customCb.style.borderColor = 'var(--border-strong)';
|
||||
customCb.style.background = 'rgba(255,255,255,0.05)';
|
||||
customCb.style.borderColor = 'rgba(255,255,255,0.2)';
|
||||
checkIcon.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
@ -8,47 +8,53 @@
|
||||
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>수집함</h1>
|
||||
<p class="sub">수집한 영상을 분류·발굴하고 재가공 대상을 관리합니다.</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-secondary" onclick="openHelp()">
|
||||
<i data-lucide="help-circle" style="width:15px;"></i> 사용법
|
||||
<header class="mb-4">
|
||||
<h1 class="text-2xl font-bold mb-2">수집함 (Collection)</h1>
|
||||
<p class="text-muted">수집한 영상을 분류·발굴하고 재가공 대상을 관리합니다.</p>
|
||||
</header>
|
||||
|
||||
<!-- 카테고리 관리 -->
|
||||
<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>
|
||||
</div>
|
||||
</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;">
|
||||
<div id="categoryChips" class="flex flex-wrap gap-2 mb-2">
|
||||
<span class="text-muted text-sm">로딩...</span>
|
||||
</div>
|
||||
<button class="btn btn-secondary" style="padding:0.4rem 0.7rem;" onclick="document.getElementById('catForm').classList.toggle('hidden')">
|
||||
<i data-lucide="plus" style="width:15px;"></i> 추가
|
||||
</button>
|
||||
<div id="catForm" class="hidden flex gap-2 mt-4" style="flex-wrap: wrap; align-items:flex-end;">
|
||||
<div style="display:flex; flex-direction:column; gap:4px;">
|
||||
<label class="text-sm text-muted">이름</label>
|
||||
<input id="catName" type="text" placeholder="예: 동물 썰"
|
||||
style="padding:8px; background:rgba(255,255,255,0.05); border:1px solid var(--glass-border); border-radius:var(--radius-md); color:white; outline:none;">
|
||||
</div>
|
||||
<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);">
|
||||
<input id="catName" type="text" placeholder="이름 (예: 동물 썰)" style="flex:1; min-width:140px;">
|
||||
<input id="catColor" type="color" value="#7C3AED" title="색상" style="padding:2px; height:38px; width:48px;">
|
||||
<button class="btn btn-primary" onclick="addCategory()">추가</button>
|
||||
<div style="display:flex; flex-direction:column; gap:4px;">
|
||||
<label class="text-sm text-muted">색상</label>
|
||||
<input id="catColor" type="color" value="#7C3AED"
|
||||
style="padding:2px; height:42px; width:56px; background:rgba(255,255,255,0.05); border:1px solid var(--glass-border); border-radius:var(--radius-md);">
|
||||
</div>
|
||||
<button class="btn btn-primary px-4 py-2" onclick="addCategory()">추가</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 필터 툴바 -->
|
||||
<!-- 필터 바 -->
|
||||
<div class="card mb-4">
|
||||
<div class="toolbar">
|
||||
<label class="chip"><input type="checkbox" id="fOutperformers" onchange="loadVideos()"> 🚀 떡상 후보</label>
|
||||
<label class="chip"><input type="checkbox" id="fShorts" onchange="loadVideos()"> Shorts</label>
|
||||
<label class="chip"><input type="checkbox" id="fBookmarked" onchange="loadVideos()"> ⭐ 북마크</label>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:16px; align-items:flex-end;">
|
||||
<label class="flex items-center gap-2 cursor-pointer" style="height:42px;">
|
||||
<input type="checkbox" id="fOutperformers" onchange="loadVideos()"> <span class="text-sm font-bold">🚀 떡상 후보만</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer" style="height:42px;">
|
||||
<input type="checkbox" id="fShorts" onchange="loadVideos()"> <span class="text-sm">Shorts만</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer" style="height:42px;">
|
||||
<input type="checkbox" id="fBookmarked" onchange="loadVideos()"> <span class="text-sm">⭐ 북마크만</span>
|
||||
</label>
|
||||
|
||||
<span class="sep"></span>
|
||||
|
||||
<label class="field">상태
|
||||
<select id="fStatus" onchange="loadVideos()">
|
||||
<div style="display:flex; flex-direction:column; gap:4px; min-width:130px;">
|
||||
<label class="text-sm text-muted">상태</label>
|
||||
<select id="fStatus" onchange="loadVideos()" class="filter-sel">
|
||||
<option value="">전체</option>
|
||||
<option value="NEW">NEW (수집됨)</option>
|
||||
<option value="REVIEWING">REVIEWING (검토중)</option>
|
||||
@ -56,98 +62,63 @@
|
||||
<option value="DONE">DONE (완료)</option>
|
||||
<option value="EXCLUDED">EXCLUDED (제외)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">카테고리
|
||||
<select id="fCategory" onchange="loadVideos()"><option value="">전체</option></select>
|
||||
</label>
|
||||
<label class="field">출처
|
||||
<select id="fSource" onchange="loadVideos()">
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:4px; min-width:150px;">
|
||||
<label class="text-sm text-muted">카테고리</label>
|
||||
<select id="fCategory" onchange="loadVideos()" class="filter-sel">
|
||||
<option value="">전체</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:4px; min-width:120px;">
|
||||
<label class="text-sm text-muted">출처</label>
|
||||
<select id="fSource" onchange="loadVideos()" class="filter-sel">
|
||||
<option value="">전체</option>
|
||||
<option value="CHANNEL">채널 수집</option>
|
||||
<option value="SEARCH">검색 수집</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">정렬
|
||||
<select id="fSort" onchange="loadVideos()">
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:4px; min-width:150px;">
|
||||
<label class="text-sm text-muted">정렬</label>
|
||||
<select id="fSort" onchange="loadVideos()" class="filter-sel">
|
||||
<option value="viewsPerSubRatio">배율(조회/구독) ↓</option>
|
||||
<option value="viewsPerHour">시간당 조회수 ↓</option>
|
||||
<option value="viewCount">조회수 ↓</option>
|
||||
<option value="publishedAt">최신순 ↓</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<span class="spacer"></span>
|
||||
<span id="resultCount" class="badge badge-muted"></span>
|
||||
<button class="btn btn-secondary" onclick="loadVideos()" title="새로고침">
|
||||
<i data-lucide="refresh-cw" style="width:15px;"></i>
|
||||
</div>
|
||||
<button class="btn btn-secondary px-4 py-2 flex items-center gap-1" onclick="loadVideos()">
|
||||
<i data-lucide="refresh-cw" style="width:16px;"></i> 새로고침
|
||||
</button>
|
||||
<span id="resultCount" class="text-sm text-muted" style="margin-left:auto; height:42px; display:flex; align-items:center;"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 결과 테이블 -->
|
||||
<div class="card p-0" style="overflow-x:auto;">
|
||||
<table class="w-full coll-table" style="border-collapse:collapse; text-align:left;">
|
||||
<thead>
|
||||
<table class="w-full" style="border-collapse:collapse; text-align:left;">
|
||||
<thead style="background:rgba(255,255,255,0.02); border-bottom:1px solid var(--glass-border);">
|
||||
<tr>
|
||||
<th>영상</th>
|
||||
<th>구독자</th>
|
||||
<th>조회수</th>
|
||||
<th>시간당</th>
|
||||
<th>배율</th>
|
||||
<th>상태</th>
|
||||
<th>카테고리</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>
|
||||
<th class="p-3 text-sm font-bold text-muted">채널</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">구독자</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">조회수</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">시간당</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">배율</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">상태</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">카테고리</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="resultBody">
|
||||
<tr><td colspan="8" class="p-8 text-center text-muted">로딩 중...</td></tr>
|
||||
<tr><td colspan="10" class="p-8 text-center text-muted">로딩 중...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</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 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,#1e1e2d); border-radius:12px; padding:16px; border:1px solid var(--glass-border);">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
|
||||
<h3 class="text-lg font-bold truncate pr-4" id="modalTitle">Video</h3>
|
||||
<button onclick="closeVideoModal()" class="text-muted hover:text-white" style="font-size:28px; line-height:1; background:none; border:none; cursor:pointer;">×</button>
|
||||
@ -158,13 +129,9 @@
|
||||
|
||||
<style>
|
||||
.hidden { display:none !important; }
|
||||
.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 { padding:4px 6px; background:rgba(255,255,255,0.05); border:1px solid var(--glass-border); border-radius:6px; color:white; font-size:0.8rem; outline:none; }
|
||||
.row-sel option { background:#1e1e2d; color:white; }
|
||||
.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>
|
||||
|
||||
<script th:inline="javascript">
|
||||
@ -272,7 +239,7 @@
|
||||
const body = document.getElementById('resultBody');
|
||||
document.getElementById('resultCount').textContent = list.length + '건';
|
||||
if(list.length===0){
|
||||
body.innerHTML = '<tr><td colspan="8" class="p-8 text-center text-muted">조건에 맞는 영상이 없습니다.</td></tr>';
|
||||
body.innerHTML = '<tr><td colspan="10" class="p-8 text-center text-muted">조건에 맞는 영상이 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
body.innerHTML = list.map(v=>{
|
||||
@ -281,31 +248,32 @@
|
||||
const statusOpts = STATUS_KEYS.map(k=>
|
||||
`<option value="${k}" ${v.interestStatus===k?'selected':''}>${STATUS_LABEL[k]}</option>`).join('');
|
||||
const star = v.bookmarked ? '#f59e0b' : 'var(--text-muted)';
|
||||
const shortsBadge = v.isShorts ? '<span class="badge badge-primary" style="margin-left:5px;">Shorts</span>' : '';
|
||||
return `<tr>
|
||||
<td>
|
||||
<div class="flex items-center gap-3" style="min-width:0;">
|
||||
<img src="${esc(v.thumbnailUrl)}" data-vid="${v.videoId}" data-title="${esc(v.title)}" onclick="openVideoModal(this.dataset.vid, this.dataset.title)"
|
||||
style="width:84px; height:47px; object-fit:cover; border-radius:6px; cursor:pointer; flex-shrink:0;">
|
||||
<div style="min-width:0;">
|
||||
<div class="font-semibold text-sm" style="display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; line-height:1.35;">
|
||||
<a href="https://www.youtube.com/watch?v=${v.videoId}" target="_blank" class="hover:underline">${esc(v.title)}</a>${shortsBadge}
|
||||
</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>
|
||||
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>' : '';
|
||||
return `<tr style="border-bottom:1px solid var(--glass-border);">
|
||||
<td class="p-3">
|
||||
<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)">
|
||||
<img src="${esc(v.thumbnailUrl)}" style="width:96px; height:54px; object-fit:cover; border-radius:6px;">
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-sm">${fmt(v.subscriberCount)}</td>
|
||||
<td class="text-sm">${fmt(v.viewCount)}</td>
|
||||
<td class="text-sm">${fmt(v.viewsPerHour)}</td>
|
||||
<td class="text-sm">${ratioBadge(v.viewsPerSubRatio)}</td>
|
||||
<td><select class="row-sel" onchange="setStatus(${v.id}, this.value)">${statusOpts}</select></td>
|
||||
<td><select class="row-sel" onchange="setCategory(${v.id}, this.value)">${catOpts}</select></td>
|
||||
<td>
|
||||
<div class="flex items-center gap-1 justify-end">
|
||||
<a class="btn btn-primary" style="padding:0.4rem;" title="재가공" href="/rework/${v.id}"><i data-lucide="wand-2" style="width:15px;"></i></a>
|
||||
<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>
|
||||
<button class="btn btn-secondary" style="padding:0.4rem;" title="삭제" onclick="deleteVideo(${v.id})"><i data-lucide="trash-2" style="width:15px;"></i></button>
|
||||
<td class="p-3" style="max-width:280px;">
|
||||
<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 text-white">${esc(v.title)}</a>${shortsBadge}
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-3 text-sm text-muted" style="max-width:130px;">
|
||||
<a href="${v.ytChannelId?('https://www.youtube.com/channel/'+v.ytChannelId):'#'}" target="_blank" class="hover:underline truncate" style="color:#e2e8f0;">${esc(v.channelTitle||'-')}</a>
|
||||
</td>
|
||||
<td class="p-3 text-sm">${fmt(v.subscriberCount)}</td>
|
||||
<td class="p-3 text-sm">${fmt(v.viewCount)}</td>
|
||||
<td class="p-3 text-sm">${fmt(v.viewsPerHour)}</td>
|
||||
<td class="p-3 text-sm">${ratioBadge(v.viewsPerSubRatio)}</td>
|
||||
<td class="p-3"><select class="row-sel" onchange="setStatus(${v.id}, this.value)">${statusOpts}</select></td>
|
||||
<td class="p-3"><select class="row-sel" onchange="setCategory(${v.id}, this.value)">${catOpts}</select></td>
|
||||
<td class="p-3">
|
||||
<div class="flex items-center gap-1">
|
||||
<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>
|
||||
</td>
|
||||
</tr>`;
|
||||
@ -344,11 +312,6 @@
|
||||
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 파라미터 필터 적용 -----
|
||||
function applyUrlFilters(){
|
||||
const q = new URLSearchParams(window.location.search);
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
<header class="mb-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold mb-1">대시보드</h1>
|
||||
<h1 class="text-xl font-bold mb-1">Dashboard</h1>
|
||||
<p class="text-muted">수집 → 큐레이션 → 재가공 → 발행 파이프라인 현황</p>
|
||||
</div>
|
||||
<button id="refreshBtn" class="btn btn-secondary px-3 py-2 flex items-center gap-1" onclick="loadDashboard()">
|
||||
@ -193,7 +193,7 @@
|
||||
else {
|
||||
opList.innerHTML = op.map((v,i)=>{
|
||||
const ratio = v.viewsPerSubRatio!=null ? Number(v.viewsPerSubRatio).toFixed(1)+'x' : '-';
|
||||
return '<a href="https://www.youtube.com/watch?v='+v.videoId+'" target="_blank" class="flex items-center justify-between p-3" style="border-radius:var(--radius-md); background:var(--surface-2); text-decoration:none;">' +
|
||||
return '<a href="https://www.youtube.com/watch?v='+v.videoId+'" target="_blank" class="flex items-center justify-between p-3" style="border-radius:var(--radius-md); background:rgba(255,255,255,0.03); text-decoration:none;">' +
|
||||
'<div class="flex items-center gap-2" style="min-width:0;">' +
|
||||
'<span class="badge badge-muted" style="flex-shrink:0;">'+(i+1)+'</span>' +
|
||||
'<img src="'+esc(v.thumbnailUrl)+'" style="width:48px;height:27px;object-fit:cover;border-radius:4px;flex-shrink:0;">' +
|
||||
@ -220,8 +220,8 @@
|
||||
? '<p class="text-muted text-sm" style="margin-top:4px;">발행 패키지가 없습니다.</p>'
|
||||
: '<div class="text-sm text-muted" style="margin:6px 0 2px;">최근</div>' + recent.map(p=>{
|
||||
const st = PUB_ST[p.status] || { t:p.status, cls:'badge-muted' };
|
||||
return '<a href="/rework/'+p.channelVideoId+'" class="flex items-center justify-between p-2" style="border-radius:6px; background:var(--surface-2); text-decoration:none;">' +
|
||||
'<span class="text-sm truncate" style="max-width:170px; color:var(--text);">'+esc(p.title||'(제목 없음)')+'</span>' +
|
||||
return '<a href="/rework/'+p.channelVideoId+'" class="flex items-center justify-between p-2" style="border-radius:6px; background:rgba(255,255,255,0.03); text-decoration:none;">' +
|
||||
'<span class="text-sm truncate" style="max-width:170px; color:#e2e8f0;">'+esc(p.title||'(제목 없음)')+'</span>' +
|
||||
'<span class="badge '+st.cls+'" style="flex-shrink:0;">'+st.t+'</span></a>';
|
||||
}).join('');
|
||||
|
||||
|
||||
@ -8,104 +8,88 @@
|
||||
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>발굴</h1>
|
||||
<p class="sub">수집한 영상 중 재가공할 떡상 후보를 빠르게 골라냅니다. (제외 처리한 영상은 숨겨집니다)</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-secondary" onclick="openHelp()"><i data-lucide="help-circle" style="width:15px;"></i> 사용법</button>
|
||||
</div>
|
||||
</div>
|
||||
<header class="mb-4">
|
||||
<h1 class="text-2xl font-bold mb-2">발굴 (Discovery)</h1>
|
||||
<p class="text-muted">수집한 영상 중 재가공할 떡상 후보를 빠르게 골라냅니다. (제외 처리한 영상은 숨겨집니다)</p>
|
||||
</header>
|
||||
|
||||
<!-- 사용법 모달 -->
|
||||
<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="trending-up"></i></div>
|
||||
<div><div class="hi-t">배율(떡상 지표)</div><div class="hi-d"><b>조회수 ÷ 구독자</b>로, 구독자 대비 조회수가 폭발한 정도입니다. 높을수록 ‘떡상’(10x↑ 빨강, 2x↑ 주황). 작은 채널의 대박 영상을 찾는 데 유용합니다.</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>로 좁히고 <b>정렬</b>을 바꿉니다. 칩 <b>Shorts</b>·<b>미처리만</b>으로 빠르게 거릅니다.</div></div>
|
||||
</div>
|
||||
<div class="help-item">
|
||||
<div class="hi-ic"><i data-lucide="wand-2"></i></div>
|
||||
<div><div class="hi-t">고른 뒤</div><div class="hi-d">표의 <b>🪄</b>로 재가공 에디터로 보내거나, 상태를 바꿔 칸반 보드/수집함에서 이어서 관리합니다.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 필터 툴바 -->
|
||||
<!-- 필터 바 -->
|
||||
<div class="card mb-4">
|
||||
<div class="toolbar">
|
||||
<label class="chip"><input type="checkbox" id="fShorts" onchange="loadVideos()"> Shorts</label>
|
||||
<label class="chip"><input type="checkbox" id="fUnprocessed" onchange="loadVideos()"> 미처리만</label>
|
||||
<span class="sep"></span>
|
||||
<label class="field">기간
|
||||
<select id="fPeriod" onchange="loadVideos()">
|
||||
<div style="display:flex; flex-wrap:wrap; gap:16px; align-items:flex-end;">
|
||||
<div style="display:flex; flex-direction:column; gap:4px; min-width:120px;">
|
||||
<label class="text-sm text-muted">기간</label>
|
||||
<select id="fPeriod" onchange="loadVideos()" class="filter-sel">
|
||||
<option value="">전체</option>
|
||||
<option value="7">최근 7일</option>
|
||||
<option value="14">최근 14일</option>
|
||||
<option value="30">최근 30일</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">최소 배율
|
||||
<select id="fMinRatio" onchange="loadVideos()">
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:4px; min-width:120px;">
|
||||
<label class="text-sm text-muted">최소 배율</label>
|
||||
<select id="fMinRatio" onchange="loadVideos()" class="filter-sel">
|
||||
<option value="">전체</option>
|
||||
<option value="2">≥ 2x</option>
|
||||
<option value="3">≥ 3x</option>
|
||||
<option value="5">≥ 5x</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">출처
|
||||
<select id="fSource" onchange="loadVideos()">
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:4px; min-width:120px;">
|
||||
<label class="text-sm text-muted">출처</label>
|
||||
<select id="fSource" onchange="loadVideos()" class="filter-sel">
|
||||
<option value="">전체</option>
|
||||
<option value="CHANNEL">채널 수집</option>
|
||||
<option value="SEARCH">검색 수집</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">정렬
|
||||
<select id="fSort" onchange="loadVideos()">
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; gap:4px; min-width:160px;">
|
||||
<label class="text-sm text-muted">정렬</label>
|
||||
<select id="fSort" onchange="loadVideos()" class="filter-sel">
|
||||
<option value="viewsPerSubRatio">배율(조회/구독) ↓</option>
|
||||
<option value="viewsPerHour">시간당 조회수 ↓</option>
|
||||
<option value="viewCount">조회수 ↓</option>
|
||||
<option value="publishedAt">최신순 ↓</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 cursor-pointer" style="height:42px;">
|
||||
<input type="checkbox" id="fShorts" onchange="loadVideos()"> <span class="text-sm">Shorts만</span>
|
||||
</label>
|
||||
<span class="spacer"></span>
|
||||
<span id="resultCount" class="badge badge-muted"></span>
|
||||
<button class="btn btn-secondary" onclick="loadVideos()" title="새로고침"><i data-lucide="refresh-cw" style="width:15px;"></i></button>
|
||||
<label class="flex items-center gap-2 cursor-pointer" style="height:42px;">
|
||||
<input type="checkbox" id="fUnprocessed" onchange="loadVideos()"> <span class="text-sm">미처리만(NEW/검토중)</span>
|
||||
</label>
|
||||
<button class="btn btn-secondary px-4 py-2 flex items-center gap-1" onclick="loadVideos()">
|
||||
<i data-lucide="refresh-cw" style="width:16px;"></i> 새로고침
|
||||
</button>
|
||||
<span id="resultCount" class="text-sm text-muted" style="margin-left:auto; height:42px; display:flex; align-items:center;"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 결과 테이블 -->
|
||||
<div class="card p-0" style="overflow-x:auto;">
|
||||
<table class="w-full coll-table" style="border-collapse:collapse; text-align:left;">
|
||||
<thead>
|
||||
<table class="w-full" style="border-collapse:collapse; text-align:left;">
|
||||
<thead style="background:rgba(255,255,255,0.02); border-bottom:1px solid var(--glass-border);">
|
||||
<tr>
|
||||
<th>영상</th>
|
||||
<th>구독자</th>
|
||||
<th>조회수</th>
|
||||
<th>시간당</th>
|
||||
<th>배율</th>
|
||||
<th>업로드</th>
|
||||
<th>상태</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>
|
||||
<th class="p-3 text-sm font-bold text-muted">채널</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">구독자</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">조회수</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">시간당</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">배율</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">업로드</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">상태</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="resultBody">
|
||||
<tr><td colspan="8" class="p-8 text-center text-muted">로딩 중...</td></tr>
|
||||
<tr><td colspan="10" class="p-8 text-center text-muted">로딩 중...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</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 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,#1e1e2d); border-radius:12px; padding:16px; border:1px solid var(--glass-border);">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
|
||||
<h3 class="text-lg font-bold truncate pr-4" id="modalTitle">Video</h3>
|
||||
<button onclick="closeVideoModal()" class="text-muted hover:text-white" style="font-size:28px; line-height:1; background:none; border:none; cursor:pointer;">×</button>
|
||||
@ -115,8 +99,8 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.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 option { background:var(--surface); color:var(--text); }
|
||||
.row-sel { padding:4px 6px; background:rgba(255,255,255,0.05); border:1px solid var(--glass-border); border-radius:6px; color:white; font-size:0.8rem; outline:none; }
|
||||
.row-sel option { background:#1e1e2d; color:white; }
|
||||
</style>
|
||||
|
||||
<script th:inline="javascript">
|
||||
@ -154,7 +138,7 @@
|
||||
let data;
|
||||
try { data = await api(API + '/discover?' + p.toString()); }
|
||||
catch(e){
|
||||
body.innerHTML = '<tr><td colspan="8" class="p-8 text-center text-danger">불러오기 실패: '+esc(e.message)+'</td></tr>';
|
||||
body.innerHTML = '<tr><td colspan="10" class="p-8 text-center text-danger">불러오기 실패: '+esc(e.message)+'</td></tr>';
|
||||
return;
|
||||
}
|
||||
renderRows(data || []);
|
||||
@ -172,37 +156,38 @@
|
||||
const body = document.getElementById('resultBody');
|
||||
document.getElementById('resultCount').textContent = list.length + '건';
|
||||
if(list.length===0){
|
||||
body.innerHTML = '<tr><td colspan="8" class="p-8 text-center text-muted">조건에 맞는 발굴 후보가 없습니다.</td></tr>';
|
||||
body.innerHTML = '<tr><td colspan="10" class="p-8 text-center text-muted">조건에 맞는 발굴 후보가 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
body.innerHTML = list.map(v=>{
|
||||
const statusOpts = STATUS_KEYS.map(k=>
|
||||
`<option value="${k}" ${v.interestStatus===k?'selected':''}>${STATUS_LABEL[k]}</option>`).join('');
|
||||
const star = v.bookmarked ? '#f59e0b' : 'var(--text-muted)';
|
||||
const shortsBadge = v.isShorts ? '<span class="badge badge-primary" style="margin-left:5px;">Shorts</span>' : '';
|
||||
return `<tr>
|
||||
<td>
|
||||
<div class="flex items-center gap-3" style="min-width:0;">
|
||||
<img src="${esc(v.thumbnailUrl)}" data-vid="${v.videoId}" data-title="${esc(v.title)}" onclick="openVideoModal(this.dataset.vid, this.dataset.title)"
|
||||
style="width:84px; height:47px; object-fit:cover; border-radius:6px; cursor:pointer; flex-shrink:0;">
|
||||
<div style="min-width:0;">
|
||||
<div class="font-semibold text-sm" style="display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; line-height:1.35;">
|
||||
<a href="https://www.youtube.com/watch?v=${v.videoId}" target="_blank" class="hover:underline">${esc(v.title)}</a>${shortsBadge}
|
||||
</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>
|
||||
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>' : '';
|
||||
return `<tr style="border-bottom:1px solid var(--glass-border);">
|
||||
<td class="p-3">
|
||||
<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)">
|
||||
<img src="${esc(v.thumbnailUrl)}" style="width:96px; height:54px; object-fit:cover; border-radius:6px;">
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-sm">${fmt(v.subscriberCount)}</td>
|
||||
<td class="text-sm">${fmt(v.viewCount)}</td>
|
||||
<td class="text-sm">${fmt(v.viewsPerHour)}</td>
|
||||
<td class="text-sm">${ratioBadge(v.viewsPerSubRatio)}</td>
|
||||
<td class="text-sm text-muted">${fmtDate(v.publishedAt)}</td>
|
||||
<td><select class="row-sel" onchange="setStatus(${v.id}, this.value)">${statusOpts}</select></td>
|
||||
<td>
|
||||
<div class="flex items-center gap-1 justify-end">
|
||||
<a class="btn btn-primary" style="padding:0.4rem;" title="재가공" href="/rework/${v.id}"><i data-lucide="wand-2" style="width:15px;"></i></a>
|
||||
<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>
|
||||
<td class="p-3" style="max-width:280px;">
|
||||
<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 text-white">${esc(v.title)}</a>${shortsBadge}
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-3 text-sm text-muted" style="max-width:130px;">
|
||||
<a href="${v.ytChannelId?('https://www.youtube.com/channel/'+v.ytChannelId):'#'}" target="_blank" class="hover:underline truncate" style="color:#e2e8f0;">${esc(v.channelTitle||'-')}</a>
|
||||
</td>
|
||||
<td class="p-3 text-sm">${fmt(v.subscriberCount)}</td>
|
||||
<td class="p-3 text-sm">${fmt(v.viewCount)}</td>
|
||||
<td class="p-3 text-sm">${fmt(v.viewsPerHour)}</td>
|
||||
<td class="p-3 text-sm">${ratioBadge(v.viewsPerSubRatio)}</td>
|
||||
<td class="p-3 text-sm text-muted">${fmtDate(v.publishedAt)}</td>
|
||||
<td class="p-3"><select class="row-sel" onchange="setStatus(${v.id}, this.value)">${statusOpts}</select></td>
|
||||
<td class="p-3">
|
||||
<div class="flex items-center gap-1">
|
||||
<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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
@ -229,10 +214,6 @@
|
||||
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(); } });
|
||||
|
||||
loadVideos();
|
||||
/*]]>*/
|
||||
</script>
|
||||
|
||||
@ -6,14 +6,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>h-lab - Dashboard</title>
|
||||
|
||||
<!-- Theme init (before paint, no flash) -->
|
||||
<script>
|
||||
(function () {
|
||||
try { document.documentElement.setAttribute('data-theme', localStorage.getItem('theme') || 'light'); }
|
||||
catch (e) { document.documentElement.setAttribute('data-theme', 'light'); }
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
@ -3,64 +3,116 @@
|
||||
|
||||
<body th:fragment="sidebar">
|
||||
<aside id="sidebar" class="sidebar">
|
||||
<!-- Brand -->
|
||||
<div class="flex items-center justify-between" style="padding: 1.1rem 1rem 0.5rem;">
|
||||
<div class="p-6 mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div style="width:30px;height:30px;background:var(--primary-gradient);border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
|
||||
<span style="font-weight:800;color:#fff;font-size:15px;">H</span>
|
||||
</div>
|
||||
<div class="brand-text" style="white-space:nowrap;">
|
||||
<div style="font-size:15px;font-weight:700;letter-spacing:-0.2px;color:var(--text);line-height:1.1;">h-lab</div>
|
||||
<div style="font-size:11px;color:var(--text-3);font-weight:500;">yanalyst</div>
|
||||
<div style="
|
||||
width: 32px; height: 32px;
|
||||
background: var(--primary-gradient);
|
||||
border-radius: 8px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: 0 0 15px var(--primary-glow);
|
||||
flex-shrink: 0;
|
||||
">
|
||||
<span style="font-weight: bold; color: white; font-size: 18px;">H</span>
|
||||
</div>
|
||||
<h1 class="text-xl font-bold brand-text" style="
|
||||
background: linear-gradient(to right, #fff, #94a3b8);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
white-space: nowrap;
|
||||
">
|
||||
h-lab
|
||||
</h1>
|
||||
</div>
|
||||
<!-- Toggle Button -->
|
||||
<button id="sidebarToggle" class="sidebar-toggle-btn">
|
||||
<i data-lucide="chevron-left" style="width:18px;height:18px;"></i>
|
||||
<i data-lucide="chevron-left" style="width: 18px; height: 18px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav style="flex:1;padding:0.5rem 0.75rem;overflow-y:auto;">
|
||||
<div class="nav-section">분석</div>
|
||||
<nav style="flex: 1; padding: 0 1rem; overflow-y: auto;">
|
||||
<ul class="flex-col gap-2">
|
||||
<li>
|
||||
<a th:href="@{/}" class="nav-item" th:classappend="${currentPage == 'dashboard'} ? 'active'">
|
||||
<i data-lucide="layout-dashboard" class="nav-icon"></i><span class="nav-text">대시보드</span>
|
||||
</a>
|
||||
<a th:href="@{/discover}" class="nav-item" th:classappend="${currentPage == 'discover'} ? 'active'">
|
||||
<i data-lucide="radar" class="nav-icon"></i><span class="nav-text">발굴</span>
|
||||
<i data-lucide="layout-dashboard" class="nav-icon"></i>
|
||||
<span class="nav-text">Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a th:href="@{/channels}" class="nav-item" th:classappend="${currentPage == 'channels'} ? 'active'">
|
||||
<i data-lucide="users" class="nav-icon"></i><span class="nav-text">채널</span>
|
||||
<i data-lucide="users" class="nav-icon"></i>
|
||||
<span class="nav-text">Channels</span>
|
||||
</a>
|
||||
|
||||
<div class="nav-section">파이프라인</div>
|
||||
<a th:href="@{/collection}" class="nav-item" th:classappend="${currentPage == 'collection'} ? 'active'">
|
||||
<i data-lucide="library" class="nav-icon"></i><span class="nav-text">수집함</span>
|
||||
</li>
|
||||
<li>
|
||||
<a th:href="@{/videos}" class="nav-item" th:classappend="${currentPage == 'videos'} ? 'active'">
|
||||
<i data-lucide="video" class="nav-icon"></i>
|
||||
<span class="nav-text">Videos</span>
|
||||
</a>
|
||||
<a th:href="@{/board}" class="nav-item" th:classappend="${currentPage == 'board'} ? 'active'">
|
||||
<i data-lucide="kanban-square" class="nav-icon"></i><span class="nav-text">칸반 보드</span>
|
||||
</li>
|
||||
<li>
|
||||
<a th:href="@{/collection}" class="nav-item"
|
||||
th:classappend="${currentPage == 'collection'} ? 'active'">
|
||||
<i data-lucide="library" class="nav-icon"></i>
|
||||
<span class="nav-text">수집함</span>
|
||||
</a>
|
||||
<a th:href="@{/publish}" class="nav-item" th:classappend="${currentPage == 'publish'} ? 'active'">
|
||||
<i data-lucide="send" class="nav-icon"></i><span class="nav-text">발행 큐</span>
|
||||
</li>
|
||||
<li>
|
||||
<a th:href="@{/discover}" class="nav-item"
|
||||
th:classappend="${currentPage == 'discover'} ? 'active'">
|
||||
<i data-lucide="radar" class="nav-icon"></i>
|
||||
<span class="nav-text">발굴</span>
|
||||
</a>
|
||||
|
||||
<div class="nav-section">제작</div>
|
||||
<a th:href="@{/production}" class="nav-item" th:classappend="${currentPage == 'production'} ? 'active'">
|
||||
<i data-lucide="clapperboard" class="nav-icon"></i><span class="nav-text">프로덕션</span>
|
||||
</li>
|
||||
<li>
|
||||
<a th:href="@{/board}" class="nav-item"
|
||||
th:classappend="${currentPage == 'board'} ? 'active'">
|
||||
<i data-lucide="kanban-square" class="nav-icon"></i>
|
||||
<span class="nav-text">보드</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a th:href="@{/publish}" class="nav-item"
|
||||
th:classappend="${currentPage == 'publish'} ? 'active'">
|
||||
<i data-lucide="send" class="nav-icon"></i>
|
||||
<span class="nav-text">발행</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a th:href="@{/production}" class="nav-item"
|
||||
th:classappend="${currentPage == 'production'} ? 'active'">
|
||||
<i data-lucide="clapperboard" class="nav-icon"></i>
|
||||
<span class="nav-text">Production</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="nav-item">
|
||||
<i data-lucide="settings" class="nav-icon"></i>
|
||||
<span class="nav-text">Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="user-profile-container">
|
||||
<button class="theme-toggle" onclick="toggleTheme()" type="button" title="테마 전환">
|
||||
<span class="moon"><i data-lucide="moon" style="width:15px;height:15px;"></i></span>
|
||||
<span class="sun"><i data-lucide="sun" style="width:15px;height:15px;"></i></span>
|
||||
<span class="nav-text">테마 전환</span>
|
||||
</button>
|
||||
<div class="user-profile-content">
|
||||
<div style="width:30px;height:30px;border-radius:50%;background:var(--surface-2);border:1px solid var(--border);flex-shrink:0;"></div>
|
||||
<div class="user-info" style="white-space:nowrap;">
|
||||
<div style="font-size:12.5px;font-weight:600;color:var(--text);">hehiho</div>
|
||||
<div style="font-size:11px;color:var(--text-3);">개인 작업공간</div>
|
||||
<div style="
|
||||
width: 36px; height: 36px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #475569 0%, #0f172a 100%);
|
||||
border: 2px solid rgba(255,255,255,0.1);
|
||||
flex-shrink: 0;
|
||||
"></div>
|
||||
<div class="user-info">
|
||||
<div class="text-sm font-bold" style="color: var(--text-primary); white-space: nowrap;">Admin User
|
||||
</div>
|
||||
<div class="text-sm" style="color: var(--text-muted); font-size: 0.75rem; white-space: nowrap;">Pro
|
||||
Plan</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-full btn-ghost sign-out-btn"
|
||||
style="justify-content: center; padding: 0.5rem; gap: 0.5rem; border-radius: 4px; background: rgba(0,0,0,0.2);">
|
||||
<i data-lucide="log-out" style="width: 14px; height: 14px;"></i> <span>Sign Out</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</body>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<div class="card p-6 mb-6">
|
||||
<div class="text-sm font-bold text-muted mb-4">Tracking <span th:text="${channels != null ? #lists.size(channels) : 0}">0</span> Channels</div>
|
||||
<div class="flex flex-wrap gap-4" th:if="${channels != null}">
|
||||
<div th:each="channel : ${channels}" class="flex items-center gap-2 p-2 rounded-lg bg-[var(--surface-2)] border border-[var(--glass-border)]">
|
||||
<div th:each="channel : ${channels}" class="flex items-center gap-2 p-2 rounded-lg bg-[rgba(255,255,255,0.02)] border border-[var(--glass-border)]">
|
||||
<img th:src="${channel.thumbnailUrl}" alt="Thumbnail"
|
||||
style="width: 32px; height: 32px; border-radius: 50%; object-fit: cover;" th:if="${channel.thumbnailUrl != null}">
|
||||
<span class="text-sm font-medium" th:text="${channel.title}">Channel Name</span>
|
||||
@ -36,20 +36,20 @@
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; flex: 1; min-width: 140px;">
|
||||
<label class="text-sm font-bold text-muted">Period</label>
|
||||
<select id="periodDays" class="w-full"
|
||||
style="padding: 8px; height: 42px; background: var(--surface-2); border: 1px solid var(--glass-border); border-radius: var(--radius-md); color: var(--text); outline: none;">
|
||||
<option value="1" style="background: var(--surface); color: var(--text);">Within 1 Day</option>
|
||||
<option value="7" style="background: var(--surface); color: var(--text);" selected>Within 7 Days</option>
|
||||
<option value="10" style="background: var(--surface); color: var(--text);">Within 10 Days</option>
|
||||
<option value="15" style="background: var(--surface); color: var(--text);">Within 15 Days</option>
|
||||
<option value="30" style="background: var(--surface); color: var(--text);">Within 30 Days</option>
|
||||
<option value="0" style="background: var(--surface); color: var(--text);">전부 (All)</option>
|
||||
style="padding: 8px; height: 42px; background: rgba(255,255,255,0.05); border: 1px solid var(--glass-border); border-radius: var(--radius-md); color: white; outline: none;">
|
||||
<option value="1" style="background: #1e1e2d; color: white;">Within 1 Day</option>
|
||||
<option value="7" style="background: #1e1e2d; color: white;" selected>Within 7 Days</option>
|
||||
<option value="10" style="background: #1e1e2d; color: white;">Within 10 Days</option>
|
||||
<option value="15" style="background: #1e1e2d; color: white;">Within 15 Days</option>
|
||||
<option value="30" style="background: #1e1e2d; color: white;">Within 30 Days</option>
|
||||
<option value="0" style="background: #1e1e2d; color: white;">전부 (All)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Format -->
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; flex: 1; min-width: 160px;">
|
||||
<label class="text-sm font-bold text-muted">Format</label>
|
||||
<div class="flex gap-4 w-full" style="padding: 8px; background: var(--surface-2); border: 1px solid var(--glass-border); border-radius: var(--radius-md); justify-content: center; height: 42px; align-items: center;">
|
||||
<div class="flex gap-4 w-full" style="padding: 8px; background: rgba(255,255,255,0.05); border: 1px solid var(--glass-border); border-radius: var(--radius-md); justify-content: center; height: 42px; align-items: center;">
|
||||
<label class="flex items-center gap-1 cursor-pointer hover:text-white">
|
||||
<input type="radio" name="format" value="SHORTS" checked> Shorts
|
||||
</label>
|
||||
@ -63,10 +63,10 @@
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; flex: 1; min-width: 120px;">
|
||||
<label class="text-sm font-bold text-muted">Load Size</label>
|
||||
<select id="pageSize" class="w-full"
|
||||
style="padding: 8px; height: 42px; background: var(--surface-2); border: 1px solid var(--glass-border); border-radius: var(--radius-md); color: var(--text); outline: none;">
|
||||
<option value="20" style="background: var(--surface); color: var(--text);">20 items</option>
|
||||
<option value="50" style="background: var(--surface); color: var(--text);" selected>50 items</option>
|
||||
<option value="100" style="background: var(--surface); color: var(--text);">100 items</option>
|
||||
style="padding: 8px; height: 42px; background: rgba(255,255,255,0.05); border: 1px solid var(--glass-border); border-radius: var(--radius-md); color: white; outline: none;">
|
||||
<option value="20" style="background: #1e1e2d; color: white;">20 items</option>
|
||||
<option value="50" style="background: #1e1e2d; color: white;" selected>50 items</option>
|
||||
<option value="100" style="background: #1e1e2d; color: white;">100 items</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -95,7 +95,7 @@
|
||||
<!-- Video List Table -->
|
||||
<div class="card p-0" style="overflow-x: auto;">
|
||||
<table class="w-full" style="border-collapse: collapse; text-align: left;">
|
||||
<thead style="background: var(--surface-2); border-bottom: 1px solid var(--glass-border);">
|
||||
<thead style="background: rgba(255,255,255,0.02); border-bottom: 1px solid var(--glass-border);">
|
||||
<tr>
|
||||
<th class="p-4 text-sm font-bold text-muted"><input type="checkbox" id="selectAll" onclick="toggleSelectAll(this.checked)" title="전체 선택"></th>
|
||||
<th class="p-4 text-sm font-bold text-muted">Thumbnail</th>
|
||||
@ -130,7 +130,7 @@
|
||||
|
||||
<!-- Video Player Modal -->
|
||||
<div id="videoModal" style="display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: 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: 1200px; background-color: var(--glass-bg, var(--surface)); border-radius: 12px; padding: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); border: 1px solid var(--glass-border);">
|
||||
<div style="position: relative; width: 95%; max-width: 1200px; background-color: var(--glass-bg, #1e1e2d); border-radius: 12px; padding: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); border: 1px solid var(--glass-border);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||||
<h3 class="text-lg font-bold truncate pr-4" id="modalTitle">Video Player</h3>
|
||||
<button onclick="closeVideoModal()" class="text-muted hover:text-white" style="font-size: 28px; line-height: 1; background: none; border: none; cursor: pointer;">×</button>
|
||||
@ -327,14 +327,14 @@
|
||||
'<div style="position: relative; display: inline-block; cursor: pointer;" data-videoid="' + video.videoId + '" data-videotitle="' + safeTitle + '" onclick="openVideoModal(this.dataset.videoid, this.dataset.videotitle)">' +
|
||||
'<img src="' + video.thumbnailUrl + '" alt="Thumb" style="width: 120px; height: 68px; object-fit: cover; border-radius: 6px; transition: transform 0.2s;" onmouseover="this.style.transform=\'scale(1.05)\'" onmouseout="this.style.transform=\'scale(1)\'">' +
|
||||
'<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.6); border-radius: 50%; padding: 8px; display: flex; align-items: center; justify-content: center;">' +
|
||||
'<i data-lucide="play" style="color: var(--text); width: 16px; height: 16px; fill: white;"></i>' +
|
||||
'<i data-lucide="play" style="color: white; width: 16px; height: 16px; fill: white;"></i>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</td>' +
|
||||
'<td class="p-4">' +
|
||||
'<div class="flex items-start justify-between gap-2" style="max-width: 300px;">' +
|
||||
'<div class="font-bold text-sm" style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; flex: 1;">' +
|
||||
'<a href="https://www.youtube.com/watch?v=' + video.videoId + '" target="_blank" class="hover:underline">' + video.title + '</a>' +
|
||||
'<a href="https://www.youtube.com/watch?v=' + video.videoId + '" target="_blank" class="hover:underline text-white">' + video.title + '</a>' +
|
||||
'</div>' +
|
||||
'<button onclick="copyToClipboard(\'https://www.youtube.com/watch?v=' + video.videoId + '\')" class="text-muted hover:text-white flex-shrink-0" title="URL 복사" style="background: none; border: none; cursor: pointer; padding: 2px;">' +
|
||||
'<i data-lucide="copy" style="width: 14px; height: 14px;"></i>' +
|
||||
|
||||
@ -4,12 +4,12 @@
|
||||
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="page-header">
|
||||
<header class="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h1>프로덕션</h1>
|
||||
<p class="sub">n8n 크롤 랭킹 스냅샷을 가져와 영상 제작에 활용합니다.</p>
|
||||
</div>
|
||||
<h1 class="text-xl font-bold mb-2">Production</h1>
|
||||
<p class="text-muted">Manage video production and rankings.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="card mb-4">
|
||||
<button id="fetchRankingsBtn" class="btn btn-primary">
|
||||
@ -18,21 +18,21 @@
|
||||
</div>
|
||||
|
||||
<div class="card p-0" style="overflow-x: auto;">
|
||||
<h3 class="p-4 text-lg font-bold" style="border-bottom:1px solid var(--border);">크롤 이력</h3>
|
||||
<h3 class="p-4 text-lg font-bold border-b border-[var(--glass-border)]">Crawl History</h3>
|
||||
<table class="w-full" style="border-collapse: collapse; text-align: left;">
|
||||
<thead>
|
||||
<thead style="background: rgba(255,255,255,0.02); border-bottom: 1px solid var(--glass-border);">
|
||||
<tr>
|
||||
<th class="p-4">ID</th>
|
||||
<th class="p-4">크롤 일시</th>
|
||||
<th class="p-4">Top N</th>
|
||||
<th class="p-4">영상 수</th>
|
||||
<th class="p-4 text-sm font-bold text-muted">ID</th>
|
||||
<th class="p-4 text-sm font-bold text-muted">Crawled Date</th>
|
||||
<th class="p-4 text-sm font-bold text-muted">Top N</th>
|
||||
<th class="p-4 text-sm font-bold text-muted">Videos Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="history : ${historyList}"
|
||||
th:onclick="'location.href=\'/production/' + ${history.id} + '\''"
|
||||
style="border-bottom: 1px solid var(--glass-border); cursor: pointer; transition: background 0.2s;"
|
||||
onmouseover="this.style.background='var(--surface-2)'"
|
||||
onmouseover="this.style.background='rgba(255,255,255,0.03)'"
|
||||
onmouseout="this.style.background='transparent'">
|
||||
<td class="p-4 text-sm" th:text="${history.id}">1</td>
|
||||
<td class="p-4 text-sm text-muted"
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
<button id="thumbnailToggle" onclick="toggleThumbnails()"
|
||||
style="position: relative; width: 48px; height: 26px; background-color: #374151; border-radius: 13px; border: none; cursor: pointer; transition: background-color 0.3s;">
|
||||
<span id="toggleKnob"
|
||||
style="position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; background-color: var(--text); border-radius: 50%; transition: transform 0.3s;"></span>
|
||||
style="position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; background-color: white; border-radius: 50%; transition: transform 0.3s;"></span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
@ -32,7 +32,7 @@
|
||||
<!-- Video List Table -->
|
||||
<div class="card p-0" style="overflow-x: auto;">
|
||||
<table class="w-full" style="border-collapse: collapse; text-align: left;">
|
||||
<thead style="background: var(--surface-2); border-bottom: 1px solid var(--glass-border);">
|
||||
<thead style="background: rgba(255,255,255,0.02); border-bottom: 1px solid var(--glass-border);">
|
||||
<tr>
|
||||
<th class="p-4 text-sm font-bold text-muted sortable" data-sort="rank"
|
||||
onclick="sortTable('rank')" style="cursor: pointer;">
|
||||
@ -66,7 +66,7 @@
|
||||
<tbody>
|
||||
<tr th:each="video : ${history.videos}"
|
||||
style="border-bottom: 1px solid var(--glass-border); transition: background 0.2s;"
|
||||
onmouseover="this.style.background='var(--surface-2)'"
|
||||
onmouseover="this.style.background='rgba(255,255,255,0.03)'"
|
||||
onmouseout="this.style.background='transparent'">
|
||||
|
||||
<td class="p-4" data-label="Rank" th:data-value="${video.rank}">
|
||||
@ -207,13 +207,13 @@
|
||||
|
||||
<!-- Popup Container (Centered Box) -->
|
||||
<div
|
||||
style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 900px; max-height: 85vh; background-color: #1a1b26; border-radius: 12px; box-shadow: 0 25px 50px rgba(0,0,0,0.5); display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--hover-strong);">
|
||||
style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 900px; max-height: 85vh; background-color: #1a1b26; border-radius: 12px; box-shadow: 0 25px 50px rgba(0,0,0,0.5); display: flex; flex-direction: column; overflow: hidden; border: 1px solid rgba(255,255,255,0.1);">
|
||||
|
||||
<!-- Header -->
|
||||
<div
|
||||
style="display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; background-color: #1a1b26; border-bottom: 1px solid var(--hover-strong);">
|
||||
style="display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; background-color: #1a1b26; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
||||
<h3
|
||||
style="font-size: 18px; font-weight: bold; color: var(--text); margin: 0; display: flex; align-items: center; gap: 8px;">
|
||||
style="font-size: 18px; font-weight: bold; color: white; margin: 0; display: flex; align-items: center; gap: 8px;">
|
||||
<i data-lucide="file-text" style="color: #6366f1; width: 20px; height: 20px;"></i>
|
||||
Transcript
|
||||
</h3>
|
||||
@ -239,13 +239,13 @@
|
||||
|
||||
<!-- Footer -->
|
||||
<div
|
||||
style="padding: 16px 24px; background-color: #1a1b26; border-top: 1px solid var(--hover-strong); display: flex; justify-content: flex-end; gap: 12px;">
|
||||
style="padding: 16px 24px; background-color: #1a1b26; border-top: 1px solid rgba(255,255,255,0.1); display: flex; justify-content: flex-end; gap: 12px;">
|
||||
<button onclick="copyScript()"
|
||||
style="padding: 8px 16px; background-color: #374151; color: var(--text); border: none; border-radius: 8px; cursor: pointer; font-weight: 500; display: flex; align-items: center; gap: 6px;">
|
||||
style="padding: 8px 16px; background-color: #374151; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; display: flex; align-items: center; gap: 6px;">
|
||||
<i data-lucide="copy" style="width: 16px; height: 16px;"></i> Copy All
|
||||
</button>
|
||||
<button onclick="closeScriptModal()"
|
||||
style="padding: 8px 24px; background: linear-gradient(135deg, #6366f1, #8b5cf6); color: var(--text); border: none; border-radius: 8px; cursor: pointer; font-weight: 500;">Close</button>
|
||||
style="padding: 8px 24px; background: linear-gradient(135deg, #6366f1, #8b5cf6); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500;">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -261,13 +261,13 @@
|
||||
|
||||
<!-- Popup Container (Centered Box) -->
|
||||
<div
|
||||
style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 1200px; max-height: 85vh; background-color: #1a1b26; border-radius: 12px; box-shadow: 0 25px 50px rgba(0,0,0,0.5); display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--hover-strong);">
|
||||
style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 1200px; max-height: 85vh; background-color: #1a1b26; border-radius: 12px; box-shadow: 0 25px 50px rgba(0,0,0,0.5); display: flex; flex-direction: column; overflow: hidden; border: 1px solid rgba(255,255,255,0.1);">
|
||||
|
||||
<!-- Header -->
|
||||
<div
|
||||
style="display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; background-color: #1a1b26; border-bottom: 1px solid var(--hover-strong);">
|
||||
style="display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; background-color: #1a1b26; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
||||
<h3
|
||||
style="font-size: 18px; font-weight: bold; color: var(--text); margin: 0; display: flex; align-items: center; gap: 8px;">
|
||||
style="font-size: 18px; font-weight: bold; color: white; margin: 0; display: flex; align-items: center; gap: 8px;">
|
||||
<i data-lucide="book-open" style="color: #34d399; width: 20px; height: 20px;"></i>
|
||||
Script Summaries
|
||||
</h3>
|
||||
@ -281,7 +281,7 @@
|
||||
<div style="display: flex; flex: 1; overflow: hidden;">
|
||||
<!-- Left Column: old_script_summary -->
|
||||
<div
|
||||
style="flex: 1; padding: 24px; overflow-y: auto; border-right: 1px solid var(--hover-strong); background-color: rgba(22,22,30,0.5);">
|
||||
style="flex: 1; padding: 24px; overflow-y: auto; border-right: 1px solid rgba(255,255,255,0.1); background-color: rgba(22,22,30,0.5);">
|
||||
<h4 style="font-size: 14px; font-weight: bold; color: #facc15; margin: 0 0 16px 0;">
|
||||
old_script_summary</h4>
|
||||
<div id="oldSummaryContent"
|
||||
@ -318,9 +318,9 @@
|
||||
|
||||
<!-- Footer -->
|
||||
<div
|
||||
style="padding: 16px 24px; background-color: #1a1b26; border-top: 1px solid var(--hover-strong); display: flex; justify-content: flex-end;">
|
||||
style="padding: 16px 24px; background-color: #1a1b26; border-top: 1px solid rgba(255,255,255,0.1); display: flex; justify-content: flex-end;">
|
||||
<button onclick="closeSummaryModal()" class="btn btn-primary"
|
||||
style="padding: 8px 24px; background: linear-gradient(135deg, #6366f1, #8b5cf6); color: var(--text); border: none; border-radius: 8px; cursor: pointer; font-weight: 500;">Close</button>
|
||||
style="padding: 8px 24px; background: linear-gradient(135deg, #6366f1, #8b5cf6); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500;">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -337,13 +337,13 @@
|
||||
|
||||
<!-- Popup Container (Centered Box) -->
|
||||
<div
|
||||
style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 95%; max-width: 1600px; height: 90vh; background-color: #1a1b26; border-radius: 12px; box-shadow: 0 25px 50px rgba(0,0,0,0.5); display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--hover-strong);">
|
||||
style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 95%; max-width: 1600px; height: 90vh; background-color: #1a1b26; border-radius: 12px; box-shadow: 0 25px 50px rgba(0,0,0,0.5); display: flex; flex-direction: column; overflow: hidden; border: 1px solid rgba(255,255,255,0.1);">
|
||||
|
||||
<!-- Header -->
|
||||
<div
|
||||
style="display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; background-color: #1a1b26; border-bottom: 1px solid var(--hover-strong);">
|
||||
style="display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; background-color: #1a1b26; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
||||
<h3
|
||||
style="font-size: 18px; font-weight: bold; color: var(--text); margin: 0; display: flex; align-items: center; gap: 8px;">
|
||||
style="font-size: 18px; font-weight: bold; color: white; margin: 0; display: flex; align-items: center; gap: 8px;">
|
||||
<i data-lucide="file-check" style="color: #22d3ee; width: 20px; height: 20px;"></i>
|
||||
Final Script
|
||||
</h3>
|
||||
@ -375,17 +375,17 @@
|
||||
|
||||
<!-- Footer -->
|
||||
<div
|
||||
style="padding: 16px 24px; background-color: #1a1b26; border-top: 1px solid var(--hover-strong); display: flex; justify-content: flex-end; gap: 12px;">
|
||||
style="padding: 16px 24px; background-color: #1a1b26; border-top: 1px solid rgba(255,255,255,0.1); display: flex; justify-content: flex-end; gap: 12px;">
|
||||
<button onclick="saveFinalScript()"
|
||||
style="padding: 8px 16px; background-color: #059669; color: var(--text); border: none; border-radius: 8px; cursor: pointer; font-weight: 500; display: flex; align-items: center; gap: 6px;">
|
||||
style="padding: 8px 16px; background-color: #059669; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; display: flex; align-items: center; gap: 6px;">
|
||||
<i data-lucide="save" style="width: 16px; height: 16px;"></i> Save
|
||||
</button>
|
||||
<button onclick="copyFinalScript()"
|
||||
style="padding: 8px 16px; background-color: #374151; color: var(--text); border: none; border-radius: 8px; cursor: pointer; font-weight: 500; display: flex; align-items: center; gap: 6px;">
|
||||
style="padding: 8px 16px; background-color: #374151; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; display: flex; align-items: center; gap: 6px;">
|
||||
<i data-lucide="copy" style="width: 16px; height: 16px;"></i> Copy All
|
||||
</button>
|
||||
<button onclick="closeFinalScriptModal()"
|
||||
style="padding: 8px 24px; background: linear-gradient(135deg, #6366f1, #8b5cf6); color: var(--text); border: none; border-radius: 8px; cursor: pointer; font-weight: 500;">Close</button>
|
||||
style="padding: 8px 24px; background: linear-gradient(135deg, #6366f1, #8b5cf6); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500;">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -8,55 +8,29 @@
|
||||
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>발행 큐</h1>
|
||||
<p class="sub">재가공한 영상의 발행 패키지를 단계별로 관리합니다. (실제 업로드는 수동 — 여기서 준비·추적)</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-secondary" onclick="openHelp()"><i data-lucide="help-circle" style="width:15px;"></i> 사용법</button>
|
||||
</div>
|
||||
</div>
|
||||
<header class="mb-4">
|
||||
<h1 class="text-2xl font-bold mb-1">발행 큐</h1>
|
||||
<p class="text-muted">재가공한 영상의 발행 패키지를 단계별로 관리합니다. (실제 업로드는 수동 — 여기서 준비·추적)</p>
|
||||
</header>
|
||||
|
||||
<!-- 사용법 모달 -->
|
||||
<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="list-checks"></i></div>
|
||||
<div><div class="hi-t">상태 탭</div><div class="hi-d"><b>작성중 → 발행대기 → 발행완료</b>로 발행 준비 단계를 거릅니다. 탭으로 상태별로 필터합니다.</div></div>
|
||||
</div>
|
||||
<div class="help-item">
|
||||
<div class="hi-ic"><i data-lucide="pencil"></i></div>
|
||||
<div><div class="hi-t">발행안 만들기</div><div class="hi-d">발행 패키지는 <b>재가공 화면 하단</b>에서 제목·설명·해시태그·플랫폼·예약을 저장하면 생성됩니다. 표의 <b>✏️</b>로 다시 편집합니다.</div></div>
|
||||
</div>
|
||||
<div class="help-item">
|
||||
<div class="hi-ic"><i data-lucide="upload-cloud"></i></div>
|
||||
<div><div class="hi-t">업로드 & 기록</div><div class="hi-d">실제 업로드는 <b>플랫폼에서 수동</b>으로 합니다. 업로드 후 URL을 기록하면 ‘발행완료’로 추적됩니다. <b>📋</b>로 설명+해시태그를 복사하세요.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mb-4" id="tabs" style="flex-wrap:wrap; align-items:center;">
|
||||
<button class="btn btn-secondary tab active" data-status="" onclick="setTab(this,'')">전체</button>
|
||||
<button class="btn btn-secondary tab" data-status="DRAFT" onclick="setTab(this,'DRAFT')">작성중</button>
|
||||
<button class="btn btn-secondary tab" data-status="READY" onclick="setTab(this,'READY')">발행대기</button>
|
||||
<button class="btn btn-secondary tab" data-status="PUBLISHED" onclick="setTab(this,'PUBLISHED')">발행완료</button>
|
||||
<span id="cnt" class="badge badge-muted" style="margin-left:auto;"></span>
|
||||
<div class="flex gap-2 mb-4" id="tabs">
|
||||
<button class="btn btn-secondary px-4 py-2 tab active" data-status="" onclick="setTab(this,'')">전체</button>
|
||||
<button class="btn btn-secondary px-4 py-2 tab" data-status="DRAFT" onclick="setTab(this,'DRAFT')">작성중</button>
|
||||
<button class="btn btn-secondary px-4 py-2 tab" data-status="READY" onclick="setTab(this,'READY')">발행대기</button>
|
||||
<button class="btn btn-secondary px-4 py-2 tab" data-status="PUBLISHED" onclick="setTab(this,'PUBLISHED')">발행완료</button>
|
||||
<span id="cnt" class="text-sm text-muted" style="margin-left:auto; align-self:center;"></span>
|
||||
</div>
|
||||
|
||||
<div class="card p-0" style="overflow-x:auto;">
|
||||
<table class="w-full" style="border-collapse:collapse; text-align:left;">
|
||||
<thead>
|
||||
<thead style="background:rgba(255,255,255,0.02); border-bottom:1px solid var(--glass-border);">
|
||||
<tr>
|
||||
<th>상태</th>
|
||||
<th>플랫폼</th>
|
||||
<th>제목</th>
|
||||
<th>예약</th>
|
||||
<th>발행 URL</th>
|
||||
<th>관리</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">상태</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">플랫폼</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">제목</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">예약</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">발행 URL</th>
|
||||
<th class="p-3 text-sm font-bold text-muted">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="body">
|
||||
@ -66,8 +40,7 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tab { padding:0.5rem 1rem; }
|
||||
.tab.active { background:var(--accent-soft); border-color:var(--accent); color:var(--accent); }
|
||||
.tab.active { border-color:#7C3AED; color:#fff; }
|
||||
</style>
|
||||
|
||||
<script th:inline="javascript">
|
||||
@ -101,7 +74,7 @@
|
||||
body.innerHTML = data.map(p=>{
|
||||
const st = ST[p.status] || {t:p.status,cls:'badge-muted'};
|
||||
const sched = p.scheduledAt ? String(p.scheduledAt).substring(0,16).replace('T',' ') : '-';
|
||||
const urlCell = p.publishedUrl ? `<a href="${esc(p.publishedUrl)}" target="_blank" class="hover:underline" style="color:var(--accent);">열기</a>` : '-';
|
||||
const urlCell = p.publishedUrl ? `<a href="${esc(p.publishedUrl)}" target="_blank" class="hover:underline" style="color:#60a5fa;">열기</a>` : '-';
|
||||
return `<tr style="border-bottom:1px solid var(--glass-border);">
|
||||
<td class="p-3"><span class="badge ${st.cls}">${st.t}</span></td>
|
||||
<td class="p-3 text-sm">${esc(p.platform||'')}</td>
|
||||
@ -123,10 +96,6 @@
|
||||
navigator.clipboard.writeText(text).then(()=>alert('설명+해시태그가 복사되었습니다.'));
|
||||
}
|
||||
|
||||
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(); });
|
||||
|
||||
load();
|
||||
/*]]>*/
|
||||
</script>
|
||||
|
||||
@ -8,47 +8,13 @@
|
||||
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<header class="mb-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<a th:href="@{/collection}" class="text-muted hover:text-white"><i data-lucide="arrow-left" style="width:20px;"></i></a>
|
||||
<h1>재가공 작업공간</h1>
|
||||
</div>
|
||||
<p class="sub" id="metaLine">로딩 중...</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-secondary" onclick="openHelp()"><i data-lucide="help-circle" style="width:15px;"></i> 사용법</button>
|
||||
</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="upload"></i></div>
|
||||
<div><div class="hi-t">영상 업로드 · 전사</div><div class="hi-d">영상 파일을 올리면 <b>Whisper</b>가 말소리를 <b>시간 싱크 자막</b>으로 추출합니다. 언어는 자동감지(틀리면 옆에서 ko/en/zh/ja 선택). ‘URL자막’은 YouTube 자막을 평문으로(시간 없음) 가져옵니다.</div></div>
|
||||
</div>
|
||||
<div class="help-item">
|
||||
<div class="hi-ic"><i data-lucide="list-video"></i></div>
|
||||
<div><div class="hi-t">세그먼트 리스트</div><div class="hi-d"><b>[00:12] 자막</b> 줄을 클릭하면 영상이 그 지점으로 이동하고, 재생 중 현재 자막이 하이라이트됩니다.</div></div>
|
||||
</div>
|
||||
<div class="help-item">
|
||||
<div class="hi-ic"><i data-lucide="scissors"></i></div>
|
||||
<div><div class="hi-t">말 없는 구간 제거 · 배속</div><div class="hi-d"><b>말 없는 구간 제거</b>를 켜면 아무도 말 안 하는 부분을 잘라 영상을 타이트하게 만듭니다. <b>배속</b>도 적용(자막 타임스탬프 자동 보정). ‘미리보기’로 결과 길이를 확인하세요.</div></div>
|
||||
</div>
|
||||
<div class="help-item">
|
||||
<div class="hi-ic"><i data-lucide="file-down"></i></div>
|
||||
<div><div class="hi-t">내보내기 → CapCut</div><div class="hi-d"><b>SRT 자막</b>과 <b>영상</b>을 내보내 CapCut에 <b>둘 다 import</b>하면 자막·컷·배속이 맞춰집니다.</div></div>
|
||||
</div>
|
||||
<div class="help-item">
|
||||
<div class="hi-ic"><i data-lucide="send"></i></div>
|
||||
<div><div class="hi-t">재작성 · 발행</div><div class="hi-d">‘재작성(내 버전)’에 각색해 저장하면 상태가 <b>TARGET</b>으로 바뀝니다. 하단 <b>발행 준비</b>에서 제목·해시태그를 저장하면 발행 큐로 들어갑니다.</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold">재가공 작업공간</h1>
|
||||
</div>
|
||||
<p class="text-muted" id="metaLine">로딩 중...</p>
|
||||
</header>
|
||||
|
||||
<div style="display:grid; grid-template-columns: 360px 1fr; gap:1.5rem; align-items:start;">
|
||||
<!-- 좌: 영상 + 정보 -->
|
||||
@ -94,32 +60,32 @@
|
||||
<div id="transcribeStatus" class="text-sm mb-2" style="display:none;"></div>
|
||||
|
||||
<!-- 영상 싱크 세그먼트 리스트 (CapCut형) -->
|
||||
<div id="segmentList" style="display:none; max-height:340px; overflow:auto; border:1px solid var(--glass-border); border-radius:var(--radius-md); background:var(--surface-2);"></div>
|
||||
<div id="segmentList" style="display:none; max-height:340px; overflow:auto; border:1px solid var(--glass-border); border-radius:var(--radius-md); background:rgba(255,255,255,0.02);"></div>
|
||||
|
||||
<!-- 평문 폴백 (URL 자막 추출 시) -->
|
||||
<textarea id="transcript" readonly placeholder="‘영상 업로드·전사’로 영상에서 싱크된 자막을 추출하거나, ‘URL자막’으로 YouTube 자막을 가져오세요."
|
||||
style="width:100%; min-height:140px; resize:vertical; padding:12px; background:var(--surface-2); border:1px solid var(--glass-border); border-radius:var(--radius-md); color:var(--text-2); outline:none; font-size:0.9rem; line-height:1.6;"></textarea>
|
||||
style="width:100%; min-height:140px; resize:vertical; padding:12px; background:rgba(255,255,255,0.03); border:1px solid var(--glass-border); border-radius:var(--radius-md); color:#cbd5e1; outline:none; font-size:0.9rem; line-height:1.6;"></textarea>
|
||||
|
||||
<!-- 내보내기 (배속 + 말 없는 구간 제거 + SRT/영상) -->
|
||||
<div id="exportBar" style="display:none; margin-top:14px; padding-top:12px; border-top:1px solid var(--glass-border);">
|
||||
<div class="flex items-center gap-2 mb-3" style="flex-wrap:wrap;">
|
||||
<label class="text-sm text-muted">배속</label>
|
||||
<input id="srtSpeed" type="number" value="1.0" step="0.1" min="0.1" max="2.0"
|
||||
style="width:64px; padding:7px; background:var(--surface-2); border:1px solid var(--glass-border); border-radius:var(--radius-md); color:var(--text); outline:none; font-size:0.9rem;">
|
||||
style="width:64px; padding:7px; background:rgba(255,255,255,0.05); border:1px solid var(--glass-border); border-radius:var(--radius-md); color:white; outline:none; font-size:0.9rem;">
|
||||
<span class="text-xs text-muted">배속 적용 시 타임스탬프 자동 보정 (0.5~2.0)</span>
|
||||
</div>
|
||||
|
||||
<div style="background:var(--surface-2); border:1px solid var(--glass-border); border-radius:var(--radius-md); padding:10px 12px;">
|
||||
<div style="background:rgba(255,255,255,0.03); border:1px solid var(--glass-border); border-radius:var(--radius-md); padding:10px 12px;">
|
||||
<div class="flex items-center gap-2 mb-2" style="flex-wrap:wrap;">
|
||||
<label class="flex items-center gap-1 text-sm" style="cursor:pointer;">
|
||||
<input type="checkbox" id="trimOn" onchange="onTrimToggle()"> 말 없는 구간 제거
|
||||
</label>
|
||||
<span class="text-xs text-muted">앞뒤 여백</span>
|
||||
<input id="trimPad" type="number" value="0.15" step="0.05" min="0" onchange="if(trimApplied)previewTrim()"
|
||||
style="width:60px; padding:5px; background:var(--surface-2); border:1px solid var(--glass-border); border-radius:6px; color:var(--text); font-size:0.85rem;">
|
||||
style="width:60px; padding:5px; background:rgba(255,255,255,0.05); border:1px solid var(--glass-border); border-radius:6px; color:white; font-size:0.85rem;">
|
||||
<span class="text-xs text-muted">최소 간격</span>
|
||||
<input id="trimGap" type="number" value="0.3" step="0.1" min="0" onchange="if(trimApplied)previewTrim()"
|
||||
style="width:60px; padding:5px; background:var(--surface-2); border:1px solid var(--glass-border); border-radius:6px; color:var(--text); font-size:0.85rem;">
|
||||
style="width:60px; padding:5px; background:rgba(255,255,255,0.05); border:1px solid var(--glass-border); border-radius:6px; color:white; font-size:0.85rem;">
|
||||
</div>
|
||||
<div id="trimInfo" class="text-xs text-muted"></div>
|
||||
</div>
|
||||
@ -138,11 +104,11 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.seg-row { display:flex; gap:10px; padding:7px 12px; cursor:pointer; border-bottom:1px solid var(--surface-2); font-size:0.9rem; line-height:1.5; }
|
||||
.seg-row:hover { background:var(--surface-2); }
|
||||
.seg-row { display:flex; gap:10px; padding:7px 12px; cursor:pointer; border-bottom:1px solid rgba(255,255,255,0.05); font-size:0.9rem; line-height:1.5; }
|
||||
.seg-row:hover { background:rgba(255,255,255,0.05); }
|
||||
.seg-row.active { background:rgba(96,165,250,0.18); }
|
||||
.seg-time { color:#60a5fa; font-variant-numeric:tabular-nums; flex-shrink:0; font-size:0.82rem; padding-top:1px; }
|
||||
.seg-text { color:var(--text-2); }
|
||||
.seg-text { color:#e2e8f0; }
|
||||
</style>
|
||||
|
||||
<!-- 재작성 에디터 -->
|
||||
@ -154,7 +120,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<textarea id="reworkText" placeholder="원본을 참고해 각색/수정한 스크립트를 여기에 작성하세요. 저장하면 상태가 TARGET(작업대상)으로 바뀝니다."
|
||||
style="width:100%; min-height:280px; resize:vertical; padding:12px; background:var(--surface-2); border:1px solid var(--glass-border); border-radius:var(--radius-md); color:var(--text); outline:none; font-size:0.95rem; line-height:1.7;"></textarea>
|
||||
style="width:100%; min-height:280px; resize:vertical; padding:12px; background:rgba(255,255,255,0.05); border:1px solid var(--glass-border); border-radius:var(--radius-md); color:white; outline:none; font-size:0.95rem; line-height:1.7;"></textarea>
|
||||
<div class="text-sm text-muted mt-2" id="saveInfo"></div>
|
||||
</div>
|
||||
|
||||
@ -215,8 +181,8 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pub-in { width:100%; padding:9px; background:var(--surface-2); border:1px solid var(--glass-border); border-radius:var(--radius-md); color:var(--text); outline:none; font-size:0.9rem; }
|
||||
.pub-in option { background:var(--surface); color:var(--text); }
|
||||
.pub-in { width:100%; padding:9px; background:rgba(255,255,255,0.05); border:1px solid var(--glass-border); border-radius:var(--radius-md); color:white; outline:none; font-size:0.9rem; }
|
||||
.pub-in option { background:#1e1e2d; color:white; }
|
||||
</style>
|
||||
|
||||
<script th:inline="javascript">
|
||||
@ -337,7 +303,7 @@
|
||||
document.getElementById('trimOn').checked = true;
|
||||
const sp = curSpeed();
|
||||
const kept = (p.keptDuration||0) / sp;
|
||||
info.style.color = 'var(--text-2)';
|
||||
info.style.color = '#cbd5e1';
|
||||
info.innerHTML = '제거 ' + (p.removedCount||0) + '구간 · 결과 길이 ≈ <b>' + kept.toFixed(1) + 's</b>'
|
||||
+ (sp!==1 ? (' (배속 ' + sp + 'x 포함)') : '')
|
||||
+ ' · SRT/영상이 이 타임라인으로 출력됩니다';
|
||||
@ -540,10 +506,6 @@
|
||||
} catch(e){ alert('발행 완료 처리 실패: ' + e.message); }
|
||||
}
|
||||
|
||||
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(); });
|
||||
|
||||
(async ()=>{ await load(); await loadPublish(document.getElementById('vTitle').textContent); })();
|
||||
/*]]>*/
|
||||
</script>
|
||||
|
||||
@ -18,14 +18,14 @@
|
||||
<label class="text-sm font-bold text-muted">Keyword</label>
|
||||
<input type="text" id="keyword" placeholder="Search keyword..."
|
||||
class="p-2 w-full"
|
||||
style="background: var(--surface-2); border: 1px solid var(--glass-border); border-radius: var(--radius-md); color: var(--text); outline: none; transition: border-color 0.2s;"
|
||||
style="background: rgba(255,255,255,0.05); border: 1px solid var(--glass-border); border-radius: var(--radius-md); color: white; outline: none; transition: border-color 0.2s;"
|
||||
onfocus="this.style.borderColor='#3b82f6'" onblur="this.style.borderColor='var(--glass-border)'">
|
||||
</div>
|
||||
|
||||
<!-- Country -->
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; flex: 1; min-width: 180px;">
|
||||
<label class="text-sm font-bold text-muted">Country</label>
|
||||
<div class="flex gap-4 w-full" style="padding: 8px; background: var(--surface-2); border: 1px solid var(--glass-border); border-radius: var(--radius-md); justify-content: center; height: 42px; align-items: center;">
|
||||
<div class="flex gap-4 w-full" style="padding: 8px; background: rgba(255,255,255,0.05); border: 1px solid var(--glass-border); border-radius: var(--radius-md); justify-content: center; height: 42px; align-items: center;">
|
||||
<label class="flex items-center gap-1 cursor-pointer hover:text-white">
|
||||
<input type="checkbox" name="region" value="JP" checked> JP
|
||||
</label>
|
||||
@ -42,20 +42,20 @@
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; flex: 1; min-width: 140px;">
|
||||
<label class="text-sm font-bold text-muted">Period</label>
|
||||
<select id="periodDays" class="w-full"
|
||||
style="padding: 8px; height: 42px; background: var(--surface-2); border: 1px solid var(--glass-border); border-radius: var(--radius-md); color: var(--text); outline: none;">
|
||||
<option value="1" style="background: var(--surface); color: var(--text);">Within 1 Day</option>
|
||||
<option value="7" style="background: var(--surface); color: var(--text);">Within 7 Days</option>
|
||||
<option value="10" style="background: var(--surface); color: var(--text);">Within 10 Days</option>
|
||||
<option value="15" style="background: var(--surface); color: var(--text);">Within 15 Days</option>
|
||||
<option value="30" style="background: var(--surface); color: var(--text);">Within 30 Days</option>
|
||||
<option value="0" style="background: var(--surface); color: var(--text);">전부 (All)</option>
|
||||
style="padding: 8px; height: 42px; background: rgba(255,255,255,0.05); border: 1px solid var(--glass-border); border-radius: var(--radius-md); color: white; outline: none;">
|
||||
<option value="1" style="background: #1e1e2d; color: white;">Within 1 Day</option>
|
||||
<option value="7" style="background: #1e1e2d; color: white;">Within 7 Days</option>
|
||||
<option value="10" style="background: #1e1e2d; color: white;">Within 10 Days</option>
|
||||
<option value="15" style="background: #1e1e2d; color: white;">Within 15 Days</option>
|
||||
<option value="30" style="background: #1e1e2d; color: white;">Within 30 Days</option>
|
||||
<option value="0" style="background: #1e1e2d; color: white;">전부 (All)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Format -->
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; flex: 1; min-width: 160px;">
|
||||
<label class="text-sm font-bold text-muted">Format</label>
|
||||
<div class="flex gap-4 w-full" style="padding: 8px; background: var(--surface-2); border: 1px solid var(--glass-border); border-radius: var(--radius-md); justify-content: center; height: 42px; align-items: center;">
|
||||
<div class="flex gap-4 w-full" style="padding: 8px; background: rgba(255,255,255,0.05); border: 1px solid var(--glass-border); border-radius: var(--radius-md); justify-content: center; height: 42px; align-items: center;">
|
||||
<label class="flex items-center gap-1 cursor-pointer hover:text-white">
|
||||
<input type="radio" name="format" value="SHORTS" checked> Shorts
|
||||
</label>
|
||||
@ -69,10 +69,10 @@
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; flex: 1; min-width: 120px;">
|
||||
<label class="text-sm font-bold text-muted">Load Size</label>
|
||||
<select id="pageSize" class="w-full"
|
||||
style="padding: 8px; height: 42px; background: var(--surface-2); border: 1px solid var(--glass-border); border-radius: var(--radius-md); color: var(--text); outline: none;">
|
||||
<option value="20" style="background: var(--surface); color: var(--text);">20 items</option>
|
||||
<option value="50" style="background: var(--surface); color: var(--text);" selected>50 items</option>
|
||||
<option value="100" style="background: var(--surface); color: var(--text);">100 items</option>
|
||||
style="padding: 8px; height: 42px; background: rgba(255,255,255,0.05); border: 1px solid var(--glass-border); border-radius: var(--radius-md); color: white; outline: none;">
|
||||
<option value="20" style="background: #1e1e2d; color: white;">20 items</option>
|
||||
<option value="50" style="background: #1e1e2d; color: white;" selected>50 items</option>
|
||||
<option value="100" style="background: #1e1e2d; color: white;">100 items</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -101,7 +101,7 @@
|
||||
<!-- Video List Table -->
|
||||
<div class="card p-0" style="overflow-x: auto;">
|
||||
<table class="w-full" style="border-collapse: collapse; text-align: left;">
|
||||
<thead style="background: var(--surface-2); border-bottom: 1px solid var(--glass-border);">
|
||||
<thead style="background: rgba(255,255,255,0.02); border-bottom: 1px solid var(--glass-border);">
|
||||
<tr>
|
||||
<th class="p-4 text-sm font-bold text-muted"><input type="checkbox" id="selectAll" onclick="toggleSelectAll(this.checked)" title="전체 선택"></th>
|
||||
<th class="p-4 text-sm font-bold text-muted">Thumbnail</th>
|
||||
@ -135,7 +135,7 @@
|
||||
|
||||
<!-- Video Player Modal -->
|
||||
<div id="videoModal" style="display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: 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: 1200px; background-color: var(--glass-bg, var(--surface)); border-radius: 12px; padding: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); border: 1px solid var(--glass-border);">
|
||||
<div style="position: relative; width: 95%; max-width: 1200px; background-color: var(--glass-bg, #1e1e2d); border-radius: 12px; padding: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); border: 1px solid var(--glass-border);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||||
<h3 class="text-lg font-bold truncate pr-4" id="modalTitle">Video Player</h3>
|
||||
<button onclick="closeVideoModal()" class="text-muted hover:text-white" style="font-size: 28px; line-height: 1; background: none; border: none; cursor: pointer;">×</button>
|
||||
@ -362,14 +362,14 @@
|
||||
'<div style="position: relative; display: inline-block; cursor: pointer;" data-videoid="' + video.videoId + '" data-videotitle="' + safeTitle + '" onclick="openVideoModal(this.dataset.videoid, this.dataset.videotitle)">' +
|
||||
'<img src="' + video.thumbnailUrl + '" alt="Thumb" style="width: 120px; height: 68px; object-fit: cover; border-radius: 6px; transition: transform 0.2s;" onmouseover="this.style.transform=\'scale(1.05)\'" onmouseout="this.style.transform=\'scale(1)\'">' +
|
||||
'<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.6); border-radius: 50%; padding: 8px; display: flex; align-items: center; justify-content: center;">' +
|
||||
'<i data-lucide="play" style="color: var(--text); width: 16px; height: 16px; fill: white;"></i>' +
|
||||
'<i data-lucide="play" style="color: white; width: 16px; height: 16px; fill: white;"></i>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</td>' +
|
||||
'<td class="p-4">' +
|
||||
'<div class="flex items-start justify-between gap-2" style="max-width: 300px;">' +
|
||||
'<div class="font-bold text-sm" style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; flex: 1;">' +
|
||||
'<a href="https://www.youtube.com/watch?v=' + video.videoId + '" target="_blank" class="hover:underline">' + video.title + '</a>' +
|
||||
'<a href="https://www.youtube.com/watch?v=' + video.videoId + '" target="_blank" class="hover:underline text-white">' + video.title + '</a>' +
|
||||
'</div>' +
|
||||
'<button onclick="copyToClipboard(\'https://www.youtube.com/watch?v=' + video.videoId + '\')" class="text-muted hover:text-white flex-shrink-0" title="URL 복사" style="background: none; border: none; cursor: pointer; padding: 2px; margin-top: 2px;">' +
|
||||
'<i data-lucide="copy" style="width: 14px; height: 14px;"></i>' +
|
||||
@ -379,7 +379,7 @@
|
||||
'<td class="p-4 text-sm text-muted">' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<a href="https://www.youtube.com/channel/' + video.channelId + '" target="_blank" class="hover:underline font-semibold" style="color: #e2e8f0;">' + video.channelTitle + '</a>' +
|
||||
'<button onclick="registerChannel(\'https://www.youtube.com/channel/' + video.channelId + '\')" class="text-xs" style="background: #3b82f6; color: var(--text); border: none; padding: 2px 6px; border-radius: 4px; cursor: pointer; transition: background 0.2s;" onmouseover="this.style.background=\'#2563eb\'" onmouseout="this.style.background=\'#3b82f6\'">등록</button>' +
|
||||
'<button onclick="registerChannel(\'https://www.youtube.com/channel/' + video.channelId + '\')" class="text-xs" style="background: #3b82f6; color: white; border: none; padding: 2px 6px; border-radius: 4px; cursor: pointer; transition: background 0.2s;" onmouseover="this.style.background=\'#2563eb\'" onmouseout="this.style.background=\'#3b82f6\'">등록</button>' +
|
||||
'</div>' +
|
||||
tagsHtml +
|
||||
'</td>' +
|
||||
|
||||
Loading…
Reference in New Issue
Block a user