/// import React, { useState, ChangeEvent, useEffect } from 'react'; import { Upload, Music, Sparkles, X, Volume2, Search, MapPin, Loader2, Type, Globe, Video, Monitor, Wand2, Layers, FileText, Home, Waves, Mountain, Heart, Users, Dog, Tent, Building, CheckCircle, Lock, ChevronDown, Image as ImageIcon, ExternalLink } from 'lucide-react'; import { BusinessInfo, MusicGenre, MusicDuration, AudioMode, TTSConfig, TTSGender, TTSAge, TTSTone, TextEffect, AspectRatio, Language, TransitionEffect, PensionCategory } from '../types'; import { filterBestImages, enrichDescriptionWithReviews, extractTextEffectFromImage } from '../services/geminiService'; import { crawlNaverPlace } from '../services/naverService'; import { searchPlaceDetails, fetchPlacePhoto } from '../services/googlePlacesService'; import { useLanguage } from '../src/contexts/LanguageContext'; import OnboardingTour from './OnboardingTour'; // shadcn/ui components import { cn } from '../src/lib/utils'; import { Button } from '../src/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../src/components/ui/card'; import { Input } from '../src/components/ui/input'; import { Label } from '../src/components/ui/label'; import { Textarea } from '../src/components/ui/textarea'; import { Badge } from '../src/components/ui/badge'; import { Separator } from '../src/components/ui/separator'; import { Switch } from '../src/components/ui/switch'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../src/components/ui/collapsible'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../src/components/ui/tooltip'; interface InputFormProps { onSubmit: (info: BusinessInfo) => void; isSubmitting: boolean; } // 국가별 장르 매핑 const GENRE_BY_LANG: Record = { 'KO': ['Auto', 'K-Pop', 'Trot', 'Ballad', 'Hip-Hop', 'R&B', 'EDM', 'Jazz', 'Rock'], 'EN': ['Auto', 'Pop', 'Country', 'Hip-Hop', 'R&B', 'EDM', 'Jazz', 'Rock', 'Ballad'], 'JP': ['Auto', 'J-Pop', 'Enka', 'Anime', 'Rock', 'Jazz', 'Ballad', 'EDM', 'Hip-Hop'], 'CN': ['Auto', 'C-Pop', 'Mandopop', 'Traditional CN', 'Ballad', 'Hip-Hop', 'EDM', 'Jazz', 'Rock'], 'TH': ['Auto', 'T-Pop', 'Luk Thung', 'Pop', 'Hip-Hop', 'Rock', 'EDM', 'Jazz', 'Ballad'], 'VN': ['Auto', 'V-Pop', 'Bolero', 'Pop', 'Hip-Hop', 'Rock', 'EDM', 'Jazz', 'Ballad'] }; // 펜션 카테고리 옵션 const PENSION_CATEGORIES: { id: PensionCategory; icon: any; labelKey: string }[] = [ { id: 'PoolVilla', icon: Waves, labelKey: 'catPoolVilla' }, { id: 'OceanView', icon: Waves, labelKey: 'catOceanView' }, { id: 'Mountain', icon: Mountain, labelKey: 'catMountain' }, { id: 'Private', icon: Home, labelKey: 'catPrivate' }, { id: 'Couple', icon: Heart, labelKey: 'catCouple' }, { id: 'Family', icon: Users, labelKey: 'catFamily' }, { id: 'Pet', icon: Dog, labelKey: 'catPet' }, { id: 'Glamping', icon: Tent, labelKey: 'catGlamping' }, { id: 'Traditional', icon: Building, labelKey: 'catTraditional' }, ]; const FLAG_CLASSES: Record = { 'KO': 'fi-kr', 'EN': 'fi-us', 'JP': 'fi-jp', 'CN': 'fi-cn', 'TH': 'fi-th', 'VN': 'fi-vn' }; const InputForm: React.FC = ({ onSubmit, isSubmitting }) => { const { language: uiLanguage, t } = useLanguage(); // 온보딩 투어 상태 const [showTour, setShowTour] = useState(false); useEffect(() => { const hasSeenTour = localStorage.getItem('castadTourCompleted'); if (!hasSeenTour) { setTimeout(() => setShowTour(true), 1000); } }, []); // 콘텐츠 생성 언어 - UI 언어와 동기화 const [contentLanguage, setContentLanguage] = useState(uiLanguage); // Sync content language with UI language useEffect(() => { setContentLanguage(uiLanguage); }, [uiLanguage]); // 펜션 카테고리 (복수 선택) const [pensionCategories, setPensionCategories] = useState([]); // 상태 관리 const [name, setName] = useState(''); const [description, setDescription] = useState(''); const [address, setAddress] = useState(''); const [category, setCategory] = useState(''); const [images, setImages] = useState([]); const [previews, setPreviews] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [isSearching, setIsSearching] = useState(false); const [searchStatus, setSearchStatus] = useState(''); const [mapLink, setMapLink] = useState(null); const [naverUrl, setNaverUrl] = useState(''); const [isCrawling, setIsCrawling] = useState(false); const [crawlStatus, setCrawlStatus] = useState(''); const [audioMode, setAudioMode] = useState('Song'); const [musicGenre, setMusicGenre] = useState('Auto'); const [musicDuration, setMusicDuration] = useState('Short'); const [activeGenres, setActiveGenres] = useState(GENRE_BY_LANG['KO']); const [ttsConfig, setTTSConfig] = useState({ gender: 'Female', age: 'Young', tone: 'Bright' }); const [visualStyle, setVisualStyle] = useState<'Video' | 'Slideshow'>('Slideshow'); const [transitionEffect, setTransitionEffect] = useState('Mix'); const [aspectRatio, setAspectRatio] = useState('9:16'); const [textEffect, setTextEffect] = useState('Cinematic'); const [customStyle, setCustomStyle] = useState(''); const [isAnalyzingStyle, setIsAnalyzingStyle] = useState(false); const [useAiImages, setUseAiImages] = useState(false); // Collapsible states const [openSections, setOpenSections] = useState({ category: true, images: true, autoSearch: false, basicInfo: true, settings: true, textStyle: true, audio: true }); useEffect(() => { setActiveGenres(GENRE_BY_LANG[contentLanguage]); setMusicGenre('Auto'); }, [contentLanguage]); const textEffects: { id: TextEffect; label: string; desc: string; previewClass?: string }[] = [ { id: 'Cinematic', label: t('textStyle') === '자막 스타일' ? '영화 같은' : 'Cinematic', desc: 'Fade', previewClass: 'font-serif tracking-widest' }, { id: 'Neon', label: t('textStyle') === '자막 스타일' ? '네온 사인' : 'Neon', desc: 'Glow', previewClass: 'effect-neon font-bold text-pink-500' }, { id: 'Glitch', label: t('textStyle') === '자막 스타일' ? '글리치' : 'Glitch', desc: 'Noise', previewClass: 'effect-glitch font-mono font-bold' }, { id: 'Typewriter', label: t('textStyle') === '자막 스타일' ? '타자기' : 'Typewriter', desc: 'Cursor', previewClass: 'font-mono border-r-2 border-white pr-1' }, { id: 'Bold', label: t('textStyle') === '자막 스타일' ? '임팩트' : 'Bold', desc: 'Strong', previewClass: 'font-black uppercase italic' }, { id: 'Motion', label: t('textStyle') === '자막 스타일' ? '모션' : 'Motion', desc: 'Dynamic', previewClass: 'effect-motion font-bold italic' }, { id: 'LineReveal', label: t('textStyle') === '자막 스타일' ? '라인 등장' : 'Line Reveal', desc: 'Minimal', previewClass: 'border-t border-b border-white py-1' }, { id: 'Boxed', label: t('textStyle') === '자막 스타일' ? '박스' : 'Boxed', desc: 'Frame', previewClass: 'border border-white p-1' }, { id: 'Elegant', label: t('textStyle') === '자막 스타일' ? '우아함' : 'Elegant', desc: 'Classic', previewClass: 'font-serif italic' }, { id: 'BlockReveal', label: t('textStyle') === '자막 스타일' ? '블록 등장' : 'Block Reveal', desc: 'Pop', previewClass: 'bg-white text-black px-1' }, { id: 'Custom', label: t('textStyle') === '자막 스타일' ? '나만의 스타일' : 'Custom', desc: 'AI', previewClass: '' }, ]; const transitionOptions: { id: TransitionEffect; labelKey: any }[] = [ { id: 'Mix', labelKey: 'transMix' }, { id: 'Zoom', labelKey: 'transZoom' }, { id: 'Slide', labelKey: 'transSlide' }, { id: 'Wipe', labelKey: 'transWipe' }, ]; // Helper Functions const updateImages = (newFiles: File[]) => { const combinedFiles = [...images, ...newFiles]; const limitedFiles = combinedFiles.slice(0, 10); setImages(limitedFiles); const newPreviews: string[] = []; let processed = 0; if (limitedFiles.length === 0) { setPreviews([]); return; } limitedFiles.forEach(file => { const reader = new FileReader(); reader.onloadend = () => { newPreviews.push(reader.result as string); processed++; if (processed === limitedFiles.length) { setPreviews([...newPreviews]); } }; reader.readAsDataURL(file); }); }; const handleImageChange = (e: ChangeEvent) => { if (e.target.files) { updateImages(Array.from(e.target.files)); } }; const removeImage = (index: number) => { const newImages = [...images]; newImages.splice(index, 1); setImages(newImages); if (newImages.length === 0) { setPreviews([]); } else { const newPreviews: string[] = []; let processed = 0; newImages.forEach(file => { const reader = new FileReader(); reader.onloadend = () => { newPreviews.push(reader.result as string); processed++; if (processed === newImages.length) setPreviews(newPreviews); }; reader.readAsDataURL(file); }); } }; const handleSearch = async () => { if (!searchQuery) return; setIsSearching(true); setSearchStatus('Processing...'); setMapLink(null); let finalQuery = searchQuery; if (searchQuery.includes('google') && searchQuery.includes('/maps/place/')) { try { const match = searchQuery.match(/\/maps\/place\/([^\/]+)/); if (match && match[1]) { finalQuery = decodeURIComponent(match[1]).replace(/\+/g, ' '); } } catch (e) { console.warn("URL Error"); } } try { const apiKey = import.meta.env.VITE_GEMINI_API_KEY; if (!apiKey) throw new Error("API Key Missing"); const details = await searchPlaceDetails(finalQuery); if (!details) { alert("Place not found"); setIsSearching(false); return; } setName(details.displayName.text); setAddress(details.formattedAddress); if (details.photos && details.photos.length > 0) { setSearchStatus(`Fetching ${details.photos.length} photos...`); const photoPromises = details.photos.slice(0, 10).map(p => fetchPlacePhoto(p.name)); const fetchedPhotos = (await Promise.all(photoPromises)).filter((f): f is File => f !== null); updateImages(fetchedPhotos); } setSearchStatus('Analyzing reviews...'); let generatedDesc = details.formattedAddress; const reviewsText = details.reviews?.map(r => r.text.text) || []; if (reviewsText.length > 0 || details.rating) { generatedDesc = await enrichDescriptionWithReviews( details.displayName.text, details.formattedAddress, reviewsText, details.rating || 0, apiKey ); } setDescription(generatedDesc); setSearchStatus('Done!'); setTimeout(() => setSearchStatus(''), 2000); } catch (error) { console.error("Search failed:", error); alert("Failed to fetch info."); setSearchStatus(''); } finally { setIsSearching(false); } }; const handleNaverCrawl = async () => { if (!naverUrl) return; const apiKey = import.meta.env.VITE_GEMINI_API_KEY; if (!apiKey) { alert("API Key Required"); return; } setIsCrawling(true); setCrawlStatus('Connecting...'); try { const result: any = await crawlNaverPlace(naverUrl, (msg) => setCrawlStatus(msg)); if (result.name) setName(result.name); if (result.description) setDescription(result.description); if (result.address) setAddress(result.address); if (result.category) setCategory(result.category); if (result.images && result.images.length > 0) { setCrawlStatus(`Fetched ${result.images.length} photos.`); updateImages(result.images); } else { setCrawlStatus('No photos found.'); } setTimeout(() => setCrawlStatus(''), 2000); } catch (e: any) { console.error("Crawl failed:", e); alert(e.message || "Failed to crawl Naver Place."); setCrawlStatus(''); } finally { setIsCrawling(false); } }; const handleStyleImageUpload = async (e: ChangeEvent) => { if (!e.target.files || e.target.files.length === 0) return; const file = e.target.files[0]; const apiKey = import.meta.env.VITE_GEMINI_API_KEY; if (!apiKey) { alert("API Key Required"); return; } setIsAnalyzingStyle(true); try { const cssCode = await extractTextEffectFromImage(file, apiKey); let styleTag = document.getElementById('custom-text-style'); if (!styleTag) { styleTag = document.createElement('style'); styleTag.id = 'custom-text-style'; document.head.appendChild(styleTag); } styleTag.textContent = cssCode; setCustomStyle(cssCode); setTextEffect('Custom'); alert("Style analysis complete!"); } catch (e) { console.error("Style analysis failed:", e); alert("Failed to analyze style."); } finally { setIsAnalyzingStyle(false); } }; const handleSubmit = (e: React.FormEvent | null, mode: 'Full' | 'AudioOnly' = 'Full') => { if (e) e.preventDefault(); if (name && description) { onSubmit({ name, description, images, audioMode, musicGenre, musicDuration, ttsConfig, textEffect, transitionEffect, visualStyle, aspectRatio, sourceUrl: naverUrl || mapLink || undefined, customStyleCSS: customStyle, creationMode: mode, address, category, language: contentLanguage, useAiImages, pensionCategories }); } else { alert(t('brandName') + " & " + t('brandDesc') + " required."); } }; // 단계별 완료 조건 const step1Complete = pensionCategories.length > 0; const step2Complete = step1Complete; const step3Complete = step2Complete && name.length > 0 && description.length > 0; const step4Complete = step3Complete && contentLanguage !== undefined; const step5Complete = step4Complete && textEffect !== undefined; const step6Complete = step5Complete; const canSubmit = step6Complete; // Progress calculation const completedSteps = [step1Complete, step2Complete, step3Complete, step4Complete, step5Complete, step6Complete].filter(Boolean).length; const progressPercent = Math.round((completedSteps / 6) * 100); // 온보딩 투어 스텝 const tourSteps = [ { target: '[data-tour="step1"]', title: '1단계: 펜션 유형 선택', description: '운영하시는 펜션의 유형을 선택해주세요. 복수 선택 가능하며, AI가 이 정보를 바탕으로 맞춤형 콘텐츠를 생성합니다.', placement: 'bottom' as const, }, { target: '[data-tour="step2"]', title: '2단계: 사진 업로드', description: '펜션 사진을 업로드하세요. 사진이 부족하면 AI가 자동으로 생성할 수도 있습니다. (선택사항)', placement: 'bottom' as const, }, { target: '[data-tour="step3"]', title: '3단계: 정보 자동 입력', description: '네이버 플레이스 URL 또는 Google Maps에서 검색하여 펜션 정보를 자동으로 가져올 수 있습니다.', placement: 'bottom' as const, }, { target: '[data-tour="step4"]', title: '4단계: 기본 정보 입력', description: '펜션 이름과 설명을 입력해주세요. 자동 입력을 사용했다면 이미 채워져 있을 거예요!', placement: 'bottom' as const, }, { target: '[data-tour="step5"]', title: '5단계: 언어 및 영상 설정', description: '영상 생성 언어, 비율(인스타/유튜브), 비주얼 스타일을 선택하세요.', placement: 'left' as const, }, { target: '[data-tour="step6"]', title: '6단계: 자막 스타일', description: '영상에 표시될 자막의 스타일을 선택하세요. 다양한 효과를 미리 볼 수 있습니다.', placement: 'top' as const, }, { target: '[data-tour="step7"]', title: '7단계: 오디오 설정', description: '노래, BGM, 성우 중 선택하고, 장르나 음성 설정을 커스터마이징하세요.', placement: 'top' as const, }, { target: '[data-tour="submit"]', title: '마지막: 영상 생성!', description: '모든 설정이 완료되면 이 버튼을 눌러 AI 영상 생성을 시작하세요!', placement: 'top' as const, }, ]; const handleTourComplete = () => { localStorage.setItem('castadTourCompleted', 'true'); setShowTour(false); }; const handleTourSkip = () => { localStorage.setItem('castadTourCompleted', 'true'); setShowTour(false); }; // Step indicator component const StepIndicator = ({ step, completed, active, locked }: { step: number; completed: boolean; active: boolean; locked: boolean }) => ( {locked ? : completed ? : step} ); return ( <> {showTour && ( )} {/* Header */} {t('appTitle')} {t('appSubtitle')} {/* Progress indicator */} {t('textStyle') === '자막 스타일' ? '진행률' : 'Progress'} {progressPercent}% handleSubmit(e, 'Full')} className="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start"> {/* [LEFT COLUMN] */} {/* STEP 1: Pension Category */} {t('pensionCategory')} {t('textStyle') === '자막 스타일' ? '복수 선택 가능' : 'Multiple selection allowed'} {step1Complete && {t('textStyle') === '자막 스타일' ? '완료' : 'Done'}} {PENSION_CATEGORIES.map((cat) => { const isSelected = pensionCategories.includes(cat.id); return ( { if (isSelected) { setPensionCategories(pensionCategories.filter(c => c !== cat.id)); } else { setPensionCategories([...pensionCategories, cat.id]); } }} className={cn( "flex flex-col items-center gap-1.5 p-3 rounded-lg border text-xs font-medium transition-all", isSelected ? "bg-primary/10 border-primary text-foreground shadow-sm" : "bg-card border-border text-muted-foreground hover:bg-muted hover:border-muted-foreground/30" )} > {t(cat.labelKey as any)} ); })} {/* STEP 2: Image Upload */} {t('uploadTitle')} {t('textStyle') === '자막 스타일' ? '최대 10장 (선택사항)' : 'Up to 10 images (optional)'} {previews.length > 0 && ( {previews.length}/10 )} 0 ? "border-primary/50 bg-primary/5" : "border-muted-foreground/30 hover:border-primary hover:bg-muted/50", !step1Complete && "pointer-events-none" )} > {t('uploadDesc')} {t('uploadMax')} {previews.length > 0 && ( {previews.map((src, idx) => ( removeImage(idx)} className="absolute top-1 right-1 p-1 bg-background/80 rounded-full opacity-0 group-hover:opacity-100 hover:bg-destructive transition-all" > ))} )} {/* AI Image Option */} {t('aiImageOption')} {t('aiImageDesc')} {/* STEP 3: Auto Data Search */} setOpenSections({...openSections, autoSearch: open})} > {t('autoInfoTitle')} {t('textStyle') === '자막 스타일' ? '네이버/구글에서 자동 가져오기' : 'Auto-fetch from Naver/Google'} {/* Naver */} N {t('naverPlace')} setNaverUrl(e.target.value)} placeholder="https://naver.me/..." disabled={!step2Complete} className="flex-1 h-9 text-sm" /> {isCrawling ? : t('fetchBtn')} {crawlStatus && {crawlStatus}} {t('textStyle') === '자막 스타일' ? '또는' : 'OR'} {/* Google */} {t('googleSearch')} setSearchQuery(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), handleSearch())} placeholder="Search place name..." disabled={!step2Complete} className="flex-1 h-9 text-sm" /> {isSearching ? : t('searchBtn')} {searchStatus && {searchStatus}} {/* STEP 4: Basic Info */} {t('textStyle') === '자막 스타일' ? '기본 정보' : 'Basic Info'} {t('textStyle') === '자막 스타일' ? '펜션 이름과 설명을 입력하세요' : 'Enter pension name and description'} {step3Complete && {t('textStyle') === '자막 스타일' ? '완료' : 'Done'}} {t('brandName')} setName(e.target.value)} placeholder="ex: Galaxy Pension" disabled={!step2Complete} className="h-11 text-base font-medium" /> {t('brandDesc')} setDescription(e.target.value)} placeholder={t('textStyle') === '자막 스타일' ? '펜션의 특징, 분위기, 시설 등을 설명해주세요...' : 'Describe features, atmosphere, facilities...'} rows={4} disabled={!step2Complete} className="resize-none" /> {/* [RIGHT COLUMN] */} {/* STEP 5: Content Language & Settings */} {t('textStyle') === '자막 스타일' ? '언어 및 영상 설정' : 'Language & Video Settings'} {/* Content Language Select */} {t('textStyle') === '자막 스타일' ? '생성 언어 (결과물)' : 'Output Language'} {(['KO', 'EN', 'JP', 'CN', 'TH', 'VN'] as Language[]).map((lang) => ( setContentLanguage(lang)} disabled={!step3Complete} className={cn( "flex items-center gap-2 px-3 py-2 rounded-lg border text-sm font-medium transition-all", contentLanguage === lang ? "bg-primary/10 border-primary text-foreground" : "bg-card border-border text-muted-foreground hover:bg-muted", !step3Complete && "pointer-events-none" )} > {lang} ))} {/* Ratio */} {t('ratio')} {(['16:9', '9:16'] as AspectRatio[]).map((r) => ( setAspectRatio(r)} disabled={!step3Complete} className={cn( "flex-1 py-2.5 rounded-lg border text-sm font-medium transition-all", aspectRatio === r ? "bg-primary/10 border-primary text-foreground" : "bg-card border-border text-muted-foreground hover:bg-muted", !step3Complete && "pointer-events-none" )} > {r} ))} {/* Visual Style */} {t('visualStyle')} setVisualStyle('Slideshow')} disabled={!step3Complete} className={cn( "flex-1 py-2.5 rounded-lg border text-sm font-medium transition-all", visualStyle === 'Slideshow' ? "bg-primary/10 border-primary text-foreground" : "bg-card border-border text-muted-foreground hover:bg-muted", !step3Complete && "pointer-events-none" )} > {t('visualSlide')} { if(prompt('Admin Password:')==='1234') setVisualStyle('Video'); }} disabled={!step3Complete} className={cn( "flex-1 py-2.5 rounded-lg border text-sm font-medium transition-all", visualStyle === 'Video' ? "bg-primary/10 border-primary text-foreground" : "bg-card border-border text-muted-foreground hover:bg-muted", !step3Complete && "pointer-events-none" )} > {t('visualVideo')} Admin only {/* STEP 6: Text & Transition */} {t('textStyle')} {textEffects.map((effect) => ( setTextEffect(effect.id)} disabled={!step4Complete} className={cn( "p-2 rounded-lg text-left border transition-all relative overflow-hidden", textEffect === effect.id ? "bg-primary/10 border-primary text-foreground ring-1 ring-primary" : "bg-card border-border text-muted-foreground hover:bg-muted", !step4Complete && "pointer-events-none" )} > {effect.label} Aa ))} {isAnalyzingStyle ? <> Analyzing...> : <> {t('customStyle')}> } {visualStyle === 'Slideshow' && ( {t('transitionEffect')} {transitionOptions.map((opt) => ( setTransitionEffect(opt.id)} disabled={!step4Complete} className={cn( "p-2 rounded-lg text-center border text-xs font-medium transition-all", transitionEffect === opt.id ? "bg-primary/10 border-primary text-foreground" : "bg-card border-border text-muted-foreground hover:bg-muted", !step4Complete && "pointer-events-none" )} > {t(opt.labelKey)} ))} )} {/* STEP 7: Audio Settings */} {t('audioSettings')} {/* Audio Mode Tabs */} {(['Song', 'Instrumental', 'Narration'] as AudioMode[]).map((mode) => ( setAudioMode(mode)} disabled={!step5Complete} className={cn( "flex-1 py-2 rounded-md text-xs font-medium transition-all", audioMode === mode ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground", !step5Complete && "pointer-events-none" )} > {mode === 'Song' ? t('song') : mode === 'Instrumental' ? t('bgm') : t('narration')} ))} {audioMode === 'Narration' ? ( {t('gender')} {(['Female', 'Male'] as TTSGender[]).map(g => ( setTTSConfig({...ttsConfig, gender: g})} disabled={!step5Complete} className={cn( "flex-1 py-1.5 rounded text-xs font-medium transition-all", ttsConfig.gender === g ? "bg-background text-foreground shadow-sm" : "text-muted-foreground" )} > {g === 'Female' ? 'F' : 'M'} ))} {t('age')} {(['Young', 'Middle'] as TTSAge[]).map(a => ( setTTSConfig({...ttsConfig, age: a})} disabled={!step5Complete} className={cn( "flex-1 py-1.5 rounded text-xs font-medium transition-all", ttsConfig.age === a ? "bg-background text-foreground shadow-sm" : "text-muted-foreground" )} > {a} ))} {t('tone')} {(['Bright', 'Calm', 'Energetic', 'Professional'] as TTSTone[]).map(tone => ( setTTSConfig({...ttsConfig, tone})} disabled={!step5Complete} className={cn( "py-1.5 rounded text-[10px] border font-medium transition-all", ttsConfig.tone === tone ? "bg-primary/10 border-primary text-foreground" : "border-border bg-card text-muted-foreground hover:bg-muted" )} > {tone} ))} ) : ( Genre {activeGenres.map((g) => ( setMusicGenre(g)} disabled={!step5Complete} className={cn( "p-2.5 rounded-lg text-sm font-medium border transition-all", musicGenre === g ? "bg-primary/10 border-primary text-foreground" : "border-border bg-card text-muted-foreground hover:bg-muted", !step5Complete && "pointer-events-none" )} > {g === 'Auto' ? Auto : g } ))} )} {/* Submit Buttons */} handleSubmit(null, 'AudioOnly')} disabled={isSubmitting || !canSubmit} className="flex-1 h-14" > {isSubmitting ? ( ) : canSubmit ? ( ) : ( )} {t('submitAudio')} {isSubmitting ? ( ) : canSubmit ? ( ) : ( )} {isSubmitting ? t('processing') : canSubmit ? t('submitVideo') : (t('textStyle') === '자막 스타일' ? '모든 단계를 완료하세요' : 'Complete all steps') } > ); }; export default InputForm;
{t('appSubtitle')}
{t('uploadDesc')}
{t('uploadMax')}
{t('aiImageOption')}
{t('aiImageDesc')}
{crawlStatus}
{searchStatus}
Admin only