diff --git a/resources/views/video/tutorial/index.blade.php b/resources/views/video/tutorial/index.blade.php index f23a1039..555f2c85 100644 --- a/resources/views/video/tutorial/index.blade.php +++ b/resources/views/video/tutorial/index.blade.php @@ -60,6 +60,16 @@ cursor: pointer; transition: all 0.2s; color: #9ca3af; flex-shrink: 0; } .screenshot-add-btn:hover { border-color: #818cf8; color: #6366f1; background: #f5f3ff; } + .progress-btn { + position: relative; overflow: hidden; min-width: 260px; + } + .progress-btn .progress-fill { + position: absolute; left: 0; top: 0; bottom: 0; + background: rgba(255,255,255,0.15); transition: width 0.3s ease-out; + } + .progress-btn .btn-content { + position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; gap: 0.5rem; + } .analysis-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 0.75rem; padding: 1rem; margin-bottom: 0.75rem; box-shadow: 0 1px 2px rgba(0,0,0,.05); @@ -130,12 +140,14 @@ // ============================================================ // Step 1: Screenshot Upload // ============================================================ -const ScreenshotUpload = ({ onUploadComplete, loading, setLoading }) => { +const ScreenshotUpload = ({ onUploadComplete, onAnalysisComplete, loading, setLoading }) => { const [files, setFiles] = useState([]); const [previews, setPreviews] = useState([]); const [pasteFlash, setPasteFlash] = useState(false); const [dragIdx, setDragIdx] = useState(null); const [dragOverIdx, setDragOverIdx] = useState(null); + const [uploadProgress, setUploadProgress] = useState(0); // 0~100 + const [uploadPhase, setUploadPhase] = useState(''); // '', 'uploading', 'analyzing' const fileInputRef = useRef(null); const filesRef = useRef(files); const previewsRef = useRef(previews); @@ -230,22 +242,74 @@ setDragOverIdx(null); }; + // XHR 업로드 (진행률 지원) + AI 분석까지 한번에 const handleUpload = async () => { if (files.length === 0) return; setLoading(true); + setUploadPhase('uploading'); + setUploadProgress(0); + try { + // Phase 1: 파일 업로드 (0~50%) const formData = new FormData(); files.forEach(f => formData.append('screenshots[]', f)); - const data = await api('/video/tutorial/upload', { - method: 'POST', - body: formData, - headers: { 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json' }, + + const uploadResult = await new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('POST', '/video/tutorial/upload'); + xhr.setRequestHeader('X-CSRF-TOKEN', CSRF); + xhr.setRequestHeader('Accept', 'application/json'); + + xhr.upload.onprogress = (e) => { + if (e.lengthComputable) { + const pct = Math.round((e.loaded / e.total) * 50); + setUploadProgress(pct); + } + }; + + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(JSON.parse(xhr.responseText)); + } else { + try { + const err = JSON.parse(xhr.responseText); + reject(new Error(err.message || `HTTP ${xhr.status}`)); + } catch { + reject(new Error(`HTTP ${xhr.status}`)); + } + } + }; + xhr.onerror = () => reject(new Error('네트워크 오류')); + xhr.send(formData); }); - onUploadComplete(data.paths); + + // Phase 2: AI 분석 (50~100%) + setUploadPhase('analyzing'); + setUploadProgress(55); + + // 분석은 시간이 걸리므로 진행률 시뮬레이션 + const analyzeTimer = setInterval(() => { + setUploadProgress(prev => prev < 90 ? prev + 3 : prev); + }, 800); + + const analyzeResult = await api('/video/tutorial/analyze', { + method: 'POST', + body: JSON.stringify({ paths: uploadResult.paths }), + }); + + clearInterval(analyzeTimer); + setUploadProgress(100); + + // 완료 후 잠시 대기 (100% 표시) + await new Promise(r => setTimeout(r, 400)); + + onAnalysisComplete(uploadResult.paths, analyzeResult.analysis); } catch (err) { alert(err.message); } finally { setLoading(false); + setUploadPhase(''); + setUploadProgress(0); } }; @@ -331,14 +395,26 @@ className={`screenshot-thumb ${dragIdx === i ? 'dragging' : ''} ${dragOverIdx == )} @@ -708,21 +784,10 @@ className="px-3 py-1.5 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700 const [tutorialId, setTutorialId] = useState(null); const [refreshKey, setRefreshKey] = useState(0); - const handleUploadComplete = async (uploadedPaths) => { + const handleAnalysisComplete = (uploadedPaths, analysisData) => { setPaths(uploadedPaths); - setLoading(true); - try { - const data = await api('/video/tutorial/analyze', { - method: 'POST', - body: JSON.stringify({ paths: uploadedPaths }), - }); - setAnalysis(data.analysis); - setStep(2); - } catch (err) { - alert('AI 분석 실패: ' + err.message); - } finally { - setLoading(false); - } + setAnalysis(analysisData); + setStep(2); }; const handleGenerate = async (editedAnalysis, title) => { @@ -761,7 +826,7 @@ className="px-3 py-1.5 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700