"use client"; import React, { useState, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Search, X, ChevronLeft, ChevronRight } from "lucide-react"; import { useDebounce } from "@/hooks/use-debounce"; import { searchMaterialsForDialog, MaterialSearchItem } from "./material-service"; /** * 자재 단일 선택 Dialog 컴포넌트 * * @description * - TanStack Table을 사용한 효율적인 자재 선택 Dialog * - 페이지네이션으로 데이터를 조회하고 클라이언트 사이드에서 검색 필터링 * - 버튼 클릭 시 Dialog가 열리고, 자재를 선택하면 Dialog가 닫히며 결과를 반환 * * @MaterialSearchItem_Structure * 상태에서 관리되는 자재 객체의 형태: * ```typescript * interface MaterialSearchItem { * materialCode: string; // 자재코드 (MATNR) * materialName: string; // 자재명 (ZZNAME) * displayText: string; // 표시용 텍스트 (code + " - " + name) * } * ``` * * @state * - open: Dialog 열림/닫힘 상태 * - selectedMaterial: 현재 선택된 자재 (단일) * - tempSelectedMaterial: Dialog 내에서 임시로 선택된 자재 (확인 버튼 클릭 전까지) * - searchQuery: 검색어 * - currentPage: 현재 페이지 * - searchResults: 검색 결과 * - isSearching: 검색 중 상태 * - error: 에러 상태 * * @callback * - onMaterialSelect: 자재 선택 완료 시 호출되는 콜백 * - 매개변수: MaterialSearchItem | null * - 선택된 자재 정보 또는 null (선택 해제 시) * * @usage * ```tsx * { * setSelectedMaterial(material); * console.log('선택된 자재:', material); * }} * placeholder="자재코드를 검색하세요..." * /> * ``` */ interface MaterialSelectorDialogSingleProps { /** Dialog를 여는 트리거 버튼 텍스트 */ triggerLabel?: string; /** 현재 선택된 자재 */ selectedMaterial?: MaterialSearchItem | null; /** 자재 선택 완료 시 호출되는 콜백 */ onMaterialSelect?: (material: MaterialSearchItem | null) => void; /** 검색 입력창 placeholder */ placeholder?: string; /** Dialog 제목 */ title?: string; /** Dialog 설명 */ description?: string; /** 트리거 버튼 비활성화 여부 */ disabled?: boolean; /** 트리거 버튼 variant */ triggerVariant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"; /** 제외할 자재코드들 */ excludeMaterialCodes?: Set; } export function MaterialSelectorDialogSingle({ triggerLabel = "자재 선택", selectedMaterial = null, onMaterialSelect, placeholder = "자재코드를 검색하세요...", title = "자재 선택", description = "원하는 자재를 검색하고 선택해주세요.", disabled = false, triggerVariant = "outline", excludeMaterialCodes, }: MaterialSelectorDialogSingleProps) { // Dialog 열림/닫힘 상태 const [open, setOpen] = useState(false); // Dialog 내에서 임시로 선택된 자재 (확인 버튼 클릭 전까지) const [tempSelectedMaterial, setTempSelectedMaterial] = useState(null); // 검색어 const [searchQuery, setSearchQuery] = useState(""); // 페이지네이션 상태 const [currentPage, setCurrentPage] = useState(1); const [searchResults, setSearchResults] = useState([]); const [pagination, setPagination] = useState({ page: 1, perPage: 10, total: 0, pageCount: 0, hasNextPage: false, hasPrevPage: false, }); // 로딩 상태 const [isSearching, setIsSearching] = useState(false); // 에러 상태 const [error, setError] = useState(null); // Debounce 적용된 검색어 const debouncedSearchQuery = useDebounce(searchQuery, 300); // 검색 실행 함수 const performSearch = useCallback(async (query: string, page: number = 1) => { setIsSearching(true); setError(null); try { const result = await searchMaterialsForDialog(query, page, 10); if (result.success) { setSearchResults(result.data); setPagination(result.pagination); setCurrentPage(page); } else { setSearchResults([]); setError(result.error || "검색 중 오류가 발생했습니다."); setPagination({ page: 1, perPage: 10, total: 0, pageCount: 0, hasNextPage: false, hasPrevPage: false, }); } } catch (err) { console.error("자재 검색 실패:", err); setSearchResults([]); setError("검색 중 오류가 발생했습니다."); setPagination({ page: 1, perPage: 10, total: 0, pageCount: 0, hasNextPage: false, hasPrevPage: false, }); } finally { setIsSearching(false); } }, []); // Dialog 열림 시 초기화 및 첫 검색 const handleOpenChange = useCallback((newOpen: boolean) => { setOpen(newOpen); if (newOpen) { setTempSelectedMaterial(selectedMaterial || null); setSearchQuery(""); setCurrentPage(1); // 초기 데이터 로드 performSearch("", 1); } }, [selectedMaterial, performSearch]); // 검색어 변경 시 검색 실행 React.useEffect(() => { if (open) { setCurrentPage(1); performSearch(debouncedSearchQuery, 1); } }, [debouncedSearchQuery, performSearch, open]); // 페이지 변경 처리 const handlePageChange = useCallback((newPage: number) => { if (newPage >= 1 && newPage <= pagination.pageCount) { setCurrentPage(newPage); performSearch(debouncedSearchQuery, newPage); } }, [pagination.pageCount, performSearch, debouncedSearchQuery]); // 자재 선택 처리 const handleMaterialSelect = useCallback((material: MaterialSearchItem) => { setTempSelectedMaterial(material); }, []); // 선택 해제 const handleClear = useCallback(() => { setTempSelectedMaterial(null); }, []); // 확인 버튼 클릭 시 선택 완료 const handleConfirm = useCallback(() => { onMaterialSelect?.(tempSelectedMaterial); setOpen(false); }, [tempSelectedMaterial, onMaterialSelect]); // 취소 버튼 클릭 시 const handleCancel = useCallback(() => { setTempSelectedMaterial(selectedMaterial || null); setOpen(false); }, [selectedMaterial]); return ( {title} {description}
{/* 검색 입력창 */}
setSearchQuery(e.target.value)} className="pl-10" />
{/* 선택된 자재 표시 */} {tempSelectedMaterial && (
선택됨: {tempSelectedMaterial.displayText}
)} {/* 테이블 */}
{isSearching ? (
검색 중...
) : error ? (
{error}
) : ( 선택 자재코드 자재명 {searchResults.length === 0 ? ( {debouncedSearchQuery.trim() ? "검색 결과가 없습니다." : "자재가 없습니다."} ) : ( searchResults.map((material) => { const isExcluded = excludeMaterialCodes?.has(material.materialCode); const isSelected = tempSelectedMaterial?.materialCode === material.materialCode; return ( { if (!isExcluded) { handleMaterialSelect(material); } }} > { if (!isExcluded) { handleMaterialSelect(material); } }} /> {material.materialCode} {isExcluded && ( 제외됨 )} {material.materialName} ); }) )}
)}
{/* 페이지네이션 */} {searchResults.length > 0 && pagination.pageCount > 1 && (
총 {pagination.total}개 중 {(pagination.page - 1) * pagination.perPage + 1}- {Math.min(pagination.page * pagination.perPage, pagination.total)}개 표시
{pagination.page} / {pagination.pageCount}
)}
{tempSelectedMaterial && ( )}
); } /** * 사용 예시: * * ```tsx * const [selectedMaterial, setSelectedMaterial] = useState(null); * * return ( * { * setSelectedMaterial(material); * if (material) { * console.log('선택된 자재:', { * code: material.materialCode, * name: material.materialName, * displayText: material.displayText * }); * } else { * console.log('자재 선택이 해제되었습니다.'); * } * }} * title="자재 선택" * description="필요한 자재를 검색하고 선택해주세요." * /> * ); * ``` */