diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index 79724a53..7b89cae3 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -55,10 +55,54 @@ export const InteractiveDataTable: React.FC = ({ // 현재 사용자 정보 const [currentUser, setCurrentUser] = useState(null); + // 테이블 컬럼 타입 정보 (웹 타입 포함) + const [tableColumns, setTableColumns] = useState([]); + + // 파일 업로드 관련 상태 + const [uploadingFiles, setUploadingFiles] = useState>({}); + const [uploadedFiles, setUploadedFiles] = useState>({}); + // 검색 가능한 컬럼만 필터링 const visibleColumns = component.columns?.filter((col: DataTableColumn) => col.visible) || []; const searchFilters = component.filters || []; + // 컬럼의 실제 웹 타입 정보 찾기 + const getColumnWebType = useCallback( + (columnName: string) => { + // 먼저 컴포넌트에 설정된 컬럼에서 찾기 (화면 관리에서 설정한 값 우선) + const componentColumn = component.columns?.find((col) => col.columnName === columnName); + if (componentColumn?.widgetType && componentColumn.widgetType !== "text") { + return componentColumn.widgetType; + } + + // 없으면 테이블 타입 관리에서 설정된 값 찾기 + const tableColumn = tableColumns.find((col) => col.columnName === columnName); + return tableColumn?.webType || "text"; + }, + [component.columns, tableColumns], + ); + + // 컬럼의 상세 설정 정보 찾기 + const getColumnDetailSettings = useCallback( + (columnName: string) => { + // 먼저 컴포넌트에 설정된 컬럼에서 찾기 (화면 관리에서 설정한 값 우선) + const componentColumn = component.columns?.find((col) => col.columnName === columnName); + if (componentColumn?.webTypeConfig) { + return componentColumn.webTypeConfig; + } + + // 없으면 테이블 타입 관리에서 설정된 값 찾기 + const tableColumn = tableColumns.find((col) => col.columnName === columnName); + try { + return tableColumn?.detailSettings ? JSON.parse(tableColumn.detailSettings) : {}; + } catch { + console.warn("상세 설정 파싱 실패:", tableColumn?.detailSettings); + return {}; + } + }, + [component.columns, tableColumns], + ); + // 그리드 컬럼 계산 const totalGridColumns = visibleColumns.reduce((sum, col) => sum + (col.gridColumns || 2), 0); @@ -119,6 +163,24 @@ export const InteractiveDataTable: React.FC = ({ fetchCurrentUser(); }, []); + // 테이블 컬럼 정보 로드 (웹 타입 정보 포함) + useEffect(() => { + const fetchTableColumns = async () => { + try { + console.log("🔄 테이블 컬럼 정보 로드 시작:", component.tableName); + const columns = await tableTypeApi.getColumns(component.tableName); + setTableColumns(columns); + console.log("✅ 테이블 컬럼 정보 로드 완료:", columns); + } catch (error) { + console.error("테이블 컬럼 정보 로드 실패:", error); + } + }; + + if (component.tableName) { + fetchTableColumns(); + } + }, [component.tableName]); + // 초기 데이터 로드 useEffect(() => { loadData(1, searchValues); @@ -302,6 +364,145 @@ export const InteractiveDataTable: React.FC = ({ })); }, []); + // 파일 업로드 핸들러 + const handleFileUpload = useCallback( + async (columnName: string, files: FileList | null, isEdit: boolean = false) => { + if (!files || files.length === 0) return; + + const detailSettings = getColumnDetailSettings(columnName); + const maxSize = detailSettings?.maxSize || 10 * 1024 * 1024; // 기본 10MB + const acceptedTypes = detailSettings?.accept + ? detailSettings.accept.split(",").map((type: string) => type.trim()) + : []; + const multiple = detailSettings?.multiple || false; + + // 파일 검증 + const validFiles: File[] = []; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + + // 크기 체크 + if (file.size > maxSize) { + alert(`파일 크기가 너무 큽니다. 최대 ${Math.round(maxSize / 1024 / 1024)}MB까지 가능합니다.`); + continue; + } + + // 타입 체크 + if ( + acceptedTypes.length > 0 && + !acceptedTypes.some((type: string) => { + if (type.startsWith(".")) { + return file.name.toLowerCase().endsWith(type.toLowerCase()); + } else { + return file.type.includes(type); + } + }) + ) { + alert(`지원하지 않는 파일 형식입니다. (${acceptedTypes.join(", ")})`); + continue; + } + + validFiles.push(file); + if (!multiple) break; // 단일 파일만 허용 + } + + if (validFiles.length === 0) return; + + try { + setUploadingFiles((prev) => ({ ...prev, [columnName]: true })); + + // TODO: 실제 파일 업로드 API 호출 + // const uploadPromises = validFiles.map(file => uploadFileToServer(file)); + // const uploadResults = await Promise.all(uploadPromises); + + // 임시: 파일 정보를 로컬 상태에 저장 + setUploadedFiles((prev) => ({ + ...prev, + [columnName]: multiple ? [...(prev[columnName] || []), ...validFiles] : validFiles, + })); + + // 폼 데이터 업데이트 + const fileNames = validFiles.map((file) => file.name).join(", "); + if (isEdit) { + handleEditFormChange(columnName, fileNames); + } else { + handleAddFormChange(columnName, fileNames); + } + + console.log("✅ 파일 업로드 완료:", validFiles); + } catch (error) { + console.error("파일 업로드 실패:", error); + alert("파일 업로드에 실패했습니다."); + } finally { + setUploadingFiles((prev) => ({ ...prev, [columnName]: false })); + } + }, + [getColumnDetailSettings, handleAddFormChange, handleEditFormChange], + ); + + // 파일 제거 핸들러 + const handleFileRemove = useCallback( + (columnName: string, fileIndex: number, isEdit: boolean = false) => { + setUploadedFiles((prev) => { + const currentFiles = prev[columnName] || []; + const newFiles = currentFiles.filter((_, index) => index !== fileIndex); + + // 폼 데이터 업데이트 + const fileNames = newFiles.map((file) => file.name).join(", "); + if (isEdit) { + handleEditFormChange(columnName, fileNames); + } else { + handleAddFormChange(columnName, fileNames); + } + + return { ...prev, [columnName]: newFiles }; + }); + }, + [handleAddFormChange, handleEditFormChange], + ); + + // 파일 목록 렌더링 컴포넌트 + const renderFileList = useCallback( + (columnName: string, isEdit: boolean = false) => { + const currentFiles = uploadedFiles[columnName] || []; + const isUploading = uploadingFiles[columnName]; + + if (currentFiles.length === 0 && !isUploading) return null; + + return ( +
+ {currentFiles.map((file, index) => ( +
+
+
📄
+
+

{file.name}

+

{(file.size / 1024).toFixed(1)} KB

+
+
+ +
+ ))} + {isUploading && ( +
+ + 업로드 중... +
+ )} +
+ ); + }, + [uploadedFiles, uploadingFiles, handleFileRemove], + ); + // 데이터 추가 제출 핸들러 const handleAddSubmit = useCallback(async () => { try { @@ -359,6 +560,7 @@ export const InteractiveDataTable: React.FC = ({ if (!isAdding) { setShowAddModal(false); setAddFormData({}); + setUploadedFiles({}); // 파일 상태 초기화 } }, [isAdding]); @@ -449,6 +651,10 @@ export const InteractiveDataTable: React.FC = ({ const isRequired = isRequiredField(column.columnName); const advancedConfig = component.addModalConfig?.advancedFieldConfigs?.[column.columnName]; + // 데이터베이스에서 실제 웹 타입 가져오기 + const actualWebType = getColumnWebType(column.columnName); + const detailSettings = getColumnDetailSettings(column.columnName); + // 자동 생성 필드는 수정에서 읽기 전용으로 처리 if (advancedConfig?.inputType === "auto") { return ( @@ -488,15 +694,17 @@ export const InteractiveDataTable: React.FC = ({ className: isRequired && !value ? "border-orange-300 focus:border-orange-500" : "", }; - switch (column.widgetType) { + // 실제 웹 타입에 따라 입력 컴포넌트 렌더링 + switch (actualWebType) { case "text": case "email": case "tel": return (
{advancedConfig?.helpText &&

{advancedConfig.helpText}

}
@@ -506,7 +714,13 @@ export const InteractiveDataTable: React.FC = ({ case "decimal": return (
- + {advancedConfig?.helpText &&

{advancedConfig.helpText}

}
); @@ -514,7 +728,7 @@ export const InteractiveDataTable: React.FC = ({ case "date": return (
- + {advancedConfig?.helpText &&

{advancedConfig.helpText}

}
); @@ -522,15 +736,136 @@ export const InteractiveDataTable: React.FC = ({ case "datetime": return (
- + {advancedConfig?.helpText &&

{advancedConfig.helpText}

}
); case "select": case "dropdown": - // TODO: 동적 옵션 로드 - return ; + // 상세 설정에서 옵션 목록 가져오기 + const options = detailSettings?.options || []; + if (options.length > 0) { + return ( +
+ + {advancedConfig?.helpText &&

{advancedConfig.helpText}

} +
+ ); + } else { + return ; + } + + case "radio": + // 상세 설정에서 옵션 목록 가져오기 + const radioOptions = detailSettings?.options || []; + if (radioOptions.length > 0) { + return ( +
+
+ {radioOptions.map((option: any, index: number) => ( +
+ handleEditFormChange(column.columnName, e.target.value)} + className="text-primary focus:ring-primary" + /> + +
+ ))} +
+ {advancedConfig?.helpText &&

{advancedConfig.helpText}

} +
+ ); + } else { + return ; + } + + case "textarea": + return ( +
+