diff --git a/backend-node/package-lock.json b/backend-node/package-lock.json index ea50778c..00f16f0d 100644 --- a/backend-node/package-lock.json +++ b/backend-node/package-lock.json @@ -35,7 +35,7 @@ "@types/jest": "^29.5.11", "@types/jsonwebtoken": "^9.0.5", "@types/morgan": "^1.9.9", - "@types/multer": "^1.4.11", + "@types/multer": "^1.4.13", "@types/node": "^20.10.5", "@types/node-cron": "^3.0.11", "@types/nodemailer": "^6.4.14", diff --git a/backend-node/package.json b/backend-node/package.json index ed6a1622..a7f15044 100644 --- a/backend-node/package.json +++ b/backend-node/package.json @@ -53,7 +53,7 @@ "@types/jest": "^29.5.11", "@types/jsonwebtoken": "^9.0.5", "@types/morgan": "^1.9.9", - "@types/multer": "^1.4.11", + "@types/multer": "^1.4.13", "@types/node": "^20.10.5", "@types/node-cron": "^3.0.11", "@types/nodemailer": "^6.4.14", diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index 7a7fa11e..83c16e2a 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -16,6 +16,7 @@ import tableManagementRoutes from "./routes/tableManagementRoutes"; import screenManagementRoutes from "./routes/screenManagementRoutes"; import commonCodeRoutes from "./routes/commonCodeRoutes"; import dynamicFormRoutes from "./routes/dynamicFormRoutes"; +import fileRoutes from "./routes/fileRoutes"; // import userRoutes from './routes/userRoutes'; // import menuRoutes from './routes/menuRoutes'; @@ -79,6 +80,7 @@ app.use("/api/table-management", tableManagementRoutes); app.use("/api/screen-management", screenManagementRoutes); app.use("/api/common-codes", commonCodeRoutes); app.use("/api/dynamic-form", dynamicFormRoutes); +app.use("/api/files", fileRoutes); // app.use('/api/users', userRoutes); // app.use('/api/menus', menuRoutes); diff --git a/backend-node/src/routes/fileRoutes.ts b/backend-node/src/routes/fileRoutes.ts new file mode 100644 index 00000000..9978526e --- /dev/null +++ b/backend-node/src/routes/fileRoutes.ts @@ -0,0 +1,306 @@ +import express from "express"; +import multer from "multer"; +import path from "path"; +import fs from "fs"; +import { authenticateToken } from "../middleware/authMiddleware"; +import { AuthenticatedRequest } from "../types/auth"; +import { logger } from "../utils/logger"; + +const router = express.Router(); + +// 파일 저장 경로 설정 +const UPLOAD_PATH = path.join(process.cwd(), "uploads"); + +// uploads 디렉토리가 없으면 생성 +if (!fs.existsSync(UPLOAD_PATH)) { + fs.mkdirSync(UPLOAD_PATH, { recursive: true }); +} + +// Multer 설정 - 파일 업로드용 +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + return cb(null, UPLOAD_PATH); + }, + filename: (req, file, cb) => { + // 파일명: timestamp_originalname + const timestamp = Date.now(); + const originalName = Buffer.from(file.originalname, "latin1").toString( + "utf8" + ); + const ext = path.extname(originalName); + const nameWithoutExt = path.basename(originalName, ext); + const safeFileName = `${timestamp}_${nameWithoutExt}${ext}`; + return cb(null, safeFileName); + }, +}); + +const upload = multer({ + storage, + limits: { + fileSize: 50 * 1024 * 1024, // 50MB 제한 + }, + fileFilter: (req, file, cb) => { + // 허용된 파일 타입 검사 (필요시 확장) + const allowedTypes = [ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "text/plain", + "text/csv", + ]; + + if (allowedTypes.includes(file.mimetype)) { + return cb(null, true); + } else { + return cb(new Error(`허용되지 않는 파일 타입입니다: ${file.mimetype}`)); + } + }, +}); + +// 모든 라우트에 인증 미들웨어 적용 +router.use(authenticateToken); + +/** + * 파일 업로드 + * POST /api/files/upload + */ +router.post( + "/upload", + upload.array("files", 10), + async (req: AuthenticatedRequest, res): Promise => { + try { + const files = req.files as Express.Multer.File[]; + + if (!files || files.length === 0) { + res.status(400).json({ + success: false, + message: "업로드할 파일이 없습니다.", + }); + return; + } + + const fileInfos = files.map((file) => ({ + id: `file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + name: Buffer.from(file.originalname, "latin1").toString("utf8"), + size: file.size, + type: file.mimetype, + extension: path.extname(file.originalname).toLowerCase().substring(1), + uploadedAt: new Date().toISOString(), + lastModified: new Date().toISOString(), + serverPath: file.path, + serverFilename: file.filename, + })); + + logger.info("파일 업로드 완료", { + userId: req.user?.userId, + fileCount: files.length, + files: fileInfos.map((f) => ({ name: f.name, size: f.size })), + }); + + res.json({ + success: true, + message: `${files.length}개 파일이 성공적으로 업로드되었습니다.`, + files: fileInfos, + }); + } catch (error) { + logger.error("파일 업로드 오류:", error); + res.status(500).json({ + success: false, + message: "파일 업로드 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }); + } + } +); + +/** + * 파일 다운로드 + * GET /api/files/download/:fileId + */ +router.get( + "/download/:fileId", + async (req: AuthenticatedRequest, res): Promise => { + try { + const { fileId } = req.params; + const { serverFilename, originalName } = req.query; + + if (!serverFilename || !originalName) { + res.status(400).json({ + success: false, + message: + "파일 정보가 부족합니다. (serverFilename, originalName 필요)", + }); + return; + } + + const filePath = path.join(UPLOAD_PATH, serverFilename as string); + + // 파일 존재 확인 + if (!fs.existsSync(filePath)) { + logger.warn("파일을 찾을 수 없음", { + fileId, + serverFilename, + filePath, + userId: req.user?.userId, + }); + + res.status(404).json({ + success: false, + message: "요청한 파일을 찾을 수 없습니다.", + }); + return; + } + + // 파일 정보 확인 + const stats = fs.statSync(filePath); + + logger.info("파일 다운로드 요청", { + fileId, + originalName, + serverFilename, + fileSize: stats.size, + userId: req.user?.userId, + }); + + // 파일명 인코딩 (한글 파일명 지원) + const encodedFilename = encodeURIComponent(originalName as string); + + // 응답 헤더 설정 + res.setHeader( + "Content-Disposition", + `attachment; filename*=UTF-8''${encodedFilename}` + ); + res.setHeader("Content-Type", "application/octet-stream"); + res.setHeader("Content-Length", stats.size); + res.setHeader("Cache-Control", "no-cache"); + + // 파일 스트림으로 전송 + const fileStream = fs.createReadStream(filePath); + + fileStream.on("error", (error) => { + logger.error("파일 스트림 오류:", error); + if (!res.headersSent) { + res.status(500).json({ + success: false, + message: "파일 전송 중 오류가 발생했습니다.", + }); + } + }); + + fileStream.pipe(res); + } catch (error) { + logger.error("파일 다운로드 오류:", error); + res.status(500).json({ + success: false, + message: "파일 다운로드 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }); + } + } +); + +/** + * 파일 삭제 + * DELETE /api/files/:fileId + */ +router.delete( + "/:fileId", + async (req: AuthenticatedRequest, res): Promise => { + try { + const { fileId } = req.params; + const { serverFilename } = req.body; + + if (!serverFilename) { + res.status(400).json({ + success: false, + message: "서버 파일명이 필요합니다.", + }); + return; + } + + const filePath = path.join(UPLOAD_PATH, serverFilename); + + // 파일 존재 확인 + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + logger.info("파일 삭제 완료", { + fileId, + serverFilename, + userId: req.user?.userId, + }); + } + + res.json({ + success: true, + message: "파일이 성공적으로 삭제되었습니다.", + }); + } catch (error) { + logger.error("파일 삭제 오류:", error); + res.status(500).json({ + success: false, + message: "파일 삭제 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }); + } + } +); + +/** + * 파일 정보 조회 + * GET /api/files/info/:fileId + */ +router.get( + "/info/:fileId", + async (req: AuthenticatedRequest, res): Promise => { + try { + const { fileId } = req.params; + const { serverFilename } = req.query; + + if (!serverFilename) { + res.status(400).json({ + success: false, + message: "서버 파일명이 필요합니다.", + }); + return; + } + + const filePath = path.join(UPLOAD_PATH, serverFilename as string); + + if (!fs.existsSync(filePath)) { + res.status(404).json({ + success: false, + message: "파일을 찾을 수 없습니다.", + }); + return; + } + + const stats = fs.statSync(filePath); + + res.json({ + success: true, + data: { + fileId, + serverFilename, + size: stats.size, + lastModified: stats.mtime.toISOString(), + exists: true, + }, + }); + } catch (error) { + logger.error("파일 정보 조회 오류:", error); + res.status(500).json({ + success: false, + message: "파일 정보 조회 중 오류가 발생했습니다.", + error: error instanceof Error ? error.message : "알 수 없는 오류", + }); + } + } +); + +export default router; diff --git a/backend-node/uploads/1757041371158_IMG_0343.jpeg b/backend-node/uploads/1757041371158_IMG_0343.jpeg new file mode 100644 index 00000000..143c1fb4 Binary files /dev/null and b/backend-node/uploads/1757041371158_IMG_0343.jpeg differ diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index 7b89cae3..13c206dd 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -18,11 +18,46 @@ import { Label } from "@/components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Separator } from "@/components/ui/separator"; -import { Search, ChevronLeft, ChevronRight, RotateCcw, Database, Loader2, Plus, Edit, Trash2 } from "lucide-react"; +import { + Search, + ChevronLeft, + ChevronRight, + RotateCcw, + Database, + Loader2, + Plus, + Edit, + Trash2, + File, + Download, + Eye, + X, +} from "lucide-react"; import { tableTypeApi } from "@/lib/api/screen"; import { getCurrentUser, UserInfo } from "@/lib/api/client"; import { DataTableComponent, DataTableColumn, DataTableFilter } from "@/types/screen"; import { cn } from "@/lib/utils"; +import { downloadFile } from "@/lib/api/file"; +import { toast } from "sonner"; + +// 파일 데이터 타입 정의 +interface FileInfo { + id: string; + name: string; + size: number; + type: string; + extension: string; + uploadedAt: string; + lastModified: string; + serverFilename?: string; // 서버에 저장된 파일명 (다운로드용) +} + +interface FileColumnData { + files: FileInfo[]; + totalCount: number; + totalSize: number; + lastModified: string; +} interface InteractiveDataTableProps { component: DataTableComponent; @@ -49,6 +84,9 @@ export const InteractiveDataTable: React.FC = ({ const [editFormData, setEditFormData] = useState>({}); const [editingRowData, setEditingRowData] = useState | null>(null); const [isEditing, setIsEditing] = useState(false); + const [showFileModal, setShowFileModal] = useState(false); + const [currentFileData, setCurrentFileData] = useState(null); + const [currentFileColumn, setCurrentFileColumn] = useState(null); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [isDeleting, setIsDeleting] = useState(false); @@ -1196,11 +1234,126 @@ export const InteractiveDataTable: React.FC = ({ } }; + // 파일 모달 열기 + const openFileModal = (fileData: FileColumnData, column: DataTableColumn) => { + setCurrentFileData(fileData); + setCurrentFileColumn(column); + setShowFileModal(true); + }; + + // 파일 다운로드 + const handleDownloadFile = useCallback(async (fileInfo: FileInfo) => { + try { + console.log("📥 파일 다운로드 시작:", fileInfo); + + // serverFilename이 없는 경우 처리 + if (!fileInfo.serverFilename) { + // _file 속성이 있는 경우 로컬 파일로 다운로드 + if ((fileInfo as any)._file) { + console.log("📁 로컬 파일 다운로드 시도:", fileInfo.name); + try { + const file = (fileInfo as any)._file; + + // File 객체 유효성 검사 + if (!(file instanceof File) && !(file instanceof Blob)) { + console.error("❌ 잘못된 파일 객체:", file); + toast.error("파일 객체가 손상되었습니다. 파일을 다시 업로드해주세요."); + return; + } + + console.log("📁 유효한 파일 객체 확인됨:", { + name: file.name || fileInfo.name, + size: file.size, + type: file.type, + }); + + const url = URL.createObjectURL(file); + const link = document.createElement("a"); + link.href = url; + link.download = fileInfo.name || file.name || "download"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + toast.success(`${fileInfo.name} 다운로드가 완료되었습니다.`); + return; + } catch (error) { + console.error("❌ 로컬 파일 다운로드 오류:", error); + toast.error("로컬 파일 다운로드에 실패했습니다. 파일을 다시 업로드해주세요."); + return; + } + } + + toast.error("이 파일은 이전 버전에서 저장된 파일입니다. 파일을 다시 업로드해주세요."); + return; + } + + toast.loading(`${fileInfo.name} 다운로드 중...`); + + await downloadFile({ + fileId: fileInfo.id, + serverFilename: fileInfo.serverFilename, + originalName: fileInfo.name, + }); + + toast.success(`${fileInfo.name} 다운로드가 완료되었습니다.`); + } catch (error) { + console.error("파일 다운로드 오류:", error); + toast.error(`${fileInfo.name} 다운로드에 실패했습니다.`); + } + }, []); + // 셀 값 포맷팅 - const formatCellValue = (value: any, column: DataTableColumn) => { + const formatCellValue = (value: any, column: DataTableColumn): React.ReactNode => { if (value === null || value === undefined) return ""; + // 디버깅을 위한 로그 추가 + if (column.columnName === "file_path") { + console.log("📊 formatCellValue (file_path 컬럼):", { + columnName: column.columnName, + widgetType: column.widgetType, + value: value, + valueType: typeof value, + fullColumn: column, + }); + } + + // file_path 컬럼은 강제로 파일 타입으로 처리 (임시 해결책) + const isFileColumn = column.widgetType === "file" || column.columnName === "file_path"; + switch (column.widgetType) { + case "file": + console.log("🗂️ 파일 타입 컬럼 처리 중:", value); + if (value) { + try { + // JSON 문자열이면 파싱 + const fileData = typeof value === "string" ? JSON.parse(value) : value; + console.log("📁 파싱된 파일 데이터:", fileData); + + if (fileData?.files && Array.isArray(fileData.files) && fileData.files.length > 0) { + return ( +
+ + + {(fileData.totalSize / 1024 / 1024).toFixed(1)}MB + +
+ ); + } + } catch (error) { + console.warn("파일 데이터 파싱 오류:", error); + } + } + return 파일 없음; + case "date": if (value) { try { @@ -1593,6 +1746,132 @@ export const InteractiveDataTable: React.FC = ({ + {/* 파일 정보 모달 */} + + + + + + 파일 정보 - {currentFileColumn?.label || "파일"} + + + {currentFileData?.totalCount === 1 + ? "1개의 파일이 저장되어 있습니다." + : `총 ${currentFileData?.totalCount}개의 파일이 저장되어 있습니다.`} + + + +
+ {currentFileData?.files && Array.isArray(currentFileData.files) && ( +
+ {currentFileData.files.map((fileInfo: FileInfo, index: number) => { + const isImage = fileInfo.type?.startsWith("image/"); + + return ( +
+
+ {/* 파일 아이콘/미리보기 */} +
+ {isImage ? ( +
+
IMG
+
+ ) : ( +
+ +
+ )} +
+ + {/* 파일 정보 */} +
+

+ {fileInfo.name} +

+
+
+ 크기: {(fileInfo.size / 1024 / 1024).toFixed(2)} MB + 타입: {fileInfo.type || "알 수 없음"} +
+
+ 확장자: {fileInfo.extension || "N/A"} + 업로드: {new Date(fileInfo.uploadedAt).toLocaleString("ko-KR")} +
+ {fileInfo.lastModified && ( +
+ 수정: {new Date(fileInfo.lastModified).toLocaleString("ko-KR")} +
+ )} +
+
+ + {/* 액션 버튼 */} +
+ {isImage && ( + + )} + +
+
+
+ ); + })} +
+ )} + + {/* 요약 정보 */} + {currentFileData && ( +
+
파일 요약
+
+
+ 총 파일 수: + {" "} + {currentFileData.totalCount}개 +
+
+ 총 크기: + {" "} + {(currentFileData.totalSize / 1024 / 1024).toFixed(2)} MB +
+
+ 최종 수정: + {" "} + {new Date(currentFileData.lastModified).toLocaleString("ko-KR")} +
+
+
+ )} +
+ + + + +
+
+ {/* 데이터 삭제 확인 다이얼로그 */} diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index de841376..dd571c97 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -9,10 +9,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Calendar } from "@/components/ui/calendar"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { CalendarIcon } from "lucide-react"; +import { CalendarIcon, File, Upload, X } from "lucide-react"; import { format } from "date-fns"; import { ko } from "date-fns/locale"; import { useAuth } from "@/hooks/useAuth"; +import { uploadFilesAndCreateData } from "@/lib/api/file"; +import { toast } from "sonner"; import { ComponentData, WidgetComponent, @@ -714,21 +716,48 @@ export const InteractiveScreenViewer: React.FC = ( case "file": { const widget = comp as WidgetComponent; const config = widget.webTypeConfig as FileTypeConfig | undefined; + + // 현재 파일 값 가져오기 + const getCurrentValue = () => { + const fieldName = widget.columnName || widget.id; + return (externalFormData?.[fieldName] || localFormData[fieldName]) as any; + }; + + const currentValue = getCurrentValue(); console.log("📁 InteractiveScreenViewer - File 위젯:", { componentId: widget.id, widgetType: widget.widgetType, config, + currentValue, appliedSettings: { accept: config?.accept, multiple: config?.multiple, maxSize: config?.maxSize, + preview: config?.preview, }, }); - const handleFileChange = (e: React.ChangeEvent) => { + const handleFileChange = async (e: React.ChangeEvent) => { const files = e.target.files; - if (!files) return; + const fieldName = widget.columnName || widget.id; + + // 파일 선택을 취소한 경우 (files가 null이거나 길이가 0) + if (!files || files.length === 0) { + console.log("📁 파일 선택 취소됨 - 기존 파일 유지"); + + // 현재 저장된 파일이 있는지 확인 + const currentStoredValue = externalFormData?.[fieldName] || localFormData[fieldName]; + if (currentStoredValue) { + console.log("📁 기존 파일 있음 - 유지:", currentStoredValue); + // 기존 파일이 있으면 그대로 유지 (아무것도 하지 않음) + return; + } else { + console.log("📁 기존 파일 없음 - 빈 상태 유지"); + // 기존 파일이 없으면 빈 상태 유지 + return; + } + } // 파일 크기 검증 if (config?.maxSize) { @@ -742,21 +771,161 @@ export const InteractiveScreenViewer: React.FC = ( } } - const file = config?.multiple ? files : files[0]; - updateFormData(fieldName, file); + // 실제 서버로 파일 업로드 + try { + toast.loading(`${files.length}개 파일 업로드 중...`); + + const uploadResult = await uploadFilesAndCreateData(files); + + if (uploadResult.success) { + console.log("📁 업로드 완료된 파일 데이터:", uploadResult.data); + + setLocalFormData(prev => ({ ...prev, [fieldName]: uploadResult.data })); + + // 외부 폼 데이터 변경 콜백 호출 + if (onFormDataChange) { + onFormDataChange(fieldName, uploadResult.data); + } + + toast.success(uploadResult.message); + } else { + throw new Error("파일 업로드에 실패했습니다."); + } + } catch (error) { + console.error("파일 업로드 오류:", error); + toast.error("파일 업로드에 실패했습니다."); + + // 파일 입력 초기화 + e.target.value = ""; + return; + } }; + const clearFile = () => { + const fieldName = widget.columnName || widget.id; + setLocalFormData(prev => ({ ...prev, [fieldName]: null })); + + // 외부 폼 데이터 변경 콜백 호출 + if (onFormDataChange) { + onFormDataChange(fieldName, null); + } + + // 파일 input 초기화 + const fileInput = document.querySelector(`input[type="file"][data-field="${fieldName}"]`) as HTMLInputElement; + if (fileInput) { + fileInput.value = ""; + } + }; + + const renderFilePreview = () => { + if (!currentValue || !config?.preview) return null; + + // 새로운 JSON 구조에서 파일 정보 추출 + const fileData = currentValue.files || []; + if (fileData.length === 0) return null; + + return ( +
+ {fileData.map((fileInfo: any, index: number) => { + const isImage = fileInfo.type?.startsWith('image/'); + + return ( +
+
+ {isImage ? ( +
IMG
+ ) : ( + + )} +
+
+

{fileInfo.name}

+

+ {(fileInfo.size / 1024 / 1024).toFixed(2)} MB +

+

{fileInfo.type || '알 수 없는 형식'}

+

업로드: {new Date(fileInfo.uploadedAt).toLocaleString('ko-KR')}

+
+ +
+ ); + })} +
+ ); + }; + + const fieldName = widget.columnName || widget.id; + return applyStyles( - , +
+ {/* 파일 선택 영역 */} +
+ +
0 ? 'border-green-300 bg-green-50' : 'border-gray-300 bg-gray-50 hover:border-gray-400 hover:bg-gray-100'} + ${readonly ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'} + `}> +
+ {currentValue && currentValue.files && currentValue.files.length > 0 ? ( + <> +
+
+ + + +
+
+

+ {currentValue.totalCount === 1 + ? '파일 선택됨' + : `${currentValue.totalCount}개 파일 선택됨`} +

+

+ 총 {(currentValue.totalSize / 1024 / 1024).toFixed(2)}MB +

+

클릭하여 다른 파일 선택

+ + ) : ( + <> + +

+ {config?.dragDrop ? '파일을 드래그하여 놓거나 클릭하여 선택' : '클릭하여 파일 선택'} +

+ {(config?.accept || config?.maxSize) && ( +
+ {config.accept &&
허용 형식: {config.accept}
} + {config.maxSize &&
최대 크기: {config.maxSize}MB
} + {config.multiple &&
다중 선택 가능
} +
+ )} + + )} +
+
+
+ + {/* 파일 미리보기 */} + {renderFilePreview()} +
); } diff --git a/frontend/components/screen/RealtimePreview.tsx b/frontend/components/screen/RealtimePreview.tsx index 0937017f..5a348eb3 100644 --- a/frontend/components/screen/RealtimePreview.tsx +++ b/frontend/components/screen/RealtimePreview.tsx @@ -46,6 +46,7 @@ import { Plus, Edit, Trash2, + Upload, } from "lucide-react"; interface RealtimePreviewProps { @@ -60,7 +61,13 @@ interface RealtimePreviewProps { // 웹 타입에 따른 위젯 렌더링 const renderWidget = (component: ComponentData) => { - const { widgetType, label, placeholder, required, readonly, columnName, style } = component; + // 위젯 컴포넌트가 아닌 경우 빈 div 반환 + if (component.type !== "widget") { + return
위젯이 아닙니다
; + } + + const widget = component as WidgetComponent; + const { widgetType, label, placeholder, required, readonly, columnName, style } = widget; // 디버깅: 실제 widgetType 값 확인 console.log("RealtimePreview - widgetType:", widgetType, "columnName:", columnName); @@ -82,7 +89,6 @@ const renderWidget = (component: ComponentData) => { case "text": case "email": case "tel": { - const widget = component as WidgetComponent; const config = widget.webTypeConfig as TextTypeConfig | undefined; // 입력 타입에 따른 처리 @@ -199,7 +205,6 @@ const renderWidget = (component: ComponentData) => { minLength: config?.minLength, maxLength: config?.maxLength, pattern: getPatternByFormat(config?.format || "none"), - onInput: handleInputChange, onChange: () => {}, // 읽기 전용으로 처리 readOnly: readonly || isAutoInput, // 자동입력인 경우 읽기 전용 className: `w-full h-full ${borderClass} ${isAutoInput ? "bg-gray-50 text-gray-600" : ""}`, @@ -215,7 +220,6 @@ const renderWidget = (component: ComponentData) => { case "number": case "decimal": { - const widget = component as WidgetComponent; const config = widget.webTypeConfig as NumberTypeConfig | undefined; // 입력 타입에 따른 처리 @@ -355,7 +359,6 @@ const renderWidget = (component: ComponentData) => { case "date": case "datetime": { - const widget = component as WidgetComponent; const config = widget.webTypeConfig as DateTypeConfig | undefined; // 입력 타입에 따른 처리 @@ -497,7 +500,6 @@ const renderWidget = (component: ComponentData) => { case "select": case "dropdown": { - const widget = component as WidgetComponent; const config = widget.webTypeConfig as SelectTypeConfig | undefined; // 디버깅: 현재 설정값 확인 @@ -524,7 +526,7 @@ const renderWidget = (component: ComponentData) => { disabled={readonly} required={required} multiple={config?.multiple} - value={config?.defaultValue || ""} + value="" onChange={() => {}} // 읽기 전용으로 처리 className={`w-full rounded-md px-3 py-2 text-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:bg-gray-100 ${ hasCustomBorder ? "!border-0" : "border border-gray-300" @@ -542,7 +544,6 @@ const renderWidget = (component: ComponentData) => { case "textarea": case "text_area": { - const widget = component as WidgetComponent; const config = widget.webTypeConfig as TextareaTypeConfig | undefined; return ( @@ -556,8 +557,8 @@ const renderWidget = (component: ComponentData) => { onChange={() => {}} // 읽기 전용으로 처리 readOnly style={{ - resize: config?.resizable === false ? "none" : "vertical", - whiteSpace: config?.wordWrap === false ? "nowrap" : "normal", + resize: config?.resize || "vertical", + whiteSpace: config?.wrap === "off" ? "nowrap" : "normal", }} /> ); @@ -565,22 +566,21 @@ const renderWidget = (component: ComponentData) => { case "boolean": case "checkbox": { - const widget = component as WidgetComponent; const config = widget.webTypeConfig as CheckboxTypeConfig | undefined; - const checkboxText = config?.checkboxText || label || columnName || "체크박스"; - const isLeftLabel = config?.labelPosition === "left"; + const checkboxText = config?.defaultChecked ? "체크됨" : label || columnName || "체크박스"; + const isLeftLabel = false; // labelPosition 속성이 없으므로 기본값 사용 return (
{isLeftLabel && ( -