Files
AutonetSellCar/temp_youtube_manager.tsx

451 lines
15 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useAuth } from "@/contexts/AuthContext";
import { contentVideosApi, ContentVideo, ContentVideoCreate } from "@/lib/api";
import {
Youtube,
Plus,
Trash2,
Edit,
GripVertical,
X,
ExternalLink,
Eye,
EyeOff,
} from "lucide-react";
interface YouTubeVideoManagerProps {
entityType: "project" | "solution" | "product";
entityId: number;
entityTitle?: string;
}
export default function YouTubeVideoManager({
entityType,
entityId,
entityTitle,
}: YouTubeVideoManagerProps) {
const { token } = useAuth();
const [videos, setVideos] = useState<ContentVideo[]>([]);
const [loading, setLoading] = useState(true);
const [showModal, setShowModal] = useState(false);
const [editingVideo, setEditingVideo] = useState<ContentVideo | null>(null);
const [formData, setFormData] = useState<Partial<ContentVideoCreate>>({
youtube_id: "",
title_ko: "",
title_en: "",
title_ja: "",
title_zh: "",
description_ko: "",
entity_type: entityType,
entity_id: entityId,
is_active: true,
});
const fetchVideos = async () => {
if (!token) return;
try {
const data = await contentVideosApi.adminList(token, entityType, entityId);
setVideos(data);
} catch (error) {
console.error("Failed to fetch videos:", error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchVideos();
}, [token, entityType, entityId]);
const extractYoutubeId = (input: string): string => {
if (!input) return "";
// Already a plain ID
if (/^[a-zA-Z0-9_-]{11}$/.test(input)) return input;
// Extract from URL
const match = input.match(
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/
);
return match ? match[1] : input;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!token) return;
try {
const youtubeId = extractYoutubeId(formData.youtube_id || "");
const payload = {
...formData,
youtube_id: youtubeId,
entity_type: entityType,
entity_id: entityId,
};
if (editingVideo) {
await contentVideosApi.adminUpdate(editingVideo.id, payload, token);
} else {
await contentVideosApi.adminCreate(payload as ContentVideoCreate, token);
}
setShowModal(false);
setEditingVideo(null);
resetForm();
fetchVideos();
} catch (error) {
console.error("Failed to save video:", error);
alert("저장에 실패했습니다.");
}
};
const handleDelete = async (id: number) => {
if (!token || !confirm("이 영상을 삭제하시겠습니까?")) return;
try {
await contentVideosApi.adminDelete(id, token);
fetchVideos();
} catch (error) {
console.error("Failed to delete video:", error);
}
};
const handleToggleActive = async (video: ContentVideo) => {
if (!token) return;
try {
await contentVideosApi.adminUpdate(
video.id,
{ is_active: !video.is_active },
token
);
fetchVideos();
} catch (error) {
console.error("Failed to toggle active:", error);
}
};
const resetForm = () => {
setFormData({
youtube_id: "",
title_ko: "",
title_en: "",
title_ja: "",
title_zh: "",
description_ko: "",
entity_type: entityType,
entity_id: entityId,
is_active: true,
});
};
const openEditModal = (video: ContentVideo) => {
setEditingVideo(video);
setFormData({
youtube_id: video.youtube_id,
title_ko: video.title_ko,
title_en: video.title_en || "",
title_ja: video.title_ja || "",
title_zh: video.title_zh || "",
description_ko: video.description_ko || "",
is_active: video.is_active,
});
setShowModal(true);
};
const openAddModal = () => {
setEditingVideo(null);
resetForm();
setShowModal(true);
};
return (
<div className="bg-white rounded-xl shadow-sm p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Youtube className="w-5 h-5 text-red-500" />
<h3 className="text-lg font-semibold text-gray-900">YouTube </h3>
{entityTitle && (
<span className="text-sm text-gray-500">- {entityTitle}</span>
)}
</div>
<button
onClick={openAddModal}
className="flex items-center gap-2 px-3 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition text-sm"
>
<Plus className="w-4 h-4" />
</button>
</div>
{loading ? (
<div className="text-center py-8 text-gray-500"> ...</div>
) : videos.length === 0 ? (
<div className="text-center py-8 text-gray-500 border-2 border-dashed rounded-lg">
<Youtube className="w-12 h-12 mx-auto mb-2 text-gray-300" />
<p> </p>
<button
onClick={openAddModal}
className="mt-2 text-red-500 hover:underline text-sm"
>
</button>
</div>
) : (
<div className="space-y-3">
{videos.map((video) => (
<div
key={video.id}
className={`flex items-center gap-4 p-3 rounded-lg border ${
video.is_active ? "bg-white" : "bg-gray-50 opacity-60"
}`}
>
<GripVertical className="w-5 h-5 text-gray-400 cursor-move" />
{/* Thumbnail */}
<div className="relative w-32 h-20 flex-shrink-0">
<img
src={video.thumbnail_url}
alt={video.title_ko}
className="w-full h-full object-cover rounded"
onError={(e) => {
(e.target as HTMLImageElement).src = `https://img.youtube.com/vi/${video.youtube_id}/hqdefault.jpg`;
}}
/>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-10 h-10 bg-red-500/80 rounded-full flex items-center justify-center">
<div className="w-0 h-0 border-l-[12px] border-l-white border-y-[8px] border-y-transparent ml-1" />
</div>
</div>
</div>
{/* Info */}
<div className="flex-1 min-w-0">
<h4 className="font-medium text-gray-900 truncate">
{video.title_ko}
</h4>
{video.title_en && (
<p className="text-sm text-gray-500 truncate">{video.title_en}</p>
)}
<p className="text-xs text-gray-400 mt-1">
ID: {video.youtube_id}
</p>
</div>
{/* Actions */}
<div className="flex items-center gap-2">
<a
href={video.youtube_url}
target="_blank"
rel="noopener noreferrer"
className="p-2 text-gray-500 hover:text-blue-500 transition"
title="YouTube에서 보기"
>
<ExternalLink className="w-4 h-4" />
</a>
<button
onClick={() => handleToggleActive(video)}
className={`p-2 transition ${
video.is_active
? "text-green-500 hover:text-green-600"
: "text-gray-400 hover:text-gray-600"
}`}
title={video.is_active ? "활성" : "비활성"}
>
{video.is_active ? (
<Eye className="w-4 h-4" />
) : (
<EyeOff className="w-4 h-4" />
)}
</button>
<button
onClick={() => openEditModal(video)}
className="p-2 text-gray-500 hover:text-blue-500 transition"
title="수정"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(video.id)}
className="p-2 text-gray-500 hover:text-red-500 transition"
title="삭제"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
)}
{/* Modal */}
{showModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto">
<div className="flex items-center justify-between p-4 border-b">
<h3 className="text-lg font-semibold">
{editingVideo ? "영상 수정" : "영상 추가"}
</h3>
<button
onClick={() => setShowModal(false)}
className="p-1 hover:bg-gray-100 rounded"
>
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-4 space-y-4">
{/* YouTube URL/ID */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
YouTube URL ID *
</label>
<input
type="text"
value={formData.youtube_id || ""}
onChange={(e) =>
setFormData({ ...formData, youtube_id: e.target.value })
}
placeholder="https://youtube.com/watch?v=... 또는 영상 ID"
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
required
/>
<p className="text-xs text-gray-500 mt-1">
URL이나 ID(11)
</p>
</div>
{/* Preview */}
{formData.youtube_id && (
<div className="relative aspect-video bg-gray-100 rounded-lg overflow-hidden">
<img
src={`https://img.youtube.com/vi/${extractYoutubeId(
formData.youtube_id
)}/maxresdefault.jpg`}
alt="Preview"
className="w-full h-full object-cover"
onError={(e) => {
(e.target as HTMLImageElement).src = `https://img.youtube.com/vi/${extractYoutubeId(
formData.youtube_id || ""
)}/hqdefault.jpg`;
}}
/>
</div>
)}
{/* Title KO */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
() *
</label>
<input
type="text"
value={formData.title_ko || ""}
onChange={(e) =>
setFormData({ ...formData, title_ko: e.target.value })
}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
required
/>
</div>
{/* Title EN */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Title (English)
</label>
<input
type="text"
value={formData.title_en || ""}
onChange={(e) =>
setFormData({ ...formData, title_en: e.target.value })
}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
/>
</div>
{/* Title JA */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
()
</label>
<input
type="text"
value={formData.title_ja || ""}
onChange={(e) =>
setFormData({ ...formData, title_ja: e.target.value })
}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
/>
</div>
{/* Title ZH */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
()
</label>
<input
type="text"
value={formData.title_zh || ""}
onChange={(e) =>
setFormData({ ...formData, title_zh: e.target.value })
}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
/>
</div>
{/* Description */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
()
</label>
<textarea
value={formData.description_ko || ""}
onChange={(e) =>
setFormData({ ...formData, description_ko: e.target.value })
}
rows={3}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
/>
</div>
{/* Active Toggle */}
<div className="flex items-center gap-2">
<input
type="checkbox"
id="is_active"
checked={formData.is_active}
onChange={(e) =>
setFormData({ ...formData, is_active: e.target.checked })
}
className="w-4 h-4 text-red-500 rounded"
/>
<label htmlFor="is_active" className="text-sm text-gray-700">
( )
</label>
</div>
{/* Buttons */}
<div className="flex justify-end gap-2 pt-4">
<button
type="button"
onClick={() => setShowModal(false)}
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition"
>
</button>
<button
type="submit"
className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition"
>
{editingVideo ? "수정" : "추가"}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}