From 120c39a3eff73f88b5a43134036c680e7dbcb1ca Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 9 May 2026 13:34:54 +0900 Subject: [PATCH] =?UTF-8?q?feat(web-ui):=20PipelineDetailModal=20+=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20mini=20=EB=AF=B8=EB=A6=AC=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/music/MusicStudio.css | 56 +++++++++ src/pages/music/components/PipelineCard.jsx | 109 ++++++++++------ .../music/components/PipelineDetailModal.jsx | 117 ++++++++++++++++++ 3 files changed, 241 insertions(+), 41 deletions(-) create mode 100644 src/pages/music/components/PipelineDetailModal.jsx diff --git a/src/pages/music/MusicStudio.css b/src/pages/music/MusicStudio.css index a3de87b..08d4027 100644 --- a/src/pages/music/MusicStudio.css +++ b/src/pages/music/MusicStudio.css @@ -3341,3 +3341,59 @@ .psm-advanced input, .psm-advanced select { width: 100%; padding: 6px 8px; background: rgba(255,255,255,.04); border: 1px solid var(--ms-line, #2a2a3a); border-radius: 6px; color: var(--ms-text, #f0f0f5); font-size: 12px; } + +/* === Pipeline Detail Modal === */ +.modal-body--lg { max-width: 720px; max-height: 90vh; overflow-y: auto; } +.pdm-header { display:flex; align-items:center; gap:12px; margin-bottom:16px; } +.pdm-header h3 { flex:1; margin:0; } +.pdm-badge { padding:2px 8px; background:rgba(56,189,248,.2); color:#bae6fd; + border-radius:6px; font-size:11px; } +.pdm-close { background:none; border:none; font-size:24px; cursor:pointer; + color:var(--ms-muted, #a0a0b0); padding:0 6px; } + +.pdm-grid { display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:16px; } +.pdm-figure { margin:0; } +.pdm-figure img { width:100%; border-radius:8px; display:block; } +.pdm-figure figcaption { font-size:11px; color:var(--ms-muted, #a0a0b0); text-align:center; margin-top:4px; } + +.pdm-video { margin-bottom:16px; } +.pdm-video video { border-radius:8px; } + +.pdm-section { margin-bottom:16px; padding-bottom:12px; border-bottom:1px solid var(--ms-line, #2a2a3a); } +.pdm-section:last-of-type { border-bottom:none; } +.pdm-section h4 { margin:0 0 8px; font-size:14px; } +.pdm-pre { background:rgba(0,0,0,.3); padding:8px; border-radius:6px; font-size:12px; + white-space:pre-wrap; overflow-x:auto; max-height:400px; } + +.pdm-verdict { padding:2px 8px; margin-left:8px; border-radius:6px; font-size:12px; font-weight:bold; } +.pdm-verdict--pass { background:rgba(34,197,94,.2); color:#86efac; } +.pdm-verdict--fail { background:rgba(248,113,113,.2); color:#fca5a5; } +.pdm-score { color:var(--ms-muted, #a0a0b0); font-size:12px; margin-left:8px; font-weight:normal; } +.pdm-review-table { width:100%; border-collapse:collapse; font-size:13px; } +.pdm-review-table td { padding:4px 8px; border-bottom:1px solid var(--ms-line, #2a2a3a); } +.pdm-review-table td:nth-child(2) { text-align:right; font-weight:bold; } +.pdm-summary { font-size:12px; color:var(--ms-muted, #a0a0b0); margin-top:8px; } + +.pdm-tracks { padding-left:24px; } +.pdm-tracks li { margin-bottom:4px; font-size:13px; } +.pdm-track-time { color:var(--ms-accent, #38bdf8); font-family:monospace; } +.pdm-track-dur { color:var(--ms-muted, #a0a0b0); font-size:11px; } + +.pdm-feedback { padding-left:0; list-style:none; } +.pdm-feedback li { padding:6px 8px; background:rgba(0,0,0,.2); border-radius:6px; + margin-bottom:4px; font-size:12px; } +.pdm-feedback code { color:#fb923c; font-size:11px; } +.pdm-feedback small { display:block; color:var(--ms-muted, #a0a0b0); margin-top:2px; } + +.pdm-youtube { display:inline-block; padding:8px 16px; background:#ff0000; color:white; + border-radius:8px; text-decoration:none; font-weight:bold; } + +/* PipelineCard mini previews + style badge */ +.pipeline-previews { display:flex; gap:8px; margin:8px 0; align-items:center; } +.pipeline-preview-mini { width:64px; height:64px; object-fit:cover; border-radius:6px; + border:1px solid var(--ms-line, #2a2a3a); } +.pipeline-video-icon { font-size:24px; color:var(--ms-accent, #38bdf8); margin-left:4px; } +.pipeline-style-badge { padding:1px 6px; background:rgba(56,189,248,.15); color:#bae6fd; + border-radius:4px; font-size:10px; } +.pipeline-card { cursor:pointer; } +.pipeline-card:hover { background:rgba(255,255,255,.02); } diff --git a/src/pages/music/components/PipelineCard.jsx b/src/pages/music/components/PipelineCard.jsx index 34461cb..952cc1b 100644 --- a/src/pages/music/components/PipelineCard.jsx +++ b/src/pages/music/components/PipelineCard.jsx @@ -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 ( -
-
-

{pipeline.track_title || `Track #${pipeline.track_id}`}

- {!['published','cancelled','failed'].includes(pipeline.state) && ( - + )} +
+ +
+ {pipeline.cover_url && ( + + )} + {pipeline.thumbnail_url && ( + + )} + {pipeline.video_url && } +
+ +
+ {STEP_LABELS.map((lbl, idx) => ( +
+ {lbl} +
+ ))} +
+ +
현재: {pipeline.state}
+ + {pipeline.review && ( +
+ AI 검토: {pipeline.review.verdict} + ({pipeline.review.weighted_total}/100) +
+ )} + + {pipeline.state === 'publish_pending' && ( + )} + + {pipeline.youtube_video_id && ( + + 유튜브에서 보기 + + )}
-
- {STEP_LABELS.map((lbl, idx) => ( -
- {lbl} -
- ))} -
-
현재: {pipeline.state}
- {pipeline.review && ( -
- AI 검토: {pipeline.review.verdict} - ({pipeline.review.weighted_total}/100) -
- )} - {pipeline.state === 'publish_pending' && ( - - )} - {pipeline.youtube_video_id && ( - - 유튜브에서 보기 - - )} - {pipeline.feedback && pipeline.feedback.length > 0 && ( -
- 피드백 히스토리 ({pipeline.feedback.length}) - {pipeline.feedback.map(f => ( -
• [{f.step}] {f.feedback_text}
- ))} -
- )} - + + {showDetail && setShowDetail(false)} />} + ); } diff --git a/src/pages/music/components/PipelineDetailModal.jsx b/src/pages/music/components/PipelineDetailModal.jsx new file mode 100644 index 0000000..fcbe5d5 --- /dev/null +++ b/src/pages/music/components/PipelineDetailModal.jsx @@ -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 ( +
+
e.stopPropagation()}> +
+

{pipeline.compile_title || pipeline.track_title || `Pipeline #${pipeline.id}`}

+ {pipeline.visual_style || 'essential'} + +
+ +
+ {pipeline.cover_url && ( +
+ cover +
커버 (배경)
+
+ )} + {pipeline.thumbnail_url && ( +
+ thumbnail +
썸네일
+
+ )} +
+ + {pipeline.video_url && ( +
+
+ )} + + {meta.title && ( +
+

메타데이터

+

제목: {meta.title}

+
+ 설명 ({(meta.description || '').length}자) +
{meta.description}
+
+

태그: {(meta.tags || []).join(', ')}

+
+ )} + + {review.weighted_total != null && ( +
+

+ AI 검토 + + {review.verdict} + + ({review.weighted_total}/100) +

+ + + + + + + +
메타데이터 품질{review.metadata_quality?.score}
콘텐츠 정책{review.policy_compliance?.score}
시청 경험{review.viewer_experience?.score}
트렌드 정렬{review.trend_alignment?.score}
+ {review.summary &&

{review.summary}

} +
+ )} + + {pipeline.tracks && pipeline.tracks.length > 1 && ( +
+

트랙 리스트 ({pipeline.tracks.length})

+
    + {pipeline.tracks.map(t => ( +
  1. + [{fmtTimestamp(t.start_offset_sec)}] + {' '}{t.title} + ({fmtTimestamp(t.duration_sec)}) +
  2. + ))} +
+
+ )} + + {pipeline.feedback && pipeline.feedback.length > 0 && ( +
+

피드백 히스토리 ({pipeline.feedback.length})

+
    + {pipeline.feedback.map(f => ( +
  • + [{f.step}] {f.feedback_text} + {(f.received_at || '').replace('T', ' ')} +
  • + ))} +
+
+ )} + + {pipeline.youtube_video_id && ( + + 🎬 YouTube에서 보기 + + )} +
+
+ ); +}