2025-09-01 14:52:25 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
import React, { useState, useEffect } from "react";
|
2025-09-19 18:43:55 +09:00
|
|
|
import { ComponentData, WebType, isWidgetComponent, isContainerComponent } from "@/types";
|
2025-09-29 13:29:03 +09:00
|
|
|
import { isFileComponent } from "@/lib/utils/componentTypeUtils";
|
2025-09-01 14:52:25 +09:00
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
2025-09-03 15:23:12 +09:00
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Separator } from "@/components/ui/separator";
|
2025-09-05 21:52:19 +09:00
|
|
|
import { FileUpload } from "./widgets/FileUpload";
|
|
|
|
|
import { useAuth } from "@/hooks/useAuth";
|
2025-09-09 14:29:04 +09:00
|
|
|
import { DynamicWebTypeRenderer, WebTypeRegistry } from "@/lib/registry";
|
2025-09-09 17:42:23 +09:00
|
|
|
import { DataTableTemplate } from "@/components/screen/templates/DataTableTemplate";
|
2025-09-01 15:22:47 +09:00
|
|
|
import {
|
|
|
|
|
Database,
|
|
|
|
|
Type,
|
|
|
|
|
Hash,
|
|
|
|
|
List,
|
|
|
|
|
AlignLeft,
|
|
|
|
|
CheckSquare,
|
|
|
|
|
Radio,
|
|
|
|
|
Calendar,
|
|
|
|
|
Code,
|
|
|
|
|
Building,
|
|
|
|
|
File,
|
|
|
|
|
Group,
|
|
|
|
|
ChevronDown,
|
|
|
|
|
ChevronRight,
|
2025-09-03 15:23:12 +09:00
|
|
|
Search,
|
|
|
|
|
RotateCcw,
|
2025-09-03 16:38:10 +09:00
|
|
|
Plus,
|
|
|
|
|
Edit,
|
|
|
|
|
Trash2,
|
2025-09-05 12:04:13 +09:00
|
|
|
Upload,
|
2025-09-08 13:10:09 +09:00
|
|
|
Square,
|
|
|
|
|
CreditCard,
|
|
|
|
|
Layout,
|
|
|
|
|
Grid3x3,
|
|
|
|
|
Columns,
|
|
|
|
|
Rows,
|
|
|
|
|
SidebarOpen,
|
|
|
|
|
Folder,
|
|
|
|
|
ChevronUp,
|
2025-09-26 13:11:34 +09:00
|
|
|
Image as ImageIcon,
|
|
|
|
|
FileText,
|
|
|
|
|
Video,
|
|
|
|
|
Music,
|
|
|
|
|
Archive,
|
|
|
|
|
Presentation,
|
2025-09-01 15:22:47 +09:00
|
|
|
} from "lucide-react";
|
2025-09-01 14:52:25 +09:00
|
|
|
|
|
|
|
|
interface RealtimePreviewProps {
|
|
|
|
|
component: ComponentData;
|
|
|
|
|
isSelected?: boolean;
|
2025-09-01 16:40:24 +09:00
|
|
|
onClick?: (e?: React.MouseEvent) => void;
|
2025-09-01 14:52:25 +09:00
|
|
|
onDragStart?: (e: React.DragEvent) => void;
|
|
|
|
|
onDragEnd?: () => void;
|
2025-09-01 15:22:47 +09:00
|
|
|
onGroupToggle?: (groupId: string) => void; // 그룹 접기/펼치기
|
|
|
|
|
children?: React.ReactNode; // 그룹 내 자식 컴포넌트들
|
2025-09-01 14:52:25 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-08 13:10:09 +09:00
|
|
|
// 영역 레이아웃에 따른 아이콘 반환
|
2025-09-19 18:43:55 +09:00
|
|
|
const getAreaIcon = (layoutDirection?: "horizontal" | "vertical") => {
|
|
|
|
|
switch (layoutDirection) {
|
|
|
|
|
case "horizontal":
|
2025-09-09 14:29:04 +09:00
|
|
|
return <Layout className="h-4 w-4 text-blue-600" />;
|
2025-09-19 18:43:55 +09:00
|
|
|
case "vertical":
|
2025-09-09 14:29:04 +09:00
|
|
|
return <Columns className="h-4 w-4 text-purple-600" />;
|
2025-09-08 13:10:09 +09:00
|
|
|
default:
|
2025-09-09 14:29:04 +09:00
|
|
|
return <Square className="h-4 w-4 text-gray-500" />;
|
2025-09-08 13:10:09 +09:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
// 영역 렌더링
|
|
|
|
|
const renderArea = (component: ComponentData, children?: React.ReactNode) => {
|
2025-09-19 18:43:55 +09:00
|
|
|
if (!isContainerComponent(component) || component.type !== "area") {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const area = component;
|
|
|
|
|
const { layoutDirection, label } = area;
|
2025-09-08 13:10:09 +09:00
|
|
|
|
|
|
|
|
const renderPlaceholder = () => (
|
2025-09-09 14:29:04 +09:00
|
|
|
<div className="flex h-full w-full items-center justify-center rounded border-2 border-dashed border-gray-300 bg-gray-50">
|
|
|
|
|
<div className="text-center">
|
2025-09-19 18:43:55 +09:00
|
|
|
{getAreaIcon(layoutDirection)}
|
|
|
|
|
<p className="mt-2 text-sm text-gray-600">{label || `${layoutDirection || "기본"} 영역`}</p>
|
2025-09-09 14:29:04 +09:00
|
|
|
<p className="text-xs text-gray-400">컴포넌트를 드래그해서 추가하세요</p>
|
2025-09-08 13:10:09 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
2025-09-09 14:29:04 +09:00
|
|
|
<div className="relative h-full w-full">
|
|
|
|
|
<div className="absolute inset-0 h-full w-full">
|
|
|
|
|
{children && React.Children.count(children) > 0 ? children : renderPlaceholder()}
|
|
|
|
|
</div>
|
2025-09-08 13:10:09 +09:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
// 동적 웹 타입 위젯 렌더링 컴포넌트
|
|
|
|
|
const WidgetRenderer: React.FC<{ component: ComponentData }> = ({ component }) => {
|
2025-09-05 12:04:13 +09:00
|
|
|
// 위젯 컴포넌트가 아닌 경우 빈 div 반환
|
2025-09-19 18:43:55 +09:00
|
|
|
if (!isWidgetComponent(component)) {
|
2025-09-05 12:04:13 +09:00
|
|
|
return <div className="text-xs text-gray-500">위젯이 아닙니다</div>;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 18:43:55 +09:00
|
|
|
const widget = component;
|
2025-09-05 12:04:13 +09:00
|
|
|
const { widgetType, label, placeholder, required, readonly, columnName, style } = widget;
|
2025-09-01 15:22:47 +09:00
|
|
|
|
2025-09-01 14:52:25 +09:00
|
|
|
// 디버깅: 실제 widgetType 값 확인
|
2025-09-09 14:29:04 +09:00
|
|
|
console.log("RealtimePreviewDynamic - widgetType:", widgetType, "columnName:", columnName);
|
2025-09-01 14:52:25 +09:00
|
|
|
|
2025-09-01 17:05:36 +09:00
|
|
|
// 사용자가 테두리를 설정했는지 확인
|
|
|
|
|
const hasCustomBorder = style && (style.borderWidth || style.borderStyle || style.borderColor || style.border);
|
|
|
|
|
|
|
|
|
|
// 기본 테두리 제거 여부 결정 - Shadcn UI 기본 border 클래스를 덮어쓰기
|
|
|
|
|
const borderClass = hasCustomBorder ? "!border-0" : "";
|
|
|
|
|
|
2025-09-01 14:52:25 +09:00
|
|
|
const commonProps = {
|
2025-09-04 11:33:52 +09:00
|
|
|
placeholder: placeholder || "입력하세요...",
|
2025-09-01 14:52:25 +09:00
|
|
|
disabled: readonly,
|
|
|
|
|
required: required,
|
2025-09-01 17:05:36 +09:00
|
|
|
className: `w-full h-full ${borderClass}`,
|
2025-09-01 14:52:25 +09:00
|
|
|
};
|
|
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
// 파일 컴포넌트 강제 체크
|
|
|
|
|
if (isFileComponent(widget)) {
|
|
|
|
|
console.log("🎯 RealtimePreview - 파일 컴포넌트 강제 감지:", {
|
|
|
|
|
componentId: widget.id,
|
|
|
|
|
widgetType: widgetType,
|
|
|
|
|
isFileComponent: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return (
|
|
|
|
|
<DynamicWebTypeRenderer
|
|
|
|
|
webType="file"
|
|
|
|
|
props={{
|
|
|
|
|
...commonProps,
|
|
|
|
|
component: widget,
|
|
|
|
|
value: undefined, // 미리보기이므로 값은 없음
|
|
|
|
|
readonly: readonly,
|
|
|
|
|
}}
|
|
|
|
|
config={widget.webTypeConfig}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`파일 웹타입 렌더링 실패:`, error);
|
|
|
|
|
return <div className="text-xs text-gray-500 p-2">파일 컴포넌트 (렌더링 오류)</div>;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
// 동적 웹타입 렌더링 사용
|
|
|
|
|
if (widgetType) {
|
|
|
|
|
try {
|
2025-09-01 14:52:25 +09:00
|
|
|
return (
|
2025-09-09 14:29:04 +09:00
|
|
|
<DynamicWebTypeRenderer
|
|
|
|
|
webType={widgetType}
|
|
|
|
|
props={{
|
|
|
|
|
...commonProps,
|
|
|
|
|
component: widget,
|
|
|
|
|
value: undefined, // 미리보기이므로 값은 없음
|
|
|
|
|
readonly: readonly,
|
2025-09-03 11:32:09 +09:00
|
|
|
}}
|
2025-09-09 14:29:04 +09:00
|
|
|
config={widget.webTypeConfig}
|
2025-09-03 11:32:09 +09:00
|
|
|
/>
|
|
|
|
|
);
|
2025-09-09 14:29:04 +09:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`웹타입 "${widgetType}" 렌더링 실패:`, error);
|
|
|
|
|
// 오류 발생 시 폴백으로 기본 input 렌더링
|
|
|
|
|
return <Input type="text" {...commonProps} placeholder={`${widgetType} (렌더링 오류)`} />;
|
2025-09-03 11:32:09 +09:00
|
|
|
}
|
2025-09-09 14:29:04 +09:00
|
|
|
}
|
2025-09-01 14:52:25 +09:00
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
// 웹타입이 없는 경우 기본 input 렌더링 (하위 호환성)
|
|
|
|
|
return <Input type="text" {...commonProps} />;
|
|
|
|
|
};
|
2025-09-01 14:52:25 +09:00
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
// 동적 위젯 타입 아이콘 (레지스트리에서 조회)
|
|
|
|
|
const getWidgetIcon = (widgetType: WebType | undefined) => {
|
|
|
|
|
if (!widgetType) {
|
|
|
|
|
return <Type className="h-4 w-4 text-gray-500" />;
|
|
|
|
|
}
|
2025-09-04 11:33:52 +09:00
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
// 레지스트리에서 웹타입 정의 조회
|
|
|
|
|
const webTypeDefinition = WebTypeRegistry.getWebType(widgetType);
|
|
|
|
|
if (webTypeDefinition && webTypeDefinition.icon) {
|
|
|
|
|
const IconComponent = webTypeDefinition.icon;
|
|
|
|
|
return <IconComponent className="h-4 w-4" />;
|
2025-09-01 14:52:25 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
// 기본 아이콘 매핑 (하위 호환성)
|
2025-09-01 14:52:25 +09:00
|
|
|
switch (widgetType) {
|
|
|
|
|
case "text":
|
|
|
|
|
case "email":
|
|
|
|
|
case "tel":
|
|
|
|
|
return <Type className="h-4 w-4 text-blue-600" />;
|
|
|
|
|
case "number":
|
|
|
|
|
case "decimal":
|
|
|
|
|
return <Hash className="h-4 w-4 text-green-600" />;
|
|
|
|
|
case "date":
|
|
|
|
|
case "datetime":
|
|
|
|
|
return <Calendar className="h-4 w-4 text-purple-600" />;
|
|
|
|
|
case "select":
|
|
|
|
|
case "dropdown":
|
|
|
|
|
return <List className="h-4 w-4 text-orange-600" />;
|
|
|
|
|
case "textarea":
|
|
|
|
|
return <AlignLeft className="h-4 w-4 text-indigo-600" />;
|
|
|
|
|
case "boolean":
|
|
|
|
|
case "checkbox":
|
|
|
|
|
return <CheckSquare className="h-4 w-4 text-blue-600" />;
|
|
|
|
|
case "radio":
|
|
|
|
|
return <Radio className="h-4 w-4 text-blue-600" />;
|
|
|
|
|
case "code":
|
|
|
|
|
return <Code className="h-4 w-4 text-gray-600" />;
|
|
|
|
|
case "entity":
|
|
|
|
|
return <Building className="h-4 w-4 text-cyan-600" />;
|
|
|
|
|
case "file":
|
|
|
|
|
return <File className="h-4 w-4 text-yellow-600" />;
|
|
|
|
|
default:
|
|
|
|
|
return <Type className="h-4 w-4 text-gray-500" />;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
2025-09-01 14:52:25 +09:00
|
|
|
component,
|
|
|
|
|
isSelected = false,
|
|
|
|
|
onClick,
|
|
|
|
|
onDragStart,
|
|
|
|
|
onDragEnd,
|
2025-09-01 15:22:47 +09:00
|
|
|
onGroupToggle,
|
2025-09-09 14:29:04 +09:00
|
|
|
children,
|
2025-09-01 14:52:25 +09:00
|
|
|
}) => {
|
2025-09-09 14:29:04 +09:00
|
|
|
const { user } = useAuth();
|
|
|
|
|
const { type, id, position, size, style = {} } = component;
|
2025-09-29 13:29:03 +09:00
|
|
|
const [fileUpdateTrigger, setFileUpdateTrigger] = useState(0);
|
|
|
|
|
|
|
|
|
|
// 전역 파일 상태 변경 감지 (해당 컴포넌트만)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleGlobalFileStateChange = (event: CustomEvent) => {
|
|
|
|
|
if (event.detail.componentId === component.id) {
|
|
|
|
|
console.log("🔄 RealtimePreview 파일 상태 변경 감지:", {
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
filesCount: event.detail.files?.length || 0,
|
|
|
|
|
action: event.detail.action
|
|
|
|
|
});
|
|
|
|
|
setFileUpdateTrigger(prev => prev + 1);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
window.addEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener('globalFileStateChanged', handleGlobalFileStateChange as EventListener);
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}, [component.id]);
|
2025-09-09 14:29:04 +09:00
|
|
|
|
|
|
|
|
// 컴포넌트 스타일 계산
|
|
|
|
|
const componentStyle = {
|
|
|
|
|
position: "absolute" as const,
|
|
|
|
|
left: position?.x || 0,
|
|
|
|
|
top: position?.y || 0,
|
|
|
|
|
width: size?.width || 200,
|
|
|
|
|
height: size?.height || 40,
|
|
|
|
|
zIndex: position?.z || 1,
|
|
|
|
|
...style,
|
2025-09-02 16:46:54 +09:00
|
|
|
};
|
|
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
// 선택된 컴포넌트 스타일
|
|
|
|
|
const selectionStyle = isSelected
|
|
|
|
|
? {
|
|
|
|
|
outline: "2px solid #3b82f6",
|
|
|
|
|
outlineOffset: "2px",
|
|
|
|
|
}
|
|
|
|
|
: {};
|
2025-09-03 15:23:12 +09:00
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
const handleClick = (e: React.MouseEvent) => {
|
2025-09-09 18:02:07 +09:00
|
|
|
// 컴포넌트 영역 내에서만 클릭 이벤트 처리
|
2025-09-09 14:29:04 +09:00
|
|
|
e.stopPropagation();
|
|
|
|
|
onClick?.(e);
|
|
|
|
|
};
|
2025-09-03 15:23:12 +09:00
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
const handleDragStart = (e: React.DragEvent) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
onDragStart?.(e);
|
|
|
|
|
};
|
2025-09-03 15:23:12 +09:00
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
const handleDragEnd = () => {
|
|
|
|
|
onDragEnd?.();
|
|
|
|
|
};
|
2025-09-03 15:23:12 +09:00
|
|
|
|
2025-09-01 14:52:25 +09:00
|
|
|
return (
|
|
|
|
|
<div
|
2025-09-09 14:29:04 +09:00
|
|
|
id={`component-${id}`}
|
|
|
|
|
className="absolute cursor-pointer"
|
|
|
|
|
style={{ ...componentStyle, ...selectionStyle }}
|
|
|
|
|
onClick={handleClick}
|
2025-09-01 14:52:25 +09:00
|
|
|
draggable
|
2025-09-09 14:29:04 +09:00
|
|
|
onDragStart={handleDragStart}
|
|
|
|
|
onDragEnd={handleDragEnd}
|
2025-09-01 14:52:25 +09:00
|
|
|
>
|
2025-09-09 14:29:04 +09:00
|
|
|
{/* 컴포넌트 타입별 렌더링 */}
|
|
|
|
|
<div className="h-full w-full">
|
|
|
|
|
{/* 영역 타입 */}
|
|
|
|
|
{type === "area" && renderArea(component, children)}
|
2025-09-01 15:22:47 +09:00
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
{/* 데이터 테이블 타입 */}
|
|
|
|
|
{type === "datatable" &&
|
2025-09-03 15:23:12 +09:00
|
|
|
(() => {
|
|
|
|
|
const dataTableComponent = component as any; // DataTableComponent 타입
|
2025-09-09 14:29:04 +09:00
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
return (
|
2025-09-09 17:42:23 +09:00
|
|
|
<DataTableTemplate
|
|
|
|
|
title={dataTableComponent.title || dataTableComponent.label}
|
|
|
|
|
description={`${dataTableComponent.label}을 표시하는 데이터 테이블`}
|
|
|
|
|
columns={dataTableComponent.columns}
|
|
|
|
|
filters={dataTableComponent.filters}
|
|
|
|
|
pagination={dataTableComponent.pagination}
|
|
|
|
|
actions={
|
|
|
|
|
dataTableComponent.actions || {
|
|
|
|
|
showSearchButton: dataTableComponent.showSearchButton ?? true,
|
|
|
|
|
searchButtonText: dataTableComponent.searchButtonText || "검색",
|
|
|
|
|
enableExport: dataTableComponent.enableExport ?? true,
|
|
|
|
|
enableRefresh: dataTableComponent.enableRefresh ?? true,
|
|
|
|
|
enableAdd: dataTableComponent.enableAdd ?? true,
|
|
|
|
|
enableEdit: dataTableComponent.enableEdit ?? true,
|
|
|
|
|
enableDelete: dataTableComponent.enableDelete ?? true,
|
|
|
|
|
addButtonText: dataTableComponent.addButtonText || "추가",
|
|
|
|
|
editButtonText: dataTableComponent.editButtonText || "수정",
|
|
|
|
|
deleteButtonText: dataTableComponent.deleteButtonText || "삭제",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
style={component.style}
|
|
|
|
|
className="h-full w-full"
|
|
|
|
|
isPreview={true}
|
|
|
|
|
/>
|
2025-09-03 15:23:12 +09:00
|
|
|
);
|
|
|
|
|
})()}
|
|
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
{/* 그룹 타입 */}
|
2025-09-02 16:46:54 +09:00
|
|
|
{type === "group" && (
|
|
|
|
|
<div className="relative h-full w-full">
|
|
|
|
|
<div className="absolute inset-0">{children}</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
{/* 위젯 타입 - 동적 렌더링 */}
|
2025-09-02 16:46:54 +09:00
|
|
|
{type === "widget" && (
|
|
|
|
|
<div className="flex h-full flex-col">
|
2025-09-09 14:29:04 +09:00
|
|
|
<div className="pointer-events-none flex-1">
|
|
|
|
|
<WidgetRenderer component={component} />
|
|
|
|
|
</div>
|
2025-09-02 16:46:54 +09:00
|
|
|
</div>
|
|
|
|
|
)}
|
2025-09-05 21:52:19 +09:00
|
|
|
|
2025-09-29 13:29:03 +09:00
|
|
|
{/* 파일 타입 - 레거시 및 신규 타입 지원 */}
|
|
|
|
|
{isFileComponent(component) && (() => {
|
2025-09-26 13:11:34 +09:00
|
|
|
const fileComponent = component as any;
|
|
|
|
|
const uploadedFiles = fileComponent.uploadedFiles || [];
|
|
|
|
|
|
|
|
|
|
// 전역 상태에서 최신 파일 정보 가져오기
|
|
|
|
|
const globalFileState = typeof window !== 'undefined' ? (window as any).globalFileState || {} : {};
|
|
|
|
|
const globalFiles = globalFileState[component.id] || [];
|
|
|
|
|
|
|
|
|
|
// 최신 파일 정보 사용 (전역 상태 > 컴포넌트 속성)
|
|
|
|
|
const currentFiles = globalFiles.length > 0 ? globalFiles : uploadedFiles;
|
|
|
|
|
|
|
|
|
|
console.log("🔍 RealtimePreview 파일 컴포넌트 렌더링:", {
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
uploadedFilesCount: uploadedFiles.length,
|
|
|
|
|
globalFilesCount: globalFiles.length,
|
|
|
|
|
currentFilesCount: currentFiles.length,
|
|
|
|
|
currentFiles: currentFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName || f.name })),
|
|
|
|
|
componentType: component.type,
|
2025-09-29 13:29:03 +09:00
|
|
|
fileUpdateTrigger: fileUpdateTrigger,
|
2025-09-26 13:11:34 +09:00
|
|
|
timestamp: new Date().toISOString()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-full flex-col">
|
|
|
|
|
<div className="pointer-events-none flex-1 rounded border-2 border-dashed border-gray-300 bg-gray-50 p-2">
|
|
|
|
|
{currentFiles.length > 0 ? (
|
|
|
|
|
<div className="h-full overflow-y-auto">
|
|
|
|
|
<div className="mb-1 text-xs font-medium text-gray-700">
|
|
|
|
|
업로드된 파일 ({currentFiles.length})
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
{currentFiles.map((file: any, index: number) => {
|
|
|
|
|
// 파일 확장자에 따른 아이콘 선택
|
|
|
|
|
const getFileIcon = (fileName: string) => {
|
|
|
|
|
const ext = fileName.split('.').pop()?.toLowerCase() || '';
|
|
|
|
|
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) {
|
|
|
|
|
return <ImageIcon className="h-4 w-4 text-green-500 flex-shrink-0" />;
|
|
|
|
|
}
|
|
|
|
|
if (['pdf', 'doc', 'docx', 'txt', 'rtf', 'hwp', 'hwpx', 'hwpml', 'pages'].includes(ext)) {
|
|
|
|
|
return <FileText className="h-4 w-4 text-red-500 flex-shrink-0" />;
|
|
|
|
|
}
|
|
|
|
|
if (['ppt', 'pptx', 'hpt', 'keynote'].includes(ext)) {
|
|
|
|
|
return <Presentation className="h-4 w-4 text-orange-600 flex-shrink-0" />;
|
|
|
|
|
}
|
|
|
|
|
if (['xls', 'xlsx', 'hcdt', 'numbers'].includes(ext)) {
|
|
|
|
|
return <FileText className="h-4 w-4 text-green-600 flex-shrink-0" />;
|
|
|
|
|
}
|
|
|
|
|
if (['mp4', 'avi', 'mov', 'wmv', 'webm', 'ogg'].includes(ext)) {
|
|
|
|
|
return <Video className="h-4 w-4 text-purple-500 flex-shrink-0" />;
|
|
|
|
|
}
|
|
|
|
|
if (['mp3', 'wav', 'flac', 'aac'].includes(ext)) {
|
|
|
|
|
return <Music className="h-4 w-4 text-orange-500 flex-shrink-0" />;
|
|
|
|
|
}
|
|
|
|
|
if (['zip', 'rar', '7z', 'tar'].includes(ext)) {
|
|
|
|
|
return <Archive className="h-4 w-4 text-yellow-500 flex-shrink-0" />;
|
|
|
|
|
}
|
|
|
|
|
return <File className="h-4 w-4 text-blue-500 flex-shrink-0" />;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={file.objid || index} className="flex items-center space-x-2 bg-white rounded p-2 text-xs">
|
|
|
|
|
{getFileIcon(file.realFileName || file.name || '')}
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<p className="truncate font-medium text-gray-900">
|
|
|
|
|
{file.realFileName || file.name || `파일 ${index + 1}`}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-gray-500">
|
|
|
|
|
{file.fileSize ? `${Math.round(file.fileSize / 1024)} KB` : ''}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex h-full flex-col items-center justify-center text-center">
|
|
|
|
|
<File className="mb-2 h-8 w-8 text-gray-400" />
|
|
|
|
|
<p className="text-xs font-medium text-gray-700 mb-1">업로드된 파일 (0)</p>
|
|
|
|
|
<p className="text-sm text-gray-600">파일 업로드 영역</p>
|
|
|
|
|
<p className="mt-1 text-xs text-gray-400">상세설정에서 파일을 업로드하세요</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-09-09 14:29:04 +09:00
|
|
|
</div>
|
2025-09-05 21:52:19 +09:00
|
|
|
</div>
|
2025-09-26 13:11:34 +09:00
|
|
|
);
|
|
|
|
|
})()}
|
2025-09-02 16:46:54 +09:00
|
|
|
</div>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
|
|
|
|
{/* 선택된 컴포넌트 정보 표시 */}
|
|
|
|
|
{isSelected && (
|
|
|
|
|
<div className="absolute -top-6 left-0 rounded bg-blue-600 px-2 py-1 text-xs text-white">
|
|
|
|
|
{type === "widget" && (
|
|
|
|
|
<div className="flex items-center gap-1">
|
2025-09-19 18:43:55 +09:00
|
|
|
{getWidgetIcon(isWidgetComponent(component) ? (component.widgetType as WebType) : undefined)}
|
|
|
|
|
{isWidgetComponent(component) ? component.widgetType || "widget" : component.type}
|
2025-09-09 14:29:04 +09:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{type !== "widget" && type}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-09-01 14:52:25 +09:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
// 기존 RealtimePreview와의 호환성을 위한 export
|
|
|
|
|
export { RealtimePreviewDynamic as RealtimePreview };
|
|
|
|
|
|
|
|
|
|
RealtimePreviewDynamic.displayName = "RealtimePreviewDynamic";
|