summaryrefslogtreecommitdiff
path: root/lib/tech-vendor-possible-items/table/add-possible-item-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/tech-vendor-possible-items/table/add-possible-item-dialog.tsx')
-rw-r--r--lib/tech-vendor-possible-items/table/add-possible-item-dialog.tsx450
1 files changed, 450 insertions, 0 deletions
diff --git a/lib/tech-vendor-possible-items/table/add-possible-item-dialog.tsx b/lib/tech-vendor-possible-items/table/add-possible-item-dialog.tsx
new file mode 100644
index 00000000..cdce60af
--- /dev/null
+++ b/lib/tech-vendor-possible-items/table/add-possible-item-dialog.tsx
@@ -0,0 +1,450 @@
+"use client";
+
+import * as React from "react";
+import { Search, Plus, X } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Badge } from "@/components/ui/badge";
+import { useToast } from "@/hooks/use-toast";
+import {
+ getAllTechVendors,
+ createTechVendorPossibleItem,
+ getItemsByVendorType
+} from "@/lib/tech-vendor-possible-items/service";
+
+interface TechVendor {
+ id: number;
+ vendorCode: string | null;
+ vendorName: string;
+ techVendorType: string;
+}
+
+interface ItemData {
+ itemCode: string;
+ itemList: string | null;
+ workType: string | null;
+ shipTypes?: string | null;
+ subItemList?: string | null;
+}
+
+interface AddPossibleItemDialogProps {
+ children?: React.ReactNode;
+ onSuccess?: () => void;
+}
+
+export function AddPossibleItemDialog({
+ children,
+ onSuccess
+}: AddPossibleItemDialogProps) {
+ const { toast } = useToast();
+ const [open, setOpen] = React.useState(false);
+
+ // 벤더 관련 상태
+ const [vendors, setVendors] = React.useState<TechVendor[]>([]);
+ const [filteredVendors, setFilteredVendors] = React.useState<TechVendor[]>([]);
+ const [vendorSearch, setVendorSearch] = React.useState("");
+ const [selectedVendor, setSelectedVendor] = React.useState<TechVendor | null>(null);
+
+ // 아이템 관련 상태
+ const [items, setItems] = React.useState<ItemData[]>([]);
+ const [filteredItems, setFilteredItems] = React.useState<ItemData[]>([]);
+ const [itemSearch, setItemSearch] = React.useState("");
+ const [selectedItems, setSelectedItems] = React.useState<ItemData[]>([]);
+
+ const [isLoading, setIsLoading] = React.useState(false);
+
+ // 벤더 목록 로드
+ React.useEffect(() => {
+ if (open) {
+ loadVendors();
+ }
+ }, [open]);
+
+ // 벤더 검색 필터링
+ React.useEffect(() => {
+ if (!vendorSearch) {
+ setFilteredVendors(vendors);
+ } else {
+ const filtered = vendors.filter(vendor =>
+ vendor.vendorName.toLowerCase().includes(vendorSearch.toLowerCase()) ||
+ vendor.vendorCode?.toLowerCase().includes(vendorSearch.toLowerCase())
+ );
+ setFilteredVendors(filtered);
+ }
+ }, [vendors, vendorSearch]);
+
+ // 아이템 검색 필터링
+ React.useEffect(() => {
+ if (!itemSearch) {
+ setFilteredItems(items);
+ } else {
+ const filtered = items.filter(item =>
+ item.itemCode.toLowerCase().includes(itemSearch.toLowerCase()) ||
+ item.itemList?.toLowerCase().includes(itemSearch.toLowerCase()) ||
+ item.workType?.toLowerCase().includes(itemSearch.toLowerCase())
+ );
+ setFilteredItems(filtered);
+ }
+ }, [items, itemSearch]);
+
+ const loadVendors = async () => {
+ try {
+ setIsLoading(true);
+ const vendorData = await getAllTechVendors();
+ setVendors(vendorData);
+ } catch (error) {
+ console.error("Failed to load vendors:", error);
+ toast({
+ title: "오류",
+ description: "벤더 목록을 불러오는데 실패했습니다.",
+ variant: "destructive",
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const loadItemsByVendorType = async (vendorTypes: string) => {
+ try {
+ setIsLoading(true);
+ console.log("Loading items for vendor types:", vendorTypes);
+ const itemData = await getItemsByVendorType(vendorTypes);
+ console.log("Loaded items:", itemData.length, itemData);
+ setItems(itemData);
+ } catch (error) {
+ console.error("Failed to load items:", error);
+ toast({
+ title: "오류",
+ description: "아이템 목록을 불러오는데 실패했습니다.",
+ variant: "destructive",
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleVendorSelect = (vendor: TechVendor) => {
+ setSelectedVendor(vendor);
+ setSelectedItems([]); // 벤더 변경시 선택된 아이템 초기화
+ loadItemsByVendorType(vendor.techVendorType);
+ };
+
+ const handleItemToggle = (item: ItemData) => {
+ setSelectedItems(prev => {
+ const isSelected = prev.some(i => i.itemCode === item.itemCode);
+ if (isSelected) {
+ return prev.filter(i => i.itemCode !== item.itemCode);
+ } else {
+ return [...prev, item];
+ }
+ });
+ };
+
+ const handleSubmit = async () => {
+ if (!selectedVendor || selectedItems.length === 0) return;
+
+ try {
+ setIsLoading(true);
+ let successCount = 0;
+ let errorCount = 0;
+
+ for (const item of selectedItems) {
+ const result = await createTechVendorPossibleItem({
+ vendorId: selectedVendor.id,
+ itemCode: item.itemCode,
+ workType: item.workType,
+ shipTypes: item.shipTypes,
+ itemList: item.itemList,
+ subItemList: item.subItemList,
+ });
+
+ if (result.success) {
+ successCount++;
+ } else {
+ errorCount++;
+ }
+ }
+
+ if (successCount > 0) {
+ toast({
+ title: "성공",
+ description: `${successCount}개의 아이템이 추가되었습니다.${errorCount > 0 ? ` (${errorCount}개 실패)` : ""}`,
+ });
+
+ handleClose();
+ onSuccess?.();
+ } else {
+ toast({
+ title: "오류",
+ description: "아이템 추가에 실패했습니다.",
+ variant: "destructive",
+ });
+ }
+ } catch (error) {
+ console.error("Failed to add items:", error);
+ toast({
+ title: "오류",
+ description: "아이템 추가 중 오류가 발생했습니다.",
+ variant: "destructive",
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleClose = () => {
+ setOpen(false);
+ setTimeout(() => {
+ setSelectedVendor(null);
+ setSelectedItems([]);
+ setVendorSearch("");
+ setItemSearch("");
+ setVendors([]);
+ setItems([]);
+ setFilteredVendors([]);
+ setFilteredItems([]);
+ }, 200);
+ };
+
+ const parseVendorTypes = (vendorType: string): string[] => {
+ if (!vendorType) return [];
+
+ // JSON 배열 형태인지 확인
+ if (vendorType.startsWith('[') && vendorType.endsWith(']')) {
+ try {
+ const parsed = JSON.parse(vendorType);
+ return Array.isArray(parsed) ? parsed.filter(Boolean) : [vendorType];
+ } catch {
+ return [vendorType];
+ }
+ }
+
+ // 콤마로 구분된 문자열인지 확인
+ if (vendorType.includes(',')) {
+ return vendorType.split(',').map(t => t.trim()).filter(Boolean);
+ }
+
+ // 단일 문자열
+ return [vendorType.trim()].filter(Boolean);
+ };
+
+ return (
+ <Dialog open={open} onOpenChange={setOpen}>
+ <DialogTrigger asChild>
+ {children || (
+ <Button size="sm">
+ <Plus className="mr-2 h-4 w-4" />
+ 추가
+ </Button>
+ )}
+ </DialogTrigger>
+ <DialogContent className="max-w-6xl max-h-[90vh] flex flex-col">
+ <DialogHeader>
+ <DialogTitle>
+ 벤더별 아이템 추가
+ </DialogTitle>
+ <DialogDescription>
+ 왼쪽에서 벤더를 선택하고, 오른쪽에서 아이템을 선택하세요.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div className="flex-1 min-h-0">
+ <div className="grid grid-cols-2 gap-4 h-[500px]">
+ {/* 왼쪽: 벤더 선택/표시 */}
+ <div className="space-y-4 h-full flex flex-col">
+ {!selectedVendor ? (
+ <>
+ <div className="space-y-2">
+ <Label htmlFor="vendor-search">벤더 검색</Label>
+ <div className="relative">
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" /> <Input
+ id="vendor-search"
+ placeholder="벤더명 또는 벤더코드로 검색..."
+ value={vendorSearch}
+ onChange={(e) => setVendorSearch(e.target.value)}
+ className="pl-10"
+ />
+ </div>
+ </div>
+
+ <div className="max-h-96 overflow-y-auto border rounded-lg bg-gray-50 p-2">
+ <div className="space-y-2">
+ {isLoading ? (
+ <div className="text-center py-4">로딩 중...</div>
+ ) : filteredVendors.length === 0 ? (
+ <div className="text-center py-4 text-muted-foreground">
+ 검색 결과가 없습니다.
+ </div>
+ ) : (
+ filteredVendors.map((vendor) => (
+ <div
+ key={vendor.id}
+ className="p-3 bg-white border rounded-lg cursor-pointer transition-colors hover:bg-gray-50"
+ onClick={() => handleVendorSelect(vendor)}
+ >
+ <div className="font-medium">{vendor.vendorName}</div>
+ <div className="text-sm text-muted-foreground">
+ {vendor.vendorCode}
+ </div>
+ <div className="flex flex-wrap gap-1 mt-2">
+ {parseVendorTypes(vendor.techVendorType).map((type, index) => (
+ <Badge key={`${vendor.id}-${type}-${index}`} variant="secondary" className="text-xs">
+ {type}
+ </Badge>
+ ))}
+ </div>
+ </div>
+ ))
+ )}
+ </div>
+ </div>
+ </>
+ ) : (
+ <div className="space-y-4">
+ <div className="flex items-center justify-between">
+ <Label>선택된 벤더</Label>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => {
+ setSelectedVendor(null);
+ setSelectedItems([]);
+ setItems([]);
+ setFilteredItems([]);
+ }}
+ >
+ 변경
+ </Button>
+ </div>
+ <div className="p-4 border rounded-md bg-muted/20">
+ <div className="font-medium">{selectedVendor?.vendorName}</div>
+ <div className="text-sm text-muted-foreground">
+ {selectedVendor?.vendorCode}
+ </div>
+ <div className="flex flex-wrap gap-1 mt-2">
+ {selectedVendor && parseVendorTypes(selectedVendor.techVendorType).map((type, index) => (
+ <Badge key={`selected-${type}-${index}`} variant="outline" className="text-xs">
+ {type}
+ </Badge>
+ ))}
+ </div>
+ </div>
+ </div>
+ )}
+ </div>
+
+
+
+ {/* 오른쪽: 아이템 선택 */}
+ <div className="space-y-4 h-full flex flex-col">
+ {selectedVendor ? (
+ <>
+
+
+ <Label htmlFor="item-search">아이템 검색</Label>
+ <div className="relative">
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" /> <Input
+ id="item-search"
+ placeholder="아이템코드, 아이템리스트, 공종으로 검색..."
+ value={itemSearch}
+ onChange={(e) => setItemSearch(e.target.value)}
+ className="pl-10"
+ />
+ </div>
+
+
+ {selectedItems.length > 0 && (
+ <div className="space-y-2">
+ <Label>선택된 아이템 ({selectedItems.length}개)</Label>
+ <div className="flex flex-wrap gap-1 p-2 border rounded-md bg-muted/50 max-h-20 overflow-y-auto">
+ {selectedItems.map((item) => (
+ <Badge key={`selected-${item.itemCode}`} variant="default" className="text-xs">
+ {item.itemCode}
+ <X
+ className="ml-1 h-3 w-3 cursor-pointer"
+ onClick={(e) => {
+ e.stopPropagation();
+ handleItemToggle(item);
+ }}
+ />
+ </Badge>
+ ))}
+ </div>
+ </div>
+ )}
+
+ <div className="max-h-80 overflow-y-auto border rounded-lg bg-gray-50 p-2">
+ <div className="space-y-2">
+ {isLoading ? (
+ <div className="text-center py-4">아이템 로딩 중...</div>
+ ) : filteredItems.length === 0 && items.length === 0 ? (
+ <div className="text-center py-4 text-muted-foreground">
+ 해당 벤더 타입에 대한 아이템이 없습니다.
+ </div>
+ ) : filteredItems.length === 0 ? (
+ <div className="text-center py-4 text-muted-foreground">
+ 검색 결과가 없습니다.
+ </div>
+ ) : (
+ filteredItems.map((item) => {
+ const isSelected = selectedItems.some(i => i.itemCode === item.itemCode);
+ return (
+ <div
+ key={`item-${item.itemCode}`}
+ className={`p-3 bg-white border rounded-lg cursor-pointer transition-colors ${
+ isSelected
+ ? "bg-primary/10 border-primary hover:bg-primary/20"
+ : "hover:bg-gray-50"
+ }`}
+ onClick={() => handleItemToggle(item)}
+ >
+ <div className="font-medium">{item.itemCode}</div>
+ <div className="text-sm text-muted-foreground">
+ {item.itemList || "-"}
+ </div>
+ <div className="flex gap-2 mt-1 text-xs">
+ <span>공종: {item.workType || "-"}</span>
+ {item.shipTypes && <span>선종: {item.shipTypes}</span>}
+ {item.subItemList && <span>서브아이템: {item.subItemList}</span>}
+ </div>
+ </div>
+ );
+ })
+ )}
+ </div>
+ </div>
+ </>
+ ) : (
+ <div className="flex-1 flex items-center justify-center text-muted-foreground">
+ 왼쪽에서 벤더를 선택하세요.
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+
+ <div className="flex justify-end gap-2 pt-4 border-t">
+ <Button variant="outline" onClick={handleClose}>
+ 취소
+ </Button>
+ <Button
+ onClick={handleSubmit}
+ disabled={!selectedVendor || selectedItems.length === 0 || isLoading}
+ >
+ {isLoading ? "추가 중..." : `추가 (${selectedItems.length})`}
+ </Button>
+ </div>
+ </DialogContent>
+ </Dialog>
+ );
+} \ No newline at end of file