파일 업로드,다운로드 기능

This commit is contained in:
kjs
2025-09-05 12:04:13 +09:00
parent aa066a1ea9
commit 53a44b901d
10 changed files with 1028 additions and 91 deletions

View File

@@ -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>
)}