diff --git a/frontend/components/unified/UnifiedRepeater.tsx b/frontend/components/unified/UnifiedRepeater.tsx index 48a099c3..71559fd7 100644 --- a/frontend/components/unified/UnifiedRepeater.tsx +++ b/frontend/components/unified/UnifiedRepeater.tsx @@ -6,45 +6,28 @@ * 렌더링 모드: * - inline: 현재 테이블 컬럼 직접 입력 * - modal: 엔티티 선택 (FK 저장) + 추가 입력 컬럼 - * - button: 버튼으로 관련 화면/모달 열기 - * - mixed: inline + modal 혼합 + * + * RepeaterTable 및 ItemSelectionModal 재사용 */ import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { Input } from "@/components/ui/input"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Plus, Trash2, GripVertical, Search, X, Check, Calendar } from "lucide-react"; +import { Plus, Columns } from "lucide-react"; import { cn } from "@/lib/utils"; import { UnifiedRepeaterConfig, UnifiedRepeaterProps, - RepeaterButtonConfig, - ButtonActionType, - RepeaterColumnConfig, + RepeaterColumnConfig as UnifiedColumnConfig, DEFAULT_REPEATER_CONFIG, } from "@/types/unified-repeater"; import { apiClient } from "@/lib/api/client"; -import { commonCodeApi } from "@/lib/api/commonCode"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { Calendar as CalendarComponent } from "@/components/ui/calendar"; -import { format } from "date-fns"; -import { ko } from "date-fns/locale"; -// 모달 크기 매핑 -const MODAL_SIZE_MAP = { - sm: "max-w-md", - md: "max-w-lg", - lg: "max-w-2xl", - xl: "max-w-4xl", - full: "max-w-[95vw]", -}; +// 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"; -// 🆕 전역 UnifiedRepeater 등록 (buttonActions에서 사용) -// 리피터가 있으면 메인 폼은 리피터 데이터에 병합되어 저장되므로 별도 저장 불필요 +// 전역 UnifiedRepeater 등록 (buttonActions에서 사용) declare global { interface Window { __unifiedRepeaterInstances?: Set; @@ -57,7 +40,6 @@ export const UnifiedRepeater: React.FC = ({ data: initialData, onDataChange, onRowClick, - onButtonClick, className, }) => { // 설정 병합 @@ -68,150 +50,96 @@ export const UnifiedRepeater: React.FC = ({ dataSource: { ...DEFAULT_REPEATER_CONFIG.dataSource, ...propConfig.dataSource }, features: { ...DEFAULT_REPEATER_CONFIG.features, ...propConfig.features }, modal: { ...DEFAULT_REPEATER_CONFIG.modal, ...propConfig.modal }, - button: { ...DEFAULT_REPEATER_CONFIG.button, ...propConfig.button }, }), [propConfig], ); - // 상태 - 메인 데이터 + // 상태 const [data, setData] = useState(initialData || []); const [selectedRows, setSelectedRows] = useState>(new Set()); - const [editingRow, setEditingRow] = useState(null); - const [editedData, setEditedData] = useState>({}); - - // 상태 - 검색 모달 (modal 모드) - const [searchModalOpen, setSearchModalOpen] = useState(false); - const [searchKeyword, setSearchKeyword] = useState(""); - const [searchResults, setSearchResults] = useState([]); - const [searchLoading, setSearchLoading] = useState(false); - const [selectedSearchItems, setSelectedSearchItems] = useState>(new Set()); - - // 상태 - 버튼 모드 - const [codeButtons, setCodeButtons] = useState<{ label: string; value: string; variant?: string }[]>([]); - - // 상태 - 컬럼별 공통코드 옵션 (code 타입용) - const [columnCodeOptions, setColumnCodeOptions] = useState>({}); - - // 상태 - 엔티티 표시 정보 캐시 (FK값 → 표시 데이터) - const [entityDisplayCache, setEntityDisplayCache] = useState>>({}); + const [modalOpen, setModalOpen] = useState(false); + const [equalizeWidthsTrigger, setEqualizeWidthsTrigger] = useState(0); - // 상태 - 소스 테이블 컬럼 정보 (라벨 매핑용) - const [sourceTableColumnMap, setSourceTableColumnMap] = useState>({}); + // 소스 테이블 컬럼 라벨 매핑 + const [sourceColumnLabels, setSourceColumnLabels] = useState>({}); - // 상태 - 현재 테이블 컬럼 정보 (inputType 매핑용) - const [currentTableColumnInfo, setCurrentTableColumnInfo] = useState>({}); + // 현재 테이블 컬럼 정보 (inputType 매핑용) + const [currentTableColumnInfo, setCurrentTableColumnInfo] = useState>({}); + + // 동적 데이터 소스 상태 + const [activeDataSources, setActiveDataSources] = useState>({}); - // 🆕 전역 등록 - 리피터가 있으면 메인 폼 단독 저장 건너뛰기 + const isModalMode = config.renderMode === "modal"; + + // 전역 리피터 등록 useEffect(() => { const tableName = config.dataSource?.tableName; - if (!tableName) return; - - // 전역 Set 초기화 - if (!window.__unifiedRepeaterInstances) { - window.__unifiedRepeaterInstances = new Set(); + if (tableName) { + if (!window.__unifiedRepeaterInstances) { + window.__unifiedRepeaterInstances = new Set(); + } + window.__unifiedRepeaterInstances.add(tableName); } - - // 등록 - window.__unifiedRepeaterInstances.add(tableName); - console.log("📦 UnifiedRepeater 등록:", tableName, Array.from(window.__unifiedRepeaterInstances)); return () => { - // 해제 - window.__unifiedRepeaterInstances?.delete(tableName); - console.log("📦 UnifiedRepeater 해제:", tableName, Array.from(window.__unifiedRepeaterInstances || [])); + if (tableName && window.__unifiedRepeaterInstances) { + window.__unifiedRepeaterInstances.delete(tableName); + } }; }, [config.dataSource?.tableName]); - // 외부 데이터 변경 시 동기화 - useEffect(() => { - if (initialData) { - setData(initialData); - } - }, [initialData]); - - // 저장 이벤트 리스너 - 상위에서 저장 버튼 클릭 시 리피터 데이터도 저장 - // 🔧 메인 폼 데이터 + 리피터 행 데이터를 병합해서 저장 + // 저장 이벤트 리스너 useEffect(() => { const handleSaveEvent = async (event: CustomEvent) => { const tableName = config.dataSource?.tableName; const eventParentId = event.detail?.parentId; - const mainFormData = event.detail?.mainFormData || {}; // 🆕 메인 폼 데이터 + const mainFormData = event.detail?.mainFormData; if (!tableName || data.length === 0) { - console.log("📦 UnifiedRepeater 저장 스킵:", { tableName, dataCount: data.length, hasTable: !!tableName }); return; } - console.log("📦 UnifiedRepeater 저장 이벤트 수신:", { - tableName, - dataCount: data.length, - eventParentId, - propsParentId: parentId, - mainFormDataKeys: Object.keys(mainFormData), - referenceKey: config.dataSource?.referenceKey - }); - try { - // 새 데이터 삽입 (메인 폼 데이터 + 리피터 행 데이터 병합) - console.log("🔄 UnifiedRepeater 데이터 삽입 시작:", { dataCount: data.length, mainFormData }); - - // 🆕 테이블에 존재하는 컬럼 목록 조회 (존재하지 않는 컬럼 필터링용) + // 테이블 유효 컬럼 조회 let validColumns: Set = 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)); - console.log("📋 테이블 유효 컬럼:", Array.from(validColumns)); } catch { - console.warn("⚠️ 테이블 컬럼 정보 조회 실패 - 모든 필드 저장 시도"); + console.warn("테이블 컬럼 정보 조회 실패"); } for (let i = 0; i < data.length; i++) { const row = data[i]; - console.log(`🔄 [${i + 1}/${data.length}] 리피터 행 데이터:`, row); - // _display_ 필드와 임시 필드 제거 + // 내부 필드 제거 const cleanRow = Object.fromEntries( Object.entries(row).filter(([key]) => !key.startsWith("_")) ); - // 🆕 메인 폼 데이터 + 리피터 행 데이터 병합 - // 리피터 행 데이터가 우선 (같은 키가 있으면 덮어씀) - // 메인 폼에서 제외할 필드: id (각 행은 새 레코드) - const { id: _mainId, ...mainFormDataWithoutId } = mainFormData; + // 메인 폼 데이터 병합 + const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {}; const mergedData = { - ...mainFormDataWithoutId, // 메인 폼 데이터 (id 제외) - ...cleanRow, // 리피터 행 데이터 (우선) + ...mainFormDataWithoutId, + ...cleanRow, }; - // 🆕 테이블에 존재하지 않는 컬럼 제거 + // 유효하지 않은 컬럼 제거 const filteredData: Record = {}; for (const [key, value] of Object.entries(mergedData)) { - // validColumns가 비어있으면 (조회 실패) 모든 필드 포함 - // validColumns가 있으면 해당 컬럼만 포함 if (validColumns.size === 0 || validColumns.has(key)) { filteredData[key] = value; - } else { - console.log(`🗑️ [${i + 1}/${data.length}] 필터링된 컬럼 (테이블에 없음): ${key}`); } } - - console.log(`📝 [${i + 1}/${data.length}] 최종 저장 데이터:`, JSON.stringify(filteredData, null, 2)); - try { - // /add 엔드포인트 사용 (INSERT) - const saveResponse = await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData); - console.log(`✅ [${i + 1}/${data.length}] 저장 성공:`, saveResponse.data); - } catch (saveError) { - console.error(`❌ [${i + 1}/${data.length}] 저장 실패:`, saveError); - throw saveError; - } + await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData); } - console.log("✅ UnifiedRepeater 전체 데이터 저장 완료:", data.length, "건"); + console.log("UnifiedRepeater 저장 완료:", data.length, "건"); } catch (error) { - console.error("❌ UnifiedRepeater 저장 실패:", error); - throw error; // 상위에서 에러 처리 + console.error("UnifiedRepeater 저장 실패:", error); + throw error; } }; @@ -219,9 +147,9 @@ export const UnifiedRepeater: React.FC = ({ return () => { window.removeEventListener("repeaterSave" as any, handleSaveEvent); }; - }, [data, config.dataSource?.tableName, config.dataSource?.referenceKey, parentId]); + }, [data, config.dataSource?.tableName, parentId]); - // 현재 테이블 컬럼 정보 로드 (inputType 매핑용) + // 현재 테이블 컬럼 정보 로드 useEffect(() => { const loadCurrentTableColumnInfo = async () => { const tableName = config.dataSource?.tableName; @@ -231,941 +159,264 @@ export const UnifiedRepeater: React.FC = ({ const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); const columns = response.data?.data?.columns || response.data?.columns || response.data || []; - const colInfo: Record = {}; - if (Array.isArray(columns)) { - columns.forEach((col: any) => { - const colName = col.columnName || col.column_name; - let detailSettings = col.detailSettings || col.detail_settings; - if (typeof detailSettings === "string") { - try { - detailSettings = JSON.parse(detailSettings); - } catch (e) { - detailSettings = null; - } - } - if (colName) { - colInfo[colName] = { - inputType: col.inputType || col.input_type || "text", - detailSettings, - }; - } - }); - } - setCurrentTableColumnInfo(colInfo); - console.log("현재 테이블 컬럼 정보 로드:", tableName, colInfo); + const columnMap: Record = {}; + 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); } catch (error) { - console.error("현재 테이블 컬럼 정보 로드 실패:", error); + console.error("컬럼 정보 로드 실패:", error); } }; - loadCurrentTableColumnInfo(); }, [config.dataSource?.tableName]); - // 소스 테이블 컬럼 정보 로드 (라벨 매핑용) + // 소스 테이블 컬럼 라벨 로드 (modal 모드) useEffect(() => { - const loadSourceTableColumns = async () => { + const loadSourceColumnLabels = async () => { const sourceTable = config.dataSource?.sourceTable; - if (!sourceTable) return; - + if (!isModalMode || !sourceTable) return; + try { const response = await apiClient.get(`/table-management/tables/${sourceTable}/columns`); - console.log("소스 테이블 컬럼 API 응답:", response.data); - - const colMap: Record = {}; - - // 응답 구조에 따라 데이터 추출 - data.data.columns 형태 지원 - let columns: any[] = []; - if (response.data?.success && response.data?.data) { - // { success: true, data: { columns: [...] } } 구조 - if (Array.isArray(response.data.data.columns)) { - columns = response.data.data.columns; - } else if (Array.isArray(response.data.data)) { - columns = response.data.data; - } - } else if (Array.isArray(response.data)) { - columns = response.data; - } + const columns = response.data?.data?.columns || response.data?.columns || response.data || []; + const labels: Record = {}; columns.forEach((col: any) => { - const colName = col.columnName || col.column_name; - const colLabel = col.displayName || col.columnLabel || col.column_label || colName; - if (colName) { - colMap[colName] = colLabel; - } + const name = col.columnName || col.column_name || col.name; + labels[name] = col.displayName || col.display_name || col.label || name; }); - - console.log("sourceTableColumnMap:", colMap); - setSourceTableColumnMap(colMap); + setSourceColumnLabels(labels); } catch (error) { - console.error("소스 테이블 컬럼 로드 실패:", error); + console.error("소스 컬럼 라벨 로드 실패:", error); } }; + loadSourceColumnLabels(); + }, [config.dataSource?.sourceTable, isModalMode]); + + // UnifiedColumnConfig → RepeaterColumnConfig 변환 + const repeaterColumns: RepeaterColumnConfig[] = useMemo(() => { + const displayColumns: RepeaterColumnConfig[] = []; - if (config.renderMode === "modal" || config.renderMode === "mixed") { - loadSourceTableColumns(); - } - }, [config.dataSource?.sourceTable, config.renderMode]); - - // 공통코드 버튼 로드 - useEffect(() => { - const loadCodeButtons = async () => { - if (config.button?.sourceType !== "commonCode" || !config.button?.commonCode?.categoryCode) return; - - try { - const response = await commonCodeApi.codes.getList(config.button.commonCode.categoryCode); - if (response.success && response.data) { - const labelField = config.button.commonCode.labelField || "codeName"; - setCodeButtons( - response.data.map((code) => ({ - label: labelField === "codeName" ? code.codeName : code.codeValue, - value: code.codeValue, - variant: config.button?.commonCode?.variantMapping?.[code.codeValue], - })), - ); - } - } catch (error) { - console.error("공통코드 버튼 로드 실패:", error); - } - }; - loadCodeButtons(); - }, [config.button?.sourceType, config.button?.commonCode]); - - // 컬럼별 공통코드 옵션 로드 (code 타입 컬럼용) - useEffect(() => { - const loadColumnCodeOptions = async () => { - // config.columns와 currentTableColumnInfo를 함께 확인하여 code 타입 컬럼 찾기 - const codeColumnsToLoad: { key: string; codeGroup: string }[] = []; - - config.columns.forEach((col) => { - // 저장된 설정에서 codeGroup 확인 - let codeGroup = col.detailSettings?.codeGroup; - let inputType = col.inputType; + // 모달 표시 컬럼 추가 (읽기 전용) + 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; - // 저장된 정보가 없으면 현재 테이블 정보에서 확인 - if (!inputType || inputType === "text") { - const tableColInfo = currentTableColumnInfo[col.key]; - if (tableColInfo) { - inputType = tableColInfo.inputType; - if (!codeGroup && tableColInfo.detailSettings?.codeGroup) { - codeGroup = tableColInfo.detailSettings.codeGroup; - } - } - } - - if (inputType === "code" && codeGroup) { - codeColumnsToLoad.push({ key: col.key, codeGroup }); + if (key && key !== "none") { + displayColumns.push({ + field: `_display_${key}`, + label, + type: "text", + editable: false, + calculated: true, + }); } }); - - if (codeColumnsToLoad.length === 0) return; - - const newOptions: Record = {}; - - await Promise.all( - codeColumnsToLoad.map(async ({ key, codeGroup }) => { - try { - const response = await commonCodeApi.codes.getList(codeGroup); - if (response.success && response.data) { - newOptions[key] = response.data.map((code) => ({ - label: code.codeName, - value: code.codeValue, - })); - } - } catch (error) { - console.error(`공통코드 로드 실패 (${codeGroup}):`, error); - } - }) - ); - - setColumnCodeOptions((prev) => ({ ...prev, ...newOptions })); - }; - - loadColumnCodeOptions(); - }, [config.columns, currentTableColumnInfo]); - - // 소스 테이블 검색 (modal 모드) - const searchSourceTable = useCallback(async () => { - const sourceTable = config.dataSource?.sourceTable; - if (!sourceTable) return; - - setSearchLoading(true); - try { - const searchParams: any = {}; - - // 검색어가 있고, 검색 필드가 설정되어 있으면 검색 - if (searchKeyword && config.modal?.searchFields?.length) { - // 검색 필드들에 검색어 적용 - config.modal.searchFields.forEach((field) => { - searchParams[field] = searchKeyword; - }); - } - - const response = await apiClient.post(`/table-management/tables/${sourceTable}/data`, { - search: Object.keys(searchParams).length > 0 ? searchParams : undefined, - page: 1, - size: 50, - }); - - if (response.data?.success && response.data?.data) { - const items = Array.isArray(response.data.data) ? response.data.data : response.data.data.data || []; - setSearchResults(items); - } - } catch (error) { - console.error("검색 실패:", error); - setSearchResults([]); - } finally { - setSearchLoading(false); - } - }, [config.dataSource?.sourceTable, config.modal?.searchFields, searchKeyword]); - - // 검색 모달 열릴 때 자동 검색 - useEffect(() => { - if (searchModalOpen) { - searchSourceTable(); - } - }, [searchModalOpen, searchSourceTable]); - - // 행 선택 토글 - const toggleRowSelection = (index: number) => { - if (!config.features?.selectable) return; - - setSelectedRows((prev) => { - const newSet = new Set(prev); - if (newSet.has(index)) { - newSet.delete(index); - } else { - if (!config.features?.multiSelect) { - newSet.clear(); - } - newSet.add(index); - } - return newSet; - }); - }; - - // 검색 결과 항목 선택 토글 (검색 모달에서는 항상 다중 선택 허용) - const toggleSearchItemSelection = (index: number) => { - setSelectedSearchItems((prev) => { - const newSet = new Set(prev); - if (newSet.has(index)) { - newSet.delete(index); - } else { - newSet.add(index); - } - return newSet; - }); - }; - - // 검색 항목 선택 확인 - FK만 저장하고 추가 입력 컬럼은 빈 값으로 - const confirmSearchSelection = () => { - const selectedItems = Array.from(selectedSearchItems).map((index) => searchResults[index]).filter(Boolean); - - console.log("confirmSearchSelection 호출:", { - selectedSearchItems: Array.from(selectedSearchItems), - selectedItems, - searchResults: searchResults.length, - foreignKey: config.dataSource?.foreignKey, - referenceKey: config.dataSource?.referenceKey, - }); - - if (selectedItems.length === 0) { - console.warn("선택된 항목이 없습니다"); - return; } - const newRows = selectedItems.map((sourceItem) => { - const newRow: any = {}; + // 입력 컬럼 추가 + const inputColumns = config.columns.map((col: UnifiedColumnConfig): RepeaterColumnConfig => { + const colInfo = currentTableColumnInfo[col.key]; + const inputType = col.inputType || colInfo?.inputType || "text"; - // 1. FK 저장 (엔티티 참조 ID만!) - if (config.dataSource?.foreignKey && config.dataSource?.referenceKey) { - newRow[config.dataSource.foreignKey] = sourceItem[config.dataSource.referenceKey]; - console.log("FK 저장:", config.dataSource.foreignKey, "=", sourceItem[config.dataSource.referenceKey]); - } + 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"; - // 2. 추가 입력 컬럼은 빈 값으로 초기화 - config.columns.forEach((col) => { - if (newRow[col.key] === undefined) { - newRow[col.key] = ""; - } - }); - - return newRow; + 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, + }; }); - - // 선택한 항목의 표시 정보를 캐시에 저장 (렌더링용) - const newDisplayCache = { ...entityDisplayCache }; - selectedItems.forEach((sourceItem) => { - const fkValue = sourceItem[config.dataSource?.referenceKey || "id"]; - if (fkValue) { - const displayData: Record = {}; - // 모달 표시 컬럼들의 값을 캐시 - sourceDisplayColumns (useMemo로 변환된 것) 사용 - sourceDisplayColumns.forEach((col) => { - displayData[col.key] = sourceItem[col.key]; - }); - // displayColumn도 캐시 - if (config.dataSource?.displayColumn) { - displayData[config.dataSource.displayColumn] = sourceItem[config.dataSource.displayColumn]; - } - newDisplayCache[fkValue] = displayData; - console.log("캐시 저장:", fkValue, displayData); - } - }); - setEntityDisplayCache(newDisplayCache); - - const newData = [...data, ...newRows]; - const firstNewRowIndex = data.length; // 기존 data 길이가 첫 새 행의 인덱스 + return [...displayColumns, ...inputColumns]; + }, [config.columns, config.modal?.sourceDisplayColumns, isModalMode, sourceColumnLabels, currentTableColumnInfo]); + + // 데이터 변경 핸들러 + const handleDataChange = useCallback((newData: any[]) => { setData(newData); onDataChange?.(newData); - - // 모달 닫기 및 초기화 - setSearchModalOpen(false); - setSelectedSearchItems(new Set()); - setSearchKeyword(""); - - // 마지막 추가된 행을 편집 모드로 (setTimeout으로 state 업데이트 후 실행) - const hasEditableColumns = config.columns.length > 0; - console.log("편집 모드 전환 체크:", { - newRowsLength: newRows.length, - inlineEdit: config.features?.inlineEdit, - hasEditableColumns, - columnsLength: config.columns.length, - columns: config.columns, - }); - - // 추가 입력 컬럼이 있으면 편집 모드 시작 - if (newRows.length > 0 && hasEditableColumns) { - setTimeout(() => { - console.log("편집 모드 시작:", firstNewRowIndex, newRows[0]); - setEditingRow(firstNewRowIndex); - setEditedData({ ...newRows[0] }); - }, 0); - } - }; + }, [onDataChange]); - // 행 추가 - const handleAddRow = () => { - if (config.renderMode === "modal" || config.renderMode === "mixed") { - // 검색 모달 열기 - setSearchModalOpen(true); - } else { - // 인라인 추가 - const newRow: any = {}; - config.columns.forEach((col) => { - newRow[col.key] = ""; - }); + // 행 변경 핸들러 + const handleRowChange = useCallback((index: number, newRow: any) => { + const newData = [...data]; + newData[index] = newRow; + setData(newData); + onDataChange?.(newData); + }, [data, onDataChange]); - const newData = [...data, newRow]; - setData(newData); - onDataChange?.(newData); - setEditingRow(newData.length - 1); - setEditedData(newRow); - } - }; - - // 행 삭제 (로컬 상태만) - const handleDeleteRow = (index: number) => { + // 행 삭제 핸들러 + const handleRowDelete = useCallback((index: number) => { const newData = data.filter((_, i) => i !== index); setData(newData); onDataChange?.(newData); - setSelectedRows((prev) => { - const newSet = new Set(prev); - newSet.delete(index); - return newSet; - }); - // 편집 중이던 행이 삭제되면 편집 취소 - if (editingRow === index) { - setEditingRow(null); - setEditedData({}); - } - }; + // 선택 상태 업데이트 + const newSelected = new Set(); + selectedRows.forEach((i) => { + if (i < index) newSelected.add(i); + else if (i > index) newSelected.add(i - 1); + }); + setSelectedRows(newSelected); + }, [data, selectedRows, onDataChange]); - // 선택된 행 일괄 삭제 - const handleDeleteSelected = () => { - if (selectedRows.size === 0) return; - - const indices = Array.from(selectedRows).sort((a, b) => b - a); - let newData = [...data]; - for (const index of indices) { - newData = newData.filter((_, i) => i !== index); - } + // 일괄 삭제 핸들러 + const handleBulkDelete = useCallback(() => { + const newData = data.filter((_, index) => !selectedRows.has(index)); setData(newData); onDataChange?.(newData); setSelectedRows(new Set()); - setEditingRow(null); - setEditedData({}); - }; + }, [data, selectedRows, onDataChange]); - // 인라인 편집 확인 - const handleConfirmEdit = () => { - if (editingRow === null) return; + // 행 추가 (inline 모드) + const handleAddRow = useCallback(() => { + if (isModalMode) { + setModalOpen(true); + } else { + const newRow: any = { _id: `new_${Date.now()}` }; + config.columns.forEach((col) => { + newRow[col.key] = ""; + }); + const newData = [...data, newRow]; + setData(newData); + onDataChange?.(newData); + } + }, [isModalMode, config.columns, data, onDataChange]); - const newData = [...data]; - newData[editingRow] = editedData; - setData(newData); - onDataChange?.(newData); - - setEditingRow(null); - setEditedData({}); - }; - - // 인라인 편집 취소 - const handleCancelEdit = () => { - setEditingRow(null); - setEditedData({}); - }; - - // 행 클릭 - const handleRowClick = (row: any, index: number) => { - console.log("handleRowClick 호출:", { - index, - row, - currentEditingRow: editingRow, - inlineEdit: config.features?.inlineEdit, - columns: config.columns + // 모달에서 항목 선택 + const handleSelectItems = useCallback((items: Record[]) => { + const fkColumn = config.dataSource?.foreignKey; + const refKey = config.dataSource?.referenceKey || "id"; + + 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; }); - if (editingRow === index) return; - - if (editingRow !== null) { - handleConfirmEdit(); - } - - if (config.features?.selectable) { - toggleRowSelection(index); - } - - onRowClick?.(row); - - // 인라인 편집 모드면 편집 시작 (모달 모드에서도 추가 입력 컬럼이 있으면 편집 가능) - const hasEditableColumns = config.columns.length > 0; - const isModalRenderMode = config.renderMode === "modal" || config.renderMode === "mixed"; - if (config.features?.inlineEdit || (isModalRenderMode && hasEditableColumns)) { - console.log("편집 모드 활성화:", { index, row }); - setEditingRow(index); - setEditedData({ ...row }); - } - }; - - // 버튼 클릭 핸들러 - const handleButtonAction = (action: ButtonActionType, row?: any, buttonConfig?: RepeaterButtonConfig) => { - onButtonClick?.(action, row, buttonConfig); - }; - - // 공통코드 버튼 클릭 - const handleCodeButtonClick = (codeValue: string, row: any, index: number) => { - const valueField = config.button?.commonCode?.valueField; - if (!valueField) return; - - const updatedRow = { ...row, [valueField]: codeValue }; - const newData = [...data]; - newData[index] = updatedRow; + const newData = [...data, ...newRows]; setData(newData); onDataChange?.(newData); - }; + setModalOpen(false); + }, [config.dataSource?.foreignKey, config.dataSource?.referenceKey, config.modal?.sourceDisplayColumns, config.columns, data, onDataChange]); - // 버튼 렌더링 - const renderButtons = (row: any, index: number) => { - const isVertical = config.button?.layout === "vertical"; - const buttonStyle = config.button?.style || "outline"; - - if (config.button?.sourceType === "commonCode") { - return ( -
- {codeButtons.map((btn) => ( - - ))} -
- ); - } - - return ( -
- {(config.button?.manualButtons || []).map((btn) => ( - - ))} -
- ); - }; - - // FK 값으로 캐시에서 표시 정보 가져오기 - const getEntityDisplayInfo = (fkValue: any): Record | null => { - if (!fkValue) return null; - return entityDisplayCache[fkValue] || null; - }; - - // inputType별 입력 컴포넌트 렌더링 - const renderColumnInput = (col: RepeaterColumnConfig, value: any, onChange: (value: any) => void) => { - // 저장된 inputType이 없거나 "text"이면 현재 테이블 정보에서 조회 - let inputType = col.inputType; - let detailSettings = col.detailSettings; - - if (!inputType || inputType === "text") { - const tableColInfo = currentTableColumnInfo[col.key]; - if (tableColInfo) { - inputType = tableColInfo.inputType; - detailSettings = tableColInfo.detailSettings || detailSettings; - } - } - inputType = inputType || "text"; - - const commonClasses = "h-8 text-sm min-w-[80px] w-full"; - - console.log("renderColumnInput:", { key: col.key, inputType, value, detailSettings }); - - switch (inputType) { - case "number": - case "decimal": - return ( - onChange(e.target.value)} - className={commonClasses} - onClick={(e) => e.stopPropagation()} - step={inputType === "decimal" ? "0.01" : "1"} - /> - ); - - case "date": - return ( - - - - - - onChange(date ? format(date, "yyyy-MM-dd") : "")} - initialFocus - /> - - - ); - - case "code": - const codeOptions = columnCodeOptions[col.key] || []; - return ( - - ); - - case "boolean": - case "checkbox": - return ( -
e.stopPropagation()}> - onChange(checked ? "Y" : "N")} - /> -
- ); - - case "text": - default: - return ( - onChange(e.target.value)} - className={commonClasses} - onClick={(e) => e.stopPropagation()} - /> - ); - } - }; - - // 테이블 렌더링 - const renderTable = () => { - if (config.renderMode === "button") return null; - - const fkColumn = config.dataSource?.foreignKey; - const isModalMode = config.renderMode === "modal" || config.renderMode === "mixed"; - - // 모달 표시 컬럼들 (호환 처리된 sourceDisplayColumns 사용) - const displayColumns = sourceDisplayColumns; - - // 전체 컬럼 수 계산 - const totalColumns = - (config.features?.selectable ? 1 : 0) + - (config.features?.showRowNumber ? 1 : 0) + - (isModalMode ? displayColumns.length : 0) + - config.columns.length + - (config.features?.showDeleteButton ? 1 : 0); - - return ( -
- - - - {config.features?.selectable && } - {config.features?.showRowNumber && #} - - {/* 모달 표시 컬럼들 (선택한 엔티티 정보 - 읽기 전용) */} - {isModalMode && displayColumns.map((col) => ( - - {col.label} - - ))} - - {/* 추가 입력 컬럼 */} - {config.columns - .filter((col) => col.visible !== false) - .map((col) => ( - - {col.title} - - ))} - - {config.features?.showDeleteButton && ( - 액션 - )} - - - - {data.length === 0 ? ( - - - {isModalMode - ? `"${config.modal?.buttonText || "검색"}" 버튼을 클릭하여 항목을 추가하세요` - : "\"추가\" 버튼을 클릭하여 데이터를 입력하세요"} - - - ) : ( - data.map((row, index) => { - // FK 값으로 캐시된 표시 정보 가져오기 - const fkValue = fkColumn ? row[fkColumn] : null; - const displayInfo = getEntityDisplayInfo(fkValue); - - return ( - handleRowClick(row, index)} - > - {config.features?.selectable && ( - - toggleRowSelection(index)} - onClick={(e) => e.stopPropagation()} - /> - - )} - {config.features?.showRowNumber && ( - {index + 1} - )} - - {/* 모달 표시 컬럼들 (읽기 전용 - 캐시에서 조회) */} - {isModalMode && displayColumns.map((col) => ( - - - {displayInfo?.[col.key] || "-"} - - - ))} - - {/* 추가 입력 컬럼 */} - {config.columns - .filter((col) => col.visible !== false) - .map((col) => ( - - {editingRow === index && col.editable !== false ? ( - renderColumnInput( - col, - editedData[col.key], - (value) => setEditedData((prev) => ({ ...prev, [col.key]: value })) - ) - ) : ( - - {col.inputType === "code" && columnCodeOptions[col.key] - ? columnCodeOptions[col.key].find((opt) => opt.value === row[col.key])?.label || row[col.key] || "-" - : row[col.key] || "-"} - - )} - - ))} - - {config.features?.showDeleteButton && ( - -
- {editingRow === index && ( - <> - - - - )} - {editingRow !== index && ( - - )} -
-
- )} -
- ); - }) - )} -
-
-
- ); - }; - - // 버튼 모드 렌더링 - const renderButtonMode = () => { - if (config.renderMode !== "button") return null; - - const isVertical = config.button?.layout === "vertical"; - - return ( -
- {data.map((row, index) => ( -
- {renderButtons(row, index)} -
- ))} -
- ); - }; - - // 소스 테이블 표시 컬럼 (라벨 포함) - 이전/새 형식 호환 + 동적 라벨 매핑 - const sourceDisplayColumns = useMemo(() => { - const rawCols = config.modal?.sourceDisplayColumns || []; - - // string[] 형식과 {key, label}[] 형식 모두 지원 - const result = rawCols - .map((col: any, idx: number) => { - if (typeof col === "string") { - // 컬럼명만 있는 경우: sourceTableColumnMap에서 라벨 조회 - return { - key: col, - label: sourceTableColumnMap[col] || col - }; - } - // 라벨이 컬럼명과 같으면 sourceTableColumnMap에서 더 좋은 라벨 찾기 - const dynamicLabel = sourceTableColumnMap[col.key]; - return { - key: col.key || `col_${idx}`, - label: (col.label && col.label !== col.key) ? col.label : (dynamicLabel || col.label || col.key || `컬럼${idx + 1}`) - }; - }) - // 잘못된 컬럼 필터링 (none, 빈값, undefined 등) - .filter((col) => col.key && col.key !== "none" && col.key !== "undefined"); - - return result; - }, [config.modal?.sourceDisplayColumns, sourceTableColumnMap]); - - const displayColumn = config.dataSource?.displayColumn; + // 소스 컬럼 목록 (모달용) + 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]); return ( -
- {/* 헤더 */} - {(config.features?.showAddButton || (config.features?.selectable && selectedRows.size > 0)) && ( -
-
- {config.features?.showAddButton && ( - - )} - {config.features?.selectable && selectedRows.size > 0 && config.features?.showDeleteButton && ( - - )} -
- 총 {data.length}건 -
- )} - - {/* 메인 컨텐츠 */} - {renderTable()} - {renderButtonMode()} - - {/* 검색 모달 (modal 모드) */} - - - - {config.modal?.title || "항목 검색"} - - - {/* 검색 입력 */} -
- setSearchKeyword(e.target.value)} - placeholder="검색어 입력..." - className="flex-1" - onKeyDown={(e) => e.key === "Enter" && searchSourceTable()} - /> - -
- - {/* 검색 결과 테이블 */} -
- - - - - {sourceDisplayColumns.length > 0 ? ( - sourceDisplayColumns.map((col) => ( - {col.label} - )) - ) : displayColumn ? ( - {displayColumn} - ) : ( - 항목 - )} - - - - {searchLoading ? ( - - - 검색 중... - - - ) : searchResults.length === 0 ? ( - - - 검색 결과가 없습니다 - - - ) : ( - searchResults.map((item, index) => ( - toggleSearchItemSelection(index)} - > - - toggleSearchItemSelection(index)} - onClick={(e) => e.stopPropagation()} - /> - - {sourceDisplayColumns.length > 0 ? ( - sourceDisplayColumns.map((col) => ( - {item[col.key]} - )) - ) : displayColumn ? ( - {item[displayColumn]} - ) : ( - {JSON.stringify(item).substring(0, 50)}... - )} - - )) - )} - -
-
- - - - - -
-
+ )} +
+
+ {selectedRows.size > 0 && ( + + )} + +
+ + + {/* Repeater 테이블 */} + { + setActiveDataSources((prev) => ({ ...prev, [field]: optionId })); + }} + selectedRows={selectedRows} + onSelectionChange={setSelectedRows} + equalizeWidthsTrigger={equalizeWidthsTrigger} + /> + + {/* 항목 선택 모달 (modal 모드) */} + {isModalMode && ( + + )} ); }; diff --git a/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx b/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx index 9a5fdc40..a4bb95ea 100644 --- a/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx +++ b/frontend/components/unified/config-panels/UnifiedRepeaterConfigPanel.tsx @@ -6,8 +6,6 @@ * 렌더링 모드별 설정: * - inline: 현재 화면 테이블 컬럼 직접 입력 * - modal: 엔티티 선택 + 추가 입력 (FK 저장 + 추가 컬럼 입력) - * - button: 버튼 클릭으로 관련 화면/모달 열기 - * - mixed: inline + modal 기능 결합 */ import React, { useState, useEffect, useMemo, useCallback } from "react"; @@ -17,7 +15,6 @@ import { Separator } from "@/components/ui/separator"; import { Checkbox } from "@/components/ui/checkbox"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Database, @@ -28,24 +25,15 @@ import { ArrowRight, Calculator, } from "lucide-react"; -import { tableTypeApi, screenApi } from "@/lib/api/screen"; -import { commonCodeApi } from "@/lib/api/commonCode"; +import { tableTypeApi } from "@/lib/api/screen"; import { cn } from "@/lib/utils"; import { UnifiedRepeaterConfig, RepeaterColumnConfig, - RepeaterButtonConfig, DEFAULT_REPEATER_CONFIG, RENDER_MODE_OPTIONS, MODAL_SIZE_OPTIONS, COLUMN_WIDTH_OPTIONS, - BUTTON_ACTION_OPTIONS, - BUTTON_VARIANT_OPTIONS, - BUTTON_LAYOUT_OPTIONS, - BUTTON_SOURCE_OPTIONS, - LABEL_FIELD_OPTIONS, - ButtonActionType, - ButtonVariant, ColumnWidthOption, } from "@/types/unified-repeater"; @@ -85,17 +73,6 @@ interface CalculationRule { label?: string; } -interface ScreenOption { - screenId: number; - screenName: string; - screenCode: string; -} - -interface CategoryOption { - categoryCode: string; - categoryName: string; -} - export const UnifiedRepeaterConfigPanel: React.FC = ({ config: propConfig, onChange, @@ -118,10 +95,6 @@ export const UnifiedRepeaterConfigPanel: React.FC([]); // 현재 테이블 컬럼 const [entityColumns, setEntityColumns] = useState([]); // 엔티티 타입 컬럼 const [sourceTableColumns, setSourceTableColumns] = useState([]); // 소스(엔티티) 테이블 컬럼 - const [screens, setScreens] = useState([]); - const [categories, setCategories] = useState([]); const [calculationRules, setCalculationRules] = useState([]); const [loadingColumns, setLoadingColumns] = useState(false); const [loadingSourceColumns, setLoadingSourceColumns] = useState(false); - const [loadingScreens, setLoadingScreens] = useState(false); - const [loadingCategories, setLoadingCategories] = useState(false); // 설정 업데이트 헬퍼 const updateConfig = useCallback( @@ -167,15 +136,6 @@ export const UnifiedRepeaterConfigPanel: React.FC { - updateConfig({ - button: { ...config.button, [field]: value }, - }); - }, - [config.button, updateConfig], - ); - const updateFeatures = useCallback( (field: string, value: boolean) => { updateConfig({ @@ -284,64 +244,11 @@ export const UnifiedRepeaterConfigPanel: React.FC { - const needScreens = config.renderMode === "button" || config.renderMode === "mixed"; - if (!needScreens) return; - - const loadScreens = async () => { - setLoadingScreens(true); - try { - const response = await screenApi.getScreens({ size: 1000 }); - setScreens( - response.data.map((s) => ({ - screenId: s.screenId!, - screenName: s.screenName, - screenCode: s.screenCode, - })), - ); - } catch (error) { - console.error("화면 목록 로드 실패:", error); - } finally { - setLoadingScreens(false); - } - }; - loadScreens(); - }, [config.renderMode]); - - // 공통코드 카테고리 로드 - useEffect(() => { - const needCategories = - (config.renderMode === "button" || config.renderMode === "mixed") && - config.button?.sourceType === "commonCode"; - if (!needCategories) return; - - const loadCategories = async () => { - setLoadingCategories(true); - try { - const response = await commonCodeApi.categories.getList(); - if (response.success && response.data) { - setCategories( - response.data.map((c) => ({ - categoryCode: c.categoryCode, - categoryName: c.categoryName, - })), - ); - } - } catch (error) { - console.error("공통코드 카테고리 로드 실패:", error); - } finally { - setLoadingCategories(false); - } - }; - loadCategories(); - }, [config.renderMode, config.button?.sourceType]); - // 컬럼 토글 (현재 테이블 컬럼 - 입력용) const toggleInputColumn = (column: ColumnOption) => { const existingIndex = config.columns.findIndex((c) => c.key === column.columnName); @@ -449,33 +356,9 @@ export const UnifiedRepeaterConfigPanel: React.FC { - const newButton: RepeaterButtonConfig = { - id: `btn_${Date.now()}`, - label: "새 버튼", - action: "create", - variant: "outline", - }; - const currentButtons = config.button?.manualButtons || []; - updateButton("manualButtons", [...currentButtons, newButton]); - }; - - const removeManualButton = (id: string) => { - const currentButtons = config.button?.manualButtons || []; - updateButton("manualButtons", currentButtons.filter((b) => b.id !== id)); - }; - - const updateManualButton = (id: string, field: keyof RepeaterButtonConfig, value: any) => { - const currentButtons = config.button?.manualButtons || []; - const updated = currentButtons.map((b) => (b.id === id ? { ...b, [field]: value } : b)); - updateButton("manualButtons", updated); - }; - // 모드 여부 const isInlineMode = config.renderMode === "inline"; - const isModalMode = config.renderMode === "modal" || config.renderMode === "mixed"; - const isButtonMode = config.renderMode === "button" || config.renderMode === "mixed"; + const isModalMode = config.renderMode === "modal"; // 엔티티 컬럼 제외한 입력 가능 컬럼 (FK 컬럼 제외) const inputableColumns = useMemo(() => { @@ -489,11 +372,10 @@ export const UnifiedRepeaterConfigPanel: React.FC - + 기본 컬럼 모달 - 버튼 {/* 기본 설정 탭 */} @@ -514,7 +396,6 @@ export const UnifiedRepeaterConfigPanel: React.FC @@ -952,252 +833,6 @@ export const UnifiedRepeaterConfigPanel: React.FC )} - - {/* 버튼 설정 탭 */} - - {isButtonMode ? ( - <> - {/* 버튼 소스 선택 */} -
- - updateButton("sourceType", value)} - className="flex gap-4" - > - {BUTTON_SOURCE_OPTIONS.map((opt) => ( -
- - -
- ))} -
-
- - - - {/* 공통코드 모드 */} - {config.button?.sourceType === "commonCode" && ( -
- - -
- - -
- -
- - -
- -
- - -
-
- )} - - {/* 수동 설정 모드 */} - {config.button?.sourceType === "manual" && ( -
-
- - -
- -
- {(config.button?.manualButtons || []).map((btn) => ( -
-
- updateManualButton(btn.id, "label", e.target.value)} - placeholder="버튼 라벨" - className="h-7 flex-1 text-xs" - /> - -
-
- - - -
- - {btn.action === "navigate" && ( - - )} -
- ))} - - {(config.button?.manualButtons || []).length === 0 && ( -

버튼을 추가해주세요

- )} -
-
- )} - - - - {/* 버튼 레이아웃 */} -
-
- - -
- -
- - -
-
- - ) : ( -

- 버튼 또는 혼합 모드에서만 설정할 수 있습니다 -

- )} -
); diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx index 88da4aef..813b0dfa 100644 --- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx @@ -515,14 +515,14 @@ export function RepeaterTable({ width: `max(100%, ${Object.values(columnWidths).reduce((sum, w) => sum + w, 0) + 74}px)`, }} > - + - {/* 드래그 핸들 헤더 */} - + {/* 드래그 핸들 헤더 - 좌측 고정 */} + 순서 - {/* 체크박스 헤더 */} - + {/* 체크박스 헤더 - 좌측 고정 */} + {({ attributes, listeners, isDragging }) => ( <> - {/* 드래그 핸들 */} - + {/* 드래그 핸들 - 좌측 고정 */} + - {/* 체크박스 */} - + {/* 체크박스 - 좌측 고정 */} + handleRowSelect(rowIndex, !!checked)} diff --git a/frontend/types/unified-repeater.ts b/frontend/types/unified-repeater.ts index e9e454e8..a53418ee 100644 --- a/frontend/types/unified-repeater.ts +++ b/frontend/types/unified-repeater.ts @@ -4,24 +4,11 @@ * 렌더링 모드: * - inline: 현재 테이블 컬럼 직접 입력 (simple-repeater-table) * - modal: 소스 테이블에서 검색/선택 후 복사 (modal-repeater-table) - * - button: 버튼으로 관련 화면 열기 (related-data-buttons) * - mixed: inline + modal 혼합 */ // 렌더링 모드 -export type RepeaterRenderMode = "inline" | "modal" | "button" | "mixed"; - -// 버튼 소스 타입 -export type ButtonSourceType = "commonCode" | "manual"; - -// 버튼 액션 타입 -export type ButtonActionType = "create" | "update" | "delete" | "view" | "navigate" | "custom"; - -// 버튼 색상/스타일 -export type ButtonVariant = "default" | "primary" | "secondary" | "destructive" | "outline" | "ghost"; - -// 버튼 레이아웃 -export type ButtonLayout = "horizontal" | "vertical"; +export type RepeaterRenderMode = "inline" | "modal"; // 모달 크기 export type ModalSize = "sm" | "md" | "lg" | "xl" | "full"; @@ -50,29 +37,6 @@ export interface RepeaterColumnConfig { }; } -// 버튼 설정 (수동 모드) -export interface RepeaterButtonConfig { - id: string; - label: string; - action: ButtonActionType; - variant: ButtonVariant; - icon?: string; - confirmMessage?: string; - // 네비게이트 액션용 - navigateScreen?: number; - navigateParams?: Record; - // 커스텀 액션용 - customHandler?: string; -} - -// 공통코드 버튼 설정 -export interface CommonCodeButtonConfig { - categoryCode: string; - labelField: "codeValue" | "codeName"; - valueField: string; // 버튼 클릭 시 전달할 값의 컬럼 - variantMapping?: Record; // 코드값별 색상 매핑 -} - // 모달 표시 컬럼 (라벨 포함) export interface ModalDisplayColumn { key: string; @@ -162,15 +126,6 @@ export interface UnifiedRepeaterConfig { // 모달 설정 (modal, mixed 모드) modal?: RepeaterModalConfig; - // 버튼 설정 (button, mixed 모드) - button?: { - sourceType: ButtonSourceType; - commonCode?: CommonCodeButtonConfig; - manualButtons?: RepeaterButtonConfig[]; - layout: ButtonLayout; - style: ButtonVariant; - }; - // 기능 옵션 features: RepeaterFeatureOptions; @@ -190,7 +145,6 @@ export interface UnifiedRepeaterProps { data?: any[]; // 초기 데이터 (없으면 API로 로드) onDataChange?: (data: any[]) => void; onRowClick?: (row: any) => void; - onButtonClick?: (action: ButtonActionType, row?: any, buttonConfig?: RepeaterButtonConfig) => void; className?: string; } @@ -204,12 +158,6 @@ export const DEFAULT_REPEATER_CONFIG: UnifiedRepeaterConfig = { sourceDisplayColumns: [], searchFields: [], }, - button: { - sourceType: "manual", - manualButtons: [], - layout: "horizontal", - style: "outline", - }, features: { showAddButton: true, showDeleteButton: true, @@ -225,8 +173,6 @@ export const DEFAULT_REPEATER_CONFIG: UnifiedRepeaterConfig = { export const RENDER_MODE_OPTIONS = [ { value: "inline", label: "인라인 (직접 입력)" }, { value: "modal", label: "모달 (검색 선택)" }, - { value: "button", label: "버튼" }, - { value: "mixed", label: "혼합 (입력 + 검색)" }, ] as const; export const MODAL_SIZE_OPTIONS = [ @@ -249,35 +195,3 @@ export const COLUMN_WIDTH_OPTIONS = [ { value: "300px", label: "300px" }, ] as const; -export const BUTTON_ACTION_OPTIONS = [ - { value: "create", label: "생성" }, - { value: "update", label: "수정" }, - { value: "delete", label: "삭제" }, - { value: "view", label: "보기" }, - { value: "navigate", label: "화면 이동" }, - { value: "custom", label: "커스텀" }, -] as const; - -export const BUTTON_VARIANT_OPTIONS = [ - { value: "default", label: "기본" }, - { value: "primary", label: "Primary" }, - { value: "secondary", label: "Secondary" }, - { value: "destructive", label: "삭제 (빨강)" }, - { value: "outline", label: "Outline" }, - { value: "ghost", label: "Ghost" }, -] as const; - -export const BUTTON_LAYOUT_OPTIONS = [ - { value: "horizontal", label: "가로 배치" }, - { value: "vertical", label: "세로 배치" }, -] as const; - -export const BUTTON_SOURCE_OPTIONS = [ - { value: "commonCode", label: "공통코드 사용" }, - { value: "manual", label: "수동 설정" }, -] as const; - -export const LABEL_FIELD_OPTIONS = [ - { value: "codeName", label: "코드명" }, - { value: "codeValue", label: "코드값" }, -] as const;