91 lines
3.6 KiB
JavaScript
91 lines
3.6 KiB
JavaScript
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 === '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" 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>
|
|
|
|
{showDetail && <PipelineDetailModal pipeline={pipeline} onClose={() => setShowDetail(false)} />}
|
|
</>
|
|
);
|
|
}
|