ERP-node/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx

349 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useEffect } 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 { Search, Loader2 } from "lucide-react";
import { useEntitySearch } from "../entity-search-input/useEntitySearch";
import { ItemSelectionModalProps } from "./types";
export function ItemSelectionModal({
open,
onOpenChange,
sourceTable,
sourceColumns,
sourceSearchFields = [],
multiSelect = true,
filterCondition = {},
modalTitle,
alreadySelected = [],
uniqueField,
onSelect,
columnLabels = {},
}: ItemSelectionModalProps) {
const [localSearchText, setLocalSearchText] = useState("");
const [selectedItems, setSelectedItems] = useState<any[]>([]);
const { results, loading, error, search, clearSearch } = useEntitySearch({
tableName: sourceTable,
searchFields: sourceSearchFields.length > 0 ? sourceSearchFields : sourceColumns,
filterCondition,
});
// 모달 열릴 때 초기 검색
useEffect(() => {
if (open) {
console.log("🚪 모달 열림 - uniqueField:", uniqueField, "multiSelect:", multiSelect);
search("", 1); // 빈 검색어로 전체 목록 조회
setSelectedItems([]);
} else {
clearSearch();
setLocalSearchText("");
setSelectedItems([]);
}
}, [open]);
const handleSearch = () => {
search(localSearchText, 1);
};
const handleToggleItem = (item: any) => {
const itemValue = uniqueField ? item[uniqueField] : undefined;
console.log("🖱️ 행 클릭:", {
item,
uniqueField,
itemValue,
currentSelected: selectedItems.length,
selectedValues: uniqueField ? selectedItems.map(s => s[uniqueField]) : []
});
if (!multiSelect) {
setSelectedItems([item]);
return;
}
// uniqueField 값이 undefined인 경우 객체 참조로 비교
if (uniqueField && (itemValue === undefined || itemValue === null)) {
console.warn(`⚠️ uniqueField "${uniqueField}"의 값이 undefined입니다. 객체 참조로 비교합니다.`);
const itemIsSelected = selectedItems.includes(item);
console.log("📊 선택 상태 (객체 참조):", itemIsSelected);
if (itemIsSelected) {
const newSelected = selectedItems.filter((selected) => selected !== item);
console.log(" 제거 후:", newSelected.length);
setSelectedItems(newSelected);
} else {
console.log(" 추가");
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;
});
console.log("📊 선택 상태:", itemIsSelected);
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;
});
console.log(" 제거 후:", newSelected.length);
setSelectedItems(newSelected);
} else {
console.log(" 추가");
setSelectedItems([...selectedItems, item]);
}
};
const handleConfirm = () => {
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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[95vw] sm:max-w-[900px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-base sm:text-lg">{modalTitle}</DialogTitle>
<DialogDescription className="text-xs sm:text-sm">
{multiSelect && " (다중 선택 가능)"}
</DialogDescription>
</DialogHeader>
{/* 검색 입력 */}
<div className="flex gap-2">
<Input
placeholder="검색어를 입력하세요"
value={localSearchText}
onChange={(e) => setLocalSearchText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSearch();
}
}}
className="h-8 text-xs sm:h-10 sm:text-sm"
/>
<Button
onClick={handleSearch}
disabled={loading}
className="h-8 text-xs sm:h-10 sm:text-sm"
>
{loading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Search className="h-4 w-4" />
)}
<span className="ml-2"></span>
</Button>
</div>
{/* 선택된 항목 수 */}
{selectedItems.length > 0 && (
<div className="text-sm text-primary">
{selectedItems.length}
{uniqueField && (
<span className="ml-2 text-xs text-muted-foreground">
({selectedItems.map(item => item[uniqueField]).join(", ")})
</span>
)}
</div>
)}
{/* 오류 메시지 */}
{error && (
<div className="text-sm text-destructive bg-destructive/10 p-3 rounded-md">
{error}
</div>
)}
{/* 검색 결과 테이블 */}
<div className="border rounded-md overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-xs sm:text-sm">
<thead className="bg-muted">
<tr>
{multiSelect && (
<th className="px-4 py-2 text-left font-medium text-muted-foreground w-12">
</th>
)}
{validColumns.map((col) => (
<th
key={col}
className="px-4 py-2 text-left font-medium text-muted-foreground"
>
{columnLabels[col] || col}
</th>
))}
</tr>
</thead>
<tbody>
{loading && filteredResults.length === 0 ? (
<tr>
<td
colSpan={totalColumns}
className="px-4 py-8 text-center"
>
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
<p className="mt-2 text-muted-foreground"> ...</p>
</td>
</tr>
) : filteredResults.length === 0 ? (
<tr>
<td
colSpan={totalColumns}
className="px-4 py-8 text-center text-muted-foreground"
>
{results.length > 0
? "모든 항목이 이미 추가되었습니다"
: "검색 결과가 없습니다"}
</td>
</tr>
) : (
filteredResults.map((item, index) => {
const selected = isSelected(item);
const uniqueFieldValue = uniqueField ? item[uniqueField] : undefined;
const itemKey = (uniqueFieldValue !== undefined && uniqueFieldValue !== null)
? uniqueFieldValue
: `item-${index}`;
return (
<tr
key={itemKey}
className={`border-t transition-colors ${
selected
? "bg-primary/10"
: "hover:bg-accent cursor-pointer"
}`}
onClick={() => handleToggleItem(item)}
>
{multiSelect && (
<td
className="px-4 py-2"
onClick={(e) => {
// 체크박스 영역 클릭을 행 클릭으로 전파
e.stopPropagation();
handleToggleItem(item);
}}
>
<div className="pointer-events-none">
<Checkbox checked={selected} readOnly />
</div>
</td>
)}
{validColumns.map((col) => (
<td key={col} className="px-4 py-2">
{item[col] || "-"}
</td>
))}
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
</Button>
<Button
onClick={handleConfirm}
disabled={selectedItems.length === 0}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>
({selectedItems.length})
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}