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

@@ -1,63 +1,90 @@
import { useState } from 'react';
import { cancelPipeline, publishPipeline } from '../../../api';
import PipelineDetailModal from './PipelineDetailModal';
const STEP_LABELS = ['커버','영상','썸네','메타','검토','발행'];
function stepIndex(state) {
if (!state) return -1;
if (state.startsWith('cover')) return 0;
if (state.startsWith('video')) return 1;
if (state.startsWith('thumb')) return 2;
if (state.startsWith('meta')) return 3;
if (state.startsWith('ai_review') || state.startsWith('publish_pending')) return 4;
if (state.startsWith('ai_review') || state === 'publish_pending') return 4;
if (state.startsWith('publish')) return 5;
if (state === 'published') return 6;
return -1;
}
export default function PipelineCard({ pipeline, onChanged }) {
const [showDetail, setShowDetail] = useState(false);
const i = stepIndex(pipeline.state);
const title = pipeline.compile_title || pipeline.track_title || `Pipeline #${pipeline.id}`;
const handleCardClick = (e) => {
if (e.target.closest('button') || e.target.closest('a')) return;
setShowDetail(true);
};
return (
<div className="pipeline-card">
<div className="pipeline-card__head">
<h4>{pipeline.track_title || `Track #${pipeline.track_id}`}</h4>
{!['published','cancelled','failed'].includes(pipeline.state) && (
<button onClick={async () => { await cancelPipeline(pipeline.id); onChanged(); }}>
취소
<>
<div className="pipeline-card" onClick={handleCardClick}>
<div className="pipeline-card__head">
<h4>{title}</h4>
{pipeline.visual_style && (
<span className="pipeline-style-badge">{pipeline.visual_style}</span>
)}
{!['published','cancelled','failed'].includes(pipeline.state) && (
<button onClick={async () => { await cancelPipeline(pipeline.id); onChanged(); }}>
취소
</button>
)}
</div>
<div className="pipeline-previews">
{pipeline.cover_url && (
<img src={pipeline.cover_url} alt="" className="pipeline-preview-mini" />
)}
{pipeline.thumbnail_url && (
<img src={pipeline.thumbnail_url} alt="" className="pipeline-preview-mini" />
)}
{pipeline.video_url && <span className="pipeline-video-icon"></span>}
</div>
<div className="pipeline-progress">
{STEP_LABELS.map((lbl, idx) => (
<div key={lbl}
className={`pipeline-dot ${idx <= i ? 'is-done' : ''} ${idx === i ? 'is-current' : ''}`}>
<span>{lbl}</span>
</div>
))}
</div>
<div className="pipeline-state">현재: {pipeline.state}</div>
{pipeline.review && (
<div className="pipeline-review">
AI 검토: <strong>{pipeline.review.verdict}</strong>
({pipeline.review.weighted_total}/100)
</div>
)}
{pipeline.state === 'publish_pending' && (
<button className="button primary"
onClick={async () => { await publishPipeline(pipeline.id); onChanged(); }}>
YouTube 업로드
</button>
)}
{pipeline.youtube_video_id && (
<a href={`https://youtu.be/${pipeline.youtube_video_id}`}
target="_blank" rel="noreferrer">
유튜브에서 보기
</a>
)}
</div>
<div className="pipeline-progress">
{STEP_LABELS.map((lbl, idx) => (
<div key={lbl} className={`pipeline-dot ${idx <= i ? 'is-done' : ''} ${idx === i ? 'is-current' : ''}`}>
<span>{lbl}</span>
</div>
))}
</div>
<div className="pipeline-state">현재: {pipeline.state}</div>
{pipeline.review && (
<div className="pipeline-review">
AI 검토: <strong>{pipeline.review.verdict}</strong>
({pipeline.review.weighted_total}/100)
</div>
)}
{pipeline.state === 'publish_pending' && (
<button className="button primary"
onClick={async () => { await publishPipeline(pipeline.id); onChanged(); }}>
YouTube 업로드
</button>
)}
{pipeline.youtube_video_id && (
<a href={`https://youtu.be/${pipeline.youtube_video_id}`} target="_blank" rel="noreferrer">
유튜브에서 보기
</a>
)}
{pipeline.feedback && pipeline.feedback.length > 0 && (
<details className="pipeline-feedback">
<summary>피드백 히스토리 ({pipeline.feedback.length})</summary>
{pipeline.feedback.map(f => (
<div key={f.id}> [{f.step}] {f.feedback_text}</div>
))}
</details>
)}
</div>
{showDetail && <PipelineDetailModal pipeline={pipeline} onClose={() => setShowDetail(false)} />}
</>
);
}