플로우 위젯 체크박스 선택 버그 수정 - 인덱스 기반에서 Primary Key 기반으로 변경

This commit is contained in:
dohyeons
2025-12-10 14:28:11 +09:00
parent 28ca4f2088
commit c7ae04859d

View File

@@ -130,7 +130,7 @@ export function FlowWidget({
const [stepData, setStepData] = useState<any[]>([]); const [stepData, setStepData] = useState<any[]>([]);
const [stepDataColumns, setStepDataColumns] = useState<string[]>([]); const [stepDataColumns, setStepDataColumns] = useState<string[]>([]);
const [stepDataLoading, setStepDataLoading] = useState(false); const [stepDataLoading, setStepDataLoading] = useState(false);
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set()); const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set()); // Primary Key 값으로 선택 관리
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑 const [columnLabels, setColumnLabels] = useState<Record<string, string>>({}); // 컬럼명 -> 라벨 매핑
// 🆕 검색 필터 관련 상태 // 🆕 검색 필터 관련 상태
@@ -753,25 +753,35 @@ export function FlowWidget({
} }
}; };
// 체크박스 토글 // Primary Key 컬럼명 (플로우 정의에서 가져오거나 기본값 id)
const toggleRowSelection = (rowIndex: number) => { const primaryKeyColumn = flowData?.primaryKey || "id";
// 행의 Primary Key 값 가져오기
const getRowKey = useCallback((row: any): string => {
const keyValue = row[primaryKeyColumn] || row.id;
return String(keyValue);
}, [primaryKeyColumn]);
// 체크박스 토글 (Primary Key 기반)
const toggleRowSelection = (row: any) => {
// 프리뷰 모드에서는 행 선택 차단 // 프리뷰 모드에서는 행 선택 차단
if (isPreviewMode) { if (isPreviewMode) {
return; return;
} }
const rowKey = getRowKey(row);
const newSelected = new Set(selectedRows); const newSelected = new Set(selectedRows);
if (newSelected.has(rowIndex)) { if (newSelected.has(rowKey)) {
newSelected.delete(rowIndex); newSelected.delete(rowKey);
} else { } else {
newSelected.add(rowIndex); newSelected.add(rowKey);
} }
setSelectedRows(newSelected); setSelectedRows(newSelected);
// 선택된 데이터를 상위로 전달 // 선택된 데이터를 상위로 전달 (stepData에서 선택된 행들 찾기)
const selectedData = Array.from(newSelected).map((index) => stepData[index]); const selectedData = stepData.filter((r) => newSelected.has(getRowKey(r)));
console.log("🌊 FlowWidget - 체크박스 토글, 상위로 전달:", { console.log("🌊 FlowWidget - 체크박스 토글, 상위로 전달:", {
rowIndex, rowKey,
newSelectedSize: newSelected.size, newSelectedSize: newSelected.size,
selectedData, selectedData,
selectedStepId, selectedStepId,
@@ -780,18 +790,18 @@ export function FlowWidget({
onSelectedDataChange?.(selectedData, selectedStepId); onSelectedDataChange?.(selectedData, selectedStepId);
}; };
// 전체 선택/해제 // 전체 선택/해제 (Primary Key 기반)
const toggleAllRows = () => { const toggleAllRows = () => {
let newSelected: Set<number>; let newSelected: Set<string>;
if (selectedRows.size === stepData.length) { if (selectedRows.size === stepData.length) {
newSelected = new Set(); newSelected = new Set();
} else { } else {
newSelected = new Set(stepData.map((_, index) => index)); newSelected = new Set(stepData.map((row) => getRowKey(row)));
} }
setSelectedRows(newSelected); setSelectedRows(newSelected);
// 선택된 데이터를 상위로 전달 // 선택된 데이터를 상위로 전달
const selectedData = Array.from(newSelected).map((index) => stepData[index]); const selectedData = stepData.filter((row) => newSelected.has(getRowKey(row)));
onSelectedDataChange?.(selectedData, selectedStepId); onSelectedDataChange?.(selectedData, selectedStepId);
}; };
@@ -951,36 +961,41 @@ export function FlowWidget({
return formatValue(value); return formatValue(value);
}, []); }, []);
// 🆕 전체 선택 핸들러 // 🆕 전체 선택 핸들러 (Primary Key 기반)
const handleSelectAll = useCallback((checked: boolean) => { const handleSelectAll = useCallback((checked: boolean) => {
if (checked) { if (checked) {
const allIndices = new Set(sortedDisplayData.map((_, idx) => idx)); const allKeys = new Set(sortedDisplayData.map((row) => getRowKey(row)));
setSelectedRows(allIndices); setSelectedRows(allKeys);
// 선택된 데이터를 상위로 전달
onSelectedDataChange?.(sortedDisplayData, selectedStepId);
} else { } else {
setSelectedRows(new Set()); setSelectedRows(new Set());
onSelectedDataChange?.([], selectedStepId);
} }
}, [sortedDisplayData]); }, [sortedDisplayData, getRowKey, onSelectedDataChange, selectedStepId]);
// 🆕 행 클릭 핸들러 // 🆕 행 클릭 핸들러
const handleRowClick = useCallback((row: any) => { const handleRowClick = useCallback((row: any) => {
// 필요 시 행 클릭 로직 추가 // 필요 시 행 클릭 로직 추가
}, []); }, []);
// 🆕 체크박스 셀 렌더링 // 🆕 체크박스 셀 렌더링 (Primary Key 기반)
const renderCheckboxCell = useCallback((row: any, index: number) => { // index 파라미터는 SingleTableWithSticky 인터페이스 호환을 위해 유지하지만 사용하지 않음
const renderCheckboxCell = useCallback((row: any, _index: number) => {
const rowKey = getRowKey(row);
return ( return (
<Checkbox <Checkbox
checked={selectedRows.has(index)} checked={selectedRows.has(rowKey)}
onCheckedChange={() => toggleRowSelection(index)} onCheckedChange={() => toggleRowSelection(row)}
/> />
); );
}, [selectedRows, toggleRowSelection]); }, [selectedRows, toggleRowSelection, getRowKey]);
// 🆕 Excel 내보내기 // 🆕 Excel 내보내기 (Primary Key 기반)
const exportToExcel = useCallback(() => { const exportToExcel = useCallback(() => {
try { try {
const exportData = selectedRows.size > 0 const exportData = selectedRows.size > 0
? sortedDisplayData.filter((_, idx) => selectedRows.has(idx)) ? sortedDisplayData.filter((row) => selectedRows.has(getRowKey(row)))
: sortedDisplayData; : sortedDisplayData;
if (exportData.length === 0) { if (exportData.length === 0) {
@@ -1010,13 +1025,13 @@ export function FlowWidget({
console.error("Excel 내보내기 오류:", error); console.error("Excel 내보내기 오류:", error);
toast.error("Excel 내보내기에 실패했습니다."); toast.error("Excel 내보내기에 실패했습니다.");
} }
}, [sortedDisplayData, selectedRows, stepDataColumns, columnLabels, flowName]); }, [sortedDisplayData, selectedRows, stepDataColumns, columnLabels, flowName, getRowKey]);
// 🆕 PDF 내보내기 (html2canvas 사용으로 한글 지원) // 🆕 PDF 내보내기 (html2canvas 사용으로 한글 지원)
const exportToPdf = useCallback(async () => { const exportToPdf = useCallback(async () => {
try { try {
const exportData = selectedRows.size > 0 const exportData = selectedRows.size > 0
? sortedDisplayData.filter((_, idx) => selectedRows.has(idx)) ? sortedDisplayData.filter((row) => selectedRows.has(getRowKey(row)))
: sortedDisplayData; : sortedDisplayData;
if (exportData.length === 0) { if (exportData.length === 0) {
@@ -1175,13 +1190,13 @@ export function FlowWidget({
console.error("PDF 내보내기 오류:", error); console.error("PDF 내보내기 오류:", error);
toast.error("PDF 내보내기에 실패했습니다.", { id: "pdf-export" }); toast.error("PDF 내보내기에 실패했습니다.", { id: "pdf-export" });
} }
}, [sortedDisplayData, selectedRows, stepDataColumns, columnLabels, flowName]); }, [sortedDisplayData, selectedRows, stepDataColumns, columnLabels, flowName, getRowKey]);
// 🆕 복사 기능 // 🆕 복사 기능 (Primary Key 기반)
const handleCopy = useCallback(() => { const handleCopy = useCallback(() => {
try { try {
const copyData = selectedRows.size > 0 const copyData = selectedRows.size > 0
? sortedDisplayData.filter((_, idx) => selectedRows.has(idx)) ? sortedDisplayData.filter((row) => selectedRows.has(getRowKey(row)))
: []; : [];
if (copyData.length === 0) { if (copyData.length === 0) {
@@ -1203,7 +1218,7 @@ export function FlowWidget({
console.error("복사 오류:", error); console.error("복사 오류:", error);
toast.error("복사에 실패했습니다."); toast.error("복사에 실패했습니다.");
} }
}, [sortedDisplayData, selectedRows, stepDataColumns, columnLabels]); }, [sortedDisplayData, selectedRows, stepDataColumns, columnLabels, getRowKey]);
// 🆕 통합 검색 실행 // 🆕 통합 검색 실행
const executeGlobalSearch = useCallback((term: string) => { const executeGlobalSearch = useCallback((term: string) => {
@@ -1828,15 +1843,15 @@ export function FlowWidget({
<div <div
key={actualIndex} key={actualIndex}
className={`bg-card rounded-md border p-3 transition-colors ${ className={`bg-card rounded-md border p-3 transition-colors ${
selectedRows.has(actualIndex) ? "bg-primary/5 border-primary/30" : "" selectedRows.has(getRowKey(row)) ? "bg-primary/5 border-primary/30" : ""
}`} }`}
> >
{allowDataMove && ( {allowDataMove && (
<div className="mb-2 flex items-center justify-between border-b pb-2"> <div className="mb-2 flex items-center justify-between border-b pb-2">
<span className="text-muted-foreground text-xs font-medium"></span> <span className="text-muted-foreground text-xs font-medium"></span>
<Checkbox <Checkbox
checked={selectedRows.has(actualIndex)} checked={selectedRows.has(getRowKey(row))}
onCheckedChange={() => toggleRowSelection(actualIndex)} onCheckedChange={() => toggleRowSelection(row)}
/> />
</div> </div>
)} )}
@@ -1919,13 +1934,13 @@ export function FlowWidget({
return ( return (
<TableRow <TableRow
key={`${group.groupKey}-${itemIndex}`} key={`${group.groupKey}-${itemIndex}`}
className={`h-16 transition-colors hover:bg-muted/50 ${selectedRows.has(actualIndex) ? "bg-primary/5" : ""}`} className={`h-16 transition-colors hover:bg-muted/50 ${selectedRows.has(getRowKey(row)) ? "bg-primary/5" : ""}`}
> >
{allowDataMove && ( {allowDataMove && (
<TableCell className="bg-background sticky left-0 z-10 border-b px-6 py-3 text-center"> <TableCell className="bg-background sticky left-0 z-10 border-b px-6 py-3 text-center">
<Checkbox <Checkbox
checked={selectedRows.has(actualIndex)} checked={selectedRows.has(getRowKey(row))}
onCheckedChange={() => toggleRowSelection(actualIndex)} onCheckedChange={() => toggleRowSelection(row)}
/> />
</TableCell> </TableCell>
)} )}