208 lines
6.7 KiB
TypeScript
208 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter, useParams } from "next/navigation";
|
|
import { useAuth } from "@/contexts/AuthContext";
|
|
import { productsApi, ProductAdmin, getUploadUrl } from "@/lib/api";
|
|
import ProductForm from "@/components/admin/ProductForm";
|
|
import { Loader2, Upload, Trash2 } from "lucide-react";
|
|
import YouTubeVideoManager from "@/components/admin/YouTubeVideoManager";
|
|
|
|
export default function EditProductPage() {
|
|
const { token } = useAuth();
|
|
const router = useRouter();
|
|
const params = useParams();
|
|
const productId = Number(params.id);
|
|
|
|
const [product, setProduct] = useState<ProductAdmin | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
|
|
const fetchProduct = async () => {
|
|
if (!token) return;
|
|
try {
|
|
const data = await productsApi.adminGetOne(productId, token);
|
|
setProduct(data);
|
|
} catch (err) {
|
|
alert("제품을 찾을 수 없습니다.");
|
|
router.push("/admin/products");
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchProduct();
|
|
}, [token, productId]);
|
|
|
|
const handleSubmit = async (data: any) => {
|
|
if (!token) return;
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
await productsApi.adminUpdate(productId, data, token);
|
|
alert("저장되었습니다.");
|
|
fetchProduct();
|
|
} catch (err) {
|
|
alert("저장에 실패했습니다.");
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleImageUpload = async (
|
|
e: React.ChangeEvent<HTMLInputElement>,
|
|
isMain: boolean
|
|
) => {
|
|
if (!token || !e.target.files?.[0]) return;
|
|
|
|
setIsUploading(true);
|
|
try {
|
|
await productsApi.adminUploadImage(
|
|
productId,
|
|
e.target.files[0],
|
|
isMain,
|
|
token
|
|
);
|
|
fetchProduct();
|
|
} catch (err) {
|
|
alert("이미지 업로드에 실패했습니다.");
|
|
} finally {
|
|
setIsUploading(false);
|
|
e.target.value = "";
|
|
}
|
|
};
|
|
|
|
const handleDeleteImage = async (imageId: number) => {
|
|
if (!token || !confirm("이미지를 삭제하시겠습니까?")) return;
|
|
|
|
try {
|
|
await productsApi.adminDeleteImage(imageId, token);
|
|
fetchProduct();
|
|
} catch (err) {
|
|
alert("이미지 삭제에 실패했습니다.");
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<Loader2 className="w-8 h-8 animate-spin text-[#3B82F6]" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!product) return null;
|
|
|
|
return (
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-8">제품 수정</h1>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
{/* Form */}
|
|
<div className="lg:col-span-2">
|
|
<ProductForm
|
|
initialData={product}
|
|
onSubmit={handleSubmit}
|
|
isSubmitting={isSubmitting}
|
|
/>
|
|
</div>
|
|
|
|
{/* Image Upload */}
|
|
<div className="space-y-6">
|
|
{/* Main Image */}
|
|
<div className="bg-white rounded-xl p-6 shadow-sm">
|
|
<h3 className="font-semibold text-gray-900 mb-4">대표 이미지</h3>
|
|
{product.main_image ? (
|
|
<div className="relative">
|
|
<img
|
|
src={getUploadUrl(product.main_image)}
|
|
alt="Main"
|
|
className="w-full h-48 object-cover rounded-lg"
|
|
/>
|
|
<label className="absolute bottom-2 right-2 px-3 py-1.5 bg-white/90 text-sm font-medium rounded-lg cursor-pointer hover:bg-white transition-colors">
|
|
변경
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
className="hidden"
|
|
onChange={(e) => handleImageUpload(e, true)}
|
|
disabled={isUploading}
|
|
/>
|
|
</label>
|
|
</div>
|
|
) : (
|
|
<label className="flex flex-col items-center justify-center h-48 border-2 border-dashed border-gray-200 rounded-lg cursor-pointer hover:border-[#3B82F6] transition-colors">
|
|
{isUploading ? (
|
|
<Loader2 className="w-8 h-8 animate-spin text-[#3B82F6]" />
|
|
) : (
|
|
<>
|
|
<Upload className="w-8 h-8 text-gray-400 mb-2" />
|
|
<span className="text-sm text-gray-500">
|
|
클릭하여 업로드
|
|
</span>
|
|
</>
|
|
)}
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
className="hidden"
|
|
onChange={(e) => handleImageUpload(e, true)}
|
|
disabled={isUploading}
|
|
/>
|
|
</label>
|
|
)}
|
|
</div>
|
|
|
|
{/* Additional Images */}
|
|
<div className="bg-white rounded-xl p-6 shadow-sm">
|
|
<h3 className="font-semibold text-gray-900 mb-4">추가 이미지</h3>
|
|
<div className="grid grid-cols-2 gap-3 mb-4">
|
|
{product.images?.map((img) => (
|
|
<div key={img.id} className="relative group">
|
|
<img
|
|
src={getUploadUrl(img.image_url)}
|
|
alt=""
|
|
className="w-full h-24 object-cover rounded-lg"
|
|
/>
|
|
<button
|
|
onClick={() => handleDeleteImage(img.id)}
|
|
className="absolute top-1 right-1 p-1 bg-red-500 text-white rounded opacity-0 group-hover:opacity-100 transition-opacity"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<label className="flex items-center justify-center py-3 border-2 border-dashed border-gray-200 rounded-lg cursor-pointer hover:border-[#3B82F6] transition-colors">
|
|
{isUploading ? (
|
|
<Loader2 className="w-5 h-5 animate-spin text-[#3B82F6]" />
|
|
) : (
|
|
<>
|
|
<Upload className="w-5 h-5 text-gray-400 mr-2" />
|
|
<span className="text-sm text-gray-500">이미지 추가</span>
|
|
</>
|
|
)}
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
className="hidden"
|
|
onChange={(e) => handleImageUpload(e, false)}
|
|
disabled={isUploading}
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
{/* YouTube Videos */}
|
|
<YouTubeVideoManager
|
|
entityType="product"
|
|
entityId={productId}
|
|
entityTitle={product.name_ko}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|