2025-12-23 14:45:19 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* UnifiedRepeater 컴포넌트
|
|
|
|
|
*
|
2025-12-23 16:44:53 +09:00
|
|
|
* 렌더링 모드:
|
|
|
|
|
* - inline: 현재 테이블 컬럼 직접 입력
|
|
|
|
|
* - modal: 엔티티 선택 (FK 저장) + 추가 입력 컬럼
|
2025-12-24 10:31:36 +09:00
|
|
|
*
|
|
|
|
|
* RepeaterTable 및 ItemSelectionModal 재사용
|
2025-12-23 14:45:19 +09:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
2025-12-24 10:31:36 +09:00
|
|
|
import { Plus, Columns } from "lucide-react";
|
2025-12-23 14:45:19 +09:00
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
import {
|
|
|
|
|
UnifiedRepeaterConfig,
|
|
|
|
|
UnifiedRepeaterProps,
|
2025-12-24 10:31:36 +09:00
|
|
|
RepeaterColumnConfig as UnifiedColumnConfig,
|
2025-12-23 14:45:19 +09:00
|
|
|
DEFAULT_REPEATER_CONFIG,
|
|
|
|
|
} from "@/types/unified-repeater";
|
|
|
|
|
import { apiClient } from "@/lib/api/client";
|
|
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// modal-repeater-table 컴포넌트 재사용
|
|
|
|
|
import { RepeaterTable } from "@/lib/registry/components/modal-repeater-table/RepeaterTable";
|
|
|
|
|
import { ItemSelectionModal } from "@/lib/registry/components/modal-repeater-table/ItemSelectionModal";
|
|
|
|
|
import { RepeaterColumnConfig } from "@/lib/registry/components/modal-repeater-table/types";
|
2025-12-23 14:45:19 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 전역 UnifiedRepeater 등록 (buttonActions에서 사용)
|
2025-12-24 09:58:22 +09:00
|
|
|
declare global {
|
|
|
|
|
interface Window {
|
|
|
|
|
__unifiedRepeaterInstances?: Set<string>;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-23 14:45:19 +09:00
|
|
|
export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
|
|
|
|
|
config: propConfig,
|
|
|
|
|
parentId,
|
|
|
|
|
data: initialData,
|
|
|
|
|
onDataChange,
|
|
|
|
|
onRowClick,
|
|
|
|
|
className,
|
|
|
|
|
}) => {
|
|
|
|
|
// 설정 병합
|
|
|
|
|
const config: UnifiedRepeaterConfig = useMemo(
|
|
|
|
|
() => ({
|
|
|
|
|
...DEFAULT_REPEATER_CONFIG,
|
|
|
|
|
...propConfig,
|
|
|
|
|
dataSource: { ...DEFAULT_REPEATER_CONFIG.dataSource, ...propConfig.dataSource },
|
|
|
|
|
features: { ...DEFAULT_REPEATER_CONFIG.features, ...propConfig.features },
|
|
|
|
|
modal: { ...DEFAULT_REPEATER_CONFIG.modal, ...propConfig.modal },
|
|
|
|
|
}),
|
|
|
|
|
[propConfig],
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 상태
|
2025-12-23 14:45:19 +09:00
|
|
|
const [data, setData] = useState<any[]>(initialData || []);
|
|
|
|
|
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
|
2025-12-24 10:31:36 +09:00
|
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
|
|
|
const [equalizeWidthsTrigger, setEqualizeWidthsTrigger] = useState(0);
|
|
|
|
|
|
|
|
|
|
// 소스 테이블 컬럼 라벨 매핑
|
|
|
|
|
const [sourceColumnLabels, setSourceColumnLabels] = useState<Record<string, string>>({});
|
2025-12-23 16:44:53 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 현재 테이블 컬럼 정보 (inputType 매핑용)
|
|
|
|
|
const [currentTableColumnInfo, setCurrentTableColumnInfo] = useState<Record<string, any>>({});
|
2025-12-24 09:58:22 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 동적 데이터 소스 상태
|
|
|
|
|
const [activeDataSources, setActiveDataSources] = useState<Record<string, string>>({});
|
|
|
|
|
|
|
|
|
|
const isModalMode = config.renderMode === "modal";
|
2025-12-24 09:58:22 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 전역 리피터 등록
|
2025-12-24 09:58:22 +09:00
|
|
|
useEffect(() => {
|
|
|
|
|
const tableName = config.dataSource?.tableName;
|
2025-12-24 10:31:36 +09:00
|
|
|
if (tableName) {
|
|
|
|
|
if (!window.__unifiedRepeaterInstances) {
|
|
|
|
|
window.__unifiedRepeaterInstances = new Set();
|
|
|
|
|
}
|
|
|
|
|
window.__unifiedRepeaterInstances.add(tableName);
|
2025-12-24 09:58:22 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return () => {
|
2025-12-24 10:31:36 +09:00
|
|
|
if (tableName && window.__unifiedRepeaterInstances) {
|
|
|
|
|
window.__unifiedRepeaterInstances.delete(tableName);
|
|
|
|
|
}
|
2025-12-24 09:58:22 +09:00
|
|
|
};
|
|
|
|
|
}, [config.dataSource?.tableName]);
|
2025-12-23 14:45:19 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 저장 이벤트 리스너
|
2025-12-24 09:58:22 +09:00
|
|
|
useEffect(() => {
|
|
|
|
|
const handleSaveEvent = async (event: CustomEvent) => {
|
|
|
|
|
const tableName = config.dataSource?.tableName;
|
|
|
|
|
const eventParentId = event.detail?.parentId;
|
2025-12-24 10:31:36 +09:00
|
|
|
const mainFormData = event.detail?.mainFormData;
|
2025-12-24 09:58:22 +09:00
|
|
|
|
|
|
|
|
if (!tableName || data.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-24 10:31:36 +09:00
|
|
|
// 테이블 유효 컬럼 조회
|
2025-12-24 09:58:22 +09:00
|
|
|
let validColumns: Set<string> = new Set();
|
|
|
|
|
try {
|
|
|
|
|
const columnsResponse = await apiClient.get(`/table-management/tables/${tableName}/columns`);
|
|
|
|
|
const columns = columnsResponse.data?.data?.columns || columnsResponse.data?.columns || columnsResponse.data || [];
|
|
|
|
|
validColumns = new Set(columns.map((col: any) => col.columnName || col.column_name || col.name));
|
|
|
|
|
} catch {
|
2025-12-24 10:31:36 +09:00
|
|
|
console.warn("테이블 컬럼 정보 조회 실패");
|
2025-12-24 09:58:22 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
|
|
|
const row = data[i];
|
|
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 내부 필드 제거
|
2025-12-24 09:58:22 +09:00
|
|
|
const cleanRow = Object.fromEntries(
|
|
|
|
|
Object.entries(row).filter(([key]) => !key.startsWith("_"))
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 메인 폼 데이터 병합
|
|
|
|
|
const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {};
|
2025-12-24 09:58:22 +09:00
|
|
|
const mergedData = {
|
2025-12-24 10:31:36 +09:00
|
|
|
...mainFormDataWithoutId,
|
|
|
|
|
...cleanRow,
|
2025-12-24 09:58:22 +09:00
|
|
|
};
|
|
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 유효하지 않은 컬럼 제거
|
2025-12-24 09:58:22 +09:00
|
|
|
const filteredData: Record<string, any> = {};
|
|
|
|
|
for (const [key, value] of Object.entries(mergedData)) {
|
|
|
|
|
if (validColumns.size === 0 || validColumns.has(key)) {
|
|
|
|
|
filteredData[key] = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData);
|
2025-12-24 09:58:22 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
console.log("UnifiedRepeater 저장 완료:", data.length, "건");
|
2025-12-24 09:58:22 +09:00
|
|
|
} catch (error) {
|
2025-12-24 10:31:36 +09:00
|
|
|
console.error("UnifiedRepeater 저장 실패:", error);
|
|
|
|
|
throw error;
|
2025-12-24 09:58:22 +09:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.addEventListener("repeaterSave" as any, handleSaveEvent);
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener("repeaterSave" as any, handleSaveEvent);
|
|
|
|
|
};
|
2025-12-24 10:31:36 +09:00
|
|
|
}, [data, config.dataSource?.tableName, parentId]);
|
2025-12-24 09:58:22 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 현재 테이블 컬럼 정보 로드
|
2025-12-24 09:58:22 +09:00
|
|
|
useEffect(() => {
|
|
|
|
|
const loadCurrentTableColumnInfo = async () => {
|
|
|
|
|
const tableName = config.dataSource?.tableName;
|
|
|
|
|
if (!tableName) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
|
|
|
|
|
const columns = response.data?.data?.columns || response.data?.columns || response.data || [];
|
|
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
const columnMap: Record<string, any> = {};
|
|
|
|
|
columns.forEach((col: any) => {
|
|
|
|
|
const name = col.columnName || col.column_name || col.name;
|
|
|
|
|
columnMap[name] = {
|
|
|
|
|
inputType: col.inputType || col.input_type || col.webType || "text",
|
|
|
|
|
displayName: col.displayName || col.display_name || col.label || name,
|
|
|
|
|
detailSettings: col.detailSettings || col.detail_settings,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
setCurrentTableColumnInfo(columnMap);
|
2025-12-24 09:58:22 +09:00
|
|
|
} catch (error) {
|
2025-12-24 10:31:36 +09:00
|
|
|
console.error("컬럼 정보 로드 실패:", error);
|
2025-12-24 09:58:22 +09:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
loadCurrentTableColumnInfo();
|
|
|
|
|
}, [config.dataSource?.tableName]);
|
|
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 소스 테이블 컬럼 라벨 로드 (modal 모드)
|
2025-12-23 16:44:53 +09:00
|
|
|
useEffect(() => {
|
2025-12-24 10:31:36 +09:00
|
|
|
const loadSourceColumnLabels = async () => {
|
2025-12-23 16:44:53 +09:00
|
|
|
const sourceTable = config.dataSource?.sourceTable;
|
2025-12-24 10:31:36 +09:00
|
|
|
if (!isModalMode || !sourceTable) return;
|
|
|
|
|
|
2025-12-23 16:44:53 +09:00
|
|
|
try {
|
|
|
|
|
const response = await apiClient.get(`/table-management/tables/${sourceTable}/columns`);
|
2025-12-24 10:31:36 +09:00
|
|
|
const columns = response.data?.data?.columns || response.data?.columns || response.data || [];
|
2025-12-23 16:44:53 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
const labels: Record<string, string> = {};
|
2025-12-23 16:44:53 +09:00
|
|
|
columns.forEach((col: any) => {
|
2025-12-24 10:31:36 +09:00
|
|
|
const name = col.columnName || col.column_name || col.name;
|
|
|
|
|
labels[name] = col.displayName || col.display_name || col.label || name;
|
2025-12-23 16:44:53 +09:00
|
|
|
});
|
2025-12-24 10:31:36 +09:00
|
|
|
setSourceColumnLabels(labels);
|
2025-12-23 16:44:53 +09:00
|
|
|
} catch (error) {
|
2025-12-24 10:31:36 +09:00
|
|
|
console.error("소스 컬럼 라벨 로드 실패:", error);
|
2025-12-23 16:44:53 +09:00
|
|
|
}
|
|
|
|
|
};
|
2025-12-24 10:31:36 +09:00
|
|
|
loadSourceColumnLabels();
|
|
|
|
|
}, [config.dataSource?.sourceTable, isModalMode]);
|
2025-12-23 16:44:53 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// UnifiedColumnConfig → RepeaterColumnConfig 변환
|
|
|
|
|
const repeaterColumns: RepeaterColumnConfig[] = useMemo(() => {
|
|
|
|
|
const displayColumns: RepeaterColumnConfig[] = [];
|
|
|
|
|
|
|
|
|
|
// 모달 표시 컬럼 추가 (읽기 전용)
|
|
|
|
|
if (isModalMode && config.modal?.sourceDisplayColumns) {
|
|
|
|
|
config.modal.sourceDisplayColumns.forEach((col) => {
|
|
|
|
|
const key = typeof col === "string" ? col : col.key;
|
|
|
|
|
const label = typeof col === "string" ? sourceColumnLabels[col] || col : col.label || sourceColumnLabels[key] || key;
|
2025-12-24 09:58:22 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
if (key && key !== "none") {
|
|
|
|
|
displayColumns.push({
|
|
|
|
|
field: `_display_${key}`,
|
|
|
|
|
label,
|
|
|
|
|
type: "text",
|
|
|
|
|
editable: false,
|
|
|
|
|
calculated: true,
|
|
|
|
|
});
|
2025-12-24 09:58:22 +09:00
|
|
|
}
|
|
|
|
|
});
|
2025-12-23 16:44:53 +09:00
|
|
|
}
|
|
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 입력 컬럼 추가
|
|
|
|
|
const inputColumns = config.columns.map((col: UnifiedColumnConfig): RepeaterColumnConfig => {
|
|
|
|
|
const colInfo = currentTableColumnInfo[col.key];
|
|
|
|
|
const inputType = col.inputType || colInfo?.inputType || "text";
|
2025-12-23 16:44:53 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
let type: "text" | "number" | "date" | "select" = "text";
|
|
|
|
|
if (inputType === "number" || inputType === "decimal") type = "number";
|
|
|
|
|
else if (inputType === "date" || inputType === "datetime") type = "date";
|
|
|
|
|
else if (inputType === "code") type = "select";
|
2025-12-23 16:44:53 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
return {
|
|
|
|
|
field: col.key,
|
|
|
|
|
label: col.title || colInfo?.displayName || col.key,
|
|
|
|
|
type,
|
|
|
|
|
editable: col.editable !== false,
|
|
|
|
|
width: col.width === "auto" ? undefined : col.width,
|
|
|
|
|
required: false,
|
|
|
|
|
};
|
2025-12-23 16:44:53 +09:00
|
|
|
});
|
2025-12-24 10:31:36 +09:00
|
|
|
|
|
|
|
|
return [...displayColumns, ...inputColumns];
|
|
|
|
|
}, [config.columns, config.modal?.sourceDisplayColumns, isModalMode, sourceColumnLabels, currentTableColumnInfo]);
|
2025-12-23 16:44:53 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 데이터 변경 핸들러
|
|
|
|
|
const handleDataChange = useCallback((newData: any[]) => {
|
|
|
|
|
setData(newData);
|
|
|
|
|
onDataChange?.(newData);
|
|
|
|
|
}, [onDataChange]);
|
2025-12-23 16:44:53 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 행 변경 핸들러
|
|
|
|
|
const handleRowChange = useCallback((index: number, newRow: any) => {
|
|
|
|
|
const newData = [...data];
|
|
|
|
|
newData[index] = newRow;
|
|
|
|
|
setData(newData);
|
|
|
|
|
onDataChange?.(newData);
|
|
|
|
|
}, [data, onDataChange]);
|
|
|
|
|
|
|
|
|
|
// 행 삭제 핸들러
|
|
|
|
|
const handleRowDelete = useCallback((index: number) => {
|
|
|
|
|
const newData = data.filter((_, i) => i !== index);
|
2025-12-23 16:44:53 +09:00
|
|
|
setData(newData);
|
|
|
|
|
onDataChange?.(newData);
|
|
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 선택 상태 업데이트
|
|
|
|
|
const newSelected = new Set<number>();
|
|
|
|
|
selectedRows.forEach((i) => {
|
|
|
|
|
if (i < index) newSelected.add(i);
|
|
|
|
|
else if (i > index) newSelected.add(i - 1);
|
2025-12-23 16:44:53 +09:00
|
|
|
});
|
2025-12-24 10:31:36 +09:00
|
|
|
setSelectedRows(newSelected);
|
|
|
|
|
}, [data, selectedRows, onDataChange]);
|
2025-12-23 16:44:53 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 일괄 삭제 핸들러
|
|
|
|
|
const handleBulkDelete = useCallback(() => {
|
|
|
|
|
const newData = data.filter((_, index) => !selectedRows.has(index));
|
|
|
|
|
setData(newData);
|
|
|
|
|
onDataChange?.(newData);
|
|
|
|
|
setSelectedRows(new Set());
|
|
|
|
|
}, [data, selectedRows, onDataChange]);
|
|
|
|
|
|
|
|
|
|
// 행 추가 (inline 모드)
|
|
|
|
|
const handleAddRow = useCallback(() => {
|
|
|
|
|
if (isModalMode) {
|
|
|
|
|
setModalOpen(true);
|
2025-12-23 14:45:19 +09:00
|
|
|
} else {
|
2025-12-24 10:31:36 +09:00
|
|
|
const newRow: any = { _id: `new_${Date.now()}` };
|
2025-12-23 14:45:19 +09:00
|
|
|
config.columns.forEach((col) => {
|
|
|
|
|
newRow[col.key] = "";
|
|
|
|
|
});
|
|
|
|
|
const newData = [...data, newRow];
|
|
|
|
|
setData(newData);
|
|
|
|
|
onDataChange?.(newData);
|
|
|
|
|
}
|
2025-12-24 10:31:36 +09:00
|
|
|
}, [isModalMode, config.columns, data, onDataChange]);
|
2025-12-23 14:45:19 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 모달에서 항목 선택
|
|
|
|
|
const handleSelectItems = useCallback((items: Record<string, unknown>[]) => {
|
|
|
|
|
const fkColumn = config.dataSource?.foreignKey;
|
|
|
|
|
const refKey = config.dataSource?.referenceKey || "id";
|
2025-12-23 16:44:53 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
const newRows = items.map((item) => {
|
|
|
|
|
const row: any = { _id: `new_${Date.now()}_${Math.random()}` };
|
|
|
|
|
|
|
|
|
|
// FK 값 저장
|
|
|
|
|
if (fkColumn && item[refKey]) {
|
|
|
|
|
row[fkColumn] = item[refKey];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 표시용 데이터 저장
|
|
|
|
|
if (config.modal?.sourceDisplayColumns) {
|
|
|
|
|
config.modal.sourceDisplayColumns.forEach((col) => {
|
|
|
|
|
const key = typeof col === "string" ? col : col.key;
|
|
|
|
|
if (key && key !== "none") {
|
|
|
|
|
row[`_display_${key}`] = item[key] || "";
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 입력 컬럼 초기화
|
|
|
|
|
config.columns.forEach((col) => {
|
|
|
|
|
if (row[col.key] === undefined) {
|
|
|
|
|
row[col.key] = "";
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return row;
|
2025-12-23 16:44:53 +09:00
|
|
|
});
|
|
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
const newData = [...data, ...newRows];
|
2025-12-23 16:44:53 +09:00
|
|
|
setData(newData);
|
|
|
|
|
onDataChange?.(newData);
|
2025-12-24 10:31:36 +09:00
|
|
|
setModalOpen(false);
|
|
|
|
|
}, [config.dataSource?.foreignKey, config.dataSource?.referenceKey, config.modal?.sourceDisplayColumns, config.columns, data, onDataChange]);
|
2025-12-23 14:45:19 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
// 소스 컬럼 목록 (모달용)
|
|
|
|
|
const sourceColumns = useMemo(() => {
|
|
|
|
|
if (!config.modal?.sourceDisplayColumns) return [];
|
|
|
|
|
return config.modal.sourceDisplayColumns
|
|
|
|
|
.map((col) => typeof col === "string" ? col : col.key)
|
|
|
|
|
.filter((key) => key && key !== "none");
|
|
|
|
|
}, [config.modal?.sourceDisplayColumns]);
|
2025-12-23 14:45:19 +09:00
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
return (
|
|
|
|
|
<div className={cn("space-y-4", className)}>
|
|
|
|
|
{/* 헤더 영역 */}
|
|
|
|
|
<div className="flex justify-between items-center">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="text-sm text-muted-foreground">
|
|
|
|
|
{data.length > 0 && `${data.length}개 항목`}
|
|
|
|
|
{selectedRows.size > 0 && ` (${selectedRows.size}개 선택됨)`}
|
|
|
|
|
</span>
|
|
|
|
|
{repeaterColumns.length > 0 && (
|
2025-12-23 14:45:19 +09:00
|
|
|
<Button
|
2025-12-24 10:31:36 +09:00
|
|
|
variant="outline"
|
2025-12-23 14:45:19 +09:00
|
|
|
size="sm"
|
2025-12-24 10:31:36 +09:00
|
|
|
onClick={() => setEqualizeWidthsTrigger((prev) => prev + 1)}
|
|
|
|
|
className="h-7 text-xs px-2"
|
|
|
|
|
title="컬럼 너비 균등 분배"
|
2025-12-23 14:45:19 +09:00
|
|
|
>
|
2025-12-24 10:31:36 +09:00
|
|
|
<Columns className="h-3.5 w-3.5 mr-1" />
|
|
|
|
|
균등 분배
|
2025-12-23 14:45:19 +09:00
|
|
|
</Button>
|
2025-12-24 10:31:36 +09:00
|
|
|
)}
|
2025-12-23 14:45:19 +09:00
|
|
|
</div>
|
2025-12-24 10:31:36 +09:00
|
|
|
<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>
|
|
|
|
|
)}
|
2025-12-23 14:45:19 +09:00
|
|
|
<Button
|
2025-12-24 10:31:36 +09:00
|
|
|
onClick={handleAddRow}
|
|
|
|
|
className="h-8 text-xs sm:h-10 sm:text-sm"
|
2025-12-23 14:45:19 +09:00
|
|
|
>
|
2025-12-24 10:31:36 +09:00
|
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
|
|
|
{isModalMode ? (config.modal?.buttonText || "검색") : "추가"}
|
2025-12-23 14:45:19 +09:00
|
|
|
</Button>
|
2025-12-24 10:31:36 +09:00
|
|
|
</div>
|
2025-12-23 14:45:19 +09:00
|
|
|
</div>
|
|
|
|
|
|
2025-12-24 10:31:36 +09:00
|
|
|
{/* Repeater 테이블 */}
|
|
|
|
|
<RepeaterTable
|
|
|
|
|
columns={repeaterColumns}
|
|
|
|
|
data={data}
|
|
|
|
|
onDataChange={handleDataChange}
|
|
|
|
|
onRowChange={handleRowChange}
|
|
|
|
|
onRowDelete={handleRowDelete}
|
|
|
|
|
activeDataSources={activeDataSources}
|
|
|
|
|
onDataSourceChange={(field, optionId) => {
|
|
|
|
|
setActiveDataSources((prev) => ({ ...prev, [field]: optionId }));
|
|
|
|
|
}}
|
|
|
|
|
selectedRows={selectedRows}
|
|
|
|
|
onSelectionChange={setSelectedRows}
|
|
|
|
|
equalizeWidthsTrigger={equalizeWidthsTrigger}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* 항목 선택 모달 (modal 모드) */}
|
|
|
|
|
{isModalMode && (
|
|
|
|
|
<ItemSelectionModal
|
|
|
|
|
open={modalOpen}
|
|
|
|
|
onOpenChange={setModalOpen}
|
|
|
|
|
sourceTable={config.dataSource?.sourceTable || ""}
|
|
|
|
|
sourceColumns={sourceColumns}
|
|
|
|
|
sourceSearchFields={config.modal?.searchFields || sourceColumns}
|
|
|
|
|
multiSelect={config.features?.multiSelect ?? true}
|
|
|
|
|
modalTitle={config.modal?.title || "항목 검색"}
|
|
|
|
|
alreadySelected={data}
|
|
|
|
|
uniqueField={config.dataSource?.referenceKey || "id"}
|
|
|
|
|
onSelect={handleSelectItems}
|
|
|
|
|
columnLabels={sourceColumnLabels}
|
|
|
|
|
/>
|
2025-12-23 14:45:19 +09:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
UnifiedRepeater.displayName = "UnifiedRepeater";
|
|
|
|
|
|
|
|
|
|
export default UnifiedRepeater;
|