278 lines
9.4 KiB
TypeScript
278 lines
9.4 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useState } from "react";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { Input } from "@/components/ui/input";
|
||
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||
|
|
import {
|
||
|
|
DropdownMenu,
|
||
|
|
DropdownMenuContent,
|
||
|
|
DropdownMenuItem,
|
||
|
|
DropdownMenuTrigger,
|
||
|
|
} from "@/components/ui/dropdown-menu";
|
||
|
|
import {
|
||
|
|
AlertDialog,
|
||
|
|
AlertDialogAction,
|
||
|
|
AlertDialogCancel,
|
||
|
|
AlertDialogContent,
|
||
|
|
AlertDialogDescription,
|
||
|
|
AlertDialogFooter,
|
||
|
|
AlertDialogHeader,
|
||
|
|
AlertDialogTitle,
|
||
|
|
} from "@/components/ui/alert-dialog";
|
||
|
|
import {
|
||
|
|
Dialog,
|
||
|
|
DialogContent,
|
||
|
|
DialogDescription,
|
||
|
|
DialogFooter,
|
||
|
|
DialogHeader,
|
||
|
|
DialogTitle,
|
||
|
|
} from "@/components/ui/dialog";
|
||
|
|
import { Label } from "@/components/ui/label";
|
||
|
|
import { Search, MoreVertical, Loader2 } from "lucide-react";
|
||
|
|
|
||
|
|
interface YardLayout {
|
||
|
|
id: number;
|
||
|
|
name: string;
|
||
|
|
description: string;
|
||
|
|
placement_count: number;
|
||
|
|
updated_at: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface YardLayoutListProps {
|
||
|
|
layouts: YardLayout[];
|
||
|
|
isLoading: boolean;
|
||
|
|
onSelect: (layout: YardLayout) => void;
|
||
|
|
onDelete: (id: number) => Promise<void>;
|
||
|
|
onDuplicate: (id: number, newName: string) => Promise<void>;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function YardLayoutList({ layouts, isLoading, onSelect, onDelete, onDuplicate }: YardLayoutListProps) {
|
||
|
|
const [searchText, setSearchText] = useState("");
|
||
|
|
const [sortOrder, setSortOrder] = useState<"recent" | "name">("recent");
|
||
|
|
const [deleteTarget, setDeleteTarget] = useState<YardLayout | null>(null);
|
||
|
|
const [duplicateTarget, setDuplicateTarget] = useState<YardLayout | null>(null);
|
||
|
|
const [duplicateName, setDuplicateName] = useState("");
|
||
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
||
|
|
const [isDuplicating, setIsDuplicating] = useState(false);
|
||
|
|
|
||
|
|
// 검색 필터링
|
||
|
|
const filteredLayouts = layouts.filter((layout) => {
|
||
|
|
if (!searchText) return true;
|
||
|
|
return (
|
||
|
|
layout.name.toLowerCase().includes(searchText.toLowerCase()) ||
|
||
|
|
layout.description?.toLowerCase().includes(searchText.toLowerCase())
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
// 정렬
|
||
|
|
const sortedLayouts = [...filteredLayouts].sort((a, b) => {
|
||
|
|
if (sortOrder === "recent") {
|
||
|
|
return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
|
||
|
|
} else {
|
||
|
|
return a.name.localeCompare(b.name);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 날짜 포맷팅
|
||
|
|
const formatDate = (dateString: string) => {
|
||
|
|
const date = new Date(dateString);
|
||
|
|
return date.toLocaleString("ko-KR", {
|
||
|
|
year: "numeric",
|
||
|
|
month: "2-digit",
|
||
|
|
day: "2-digit",
|
||
|
|
hour: "2-digit",
|
||
|
|
minute: "2-digit",
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
// 삭제 확인
|
||
|
|
const handleDeleteConfirm = async () => {
|
||
|
|
if (!deleteTarget) return;
|
||
|
|
|
||
|
|
setIsDeleting(true);
|
||
|
|
try {
|
||
|
|
await onDelete(deleteTarget.id);
|
||
|
|
setDeleteTarget(null);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("삭제 실패:", error);
|
||
|
|
} finally {
|
||
|
|
setIsDeleting(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 복제 실행
|
||
|
|
const handleDuplicateConfirm = async () => {
|
||
|
|
if (!duplicateTarget || !duplicateName.trim()) return;
|
||
|
|
|
||
|
|
setIsDuplicating(true);
|
||
|
|
try {
|
||
|
|
await onDuplicate(duplicateTarget.id, duplicateName);
|
||
|
|
setDuplicateTarget(null);
|
||
|
|
setDuplicateName("");
|
||
|
|
} catch (error) {
|
||
|
|
console.error("복제 실패:", error);
|
||
|
|
} finally {
|
||
|
|
setIsDuplicating(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// 복제 모달 열기
|
||
|
|
const handleDuplicateClick = (layout: YardLayout) => {
|
||
|
|
setDuplicateTarget(layout);
|
||
|
|
setDuplicateName(`${layout.name} (복사본)`);
|
||
|
|
};
|
||
|
|
|
||
|
|
if (isLoading) {
|
||
|
|
return (
|
||
|
|
<div className="flex h-full items-center justify-center">
|
||
|
|
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex h-full flex-col space-y-4">
|
||
|
|
{/* 검색 및 정렬 */}
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<div className="relative flex-1">
|
||
|
|
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||
|
|
<Input
|
||
|
|
placeholder="야드 이름 또는 설명 검색..."
|
||
|
|
value={searchText}
|
||
|
|
onChange={(e) => setSearchText(e.target.value)}
|
||
|
|
className="pl-9"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<select
|
||
|
|
value={sortOrder}
|
||
|
|
onChange={(e) => setSortOrder(e.target.value as "recent" | "name")}
|
||
|
|
className="rounded-md border border-gray-300 px-3 py-2 text-sm"
|
||
|
|
>
|
||
|
|
<option value="recent">최근 수정순</option>
|
||
|
|
<option value="name">이름순</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 테이블 */}
|
||
|
|
{sortedLayouts.length === 0 ? (
|
||
|
|
<div className="flex flex-1 items-center justify-center">
|
||
|
|
<div className="text-center text-gray-500">
|
||
|
|
{searchText ? "검색 결과가 없습니다" : "등록된 야드가 없습니다"}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div className="flex-1 overflow-auto rounded-md border">
|
||
|
|
<Table>
|
||
|
|
<TableHeader>
|
||
|
|
<TableRow>
|
||
|
|
<TableHead>야드명</TableHead>
|
||
|
|
<TableHead>설명</TableHead>
|
||
|
|
<TableHead className="text-center">배치 자재</TableHead>
|
||
|
|
<TableHead>최종 수정</TableHead>
|
||
|
|
<TableHead className="w-[80px] text-center">작업</TableHead>
|
||
|
|
</TableRow>
|
||
|
|
</TableHeader>
|
||
|
|
<TableBody>
|
||
|
|
{sortedLayouts.map((layout) => (
|
||
|
|
<TableRow key={layout.id} className="cursor-pointer hover:bg-gray-50" onClick={() => onSelect(layout)}>
|
||
|
|
<TableCell className="font-medium">{layout.name}</TableCell>
|
||
|
|
<TableCell className="text-gray-600">{layout.description || "-"}</TableCell>
|
||
|
|
<TableCell className="text-center">{layout.placement_count}개</TableCell>
|
||
|
|
<TableCell className="text-sm text-gray-500">{formatDate(layout.updated_at)}</TableCell>
|
||
|
|
<TableCell className="text-center">
|
||
|
|
<DropdownMenu>
|
||
|
|
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||
|
|
<Button variant="ghost" size="sm">
|
||
|
|
<MoreVertical className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
</DropdownMenuTrigger>
|
||
|
|
<DropdownMenuContent align="end">
|
||
|
|
<DropdownMenuItem onClick={() => onSelect(layout)}>편집</DropdownMenuItem>
|
||
|
|
<DropdownMenuItem onClick={() => handleDuplicateClick(layout)}>복제</DropdownMenuItem>
|
||
|
|
<DropdownMenuItem onClick={() => setDeleteTarget(layout)} className="text-red-600">
|
||
|
|
삭제
|
||
|
|
</DropdownMenuItem>
|
||
|
|
</DropdownMenuContent>
|
||
|
|
</DropdownMenu>
|
||
|
|
</TableCell>
|
||
|
|
</TableRow>
|
||
|
|
))}
|
||
|
|
</TableBody>
|
||
|
|
</Table>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 총 개수 */}
|
||
|
|
<div className="text-sm text-gray-500">총 {sortedLayouts.length}개</div>
|
||
|
|
|
||
|
|
{/* 삭제 확인 모달 */}
|
||
|
|
<AlertDialog open={!!deleteTarget} onOpenChange={() => setDeleteTarget(null)}>
|
||
|
|
<AlertDialogContent>
|
||
|
|
<AlertDialogHeader>
|
||
|
|
<AlertDialogTitle>야드 삭제</AlertDialogTitle>
|
||
|
|
<AlertDialogDescription>
|
||
|
|
정말로 "{deleteTarget?.name}" 야드를 삭제하시겠습니까?
|
||
|
|
<br />
|
||
|
|
배치된 자재 정보도 함께 삭제됩니다.
|
||
|
|
</AlertDialogDescription>
|
||
|
|
</AlertDialogHeader>
|
||
|
|
<AlertDialogFooter>
|
||
|
|
<AlertDialogCancel disabled={isDeleting}>취소</AlertDialogCancel>
|
||
|
|
<AlertDialogAction
|
||
|
|
onClick={handleDeleteConfirm}
|
||
|
|
disabled={isDeleting}
|
||
|
|
className="bg-red-600 hover:bg-red-700"
|
||
|
|
>
|
||
|
|
{isDeleting ? (
|
||
|
|
<>
|
||
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||
|
|
삭제 중...
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
"삭제"
|
||
|
|
)}
|
||
|
|
</AlertDialogAction>
|
||
|
|
</AlertDialogFooter>
|
||
|
|
</AlertDialogContent>
|
||
|
|
</AlertDialog>
|
||
|
|
|
||
|
|
{/* 복제 모달 */}
|
||
|
|
<Dialog open={!!duplicateTarget} onOpenChange={() => setDuplicateTarget(null)}>
|
||
|
|
<DialogContent>
|
||
|
|
<DialogHeader>
|
||
|
|
<DialogTitle>야드 복제</DialogTitle>
|
||
|
|
<DialogDescription>새로운 야드의 이름을 입력하세요</DialogDescription>
|
||
|
|
</DialogHeader>
|
||
|
|
<div className="space-y-4 py-4">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="duplicate-name">야드 이름</Label>
|
||
|
|
<Input
|
||
|
|
id="duplicate-name"
|
||
|
|
value={duplicateName}
|
||
|
|
onChange={(e) => setDuplicateName(e.target.value)}
|
||
|
|
placeholder="야드 이름을 입력하세요"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<DialogFooter>
|
||
|
|
<Button variant="outline" onClick={() => setDuplicateTarget(null)} disabled={isDuplicating}>
|
||
|
|
취소
|
||
|
|
</Button>
|
||
|
|
<Button onClick={handleDuplicateConfirm} disabled={!duplicateName.trim() || isDuplicating}>
|
||
|
|
{isDuplicating ? (
|
||
|
|
<>
|
||
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||
|
|
복제 중...
|
||
|
|
</>
|
||
|
|
) : (
|
||
|
|
"복제"
|
||
|
|
)}
|
||
|
|
</Button>
|
||
|
|
</DialogFooter>
|
||
|
|
</DialogContent>
|
||
|
|
</Dialog>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|