fix: Remove car_id property from adminAddVehicle call to fix TypeScript error
This commit is contained in:
450
temp_youtube_manager.tsx
Normal file
450
temp_youtube_manager.tsx
Normal file
@@ -0,0 +1,450 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user