파일 업로드,다운로드 기능
This commit is contained in:
@@ -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 <div className="text-xs text-gray-500">위젯이 아닙니다</div>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex items-center space-x-2">
|
||||
{isLeftLabel && (
|
||||
<Label htmlFor={`checkbox-${component.id}`} className="text-sm">
|
||||
<Label htmlFor={`checkbox-${widget.id}`} className="text-sm">
|
||||
{checkboxText}
|
||||
</Label>
|
||||
)}
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`checkbox-${component.id}`}
|
||||
id={`checkbox-${widget.id}`}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
checked={config?.defaultChecked || false}
|
||||
@@ -589,7 +589,7 @@ const renderWidget = (component: ComponentData) => {
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
{!isLeftLabel && (
|
||||
<Label htmlFor={`checkbox-${component.id}`} className="text-sm">
|
||||
<Label htmlFor={`checkbox-${widget.id}`} className="text-sm">
|
||||
{checkboxText}
|
||||
</Label>
|
||||
)}
|
||||
@@ -598,7 +598,6 @@ const renderWidget = (component: ComponentData) => {
|
||||
}
|
||||
|
||||
case "radio": {
|
||||
const widget = component as WidgetComponent;
|
||||
const config = widget.webTypeConfig as RadioTypeConfig | undefined;
|
||||
|
||||
const options = config?.options || [
|
||||
@@ -606,12 +605,7 @@ const renderWidget = (component: ComponentData) => {
|
||||
{ label: "옵션 2", value: "option2" },
|
||||
];
|
||||
|
||||
const layoutClass =
|
||||
config?.layout === "horizontal"
|
||||
? "flex flex-row space-x-4"
|
||||
: config?.layout === "grid"
|
||||
? "grid grid-cols-2 gap-2"
|
||||
: "space-y-2";
|
||||
const layoutClass = config?.inline ? "flex flex-row space-x-4" : "space-y-2";
|
||||
|
||||
return (
|
||||
<div className={layoutClass}>
|
||||
@@ -619,8 +613,8 @@ const renderWidget = (component: ComponentData) => {
|
||||
<div key={option.value} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id={`radio-${component.id}-${index}`}
|
||||
name={`radio-group-${component.id}`}
|
||||
id={`radio-${widget.id}-${index}`}
|
||||
name={`radio-group-${widget.id}`}
|
||||
value={option.value}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
@@ -629,7 +623,7 @@ const renderWidget = (component: ComponentData) => {
|
||||
readOnly
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Label htmlFor={`radio-${component.id}-${index}`} className="text-sm">
|
||||
<Label htmlFor={`radio-${widget.id}-${index}`} className="text-sm">
|
||||
{option.label}
|
||||
</Label>
|
||||
</div>
|
||||
@@ -639,7 +633,6 @@ const renderWidget = (component: ComponentData) => {
|
||||
}
|
||||
|
||||
case "code": {
|
||||
const widget = component as WidgetComponent;
|
||||
const config = widget.webTypeConfig as CodeTypeConfig | undefined;
|
||||
|
||||
console.log("💻 코드 위젯 렌더링:", {
|
||||
@@ -647,37 +640,32 @@ const renderWidget = (component: ComponentData) => {
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
language: config?.language,
|
||||
theme: config?.theme,
|
||||
fontSize: config?.fontSize,
|
||||
defaultValue: config?.defaultValue,
|
||||
readOnly: config?.readOnly,
|
||||
wordWrap: config?.wordWrap,
|
||||
placeholder: config?.placeholder,
|
||||
// CodeTypeConfig에는 language, theme, fontSize 등의 속성이 없음
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
{...commonProps}
|
||||
rows={config?.rows || 4}
|
||||
rows={4}
|
||||
className={`w-full font-mono text-sm ${borderClass}`}
|
||||
placeholder={config?.placeholder || "코드를 입력하세요..."}
|
||||
value={config?.defaultValue || ""}
|
||||
value=""
|
||||
onChange={() => {}} // 읽기 전용으로 처리
|
||||
readOnly
|
||||
style={{
|
||||
fontSize: `${config?.fontSize || 14}px`,
|
||||
backgroundColor: config?.theme === "dark" ? "#1e1e1e" : "#ffffff",
|
||||
color: config?.theme === "dark" ? "#ffffff" : "#000000",
|
||||
whiteSpace: config?.wordWrap === false ? "nowrap" : "normal",
|
||||
tabSize: config?.tabSize || 2,
|
||||
fontSize: "14px",
|
||||
backgroundColor: "#f8f9fa",
|
||||
color: "#000000",
|
||||
whiteSpace: "pre",
|
||||
tabSize: 2,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case "entity": {
|
||||
const widget = component as WidgetComponent;
|
||||
const config = widget.webTypeConfig as EntityTypeConfig | undefined;
|
||||
|
||||
console.log("🏢 엔티티 위젯 렌더링:", {
|
||||
@@ -685,13 +673,10 @@ const renderWidget = (component: ComponentData) => {
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
entityName: config?.entityName,
|
||||
displayField: config?.displayField,
|
||||
valueField: config?.valueField,
|
||||
multiple: config?.multiple,
|
||||
referenceTable: config?.referenceTable,
|
||||
referenceColumn: config?.referenceColumn,
|
||||
searchable: config?.searchable,
|
||||
allowClear: config?.allowClear,
|
||||
maxSelections: config?.maxSelections,
|
||||
// EntityTypeConfig에는 entityName, displayField, valueField, multiple, maxSelections 속성이 없음
|
||||
},
|
||||
});
|
||||
|
||||
@@ -707,8 +692,7 @@ const renderWidget = (component: ComponentData) => {
|
||||
<select
|
||||
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"
|
||||
@@ -717,9 +701,7 @@ const renderWidget = (component: ComponentData) => {
|
||||
<option value="">{config?.placeholder || "엔티티를 선택하세요..."}</option>
|
||||
{defaultOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{config?.displayFormat
|
||||
? config.displayFormat.replace("{label}", option.label).replace("{value}", option.value)
|
||||
: option.label}
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -727,7 +709,6 @@ const renderWidget = (component: ComponentData) => {
|
||||
}
|
||||
|
||||
case "file": {
|
||||
const widget = component as WidgetComponent;
|
||||
const config = widget.webTypeConfig as FileTypeConfig | undefined;
|
||||
|
||||
console.log("📁 파일 위젯 렌더링:", {
|
||||
@@ -739,7 +720,7 @@ const renderWidget = (component: ComponentData) => {
|
||||
multiple: config?.multiple,
|
||||
maxSize: config?.maxSize,
|
||||
preview: config?.preview,
|
||||
allowedTypes: config?.allowedTypes,
|
||||
dragDrop: config?.dragDrop,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -760,20 +741,47 @@ const renderWidget = (component: ComponentData) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<input
|
||||
type="file"
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
multiple={config?.multiple}
|
||||
accept={config?.accept}
|
||||
onChange={handleFileChange}
|
||||
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"
|
||||
}`}
|
||||
/>
|
||||
{config?.maxSize && <div className="mt-1 text-xs text-gray-500">최대 파일 크기: {config.maxSize}MB</div>}
|
||||
{config?.accept && <div className="mt-1 text-xs text-gray-500">허용된 파일 형식: {config.accept}</div>}
|
||||
<div className="w-full space-y-2">
|
||||
{/* 파일 선택 영역 */}
|
||||
<div className="relative">
|
||||
<input
|
||||
type="file"
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
multiple={config?.multiple}
|
||||
accept={config?.accept}
|
||||
onChange={handleFileChange}
|
||||
className="absolute inset-0 h-full w-full cursor-pointer opacity-0 disabled:cursor-not-allowed"
|
||||
style={{ zIndex: 1 }}
|
||||
/>
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-3 text-center transition-colors hover:border-gray-400 hover:bg-gray-100 ${readonly ? "cursor-not-allowed opacity-50" : "cursor-pointer"} `}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<Upload className="mx-auto h-6 w-6 text-gray-400" />
|
||||
<p className="text-xs text-gray-600">
|
||||
{config?.dragDrop ? "파일을 드래그하여 놓거나 클릭하여 선택" : "클릭하여 파일 선택"}
|
||||
</p>
|
||||
{(config?.accept || config?.maxSize) && (
|
||||
<div className="space-y-1 text-xs text-gray-500">
|
||||
{config.accept && <div>허용 형식: {config.accept}</div>}
|
||||
{config.maxSize && <div>최대 크기: {config.maxSize}MB</div>}
|
||||
{config.multiple && <div>다중 선택 가능</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 미리보기 영역 (설계 모드에서는 간단한 표시) */}
|
||||
{config?.preview && (
|
||||
<div className="rounded border bg-gray-50 p-2 text-center">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<File className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-xs text-gray-500">미리보기 영역</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -842,7 +850,11 @@ export const RealtimePreview: React.FC<RealtimePreviewProps> = ({
|
||||
children,
|
||||
onGroupToggle,
|
||||
}) => {
|
||||
const { type, label, tableName, columnName, widgetType, size, style } = component;
|
||||
const { type, label, tableName, size, style } = component;
|
||||
|
||||
// 위젯 컴포넌트인 경우에만 columnName과 widgetType 접근
|
||||
const columnName = component.type === "widget" ? (component as WidgetComponent).columnName : undefined;
|
||||
const widgetType = component.type === "widget" ? (component as WidgetComponent).widgetType : undefined;
|
||||
|
||||
// 사용자가 테두리를 설정했는지 확인
|
||||
const hasCustomBorder = style && (style.borderWidth || style.borderStyle || style.borderColor || style.border);
|
||||
@@ -868,8 +880,12 @@ export const RealtimePreview: React.FC<RealtimePreviewProps> = ({
|
||||
const shouldShowLabel = component.style?.labelDisplay !== false && (component.label || component.style?.labelText);
|
||||
const labelText = component.style?.labelText || component.label || "";
|
||||
|
||||
// 위젯 타입 확인을 위한 타입 가드
|
||||
const isWidget = component.type === "widget";
|
||||
const widgetComponent = isWidget ? (component as WidgetComponent) : null;
|
||||
|
||||
// 라벨 하단 여백 값 추출 (px 단위 숫자로 변환)
|
||||
const labelMarginBottomValue = parseInt(component.style?.labelMarginBottom || "4px", 10);
|
||||
const labelMarginBottomValue = parseInt(String(component.style?.labelMarginBottom || "4px"), 10);
|
||||
|
||||
const labelStyle: React.CSSProperties = {
|
||||
fontSize: component.style?.labelFontSize || "12px",
|
||||
@@ -920,7 +936,9 @@ export const RealtimePreview: React.FC<RealtimePreviewProps> = ({
|
||||
}}
|
||||
>
|
||||
{labelText}
|
||||
{component.required && <span style={{ color: "#f97316", marginLeft: "2px" }}>*</span>}
|
||||
{component.type === "widget" && (component as WidgetComponent).required && (
|
||||
<span style={{ color: "#f97316", marginLeft: "2px" }}>*</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1135,7 +1153,9 @@ export const RealtimePreview: React.FC<RealtimePreviewProps> = ({
|
||||
}}
|
||||
>
|
||||
{labelText}
|
||||
{component.required && <span style={{ color: "#f97316", marginLeft: "2px" }}>*</span>}
|
||||
{component.type === "widget" && (component as WidgetComponent).required && (
|
||||
<span style={{ color: "#f97316", marginLeft: "2px" }}>*</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user