feat(web-ui): PipelineDetailModal + 카드 mini 미리보기

This commit is contained in:
2026-05-09 13:34:54 +09:00
parent 08fce2d4f6
commit 120c39a3ef
3 changed files with 241 additions and 41 deletions

View File

@@ -0,0 +1,117 @@
const fmtTimestamp = (sec) => {
if (sec == null) return '';
const total = Math.floor(sec);
const h = Math.floor(total / 3600);
const m = Math.floor((total % 3600) / 60);
const s = total % 60;
if (h) return `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
return `${m}:${String(s).padStart(2,'0')}`;
};
export default function PipelineDetailModal({ pipeline, onClose }) {
if (!pipeline) return null;
const meta = pipeline.metadata || {};
const review = pipeline.review || {};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-body modal-body--lg" onClick={e => e.stopPropagation()}>
<header className="pdm-header">
<h3>{pipeline.compile_title || pipeline.track_title || `Pipeline #${pipeline.id}`}</h3>
<span className="pdm-badge">{pipeline.visual_style || 'essential'}</span>
<button onClick={onClose} className="pdm-close" aria-label="close">×</button>
</header>
<div className="pdm-grid">
{pipeline.cover_url && (
<figure className="pdm-figure">
<img src={pipeline.cover_url} alt="cover" />
<figcaption>커버 (배경)</figcaption>
</figure>
)}
{pipeline.thumbnail_url && (
<figure className="pdm-figure">
<img src={pipeline.thumbnail_url} alt="thumbnail" />
<figcaption>썸네일</figcaption>
</figure>
)}
</div>
{pipeline.video_url && (
<div className="pdm-video">
<video src={pipeline.video_url} controls preload="metadata" width="100%" />
</div>
)}
{meta.title && (
<section className="pdm-section">
<h4>메타데이터</h4>
<p><strong>제목:</strong> {meta.title}</p>
<details>
<summary>설명 ({(meta.description || '').length})</summary>
<pre className="pdm-pre">{meta.description}</pre>
</details>
<p><strong>태그:</strong> {(meta.tags || []).join(', ')}</p>
</section>
)}
{review.weighted_total != null && (
<section className="pdm-section">
<h4>
AI 검토
<span className={`pdm-verdict pdm-verdict--${review.verdict}`}>
{review.verdict}
</span>
<span className="pdm-score">({review.weighted_total}/100)</span>
</h4>
<table className="pdm-review-table">
<tbody>
<tr><td>메타데이터 품질</td><td>{review.metadata_quality?.score}</td></tr>
<tr><td>콘텐츠 정책</td><td>{review.policy_compliance?.score}</td></tr>
<tr><td>시청 경험</td><td>{review.viewer_experience?.score}</td></tr>
<tr><td>트렌드 정렬</td><td>{review.trend_alignment?.score}</td></tr>
</tbody>
</table>
{review.summary && <p className="pdm-summary"><em>{review.summary}</em></p>}
</section>
)}
{pipeline.tracks && pipeline.tracks.length > 1 && (
<section className="pdm-section">
<h4>트랙 리스트 ({pipeline.tracks.length})</h4>
<ol className="pdm-tracks">
{pipeline.tracks.map(t => (
<li key={t.id}>
<span className="pdm-track-time">[{fmtTimestamp(t.start_offset_sec)}]</span>
{' '}{t.title}
<span className="pdm-track-dur"> ({fmtTimestamp(t.duration_sec)})</span>
</li>
))}
</ol>
</section>
)}
{pipeline.feedback && pipeline.feedback.length > 0 && (
<section className="pdm-section">
<h4>피드백 히스토리 ({pipeline.feedback.length})</h4>
<ul className="pdm-feedback">
{pipeline.feedback.map(f => (
<li key={f.id}>
<code>[{f.step}]</code> {f.feedback_text}
<small> {(f.received_at || '').replace('T', ' ')}</small>
</li>
))}
</ul>
</section>
)}
{pipeline.youtube_video_id && (
<a href={`https://youtu.be/${pipeline.youtube_video_id}`}
target="_blank" rel="noreferrer" className="pdm-youtube">
🎬 YouTube에서 보기
</a>
)}
</div>
</div>
);
}