Add intro text improvements and BGM download management
- Intro text: Add 0.7s freeze frame effect before video plays - Intro text: Auto-scale font size to prevent overflow - Intro text: Split long text into 2 lines automatically - BGM Page: Add free BGM download section with 6 categories - BGM Page: Support individual and bulk download from Freesound.org 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,24 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Upload, Trash2, Play, Pause, Music } from 'lucide-react';
|
||||
import { Upload, Trash2, Play, Pause, Music, Download, Loader2, CheckCircle, XCircle } from 'lucide-react';
|
||||
import { bgmApi } from '../api/client';
|
||||
|
||||
// 무료 BGM 카테고리
|
||||
const BGM_CATEGORIES = [
|
||||
{ id: 'upbeat', name: '신나는', keywords: ['upbeat', 'energetic', 'happy'] },
|
||||
{ id: 'chill', name: '차분한', keywords: ['chill', 'calm', 'relaxing'] },
|
||||
{ id: 'cinematic', name: '시네마틱', keywords: ['cinematic', 'epic', 'dramatic'] },
|
||||
{ id: 'acoustic', name: '어쿠스틱', keywords: ['acoustic', 'guitar', 'folk'] },
|
||||
{ id: 'electronic', name: '일렉트로닉', keywords: ['electronic', 'edm', 'dance'] },
|
||||
{ id: 'lofi', name: '로파이', keywords: ['lofi', 'hip hop', 'beats'] },
|
||||
];
|
||||
|
||||
export default function BGMPage() {
|
||||
const [bgmList, setBgmList] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [playingId, setPlayingId] = useState(null);
|
||||
const [downloading, setDownloading] = useState(null); // 현재 다운로드 중인 카테고리 ID
|
||||
const [downloadStatus, setDownloadStatus] = useState({}); // 각 카테고리별 다운로드 상태
|
||||
const audioRef = useRef(null);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
@@ -85,6 +97,67 @@ export default function BGMPage() {
|
||||
return `${mins}:${String(secs).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// 무료 BGM 다운로드
|
||||
const handleDownloadBgm = async (category) => {
|
||||
if (downloading) return; // 이미 다운로드 중이면 무시
|
||||
|
||||
setDownloading(category.id);
|
||||
setDownloadStatus(prev => ({ ...prev, [category.id]: 'downloading' }));
|
||||
|
||||
try {
|
||||
const res = await bgmApi.autoDownload(category.keywords, 120);
|
||||
if (res.data.success) {
|
||||
setDownloadStatus(prev => ({ ...prev, [category.id]: 'success' }));
|
||||
// BGM 리스트 새로고침
|
||||
await fetchBgmList();
|
||||
} else {
|
||||
setDownloadStatus(prev => ({ ...prev, [category.id]: 'error' }));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('BGM download failed:', err);
|
||||
setDownloadStatus(prev => ({ ...prev, [category.id]: 'error' }));
|
||||
} finally {
|
||||
setDownloading(null);
|
||||
// 3초 후 상태 초기화
|
||||
setTimeout(() => {
|
||||
setDownloadStatus(prev => ({ ...prev, [category.id]: null }));
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
// 전체 카테고리 다운로드
|
||||
const handleDownloadAll = async () => {
|
||||
if (downloading) return;
|
||||
|
||||
for (const category of BGM_CATEGORIES) {
|
||||
setDownloading(category.id);
|
||||
setDownloadStatus(prev => ({ ...prev, [category.id]: 'downloading' }));
|
||||
|
||||
try {
|
||||
const res = await bgmApi.autoDownload(category.keywords, 120);
|
||||
if (res.data.success) {
|
||||
setDownloadStatus(prev => ({ ...prev, [category.id]: 'success' }));
|
||||
} else {
|
||||
setDownloadStatus(prev => ({ ...prev, [category.id]: 'error' }));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`BGM download failed for ${category.name}:`, err);
|
||||
setDownloadStatus(prev => ({ ...prev, [category.id]: 'error' }));
|
||||
}
|
||||
|
||||
// 잠시 대기 (API 제한 방지)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
setDownloading(null);
|
||||
await fetchBgmList();
|
||||
|
||||
// 5초 후 상태 초기화
|
||||
setTimeout(() => {
|
||||
setDownloadStatus({});
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -109,6 +182,75 @@ export default function BGMPage() {
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
{/* 무료 BGM 다운로드 섹션 */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="font-medium flex items-center gap-2">
|
||||
<Download size={18} />
|
||||
무료 BGM 다운로드
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
Freesound.org에서 CC0 라이선스 BGM을 다운로드합니다 (상업용 가능)
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDownloadAll}
|
||||
disabled={!!downloading}
|
||||
className="btn-secondary text-sm flex items-center gap-2"
|
||||
>
|
||||
{downloading ? (
|
||||
<>
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
다운로드 중...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download size={14} />
|
||||
전체 다운로드
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||
{BGM_CATEGORIES.map((category) => {
|
||||
const status = downloadStatus[category.id];
|
||||
const isDownloading = downloading === category.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={category.id}
|
||||
onClick={() => handleDownloadBgm(category)}
|
||||
disabled={!!downloading}
|
||||
className={`p-3 rounded-lg border text-center transition-all ${
|
||||
status === 'success'
|
||||
? 'bg-green-900/30 border-green-600 text-green-400'
|
||||
: status === 'error'
|
||||
? 'bg-red-900/30 border-red-600 text-red-400'
|
||||
: isDownloading
|
||||
? 'bg-blue-900/30 border-blue-600 text-blue-400'
|
||||
: 'bg-gray-800 border-gray-700 hover:border-gray-600 text-gray-300'
|
||||
} ${downloading && !isDownloading ? 'opacity-50' : ''}`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{isDownloading ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : status === 'success' ? (
|
||||
<CheckCircle size={16} />
|
||||
) : status === 'error' ? (
|
||||
<XCircle size={16} />
|
||||
) : (
|
||||
<Music size={16} />
|
||||
)}
|
||||
<span className="font-medium">{category.name}</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="card text-center py-12 text-gray-500">
|
||||
로딩 중...
|
||||
|
||||
Reference in New Issue
Block a user