Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management
This commit is contained in:
@@ -48,6 +48,7 @@ import { downloadFile, getLinkedFiles, getFilePreviewUrl, getDirectFileUrl } fro
|
||||
import { toast } from "sonner";
|
||||
import { FileUpload } from "@/components/screen/widgets/FileUpload";
|
||||
import { AdvancedSearchFilters } from "./filters/AdvancedSearchFilters";
|
||||
import { SaveModal } from "./SaveModal";
|
||||
|
||||
// 파일 데이터 타입 정의 (AttachedFileInfo와 호환)
|
||||
interface FileInfo {
|
||||
@@ -87,12 +88,14 @@ interface InteractiveDataTableProps {
|
||||
component: DataTableComponent;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
onRefresh?: () => void; // 테이블 새로고침 콜백
|
||||
}
|
||||
|
||||
export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
component,
|
||||
className = "",
|
||||
style = {},
|
||||
onRefresh,
|
||||
}) => {
|
||||
const [data, setData] = useState<Record<string, any>[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -101,13 +104,11 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [addFormData, setAddFormData] = useState<Record<string, any>>({});
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [editFormData, setEditFormData] = useState<Record<string, any>>({});
|
||||
const [editingRowData, setEditingRowData] = useState<Record<string, any> | null>(null);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
// SaveModal 상태 (등록/수정 통합)
|
||||
const [showSaveModal, setShowSaveModal] = useState(false);
|
||||
const [saveModalData, setSaveModalData] = useState<Record<string, any> | undefined>(undefined);
|
||||
const [saveModalScreenId, setSaveModalScreenId] = useState<number | undefined>(undefined);
|
||||
|
||||
// 이미지 미리보기 상태
|
||||
const [previewImage, setPreviewImage] = useState<FileInfo | null>(null);
|
||||
@@ -150,7 +151,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
return options;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`공통코드 옵션 로드 실패: ${categoryCode}`, error);
|
||||
// console.error(`공통코드 옵션 로드 실패: ${categoryCode}`, error);
|
||||
}
|
||||
|
||||
return [];
|
||||
@@ -158,6 +159,22 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
[codeOptions],
|
||||
);
|
||||
|
||||
// 🆕 전역 테이블 새로고침 이벤트 리스너
|
||||
useEffect(() => {
|
||||
const handleRefreshTable = () => {
|
||||
console.log("🔄 InteractiveDataTable: 전역 새로고침 이벤트 수신");
|
||||
if (component.tableName) {
|
||||
loadData(currentPage, searchValues);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("refreshTable", handleRefreshTable);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("refreshTable", handleRefreshTable);
|
||||
};
|
||||
}, [currentPage, searchValues, loadData, component.tableName]);
|
||||
|
||||
// 파일 상태 확인 함수
|
||||
const checkFileStatus = useCallback(
|
||||
async (rowData: Record<string, any>) => {
|
||||
@@ -176,7 +193,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
|
||||
return { hasFiles, fileCount, files: response.files || [] };
|
||||
} catch (error) {
|
||||
console.error("파일 상태 확인 오류:", error);
|
||||
// console.error("파일 상태 확인 오류:", error);
|
||||
return { hasFiles: false, fileCount: 0, files: [] };
|
||||
}
|
||||
},
|
||||
@@ -235,7 +252,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
|
||||
return { hasFiles, fileCount, files, targetObjid };
|
||||
} catch (error) {
|
||||
console.error("컬럼별 파일 상태 확인 오류:", error);
|
||||
// console.error("컬럼별 파일 상태 확인 오류:", error);
|
||||
return { hasFiles: false, fileCount: 0, files: [], targetObjid: null };
|
||||
}
|
||||
},
|
||||
@@ -301,13 +318,13 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
// 이미지 로딩 실패 시 대체 URL 시도
|
||||
const handleImageError = useCallback(() => {
|
||||
if (!imageLoadError && previewImage) {
|
||||
console.error("이미지 로딩 실패:", previewImage);
|
||||
// console.error("이미지 로딩 실패:", previewImage);
|
||||
setImageLoadError(true);
|
||||
|
||||
// 대체 URL 생성 (직접 파일 경로 사용)
|
||||
if (previewImage.path) {
|
||||
const altUrl = getDirectFileUrl(previewImage.path);
|
||||
console.log("대체 URL 시도:", altUrl);
|
||||
// console.log("대체 URL 시도:", altUrl);
|
||||
setAlternativeImageUrl(altUrl);
|
||||
} else {
|
||||
toast.error("이미지를 불러올 수 없습니다.");
|
||||
@@ -365,7 +382,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
try {
|
||||
return tableColumn?.detailSettings ? JSON.parse(tableColumn.detailSettings) : {};
|
||||
} catch {
|
||||
console.warn("상세 설정 파싱 실패:", tableColumn?.detailSettings);
|
||||
// console.warn("상세 설정 파싱 실패:", tableColumn?.detailSettings);
|
||||
return {};
|
||||
}
|
||||
},
|
||||
@@ -483,7 +500,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
setFileStatusMap(statusMap);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("❌ 테이블 데이터 조회 실패:", error);
|
||||
// console.error("❌ 테이블 데이터 조회 실패:", error);
|
||||
setData([]);
|
||||
setTotal(0);
|
||||
setTotalPages(1);
|
||||
@@ -503,7 +520,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
setCurrentUser(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("현재 사용자 정보 로드 실패:", error);
|
||||
// console.error("현재 사용자 정보 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -514,40 +531,40 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
useEffect(() => {
|
||||
const handleRefreshFileStatus = async (event: CustomEvent) => {
|
||||
const { tableName, recordId, columnName, targetObjid, fileCount } = event.detail;
|
||||
|
||||
console.log("🔄 InteractiveDataTable 파일 상태 새로고침 이벤트 수신:", {
|
||||
tableName,
|
||||
recordId,
|
||||
columnName,
|
||||
targetObjid,
|
||||
fileCount,
|
||||
currentTableName: component.tableName
|
||||
});
|
||||
|
||||
// console.log("🔄 InteractiveDataTable 파일 상태 새로고침 이벤트 수신:", {
|
||||
// tableName,
|
||||
// recordId,
|
||||
// columnName,
|
||||
// targetObjid,
|
||||
// fileCount,
|
||||
// currentTableName: component.tableName
|
||||
// });
|
||||
|
||||
// 현재 테이블과 일치하는지 확인
|
||||
if (tableName === component.tableName) {
|
||||
// 해당 행의 파일 상태 업데이트
|
||||
const columnKey = `${recordId}_${columnName}`;
|
||||
setFileStatusMap(prev => ({
|
||||
setFileStatusMap((prev) => ({
|
||||
...prev,
|
||||
[recordId]: { hasFiles: fileCount > 0, fileCount },
|
||||
[columnKey]: { hasFiles: fileCount > 0, fileCount }
|
||||
[columnKey]: { hasFiles: fileCount > 0, fileCount },
|
||||
}));
|
||||
|
||||
console.log("✅ 파일 상태 업데이트 완료:", {
|
||||
recordId,
|
||||
columnKey,
|
||||
hasFiles: fileCount > 0,
|
||||
fileCount
|
||||
});
|
||||
// console.log("✅ 파일 상태 업데이트 완료:", {
|
||||
// recordId,
|
||||
// columnKey,
|
||||
// hasFiles: fileCount > 0,
|
||||
// fileCount
|
||||
// });
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('refreshFileStatus', handleRefreshFileStatus as EventListener);
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("refreshFileStatus", handleRefreshFileStatus as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('refreshFileStatus', handleRefreshFileStatus as EventListener);
|
||||
window.removeEventListener("refreshFileStatus", handleRefreshFileStatus as EventListener);
|
||||
};
|
||||
}
|
||||
}, [component.tableName]);
|
||||
@@ -559,7 +576,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
const columns = await tableTypeApi.getColumns(component.tableName);
|
||||
setTableColumns(columns);
|
||||
} catch (error) {
|
||||
console.error("테이블 컬럼 정보 로드 실패:", error);
|
||||
// console.error("테이블 컬럼 정보 로드 실패:", error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -713,19 +730,21 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
setAddFormData(initialData);
|
||||
setShowAddModal(true);
|
||||
// SaveModal 열기 (등록 모드)
|
||||
const screenId = component.addModalConfig?.screenId;
|
||||
|
||||
if (!screenId) {
|
||||
toast.error("화면 설정이 필요합니다. 테이블 설정에서 추가 모달 화면을 지정해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
// SaveModal 사용
|
||||
setSaveModalData(undefined); // undefined = 등록 모드
|
||||
setSaveModalScreenId(screenId);
|
||||
setShowSaveModal(true);
|
||||
}, [getDisplayColumns, generateAutoValue, component.addModalConfig]);
|
||||
|
||||
// 추가 폼 데이터 변경 핸들러
|
||||
const handleAddFormChange = useCallback((columnName: string, value: any) => {
|
||||
setAddFormData((prev) => ({
|
||||
...prev,
|
||||
[columnName]: value,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// 데이터 수정 핸들러
|
||||
// 데이터 수정 핸들러 (SaveModal 사용)
|
||||
const handleEditData = useCallback(() => {
|
||||
if (selectedRows.size !== 1) return;
|
||||
|
||||
@@ -734,6 +753,13 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
|
||||
if (!selectedRowData) return;
|
||||
|
||||
const screenId = component.addModalConfig?.screenId;
|
||||
|
||||
if (!screenId) {
|
||||
toast.error("화면 설정이 필요합니다. 테이블 설정에서 수정 모달 화면을 지정해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 수정할 데이터로 폼 초기화
|
||||
const initialData: Record<string, any> = {};
|
||||
const displayColumns = getDisplayColumns();
|
||||
@@ -744,13 +770,13 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
|
||||
setEditFormData(initialData);
|
||||
setEditingRowData(selectedRowData);
|
||||
|
||||
|
||||
// 수정 모달 설정에서 제목과 설명 가져오기
|
||||
const editModalTitle = component.editModalConfig?.title || "";
|
||||
const editModalDescription = component.editModalConfig?.description || "";
|
||||
|
||||
|
||||
console.log("📝 수정 모달 설정:", { editModalTitle, editModalDescription });
|
||||
|
||||
|
||||
setShowEditModal(true);
|
||||
}, [selectedRows, data, getDisplayColumns, component.editModalConfig]);
|
||||
|
||||
@@ -827,7 +853,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
handleAddFormChange(columnName, fileNames);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("파일 업로드 실패:", error);
|
||||
// console.error("파일 업로드 실패:", error);
|
||||
alert("파일 업로드에 실패했습니다.");
|
||||
} finally {
|
||||
setUploadingFiles((prev) => ({ ...prev, [columnName]: false }));
|
||||
@@ -870,7 +896,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
{currentFiles.map((file, index) => (
|
||||
<div key={index} className="flex items-center justify-between rounded border bg-gray-50 p-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="text-xs text-gray-600">📄</div>
|
||||
<div className="text-muted-foreground text-xs">📄</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{file.name}</p>
|
||||
<p className="text-xs text-gray-500">{(file.size / 1024).toFixed(1)} KB</p>
|
||||
@@ -888,9 +914,9 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
</div>
|
||||
))}
|
||||
{isUploading && (
|
||||
<div className="flex items-center space-x-2 rounded border bg-blue-50 p-2">
|
||||
<div className="bg-accent flex items-center space-x-2 rounded border p-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span className="text-sm text-blue-600">업로드 중...</span>
|
||||
<span className="text-primary text-sm">업로드 중...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -905,7 +931,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
setIsAdding(true);
|
||||
|
||||
// 실제 API 호출로 데이터 추가
|
||||
console.log("🔥 추가할 데이터:", addFormData);
|
||||
// console.log("🔥 추가할 데이터:", addFormData);
|
||||
await tableTypeApi.addTableData(component.tableName, addFormData);
|
||||
|
||||
// 모달 닫기 및 폼 초기화
|
||||
@@ -915,7 +941,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
// 첫 페이지로 이동하여 새 데이터 확인
|
||||
loadData(1, searchValues);
|
||||
} catch (error) {
|
||||
console.error("데이터 추가 실패:", error);
|
||||
// console.error("데이터 추가 실패:", error);
|
||||
alert("데이터 추가에 실패했습니다.");
|
||||
} finally {
|
||||
setIsAdding(false);
|
||||
@@ -928,8 +954,8 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
setIsEditing(true);
|
||||
|
||||
// 실제 API 호출로 데이터 수정
|
||||
console.log("🔥 수정할 데이터:", editFormData);
|
||||
console.log("🔥 원본 데이터:", editingRowData);
|
||||
// console.log("🔥 수정할 데이터:", editFormData);
|
||||
// console.log("🔥 원본 데이터:", editingRowData);
|
||||
|
||||
if (editingRowData) {
|
||||
await tableTypeApi.editTableData(component.tableName, editingRowData, editFormData);
|
||||
@@ -944,7 +970,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
loadData(currentPage, searchValues);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("데이터 수정 실패:", error);
|
||||
// console.error("데이터 수정 실패:", error);
|
||||
alert("데이터 수정에 실패했습니다.");
|
||||
} finally {
|
||||
setIsEditing(false);
|
||||
@@ -978,7 +1004,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
const selectedData = Array.from(selectedRows).map((index) => data[index]);
|
||||
|
||||
// 실제 삭제 API 호출
|
||||
console.log("🗑️ 삭제할 데이터:", selectedData);
|
||||
// console.log("🗑️ 삭제할 데이터:", selectedData);
|
||||
await tableTypeApi.deleteTableData(component.tableName, selectedData);
|
||||
|
||||
// 선택 해제 및 다이얼로그 닫기
|
||||
@@ -988,7 +1014,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
// 데이터 새로고침
|
||||
loadData(currentPage, searchValues);
|
||||
} catch (error) {
|
||||
console.error("데이터 삭제 실패:", error);
|
||||
// console.error("데이터 삭제 실패:", error);
|
||||
alert("데이터 삭제에 실패했습니다.");
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
@@ -1551,7 +1577,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
|
||||
// File 객체 유효성 검사
|
||||
if (!(file instanceof File) && !(file instanceof Blob)) {
|
||||
console.error("❌ 잘못된 파일 객체:", file);
|
||||
// console.error("❌ 잘못된 파일 객체:", file);
|
||||
toast.error("파일 객체가 손상되었습니다. 파일을 다시 업로드해주세요.");
|
||||
return;
|
||||
}
|
||||
@@ -1567,7 +1593,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
toast.success(`${fileInfo.name} 다운로드가 완료되었습니다.`);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error("❌ 로컬 파일 다운로드 오류:", error);
|
||||
// console.error("❌ 로컬 파일 다운로드 오류:", error);
|
||||
toast.error("로컬 파일 다운로드에 실패했습니다. 파일을 다시 업로드해주세요.");
|
||||
return;
|
||||
}
|
||||
@@ -1587,7 +1613,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
|
||||
toast.success(`${fileInfo.name} 다운로드가 완료되었습니다.`);
|
||||
} catch (error) {
|
||||
console.error("파일 다운로드 오류:", error);
|
||||
// console.error("파일 다운로드 오류:", error);
|
||||
toast.error(`${fileInfo.name} 다운로드에 실패했습니다.`);
|
||||
}
|
||||
}, []);
|
||||
@@ -1596,7 +1622,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
const handleDeleteLinkedFile = useCallback(
|
||||
async (fileId: string, fileName: string) => {
|
||||
try {
|
||||
console.log("🗑️ 파일 삭제 시작:", { fileId, fileName });
|
||||
// console.log("🗑️ 파일 삭제 시작:", { fileId, fileName });
|
||||
|
||||
// 삭제 확인 다이얼로그
|
||||
if (!confirm(`"${fileName}" 파일을 삭제하시겠습니까?`)) {
|
||||
@@ -1612,7 +1638,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
});
|
||||
|
||||
const result = response.data;
|
||||
console.log("📡 파일 삭제 API 응답:", result);
|
||||
// console.log("📡 파일 삭제 API 응답:", result);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || "파일 삭제 실패");
|
||||
@@ -1629,15 +1655,15 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
try {
|
||||
const response = await getLinkedFiles(component.tableName, recordId);
|
||||
setLinkedFiles(response.files || []);
|
||||
console.log("📁 파일 목록 새로고침 완료:", response.files?.length || 0);
|
||||
// console.log("📁 파일 목록 새로고침 완료:", response.files?.length || 0);
|
||||
} catch (error) {
|
||||
console.error("파일 목록 새로고침 실패:", error);
|
||||
// console.error("파일 목록 새로고침 실패:", error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ 파일 삭제 완료:", fileName);
|
||||
// console.log("✅ 파일 삭제 완료:", fileName);
|
||||
} catch (error) {
|
||||
console.error("❌ 파일 삭제 실패:", error);
|
||||
// console.error("❌ 파일 삭제 실패:", error);
|
||||
toast.error(`"${fileName}" 파일 삭제에 실패했습니다.`);
|
||||
}
|
||||
},
|
||||
@@ -1670,13 +1696,13 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 hover:bg-blue-50"
|
||||
className="hover:bg-accent h-8 w-8 p-0"
|
||||
onClick={() => handleColumnFileClick(rowData, column)}
|
||||
title={hasFiles ? `${fileCount}개 파일 보기` : "파일 업로드"}
|
||||
>
|
||||
{hasFiles ? (
|
||||
<div className="relative">
|
||||
<FolderOpen className="h-4 w-4 text-blue-600" />
|
||||
<FolderOpen className="text-primary h-4 w-4" />
|
||||
{fileCount > 0 && (
|
||||
<div className="absolute -top-1 -right-1 flex h-3 w-3 items-center justify-center rounded-full bg-blue-600 text-[10px] text-white">
|
||||
{fileCount > 9 ? "9+" : fileCount}
|
||||
@@ -1729,7 +1755,13 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex h-full flex-col rounded-xl border border-gray-200/60 bg-gradient-to-br from-white to-gray-50/30 shadow-sm", className)} style={{ ...style, minHeight: "680px" }}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full flex-col rounded-xl border border-gray-200/60 bg-gradient-to-br from-white to-gray-50/30 shadow-sm",
|
||||
className,
|
||||
)}
|
||||
style={{ ...style, minHeight: "680px" }}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="p-6 pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -1819,85 +1851,85 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
<div className="flex h-full flex-col">
|
||||
{visibleColumns.length > 0 ? (
|
||||
<>
|
||||
<div className="rounded-lg border border-gray-200/60 bg-white shadow-sm overflow-hidden">
|
||||
<div className="overflow-hidden rounded-lg border border-gray-200/60 bg-white shadow-sm">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{/* 체크박스 컬럼 (삭제 기능이 활성화된 경우) */}
|
||||
{component.enableDelete && (
|
||||
<TableHead className="w-12 px-4">
|
||||
<Checkbox
|
||||
checked={selectedRows.size === data.length && data.length > 0}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
)}
|
||||
{visibleColumns.map((column: DataTableColumn) => (
|
||||
<TableHead
|
||||
key={column.id}
|
||||
className="px-4 font-semibold text-gray-700 bg-gradient-to-r from-gray-50 to-slate-50"
|
||||
style={{ width: `${((column.gridColumns || 2) / totalGridColumns) * 100}%` }}
|
||||
>
|
||||
{column.label}
|
||||
</TableHead>
|
||||
))}
|
||||
{/* 자동 파일 컬럼 표시 제거됨 - 명시적으로 추가된 파일 컬럼만 표시 */}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={visibleColumns.length + (component.enableDelete ? 1 : 0)}
|
||||
className="h-32 text-center"
|
||||
>
|
||||
<div className="text-muted-foreground flex items-center justify-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
데이터를 불러오는 중...
|
||||
</div>
|
||||
</TableCell>
|
||||
{/* 체크박스 컬럼 (삭제 기능이 활성화된 경우) */}
|
||||
{component.enableDelete && (
|
||||
<TableHead className="w-12 px-4">
|
||||
<Checkbox
|
||||
checked={selectedRows.size === data.length && data.length > 0}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
)}
|
||||
{visibleColumns.map((column: DataTableColumn) => (
|
||||
<TableHead
|
||||
key={column.id}
|
||||
className="bg-gradient-to-r from-gray-50 to-slate-50 px-4 font-semibold text-gray-700"
|
||||
style={{ width: `${((column.gridColumns || 2) / totalGridColumns) * 100}%` }}
|
||||
>
|
||||
{column.label}
|
||||
</TableHead>
|
||||
))}
|
||||
{/* 자동 파일 컬럼 표시 제거됨 - 명시적으로 추가된 파일 컬럼만 표시 */}
|
||||
</TableRow>
|
||||
) : data.length > 0 ? (
|
||||
data.map((row, rowIndex) => (
|
||||
<TableRow key={rowIndex} className="hover:bg-gradient-to-r hover:from-blue-50/50 hover:to-indigo-50/30 transition-all duration-200">
|
||||
{/* 체크박스 셀 (삭제 기능이 활성화된 경우) */}
|
||||
{component.enableDelete && (
|
||||
<TableCell className="w-12 px-4">
|
||||
<Checkbox
|
||||
checked={selectedRows.has(rowIndex)}
|
||||
onCheckedChange={(checked) => handleRowSelect(rowIndex, checked as boolean)}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
{visibleColumns.map((column: DataTableColumn) => (
|
||||
<TableCell key={column.id} className="px-4 text-sm font-medium text-gray-900">
|
||||
{formatCellValue(row[column.columnName], column, row)}
|
||||
</TableCell>
|
||||
))}
|
||||
{/* 자동 파일 셀 표시 제거됨 - 명시적으로 추가된 파일 컬럼만 표시 */}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={visibleColumns.length + (component.enableDelete ? 1 : 0)}
|
||||
className="h-32 text-center"
|
||||
>
|
||||
<div className="text-muted-foreground flex items-center justify-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
데이터를 불러오는 중...
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={visibleColumns.length + (component.enableDelete ? 1 : 0)}
|
||||
className="h-32 text-center"
|
||||
>
|
||||
<div className="text-muted-foreground flex flex-col items-center gap-2">
|
||||
<Database className="h-8 w-8" />
|
||||
<p>검색 결과가 없습니다</p>
|
||||
<p className="text-xs">검색 조건을 변경하거나 새로고침을 시도해보세요</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : data.length > 0 ? (
|
||||
data.map((row, rowIndex) => (
|
||||
<TableRow key={rowIndex} className="transition-all duration-200 hover:bg-orange-100">
|
||||
{/* 체크박스 셀 (삭제 기능이 활성화된 경우) */}
|
||||
{component.enableDelete && (
|
||||
<TableCell className="w-12 px-4">
|
||||
<Checkbox
|
||||
checked={selectedRows.has(rowIndex)}
|
||||
onCheckedChange={(checked) => handleRowSelect(rowIndex, checked as boolean)}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
{visibleColumns.map((column: DataTableColumn) => (
|
||||
<TableCell key={column.id} className="px-4 text-sm font-medium text-gray-900">
|
||||
{formatCellValue(row[column.columnName], column, row)}
|
||||
</TableCell>
|
||||
))}
|
||||
{/* 자동 파일 셀 표시 제거됨 - 명시적으로 추가된 파일 컬럼만 표시 */}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={visibleColumns.length + (component.enableDelete ? 1 : 0)}
|
||||
className="h-32 text-center"
|
||||
>
|
||||
<div className="text-muted-foreground flex flex-col items-center gap-2">
|
||||
<Database className="h-8 w-8" />
|
||||
<p>검색 결과가 없습니다</p>
|
||||
<p className="text-xs">검색 조건을 변경하거나 새로고침을 시도해보세요</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
{component.pagination?.enabled && totalPages > 1 && (
|
||||
<div className="bg-gradient-to-r from-gray-50 to-slate-50 mt-auto border-t border-gray-200/60">
|
||||
<div className="mt-auto border-t border-gray-200/60 bg-gradient-to-r from-gray-50 to-slate-50">
|
||||
<div className="flex items-center justify-between px-6 py-3">
|
||||
{component.pagination.showPageInfo && (
|
||||
<div className="text-muted-foreground text-sm">
|
||||
@@ -1971,8 +2003,26 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 데이터 추가 모달 */}
|
||||
<Dialog open={showAddModal} onOpenChange={handleAddModalClose}>
|
||||
{/* SaveModal (등록/수정 통합) */}
|
||||
<SaveModal
|
||||
isOpen={showSaveModal}
|
||||
onClose={() => {
|
||||
setShowSaveModal(false);
|
||||
setSaveModalData(undefined);
|
||||
setSaveModalScreenId(undefined);
|
||||
}}
|
||||
screenId={saveModalScreenId}
|
||||
modalSize={component.addModalConfig?.modalSize || "lg"}
|
||||
initialData={saveModalData}
|
||||
onSaveSuccess={() => {
|
||||
// 저장 성공 시 테이블 새로고침
|
||||
loadData(currentPage, searchValues); // 현재 페이지로 다시 로드
|
||||
setSelectedRows(new Set()); // 선택 해제
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 기존 데이터 추가 모달 (제거 예정 - SaveModal로 대체됨) */}
|
||||
<Dialog open={false} onOpenChange={() => {}}>
|
||||
<DialogContent className={`max-h-[80vh] overflow-y-auto ${getModalSizeClass()}`}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{component.addModalConfig?.title || "새 데이터 추가"}</DialogTitle>
|
||||
@@ -2017,18 +2067,8 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 데이터 수정 모달 */}
|
||||
<Dialog
|
||||
open={showEditModal}
|
||||
onOpenChange={(open) => {
|
||||
if (!isEditing && !open) {
|
||||
setShowEditModal(false);
|
||||
setEditFormData({});
|
||||
setEditingRowData(null);
|
||||
setUploadedFiles({}); // 파일 상태 초기화
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* 기존 데이터 수정 모달 (제거 예정 - SaveModal로 대체됨) */}
|
||||
<Dialog open={false} onOpenChange={() => {}}>
|
||||
<DialogContent className={`max-h-[80vh] overflow-y-auto ${getModalSizeClass()}`}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>데이터 수정</DialogTitle>
|
||||
@@ -2121,7 +2161,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
<h4 className="truncate font-medium text-gray-900" title={fileInfo.name}>
|
||||
{fileInfo.name}
|
||||
</h4>
|
||||
<div className="mt-1 space-y-1 text-sm text-gray-600">
|
||||
<div className="text-muted-foreground mt-1 space-y-1 text-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<span>크기: {(fileInfo.size / 1024 / 1024).toFixed(2)} MB</span>
|
||||
<span>타입: {fileInfo.type || "알 수 없음"}</span>
|
||||
@@ -2167,7 +2207,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
|
||||
{/* 요약 정보 */}
|
||||
{currentFileData && (
|
||||
<div className="mt-4 rounded-lg border border-blue-200 bg-blue-50 p-3">
|
||||
<div className="border-primary/20 bg-accent mt-4 rounded-lg border p-3">
|
||||
<h5 className="mb-2 font-medium text-blue-900">파일 요약</h5>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm text-blue-800">
|
||||
<div>
|
||||
@@ -2202,7 +2242,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
<DialogDescription>
|
||||
선택된 <strong>{selectedRows.size}개</strong>의 데이터를 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="text-red-600">이 작업은 되돌릴 수 없습니다.</span>
|
||||
<span className="text-destructive">이 작업은 되돌릴 수 없습니다.</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -2311,7 +2351,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
{linkedFiles.map((file: any, index: number) => (
|
||||
<div key={index} className="flex items-center justify-between rounded-lg border p-3">
|
||||
<div className="flex items-center space-x-3">
|
||||
<File className="h-5 w-5 text-blue-600" />
|
||||
<File className="text-primary h-5 w-5" />
|
||||
<div>
|
||||
<div className="font-medium">{file.realFileName}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
@@ -2370,7 +2410,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleDeleteLinkedFile(file.objid, file.realFileName)}
|
||||
className="text-red-500 hover:bg-red-50 hover:text-red-700"
|
||||
className="hover:bg-destructive/10 text-red-500 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user