"use client"; import React, { useState, useEffect, useMemo } from "react"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Search, Loader2 } from "lucide-react"; import { useEntitySearch } from "../entity-search-input/useEntitySearch"; import { ItemSelectionModalProps, ModalFilterConfig } from "./types"; import { apiClient } from "@/lib/api/client"; import { getCategoryLabelsByCodes } from "@/lib/api/tableCategoryValue"; export function ItemSelectionModal({ open, onOpenChange, sourceTable, sourceColumns, sourceSearchFields = [], multiSelect = true, filterCondition = {}, modalTitle, alreadySelected = [], uniqueField, onSelect, columnLabels = {}, modalFilters = [], categoryColumns = [], }: ItemSelectionModalProps) { const [localSearchText, setLocalSearchText] = useState(""); const [selectedItems, setSelectedItems] = useState([]); // 모달 필터 값 상태 const [modalFilterValues, setModalFilterValues] = useState>({}); // 카테고리 옵션 상태 (categoryRef별로 로드된 옵션) const [categoryOptions, setCategoryOptions] = useState>({}); // 카테고리 코드 → 라벨 매핑 (테이블 데이터 표시용) const [categoryLabelMap, setCategoryLabelMap] = useState>({}); // 모달 필터 값과 기본 filterCondition을 합친 최종 필터 조건 const combinedFilterCondition = useMemo(() => { const combined = { ...filterCondition }; // 모달 필터 값 추가 (빈 값은 제외) for (const [key, value] of Object.entries(modalFilterValues)) { if (value !== undefined && value !== null && value !== "") { combined[key] = value; } } return combined; }, [filterCondition, modalFilterValues]); const { results, loading, error, search, clearSearch } = useEntitySearch({ tableName: sourceTable, searchFields: sourceSearchFields.length > 0 ? sourceSearchFields : sourceColumns, filterCondition: combinedFilterCondition, }); // 필터 옵션 로드 - 소스 테이블 컬럼의 distinct 값 조회 const loadFilterOptions = async (filter: ModalFilterConfig) => { // 드롭다운 타입만 옵션 로드 필요 (select, category 지원) const isDropdownType = filter.type === "select" || filter.type === "category"; if (!isDropdownType) return; const cacheKey = `${sourceTable}.${filter.column}`; // 이미 로드된 경우 스킵 if (categoryOptions[cacheKey]) return; try { // 소스 테이블에서 해당 컬럼의 데이터 조회 (POST 메서드 사용) // 백엔드는 'size' 파라미터를 사용함 const response = await apiClient.post(`/table-management/tables/${sourceTable}/data`, { page: 1, size: 10000, // 모든 데이터 조회를 위해 큰 값 설정 }); if (response.data?.success) { // 응답 구조에 따라 rows 추출 const rows = response.data.data?.rows || response.data.data?.data || response.data.data || []; if (Array.isArray(rows)) { // 컬럼 값 중복 제거 const uniqueValues = new Set(); for (const row of rows) { const val = row[filter.column]; if (val !== null && val !== undefined && val !== "") { uniqueValues.add(String(val)); } } // 🆕 CATEGORY_ 코드가 있는지 확인하고 라벨 조회 const allCodes = new Set(); for (const val of uniqueValues) { // 콤마로 구분된 다중 값도 처리 const codes = val.split(",").map(c => c.trim()); codes.forEach(code => { if (code.startsWith("CATEGORY_")) { allCodes.add(code); } }); } // CATEGORY_ 코드가 있으면 라벨 조회 let labelMap: Record = {}; if (allCodes.size > 0) { try { const labelResponse = await getCategoryLabelsByCodes(Array.from(allCodes)); if (labelResponse.success && labelResponse.data) { labelMap = labelResponse.data; } } catch (labelError) { console.error("카테고리 라벨 조회 실패:", labelError); } } // 정렬 후 옵션으로 변환 (라벨 적용) const options = Array.from(uniqueValues) .sort() .map((val) => { // 콤마로 구분된 다중 값 처리 if (val.includes(",")) { const codes = val.split(",").map(c => c.trim()); const labels = codes.map(code => labelMap[code] || code); return { value: val, label: labels.join(", ") }; } // 단일 값 return { value: val, label: labelMap[val] || val }; }); setCategoryOptions((prev) => ({ ...prev, [cacheKey]: options, })); } } } catch (error) { console.error(`필터 옵션 로드 실패 (${cacheKey}):`, error); setCategoryOptions((prev) => ({ ...prev, [cacheKey]: [], })); } }; // 모달 열릴 때 초기 검색 및 필터 초기화 useEffect(() => { if (open) { // 모달 필터 기본값 설정 & 옵션 로드 const initialFilterValues: Record = {}; for (const filter of modalFilters) { if (filter.defaultValue !== undefined) { initialFilterValues[filter.column] = filter.defaultValue; } // 드롭다운 타입이면 옵션 로드 (소스 테이블에서 distinct 값 조회) const isDropdownType = filter.type === "select" || filter.type === "category"; if (isDropdownType) { loadFilterOptions(filter); } } setModalFilterValues(initialFilterValues); search("", 1); // 빈 검색어로 전체 목록 조회 setSelectedItems([]); } else { clearSearch(); setLocalSearchText(""); setSelectedItems([]); setModalFilterValues({}); } }, [open]); // 모달 필터 값 변경 시 재검색 useEffect(() => { if (open) { search(localSearchText, 1); } }, [modalFilterValues]); // 검색 결과가 변경되면 카테고리 값들의 라벨 조회 useEffect(() => { const loadCategoryLabels = async () => { if (!open || categoryColumns.length === 0 || results.length === 0) { return; } // 현재 결과에서 카테고리 컬럼의 모든 고유한 값 수집 // 쉼표로 구분된 다중 값도 개별적으로 수집 const allCodes = new Set(); for (const row of results) { for (const col of categoryColumns) { const val = row[col]; if (val && typeof val === "string") { // 쉼표로 구분된 다중 값 처리 const codes = val.split(",").map((c) => c.trim()).filter(Boolean); for (const code of codes) { if (!categoryLabelMap[code]) { allCodes.add(code); } } } } } if (allCodes.size === 0) { return; } try { const response = await apiClient.post("/table-categories/labels-by-codes", { valueCodes: Array.from(allCodes), }); if (response.data?.success && response.data.data) { setCategoryLabelMap((prev) => ({ ...prev, ...response.data.data, })); } } catch (error) { console.error("카테고리 라벨 조회 실패:", error); } }; loadCategoryLabels(); }, [open, results, categoryColumns]); // 모달 필터 값 변경 핸들러 const handleModalFilterChange = (column: string, value: any) => { setModalFilterValues((prev) => ({ ...prev, [column]: value, })); }; const handleSearch = () => { search(localSearchText, 1); }; const handleToggleItem = (item: any) => { const itemValue = uniqueField ? item[uniqueField] : undefined; if (!multiSelect) { setSelectedItems([item]); return; } // uniqueField 값이 undefined인 경우 객체 참조로 비교 if (uniqueField && (itemValue === undefined || itemValue === null)) { console.warn(`⚠️ uniqueField "${uniqueField}"의 값이 undefined입니다. 객체 참조로 비교합니다.`); const itemIsSelected = selectedItems.includes(item); if (itemIsSelected) { const newSelected = selectedItems.filter((selected) => selected !== item); setSelectedItems(newSelected); } else { setSelectedItems([...selectedItems, item]); } return; } const itemIsSelected = selectedItems.some((selected) => { if (!uniqueField) { return selected === item; } const selectedValue = selected[uniqueField]; if (selectedValue === undefined || selectedValue === null) { return false; } return selectedValue === itemValue; }); if (itemIsSelected) { const newSelected = selectedItems.filter((selected) => { if (!uniqueField) { return selected !== item; } const selectedValue = selected[uniqueField]; if (selectedValue === undefined || selectedValue === null) { return true; } return selectedValue !== itemValue; }); setSelectedItems(newSelected); } else { setSelectedItems([...selectedItems, item]); } }; const handleConfirm = () => { console.log("✅ ItemSelectionModal 추가:", selectedItems.length, "개 항목"); onSelect(selectedItems); onOpenChange(false); }; // 이미 추가된 항목인지 확인 const isAlreadyAdded = (item: any): boolean => { if (!uniqueField) return false; return alreadySelected.some( (selected) => selected[uniqueField] === item[uniqueField] ); }; // 이미 추가된 항목 제외한 결과 필터링 const filteredResults = results.filter((item) => !isAlreadyAdded(item)); // 선택된 항목인지 확인 const isSelected = (item: any): boolean => { if (!uniqueField) { return selectedItems.includes(item); } const itemValue = item[uniqueField]; // uniqueField 값이 undefined인 경우 객체 참조로 비교 if (itemValue === undefined || itemValue === null) { return selectedItems.includes(item); } const result = selectedItems.some((selected) => { const selectedValue = selected[uniqueField]; // selectedValue도 undefined면 안전하게 처리 if (selectedValue === undefined || selectedValue === null) { return false; } const isMatch = selectedValue === itemValue; if (isMatch) { console.log("✅ 매칭 발견:", { selectedValue, itemValue, uniqueField }); } return isMatch; }); return result; }; // 유효한 컬럼만 필터링 const validColumns = sourceColumns.filter(col => col != null && col !== ""); const totalColumns = validColumns.length + (multiSelect ? 1 : 0); return ( {modalTitle} 항목을 검색하고 선택하세요 {multiSelect && " (다중 선택 가능)"} {/* 검색 입력 */}
setLocalSearchText(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { handleSearch(); } }} className="h-8 text-xs sm:h-10 sm:text-sm" />
{/* 모달 필터 */} {modalFilters.length > 0 && (
{modalFilters.map((filter) => { // 소스 테이블의 해당 컬럼에서 로드된 옵션 const options = categoryOptions[`${sourceTable}.${filter.column}`] || []; // 드롭다운 타입인지 확인 (select, category 모두 드롭다운으로 처리) const isDropdownType = filter.type === "select" || filter.type === "category"; return (
{filter.label}: {isDropdownType && ( )} {filter.type === "text" && ( handleModalFilterChange(filter.column, e.target.value)} placeholder={filter.label} className="h-7 text-xs w-[120px]" /> )}
); })}
)} {/* 선택된 항목 수 */} {selectedItems.length > 0 && (
{selectedItems.length}개 항목 선택됨 {uniqueField && ( ({selectedItems.map(item => item[uniqueField]).join(", ")}) )}
)} {/* 오류 메시지 */} {error && (
{error}
)} {/* 검색 결과 테이블 */}
{multiSelect && ( )} {validColumns.map((col) => ( ))} {loading && filteredResults.length === 0 ? ( ) : filteredResults.length === 0 ? ( ) : ( filteredResults.map((item, index) => { const selected = isSelected(item); // 🔧 index를 조합하여 항상 고유한 key 생성 (중복 데이터 대응) const itemKey = `row-${index}`; return ( handleToggleItem(item)} > {multiSelect && ( )} {validColumns.map((col) => { const rawValue = item[col]; // 카테고리 컬럼이면 라벨로 변환 const isCategory = categoryColumns.includes(col); let displayValue = rawValue; if (isCategory && rawValue && typeof rawValue === "string") { // 쉼표로 구분된 다중 값 처리 const codes = rawValue.split(",").map((c) => c.trim()).filter(Boolean); const labels = codes.map((code) => categoryLabelMap[code] || code); displayValue = labels.join(", "); } return ( ); })} ); }) )}
선택 {columnLabels[col] || col}

검색 중...

{results.length > 0 ? "모든 항목이 이미 추가되었습니다" : "검색 결과가 없습니다"}
{ // 체크박스 영역 클릭을 행 클릭으로 전파 e.stopPropagation(); handleToggleItem(item); }} >
{displayValue || "-"}
); }