o2o-infinith-frontend/src/features/distribution/pages/DistributionPage.tsx

357 lines
17 KiB
TypeScript

import { useState, useCallback } from 'react';
import { motion } from 'motion/react';
import {
VideoFilled,
ShareFilled,
} from '@/shared/icons/FilledIcons';
import { Button } from '@/shared/ui/button';
import { Input } from '@/shared/ui/input';
import { Textarea } from '@/shared/ui/textarea';
import { MOCK_CONTENT, INITIAL_CHANNELS } from '../data/distributionMocks';
// ─── 컴포넌트 ───
export default function DistributionPage() {
const [channels, setChannels] = useState(INITIAL_CHANNELS);
const [title, setTitle] = useState(MOCK_CONTENT.title);
const [description, setDescription] = useState(MOCK_CONTENT.description);
const [tags] = useState(MOCK_CONTENT.tags);
const [scheduleMode, setScheduleMode] = useState<'now' | 'scheduled'>('now');
const [scheduleDate, setScheduleDate] = useState('');
const [scheduleHour, setScheduleHour] = useState(9);
const [scheduleMinute, setScheduleMinute] = useState(0);
const [schedulePeriod, setSchedulePeriod] = useState<'AM' | 'PM'>('AM');
const [isPublishing, setIsPublishing] = useState(false);
const selectedChannels = channels.filter(c => c.connected && c.selected);
const allPublished = selectedChannels.length > 0 && selectedChannels.every(c => c.status === 'published');
const toggleChannel = useCallback((id: string) => {
setChannels(prev => prev.map(c =>
c.id === id && c.connected ? { ...c, selected: !c.selected } : c
));
}, []);
const handlePublish = useCallback(() => {
setIsPublishing(true);
// 순차 발행 시뮬레이션
const selected = channels.filter(c => c.connected && c.selected);
selected.forEach((ch, i) => {
setTimeout(() => {
setChannels(prev => prev.map(c =>
c.id === ch.id ? { ...c, status: 'publishing' } : c
));
}, i * 1500);
setTimeout(() => {
setChannels(prev => prev.map(c =>
c.id === ch.id ? { ...c, status: 'published' } : c
));
if (i === selected.length - 1) setIsPublishing(false);
}, (i + 1) * 1500 + 500);
});
}, [channels]);
return (
<div className="pt-20 min-h-screen">
{/* Header */}
<div className="bg-brand-navy py-14 px-6 relative overflow-hidden">
<div className="absolute top-0 right-0 w-[500px] h-[500px] rounded-full bg-brand-purple-vivid/10 blur-[120px]" />
<div className="max-w-5xl mx-auto relative">
<p className="text-xs font-semibold text-purple-300 tracking-widest uppercase mb-3">Content Distribution</p>
<h1 className="font-serif text-3xl md:text-4xl font-bold text-white mb-3">
</h1>
<p className="text-purple-200/70 max-w-xl">
.
</p>
</div>
</div>
<div className="max-w-5xl mx-auto px-6 py-10">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left: Content Preview + Meta */}
<div className="lg:col-span-1">
<h3 className="font-serif font-bold text-xl text-brand-navy mb-4"></h3>
{/* Video Preview */}
<div className="w-full aspect-[9/16] max-w-[220px] mx-auto rounded-2xl bg-gradient-to-br from-brand-tint-purple via-white to-brand-earth-bg border border-slate-200 flex flex-col items-center justify-center mb-6">
<VideoFilled size={32} className="text-brand-purple-soft mb-3" />
<p className="text-xs text-slate-500">{MOCK_CONTENT.aspectRatio}</p>
<p className="text-xs text-slate-400 mt-1">{MOCK_CONTENT.duration}</p>
</div>
{/* Title */}
<div className="mb-4">
<label className="text-xs font-medium text-slate-600 mb-1 block"></label>
<Input
value={title}
onChange={e => setTitle(e.target.value)}
className="w-full px-4 py-3 h-auto rounded-xl border-slate-200 text-sm text-slate-700 focus-visible:border-brand-purple-vivid focus-visible:ring-brand-purple-vivid/20"
/>
</div>
{/* Description */}
<div className="mb-4">
<label className="text-xs font-medium text-slate-600 mb-1 block"></label>
<Textarea
value={description}
onChange={e => setDescription(e.target.value)}
rows={4}
className="w-full px-4 py-3 rounded-xl border-slate-200 text-sm text-slate-700 resize-y focus-visible:border-brand-purple-vivid focus-visible:ring-brand-purple-vivid/20"
/>
</div>
{/* Tags */}
<div>
<label className="text-xs font-medium text-slate-600 mb-2 block"></label>
<div className="flex flex-wrap gap-2">
{tags.map(tag => (
<span
key={tag}
className="px-3 py-1 rounded-full bg-brand-tint-purple text-brand-purple-muted text-xs font-medium border border-brand-tint-lavender"
>
#{tag}
</span>
))}
</div>
</div>
</div>
{/* Right: Channel Selection + Schedule + Publish */}
<div className="lg:col-span-2">
{/* Channel Selection */}
<h3 className="font-serif font-bold text-xl text-brand-navy mb-4"> </h3>
<div className="space-y-3 mb-8">
{channels.map(ch => {
const Icon = ch.icon;
return (
<motion.div
key={ch.id}
layout
className={`flex items-center gap-4 p-4 rounded-2xl border-2 transition-all ${
!ch.connected
? 'border-slate-100 bg-slate-50/50 opacity-50'
: ch.selected
? 'border-brand-purple-vivid bg-brand-tint-purple/20 shadow-[3px_4px_12px_rgba(108,92,231,0.08)]'
: 'border-slate-100 bg-white shadow-[3px_4px_12px_rgba(0,0,0,0.04)] hover:border-brand-tint-lavender'
}`}
>
{/* Checkbox */}
<Button
variant="ghost"
size="icon-sm"
onClick={() => toggleChannel(ch.id)}
disabled={!ch.connected}
className={`w-5 h-5 rounded border-2 flex items-center justify-center shrink-0 p-0 transition-all hover:bg-transparent ${
ch.selected && ch.connected
? 'border-brand-purple-vivid bg-brand-purple-vivid hover:bg-brand-purple-vivid'
: 'border-slate-300 bg-white'
} disabled:cursor-not-allowed`}
>
{ch.selected && ch.connected && (
<svg width="10" height="10" viewBox="0 0 14 14" fill="none">
<path d="M2 7L5.5 10.5L12 3.5" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)}
</Button>
{/* Icon */}
<div
className="w-10 h-10 rounded-xl flex items-center justify-center shrink-0"
style={{ backgroundColor: ch.bgColor }}
>
<Icon size={20} style={{ color: ch.brandColor }} />
</div>
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-semibold text-brand-navy">{ch.name}</span>
{!ch.connected && (
<span className="px-2 py-1 rounded-full bg-brand-earth-bg text-brand-earth text-xs font-semibold border border-brand-earth-soft">
</span>
)}
</div>
<p className="text-xs text-slate-400">{ch.format}</p>
</div>
{/* Status */}
<div className="shrink-0">
{ch.status === 'publishing' && (
<div className="w-5 h-5 border-2 border-brand-purple-vivid border-t-transparent rounded-full animate-spin" />
)}
{ch.status === 'published' && (
<div className="w-6 h-6 rounded-full bg-brand-purple-vivid flex items-center justify-center">
<svg width="12" height="12" viewBox="0 0 14 14" fill="none">
<path d="M2 7L5.5 10.5L12 3.5" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
)}
{ch.status === 'failed' && (
<div className="w-6 h-6 rounded-full bg-brand-rose-bg flex items-center justify-center border border-brand-rose-soft">
<span className="text-brand-rose text-xs font-bold">!</span>
</div>
)}
</div>
</motion.div>
);
})}
</div>
{/* Schedule */}
<h3 className="font-serif font-bold text-xl text-brand-navy mb-4"> </h3>
<div className="flex gap-2 mb-4">
{([
{ key: 'now' as const, label: '즉시 배포' },
{ key: 'scheduled' as const, label: '예약 배포' },
]).map(opt => (
<Button
key={opt.key}
variant="ghost"
onClick={() => setScheduleMode(opt.key)}
className={`px-5 py-3 h-auto rounded-full text-sm font-medium transition-all ${
scheduleMode === opt.key
? 'bg-gradient-to-r from-brand-purple to-brand-purple-deep text-white shadow-md hover:from-brand-purple hover:to-brand-purple-deep hover:text-white'
: 'bg-white border border-slate-200 text-slate-600 hover:bg-slate-50'
}`}
>
{opt.label}
</Button>
))}
</div>
{scheduleMode === 'scheduled' && (
<div className="mb-6 space-y-4">
{/* Date */}
<div>
<label className="text-xs font-medium text-slate-600 mb-2 block"></label>
<Input
type="date"
value={scheduleDate}
onChange={e => setScheduleDate(e.target.value)}
className="w-full px-4 py-3 h-auto rounded-xl border-slate-200 text-sm text-slate-700 focus-visible:border-brand-purple-vivid appearance-none"
/>
</div>
{/* Custom Time Picker */}
<div>
<label className="text-xs font-medium text-slate-600 mb-2 block"></label>
<div className="flex items-center gap-3">
{/* Hour */}
<div className="flex items-center gap-1">
<Button
variant="outline"
size="icon-sm"
onClick={() => setScheduleHour(h => h <= 1 ? 12 : h - 1)}
className="w-8 h-8 rounded-lg bg-slate-50 border-slate-200 text-slate-500 hover:bg-slate-100"
>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 6h8M6 2l-4 4 4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>
</Button>
<div className="w-12 h-10 rounded-xl bg-brand-tint-purple border border-brand-tint-lavender flex items-center justify-center text-lg font-semibold text-brand-navy">
{String(scheduleHour).padStart(2, '0')}
</div>
<Button
variant="outline"
size="icon-sm"
onClick={() => setScheduleHour(h => h >= 12 ? 1 : h + 1)}
className="w-8 h-8 rounded-lg bg-slate-50 border-slate-200 text-slate-500 hover:bg-slate-100"
>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M10 6H2M6 10l4-4-4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>
</Button>
</div>
<span className="text-xl font-bold text-slate-300">:</span>
{/* Minute */}
<div className="flex items-center gap-1">
<Button
variant="outline"
size="icon-sm"
onClick={() => setScheduleMinute(m => m <= 0 ? 55 : m - 5)}
className="w-8 h-8 rounded-lg bg-slate-50 border-slate-200 text-slate-500 hover:bg-slate-100"
>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M2 6h8M6 2l-4 4 4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>
</Button>
<div className="w-12 h-10 rounded-xl bg-brand-tint-purple border border-brand-tint-lavender flex items-center justify-center text-lg font-semibold text-brand-navy">
{String(scheduleMinute).padStart(2, '0')}
</div>
<Button
variant="outline"
size="icon-sm"
onClick={() => setScheduleMinute(m => m >= 55 ? 0 : m + 5)}
className="w-8 h-8 rounded-lg bg-slate-50 border-slate-200 text-slate-500 hover:bg-slate-100"
>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none"><path d="M10 6H2M6 10l4-4-4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /></svg>
</Button>
</div>
{/* AM/PM */}
<div className="flex rounded-xl overflow-hidden border border-slate-200">
{(['AM', 'PM'] as const).map(p => (
<Button
key={p}
variant="ghost"
onClick={() => setSchedulePeriod(p)}
className={`px-4 py-2 h-auto rounded-none text-sm font-medium transition-all ${
schedulePeriod === p
? 'bg-gradient-to-r from-brand-purple to-brand-purple-deep text-white hover:from-brand-purple hover:to-brand-purple-deep hover:text-white'
: 'bg-white text-slate-500 hover:bg-slate-50'
}`}
>
{p}
</Button>
))}
</div>
</div>
</div>
</div>
)}
{/* Publish Button */}
{!allPublished ? (
<Button
onClick={handlePublish}
disabled={selectedChannels.length === 0 || isPublishing}
className="w-full h-auto flex items-center justify-center gap-2 py-4 rounded-full bg-gradient-to-r from-brand-purple to-brand-purple-deep text-white text-sm font-medium shadow-md hover:shadow-lg transition-all disabled:opacity-40 disabled:cursor-not-allowed"
>
{isPublishing ? (
<>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
... ({selectedChannels.filter(c => c.status === 'published').length}/{selectedChannels.length})
</>
) : (
<>
<ShareFilled size={18} className="text-white" />
{selectedChannels.length} {scheduleMode === 'now' ? '즉시 배포' : '예약 배포'}
</>
)}
</Button>
) : (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="w-full py-6 rounded-2xl bg-brand-tint-purple border border-brand-tint-lavender text-center"
>
<div className="w-12 h-12 rounded-full bg-brand-purple-vivid flex items-center justify-center mx-auto mb-3">
<svg width="20" height="20" viewBox="0 0 14 14" fill="none">
<path d="M2 7L5.5 10.5L12 3.5" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
<p className="text-lg font-semibold text-brand-navy mb-1"> </p>
<p className="text-sm text-brand-purple-muted">
{selectedChannels.length}
</p>
</motion.div>
)}
</div>
</div>
</div>
</div>
);
}