feat(modal-repeater-table): 체크박스 기반 일괄 삭제 기능 추가
- RepeaterTable: 체크박스 컬럼 추가 (전체 선택/개별 선택 지원) - RepeaterTable: 선택된 행 시각적 피드백 (bg-blue-50) - RepeaterTable: 기존 개별 삭제 버튼 컬럼 제거 - ModalRepeaterTableComponent: selectedRows 상태 및 handleBulkDelete 함수 추가 - ModalRepeaterTableComponent: "선택 삭제" 버튼 UI 추가 - RepeatScreenModalConfigPanel: 행 번호 컬럼 선택에서 빈 값 필터링
This commit is contained in:
parent
4cff9e4cec
commit
56608001ff
|
|
@ -328,6 +328,9 @@ export function ModalRepeaterTableComponent({
|
|||
const companyCode = componentConfig?.companyCode || propCompanyCode;
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
|
||||
// 체크박스 선택 상태
|
||||
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
|
||||
|
||||
// 🆕 납기일 일괄 적용 플래그 (딱 한 번만 실행)
|
||||
const [isDeliveryDateApplied, setIsDeliveryDateApplied] = useState(false);
|
||||
|
||||
|
|
@ -794,6 +797,18 @@ export function ModalRepeaterTableComponent({
|
|||
handleChange(newData);
|
||||
};
|
||||
|
||||
// 선택된 항목 일괄 삭제 핸들러
|
||||
const handleBulkDelete = () => {
|
||||
if (selectedRows.size === 0) return;
|
||||
|
||||
// 선택되지 않은 항목만 남김
|
||||
const newData = localValue.filter((_, index) => !selectedRows.has(index));
|
||||
|
||||
// 데이터 업데이트 및 선택 상태 초기화
|
||||
handleChange(newData);
|
||||
setSelectedRows(new Set());
|
||||
};
|
||||
|
||||
// 컬럼명 -> 라벨명 매핑 생성 (sourceColumnLabels 우선, 없으면 columns에서 가져옴)
|
||||
const columnLabels = columns.reduce((acc, col) => {
|
||||
// sourceColumnLabels에 정의된 라벨 우선 사용
|
||||
|
|
@ -807,14 +822,26 @@ export function ModalRepeaterTableComponent({
|
|||
<div className="flex justify-between items-center">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{localValue.length > 0 && `${localValue.length}개 항목`}
|
||||
{selectedRows.size > 0 && ` (${selectedRows.size}개 선택됨)`}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{selectedRows.size > 0 && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleBulkDelete}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
>
|
||||
선택 삭제 ({selectedRows.size})
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => setModalOpen(true)}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{modalButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setModalOpen(true)}
|
||||
className="h-8 text-xs sm:h-10 sm:text-sm"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{modalButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Repeater 테이블 */}
|
||||
|
|
@ -826,6 +853,8 @@ export function ModalRepeaterTableComponent({
|
|||
onRowDelete={handleRowDelete}
|
||||
activeDataSources={activeDataSources}
|
||||
onDataSourceChange={handleDataSourceChange}
|
||||
selectedRows={selectedRows}
|
||||
onSelectionChange={setSelectedRows}
|
||||
/>
|
||||
|
||||
{/* 항목 선택 모달 */}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Trash2, ChevronDown, Check } from "lucide-react";
|
||||
import { ChevronDown, Check } from "lucide-react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { RepeaterColumnConfig } from "./types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
|
@ -18,6 +18,9 @@ interface RepeaterTableProps {
|
|||
// 동적 데이터 소스 관련
|
||||
activeDataSources?: Record<string, string>; // 컬럼별 현재 활성화된 데이터 소스 ID
|
||||
onDataSourceChange?: (columnField: string, optionId: string) => void; // 데이터 소스 변경 콜백
|
||||
// 체크박스 선택 관련
|
||||
selectedRows: Set<number>; // 선택된 행 인덱스
|
||||
onSelectionChange: (selectedRows: Set<number>) => void; // 선택 변경 콜백
|
||||
}
|
||||
|
||||
export function RepeaterTable({
|
||||
|
|
@ -28,6 +31,8 @@ export function RepeaterTable({
|
|||
onRowDelete,
|
||||
activeDataSources = {},
|
||||
onDataSourceChange,
|
||||
selectedRows,
|
||||
onSelectionChange,
|
||||
}: RepeaterTableProps) {
|
||||
const [editingCell, setEditingCell] = useState<{
|
||||
rowIndex: number;
|
||||
|
|
@ -112,6 +117,33 @@ export function RepeaterTable({
|
|||
onRowChange(rowIndex, newRow);
|
||||
};
|
||||
|
||||
// 전체 선택 체크박스 핸들러
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
// 모든 행 선택
|
||||
const allIndices = new Set(data.map((_, index) => index));
|
||||
onSelectionChange(allIndices);
|
||||
} else {
|
||||
// 전체 해제
|
||||
onSelectionChange(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
// 개별 행 선택 핸들러
|
||||
const handleRowSelect = (rowIndex: number, checked: boolean) => {
|
||||
const newSelection = new Set(selectedRows);
|
||||
if (checked) {
|
||||
newSelection.add(rowIndex);
|
||||
} else {
|
||||
newSelection.delete(rowIndex);
|
||||
}
|
||||
onSelectionChange(newSelection);
|
||||
};
|
||||
|
||||
// 전체 선택 상태 계산
|
||||
const isAllSelected = data.length > 0 && selectedRows.size === data.length;
|
||||
const isIndeterminate = selectedRows.size > 0 && selectedRows.size < data.length;
|
||||
|
||||
const renderCell = (
|
||||
row: any,
|
||||
column: RepeaterColumnConfig,
|
||||
|
|
@ -215,8 +247,17 @@ export function RepeaterTable({
|
|||
<table className="w-full text-xs border-collapse">
|
||||
<thead className="bg-gray-50 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700 border-b border-r border-gray-200 w-12">
|
||||
#
|
||||
<th className="px-3 py-2 text-center font-medium text-gray-700 border-b border-r border-gray-200 w-10">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
// @ts-ignore - indeterminate는 HTML 속성
|
||||
data-indeterminate={isIndeterminate}
|
||||
onCheckedChange={handleSelectAll}
|
||||
className={cn(
|
||||
"border-gray-400",
|
||||
isIndeterminate && "data-[state=checked]:bg-primary"
|
||||
)}
|
||||
/>
|
||||
</th>
|
||||
{columns.map((col) => {
|
||||
const hasDynamicSource = col.dynamicDataSource?.enabled && col.dynamicDataSource.options.length > 0;
|
||||
|
|
@ -303,16 +344,13 @@ export function RepeaterTable({
|
|||
</th>
|
||||
);
|
||||
})}
|
||||
<th className="px-3 py-2 text-left font-medium text-gray-700 border-b border-r border-gray-200 w-20">
|
||||
삭제
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white">
|
||||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={columns.length + 2}
|
||||
colSpan={columns.length + 1}
|
||||
className="px-4 py-8 text-center text-gray-500 border-b border-gray-200"
|
||||
>
|
||||
추가된 항목이 없습니다
|
||||
|
|
@ -320,25 +358,25 @@ export function RepeaterTable({
|
|||
</tr>
|
||||
) : (
|
||||
data.map((row, rowIndex) => (
|
||||
<tr key={rowIndex} className="hover:bg-blue-50/50 transition-colors">
|
||||
<td className="px-3 py-1 text-center text-gray-600 border-b border-r border-gray-200">
|
||||
{rowIndex + 1}
|
||||
<tr
|
||||
key={rowIndex}
|
||||
className={cn(
|
||||
"hover:bg-blue-50/50 transition-colors",
|
||||
selectedRows.has(rowIndex) && "bg-blue-50"
|
||||
)}
|
||||
>
|
||||
<td className="px-3 py-1 text-center border-b border-r border-gray-200">
|
||||
<Checkbox
|
||||
checked={selectedRows.has(rowIndex)}
|
||||
onCheckedChange={(checked) => handleRowSelect(rowIndex, !!checked)}
|
||||
className="border-gray-400"
|
||||
/>
|
||||
</td>
|
||||
{columns.map((col) => (
|
||||
<td key={col.field} className="px-1 py-1 border-b border-r border-gray-200">
|
||||
{renderCell(row, col, rowIndex)}
|
||||
</td>
|
||||
))}
|
||||
<td className="px-3 py-1 text-center border-b border-r border-gray-200">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onRowDelete(rowIndex)}
|
||||
className="h-7 w-7 p-0 text-red-500 hover:text-red-700 hover:bg-red-50"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1744,11 +1744,13 @@ function RowNumberingConfigSection({
|
|||
<SelectValue placeholder="컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{tableColumns.map((col, index) => (
|
||||
<SelectItem key={col.id || `col-${index}`} value={col.field} className="text-xs">
|
||||
{col.label || col.field}
|
||||
</SelectItem>
|
||||
))}
|
||||
{tableColumns
|
||||
.filter((col) => col.field && col.field.trim() !== "")
|
||||
.map((col, index) => (
|
||||
<SelectItem key={col.id || `col-${index}`} value={col.field} className="text-xs">
|
||||
{col.label || col.field}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-[9px] text-muted-foreground">
|
||||
|
|
|
|||
Loading…
Reference in New Issue