실제 화면에 상세설정 적용
This commit is contained in:
parent
f82d18575e
commit
f2bdf5356a
|
|
@ -11,7 +11,20 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
|||
import { CalendarIcon } from "lucide-react";
|
||||
import { format } from "date-fns";
|
||||
import { ko } from "date-fns/locale";
|
||||
import { ComponentData } from "@/types/screen";
|
||||
import {
|
||||
ComponentData,
|
||||
WidgetComponent,
|
||||
TextTypeConfig,
|
||||
NumberTypeConfig,
|
||||
DateTypeConfig,
|
||||
SelectTypeConfig,
|
||||
RadioTypeConfig,
|
||||
CheckboxTypeConfig,
|
||||
TextareaTypeConfig,
|
||||
FileTypeConfig,
|
||||
CodeTypeConfig,
|
||||
EntityTypeConfig,
|
||||
} from "@/types/screen";
|
||||
|
||||
interface InteractiveScreenViewerProps {
|
||||
component: ComponentData;
|
||||
|
|
@ -78,192 +91,553 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
|||
switch (widgetType) {
|
||||
case "text":
|
||||
case "email":
|
||||
case "tel":
|
||||
case "tel": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as TextTypeConfig | undefined;
|
||||
|
||||
console.log("📝 InteractiveScreenViewer - Text 위젯:", {
|
||||
componentId: widget.id,
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
format: config?.format,
|
||||
minLength: config?.minLength,
|
||||
maxLength: config?.maxLength,
|
||||
pattern: config?.pattern,
|
||||
placeholder: config?.placeholder,
|
||||
},
|
||||
});
|
||||
|
||||
// 형식별 패턴 생성
|
||||
const getPatternByFormat = (format: string) => {
|
||||
switch (format) {
|
||||
case "korean":
|
||||
return "[가-힣\\s]*";
|
||||
case "english":
|
||||
return "[a-zA-Z\\s]*";
|
||||
case "alphanumeric":
|
||||
return "[a-zA-Z0-9]*";
|
||||
case "numeric":
|
||||
return "[0-9]*";
|
||||
case "email":
|
||||
return "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}";
|
||||
case "phone":
|
||||
return "\\d{3}-\\d{4}-\\d{4}";
|
||||
case "url":
|
||||
return "https?://[\\w\\-]+(\\.[\\w\\-]+)+([\\w\\-\\.,@?^=%&:/~\\+#]*[\\w\\-\\@?^=%&/~\\+#])?";
|
||||
default:
|
||||
return config?.pattern || undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// 입력 검증 함수
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
|
||||
// 형식별 실시간 검증
|
||||
if (config?.format && config.format !== "none") {
|
||||
const pattern = getPatternByFormat(config.format);
|
||||
if (pattern) {
|
||||
const regex = new RegExp(`^${pattern}$`);
|
||||
if (value && !regex.test(value)) {
|
||||
return; // 유효하지 않은 입력 차단
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 길이 제한 검증
|
||||
if (config?.maxLength && value.length > config.maxLength) {
|
||||
return; // 최대 길이 초과 차단
|
||||
}
|
||||
|
||||
updateFormData(fieldName, value);
|
||||
};
|
||||
|
||||
const finalPlaceholder = config?.placeholder || placeholder || "입력하세요...";
|
||||
const inputType = widgetType === "email" ? "email" : widgetType === "tel" ? "tel" : "text";
|
||||
|
||||
return applyStyles(
|
||||
<Input
|
||||
type={widgetType === "email" ? "email" : widgetType === "tel" ? "tel" : "text"}
|
||||
placeholder={placeholder || "입력하세요..."}
|
||||
type={inputType}
|
||||
placeholder={finalPlaceholder}
|
||||
value={currentValue}
|
||||
onChange={(e) => updateFormData(fieldName, e.target.value)}
|
||||
onChange={handleInputChange}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
minLength={config?.minLength}
|
||||
maxLength={config?.maxLength}
|
||||
pattern={getPatternByFormat(config?.format || "none")}
|
||||
className="h-full w-full"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
case "number":
|
||||
case "decimal":
|
||||
case "decimal": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as NumberTypeConfig | undefined;
|
||||
|
||||
console.log("🔢 InteractiveScreenViewer - Number 위젯:", {
|
||||
componentId: widget.id,
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
format: config?.format,
|
||||
min: config?.min,
|
||||
max: config?.max,
|
||||
step: config?.step,
|
||||
decimalPlaces: config?.decimalPlaces,
|
||||
thousandSeparator: config?.thousandSeparator,
|
||||
prefix: config?.prefix,
|
||||
suffix: config?.suffix,
|
||||
},
|
||||
});
|
||||
|
||||
const step = config?.step || (widgetType === "decimal" ? 0.01 : 1);
|
||||
const finalPlaceholder = config?.placeholder || placeholder || "숫자를 입력하세요...";
|
||||
|
||||
return applyStyles(
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={placeholder || "숫자를 입력하세요..."}
|
||||
placeholder={finalPlaceholder}
|
||||
value={currentValue}
|
||||
onChange={(e) => updateFormData(fieldName, e.target.valueAsNumber || 0)}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
min={config?.min}
|
||||
max={config?.max}
|
||||
step={step}
|
||||
className="h-full w-full"
|
||||
step={widgetType === "decimal" ? "0.01" : "1"}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
case "textarea":
|
||||
case "text_area":
|
||||
case "text_area": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as TextareaTypeConfig | undefined;
|
||||
|
||||
console.log("📄 InteractiveScreenViewer - Textarea 위젯:", {
|
||||
componentId: widget.id,
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
rows: config?.rows,
|
||||
maxLength: config?.maxLength,
|
||||
minLength: config?.minLength,
|
||||
placeholder: config?.placeholder,
|
||||
defaultValue: config?.defaultValue,
|
||||
resizable: config?.resizable,
|
||||
wordWrap: config?.wordWrap,
|
||||
},
|
||||
});
|
||||
|
||||
const finalPlaceholder = config?.placeholder || placeholder || "내용을 입력하세요...";
|
||||
const rows = config?.rows || 3;
|
||||
|
||||
return applyStyles(
|
||||
<Textarea
|
||||
placeholder={placeholder || "내용을 입력하세요..."}
|
||||
value={currentValue}
|
||||
placeholder={finalPlaceholder}
|
||||
value={currentValue || config?.defaultValue || ""}
|
||||
onChange={(e) => updateFormData(fieldName, e.target.value)}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
className="h-full w-full resize-none"
|
||||
rows={3}
|
||||
minLength={config?.minLength}
|
||||
maxLength={config?.maxLength}
|
||||
rows={rows}
|
||||
className={`h-full w-full ${config?.resizable === false ? "resize-none" : ""}`}
|
||||
style={{
|
||||
whiteSpace: config?.wordWrap === false ? "nowrap" : "normal",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
case "select":
|
||||
case "dropdown":
|
||||
case "dropdown": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as SelectTypeConfig | undefined;
|
||||
|
||||
console.log("📋 InteractiveScreenViewer - Select 위젯:", {
|
||||
componentId: widget.id,
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
options: config?.options,
|
||||
multiple: config?.multiple,
|
||||
searchable: config?.searchable,
|
||||
placeholder: config?.placeholder,
|
||||
defaultValue: config?.defaultValue,
|
||||
},
|
||||
});
|
||||
|
||||
const finalPlaceholder = config?.placeholder || placeholder || "선택하세요...";
|
||||
const options = config?.options || [
|
||||
{ label: "옵션 1", value: "option1" },
|
||||
{ label: "옵션 2", value: "option2" },
|
||||
{ label: "옵션 3", value: "option3" },
|
||||
];
|
||||
|
||||
return applyStyles(
|
||||
<Select
|
||||
value={currentValue}
|
||||
value={currentValue || config?.defaultValue || ""}
|
||||
onValueChange={(value) => updateFormData(fieldName, value)}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
>
|
||||
<SelectTrigger className="h-full w-full">
|
||||
<SelectValue placeholder={placeholder || "선택하세요..."} />
|
||||
<SelectValue placeholder={finalPlaceholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="option1">옵션 1</SelectItem>
|
||||
<SelectItem value="option2">옵션 2</SelectItem>
|
||||
<SelectItem value="option3">옵션 3</SelectItem>
|
||||
{options.map((option, index) => (
|
||||
<SelectItem key={index} value={option.value} disabled={option.disabled}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>,
|
||||
);
|
||||
}
|
||||
|
||||
case "checkbox":
|
||||
case "boolean":
|
||||
case "boolean": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as CheckboxTypeConfig | undefined;
|
||||
|
||||
console.log("☑️ InteractiveScreenViewer - Checkbox 위젯:", {
|
||||
componentId: widget.id,
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
defaultChecked: config?.defaultChecked,
|
||||
labelPosition: config?.labelPosition,
|
||||
checkboxText: config?.checkboxText,
|
||||
trueValue: config?.trueValue,
|
||||
falseValue: config?.falseValue,
|
||||
},
|
||||
});
|
||||
|
||||
const isChecked = currentValue === true || currentValue === "true" || config?.defaultChecked;
|
||||
const checkboxText = config?.checkboxText || label || "확인";
|
||||
const labelPosition = config?.labelPosition || "right";
|
||||
|
||||
return applyStyles(
|
||||
<div className="flex h-full w-full items-center space-x-2">
|
||||
<div
|
||||
className={`flex h-full w-full items-center space-x-2 ${labelPosition === "left" ? "flex-row-reverse" : ""}`}
|
||||
>
|
||||
<Checkbox
|
||||
id={fieldName}
|
||||
checked={currentValue === true || currentValue === "true"}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(checked) => updateFormData(fieldName, checked)}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
/>
|
||||
<label htmlFor={fieldName} className="text-sm">
|
||||
{label || "확인"}
|
||||
{checkboxText}
|
||||
</label>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
case "radio": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as RadioTypeConfig | undefined;
|
||||
|
||||
console.log("🔘 InteractiveScreenViewer - Radio 위젯:", {
|
||||
componentId: widget.id,
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
options: config?.options,
|
||||
defaultValue: config?.defaultValue,
|
||||
layout: config?.layout,
|
||||
allowNone: config?.allowNone,
|
||||
},
|
||||
});
|
||||
|
||||
const options = config?.options || [
|
||||
{ label: "옵션 1", value: "option1" },
|
||||
{ label: "옵션 2", value: "option2" },
|
||||
{ label: "옵션 3", value: "option3" },
|
||||
];
|
||||
|
||||
const layout = config?.layout || "vertical";
|
||||
const selectedValue = currentValue || config?.defaultValue || "";
|
||||
|
||||
case "radio":
|
||||
return applyStyles(
|
||||
<div className="h-full w-full space-y-2">
|
||||
{["옵션 1", "옵션 2", "옵션 3"].map((option, index) => (
|
||||
<div key={index} className="flex items-center space-x-2">
|
||||
<div className={`h-full w-full ${layout === "horizontal" ? "flex flex-wrap gap-4" : "space-y-2"}`}>
|
||||
{config?.allowNone && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id={`${fieldName}_${index}`}
|
||||
id={`${fieldName}_none`}
|
||||
name={fieldName}
|
||||
value={option}
|
||||
checked={currentValue === option}
|
||||
value=""
|
||||
checked={selectedValue === ""}
|
||||
onChange={(e) => updateFormData(fieldName, e.target.value)}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<label htmlFor={`${fieldName}_none`} className="text-sm">
|
||||
선택 안함
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{options.map((option, index) => (
|
||||
<div key={index} className="flex items-center space-x-2">
|
||||
<input
|
||||
type="radio"
|
||||
id={`${fieldName}_${index}`}
|
||||
name={fieldName}
|
||||
value={option.value}
|
||||
checked={selectedValue === option.value}
|
||||
onChange={(e) => updateFormData(fieldName, e.target.value)}
|
||||
disabled={readonly || option.disabled}
|
||||
required={required}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<label htmlFor={`${fieldName}_${index}`} className="text-sm">
|
||||
{option}
|
||||
{option.label}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
case "date":
|
||||
const dateValue = dateValues[fieldName];
|
||||
return applyStyles(
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-full w-full justify-start text-left font-normal"
|
||||
disabled={readonly}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{dateValue ? format(dateValue, "PPP", { locale: ko }) : placeholder || "날짜를 선택하세요"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dateValue}
|
||||
onSelect={(date) => updateDateValue(fieldName, date)}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>,
|
||||
);
|
||||
case "date": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as DateTypeConfig | undefined;
|
||||
|
||||
console.log("📅 InteractiveScreenViewer - Date 위젯:", {
|
||||
componentId: widget.id,
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
format: config?.format,
|
||||
showTime: config?.showTime,
|
||||
defaultValue: config?.defaultValue,
|
||||
minDate: config?.minDate,
|
||||
maxDate: config?.maxDate,
|
||||
},
|
||||
});
|
||||
|
||||
const shouldShowTime = config?.showTime || config?.format?.includes("HH:mm");
|
||||
const finalPlaceholder = config?.placeholder || placeholder || "날짜를 선택하세요";
|
||||
|
||||
if (shouldShowTime) {
|
||||
// 시간 포함 날짜 입력
|
||||
return applyStyles(
|
||||
<Input
|
||||
type="datetime-local"
|
||||
placeholder={finalPlaceholder}
|
||||
value={currentValue || config?.defaultValue || ""}
|
||||
onChange={(e) => updateFormData(fieldName, e.target.value)}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
min={config?.minDate}
|
||||
max={config?.maxDate}
|
||||
className="h-full w-full"
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
// 날짜만 입력
|
||||
const dateValue = dateValues[fieldName];
|
||||
return applyStyles(
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-full w-full justify-start text-left font-normal"
|
||||
disabled={readonly}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{dateValue ? format(dateValue, "PPP", { locale: ko }) : config?.defaultValue || finalPlaceholder}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dateValue}
|
||||
onSelect={(date) => updateDateValue(fieldName, date)}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
case "datetime": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as DateTypeConfig | undefined;
|
||||
|
||||
console.log("🕐 InteractiveScreenViewer - DateTime 위젯:", {
|
||||
componentId: widget.id,
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
format: config?.format,
|
||||
defaultValue: config?.defaultValue,
|
||||
minDate: config?.minDate,
|
||||
maxDate: config?.maxDate,
|
||||
},
|
||||
});
|
||||
|
||||
const finalPlaceholder = config?.placeholder || placeholder || "날짜와 시간을 입력하세요...";
|
||||
|
||||
case "datetime":
|
||||
return applyStyles(
|
||||
<Input
|
||||
type="datetime-local"
|
||||
placeholder={placeholder || "날짜와 시간을 입력하세요..."}
|
||||
value={currentValue}
|
||||
placeholder={finalPlaceholder}
|
||||
value={currentValue || config?.defaultValue || ""}
|
||||
onChange={(e) => updateFormData(fieldName, e.target.value)}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
min={config?.minDate}
|
||||
max={config?.maxDate}
|
||||
className="h-full w-full"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
case "file": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as FileTypeConfig | undefined;
|
||||
|
||||
console.log("📁 InteractiveScreenViewer - File 위젯:", {
|
||||
componentId: widget.id,
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
accept: config?.accept,
|
||||
multiple: config?.multiple,
|
||||
maxSize: config?.maxSize,
|
||||
},
|
||||
});
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (!files) return;
|
||||
|
||||
// 파일 크기 검증
|
||||
if (config?.maxSize) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (file.size > config.maxSize * 1024 * 1024) {
|
||||
alert(`파일 크기가 ${config.maxSize}MB를 초과합니다: ${file.name}`);
|
||||
e.target.value = "";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const file = config?.multiple ? files : files[0];
|
||||
updateFormData(fieldName, file);
|
||||
};
|
||||
|
||||
case "file":
|
||||
return applyStyles(
|
||||
<Input
|
||||
type="file"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
updateFormData(fieldName, file);
|
||||
}}
|
||||
onChange={handleFileChange}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
multiple={config?.multiple}
|
||||
accept={config?.accept}
|
||||
className="h-full w-full"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
case "code": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as CodeTypeConfig | undefined;
|
||||
|
||||
console.log("💻 InteractiveScreenViewer - Code 위젯:", {
|
||||
componentId: widget.id,
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
language: config?.language,
|
||||
theme: config?.theme,
|
||||
fontSize: config?.fontSize,
|
||||
defaultValue: config?.defaultValue,
|
||||
wordWrap: config?.wordWrap,
|
||||
tabSize: config?.tabSize,
|
||||
},
|
||||
});
|
||||
|
||||
const finalPlaceholder = config?.placeholder || "코드를 입력하세요...";
|
||||
const rows = config?.rows || 4;
|
||||
|
||||
case "code":
|
||||
return applyStyles(
|
||||
<Textarea
|
||||
placeholder="코드를 입력하세요..."
|
||||
value={currentValue}
|
||||
placeholder={finalPlaceholder}
|
||||
value={currentValue || config?.defaultValue || ""}
|
||||
onChange={(e) => updateFormData(fieldName, e.target.value)}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
rows={rows}
|
||||
className="h-full w-full resize-none font-mono text-sm"
|
||||
rows={4}
|
||||
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,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
case "entity": {
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as EntityTypeConfig | undefined;
|
||||
|
||||
console.log("🏢 InteractiveScreenViewer - Entity 위젯:", {
|
||||
componentId: widget.id,
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
entityName: config?.entityName,
|
||||
displayField: config?.displayField,
|
||||
valueField: config?.valueField,
|
||||
multiple: config?.multiple,
|
||||
defaultValue: config?.defaultValue,
|
||||
},
|
||||
});
|
||||
|
||||
const finalPlaceholder = config?.placeholder || "엔티티를 선택하세요...";
|
||||
const defaultOptions = [
|
||||
{ label: "사용자", value: "user" },
|
||||
{ label: "제품", value: "product" },
|
||||
{ label: "주문", value: "order" },
|
||||
{ label: "카테고리", value: "category" },
|
||||
];
|
||||
|
||||
case "entity":
|
||||
return applyStyles(
|
||||
<Select
|
||||
value={currentValue}
|
||||
value={currentValue || config?.defaultValue || ""}
|
||||
onValueChange={(value) => updateFormData(fieldName, value)}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
>
|
||||
<SelectTrigger className="h-full w-full">
|
||||
<SelectValue placeholder="엔티티를 선택하세요..." />
|
||||
<SelectValue placeholder={finalPlaceholder} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="user">사용자</SelectItem>
|
||||
<SelectItem value="product">제품</SelectItem>
|
||||
<SelectItem value="order">주문</SelectItem>
|
||||
{defaultOptions.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{config?.displayFormat
|
||||
? config.displayFormat.replace("{label}", option.label).replace("{value}", option.value)
|
||||
: option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>,
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return applyStyles(
|
||||
|
|
|
|||
|
|
@ -80,29 +80,69 @@ const renderWidget = (component: ComponentData) => {
|
|||
|
||||
const inputType = widgetType === "email" ? "email" : widgetType === "tel" ? "tel" : "text";
|
||||
|
||||
// 형식별 패턴 생성
|
||||
const getPatternByFormat = (format: string) => {
|
||||
switch (format) {
|
||||
case "korean":
|
||||
return "[가-힣\\s]*";
|
||||
case "english":
|
||||
return "[a-zA-Z\\s]*";
|
||||
case "alphanumeric":
|
||||
return "[a-zA-Z0-9]*";
|
||||
case "numeric":
|
||||
return "[0-9]*";
|
||||
case "email":
|
||||
return "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}";
|
||||
case "phone":
|
||||
return "\\d{3}-\\d{4}-\\d{4}";
|
||||
case "url":
|
||||
return "https?://[\\w\\-]+(\\.[\\w\\-]+)+([\\w\\-\\.,@?^=%&:/~\\+#]*[\\w\\-\\@?^=%&/~\\+#])?";
|
||||
default:
|
||||
return config?.pattern || undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// 입력 검증 함수
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const value = e.target.value;
|
||||
|
||||
// 형식별 실시간 검증
|
||||
if (config?.format && config.format !== "none") {
|
||||
const pattern = getPatternByFormat(config.format);
|
||||
if (pattern) {
|
||||
const regex = new RegExp(`^${pattern}$`);
|
||||
if (value && !regex.test(value)) {
|
||||
// 유효하지 않은 입력은 무시
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 길이 제한 검증
|
||||
if (config?.maxLength && value.length > config.maxLength) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const inputProps = {
|
||||
...commonProps,
|
||||
placeholder: finalPlaceholder,
|
||||
minLength: config?.minLength,
|
||||
maxLength: config?.maxLength,
|
||||
pattern: getPatternByFormat(config?.format || "none"),
|
||||
onInput: handleInputChange,
|
||||
onChange: () => {}, // 읽기 전용으로 처리
|
||||
readOnly: true,
|
||||
};
|
||||
|
||||
// multiline이면 Textarea로 렌더링
|
||||
if (config?.multiline) {
|
||||
return (
|
||||
<Textarea
|
||||
{...commonProps}
|
||||
placeholder={finalPlaceholder}
|
||||
minLength={config?.minLength}
|
||||
maxLength={config?.maxLength}
|
||||
pattern={config?.pattern}
|
||||
/>
|
||||
);
|
||||
return <Textarea {...inputProps} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
type={inputType}
|
||||
{...commonProps}
|
||||
placeholder={finalPlaceholder}
|
||||
minLength={config?.minLength}
|
||||
maxLength={config?.maxLength}
|
||||
pattern={config?.pattern}
|
||||
/>
|
||||
);
|
||||
return <Input type={inputType} {...inputProps} />;
|
||||
}
|
||||
|
||||
case "number":
|
||||
|
|
@ -124,6 +164,31 @@ const renderWidget = (component: ComponentData) => {
|
|||
// 플레이스홀더 처리
|
||||
const finalPlaceholder = config?.placeholder || placeholder || "숫자를 입력하세요";
|
||||
|
||||
// 형식에 따른 표시값 처리
|
||||
const formatValue = (value: string) => {
|
||||
if (!value || !config) return value;
|
||||
|
||||
const num = parseFloat(value);
|
||||
if (isNaN(num)) return value;
|
||||
|
||||
switch (config.format) {
|
||||
case "currency":
|
||||
return new Intl.NumberFormat("ko-KR", {
|
||||
style: "currency",
|
||||
currency: "KRW",
|
||||
}).format(num);
|
||||
case "percentage":
|
||||
return `${num}%`;
|
||||
case "decimal":
|
||||
return num.toFixed(config.decimalPlaces || 2);
|
||||
default:
|
||||
if (config.thousandSeparator) {
|
||||
return new Intl.NumberFormat("ko-KR").format(num);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
// 접두사/접미사가 있는 경우 표시용 컨테이너 사용
|
||||
if (config?.prefix || config?.suffix) {
|
||||
return (
|
||||
|
|
@ -141,6 +206,8 @@ const renderWidget = (component: ComponentData) => {
|
|||
{...commonProps}
|
||||
placeholder={finalPlaceholder}
|
||||
className={`${config?.prefix ? "rounded-l-none" : ""} ${config?.suffix ? "rounded-r-none" : ""} ${borderClass}`}
|
||||
onChange={() => {}} // 읽기 전용으로 처리
|
||||
readOnly
|
||||
/>
|
||||
{config.suffix && (
|
||||
<span className="rounded-r border border-l-0 bg-gray-50 px-2 py-2 text-sm text-gray-600">
|
||||
|
|
@ -159,6 +226,8 @@ const renderWidget = (component: ComponentData) => {
|
|||
max={config?.max}
|
||||
{...commonProps}
|
||||
placeholder={finalPlaceholder}
|
||||
onChange={() => {}} // 읽기 전용으로 처리
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -288,6 +357,8 @@ const renderWidget = (component: ComponentData) => {
|
|||
disabled={readonly}
|
||||
required={required}
|
||||
multiple={config?.multiple}
|
||||
value={config?.defaultValue || ""}
|
||||
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"
|
||||
}`}
|
||||
|
|
@ -314,6 +385,9 @@ const renderWidget = (component: ComponentData) => {
|
|||
placeholder={config?.placeholder || placeholder || "텍스트를 입력하세요"}
|
||||
minLength={config?.minLength}
|
||||
maxLength={config?.maxLength}
|
||||
value={config?.defaultValue || ""}
|
||||
onChange={() => {}} // 읽기 전용으로 처리
|
||||
readOnly
|
||||
style={{
|
||||
resize: config?.resizable === false ? "none" : "vertical",
|
||||
whiteSpace: config?.wordWrap === false ? "nowrap" : "normal",
|
||||
|
|
@ -342,7 +416,9 @@ const renderWidget = (component: ComponentData) => {
|
|||
id={`checkbox-${component.id}`}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
defaultChecked={config?.defaultChecked}
|
||||
checked={config?.defaultChecked || false}
|
||||
onChange={() => {}} // 읽기 전용으로 처리
|
||||
readOnly
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
{!isLeftLabel && (
|
||||
|
|
@ -381,7 +457,9 @@ const renderWidget = (component: ComponentData) => {
|
|||
value={option.value}
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
defaultChecked={config?.defaultValue === option.value}
|
||||
checked={config?.defaultValue === option.value}
|
||||
onChange={() => {}} // 읽기 전용으로 처리
|
||||
readOnly
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Label htmlFor={`radio-${component.id}-${index}`} className="text-sm">
|
||||
|
|
@ -397,18 +475,35 @@ const renderWidget = (component: ComponentData) => {
|
|||
const widget = component as WidgetComponent;
|
||||
const config = widget.webTypeConfig as CodeTypeConfig | undefined;
|
||||
|
||||
console.log("💻 코드 위젯 렌더링:", {
|
||||
componentId: widget.id,
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
language: config?.language,
|
||||
theme: config?.theme,
|
||||
fontSize: config?.fontSize,
|
||||
defaultValue: config?.defaultValue,
|
||||
readOnly: config?.readOnly,
|
||||
wordWrap: config?.wordWrap,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
{...commonProps}
|
||||
rows={4}
|
||||
rows={config?.rows || 4}
|
||||
className={`w-full font-mono text-sm ${borderClass}`}
|
||||
placeholder={config?.placeholder || "코드를 입력하세요..."}
|
||||
readOnly={config?.readOnly}
|
||||
value={config?.defaultValue || ""}
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
@ -418,19 +513,48 @@ const renderWidget = (component: ComponentData) => {
|
|||
const widget = component as WidgetComponent;
|
||||
const config = widget.webTypeConfig as EntityTypeConfig | undefined;
|
||||
|
||||
console.log("🏢 엔티티 위젯 렌더링:", {
|
||||
componentId: widget.id,
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
entityName: config?.entityName,
|
||||
displayField: config?.displayField,
|
||||
valueField: config?.valueField,
|
||||
multiple: config?.multiple,
|
||||
searchable: config?.searchable,
|
||||
allowClear: config?.allowClear,
|
||||
maxSelections: config?.maxSelections,
|
||||
},
|
||||
});
|
||||
|
||||
// 기본 옵션들 (실제로는 API에서 가져와야 함)
|
||||
const defaultOptions = [
|
||||
{ label: "사용자", value: "user" },
|
||||
{ label: "제품", value: "product" },
|
||||
{ label: "주문", value: "order" },
|
||||
{ label: "카테고리", value: "category" },
|
||||
];
|
||||
|
||||
return (
|
||||
<select
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
multiple={config?.multiple}
|
||||
value={config?.defaultValue || ""}
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
<option value="">{config?.placeholder || "엔티티를 선택하세요..."}</option>
|
||||
<option value="user">사용자</option>
|
||||
<option value="product">제품</option>
|
||||
<option value="order">주문</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>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
|
@ -439,17 +563,51 @@ const renderWidget = (component: ComponentData) => {
|
|||
const widget = component as WidgetComponent;
|
||||
const config = widget.webTypeConfig as FileTypeConfig | undefined;
|
||||
|
||||
console.log("📁 파일 위젯 렌더링:", {
|
||||
componentId: widget.id,
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
appliedSettings: {
|
||||
accept: config?.accept,
|
||||
multiple: config?.multiple,
|
||||
maxSize: config?.maxSize,
|
||||
preview: config?.preview,
|
||||
allowedTypes: config?.allowedTypes,
|
||||
},
|
||||
});
|
||||
|
||||
// 파일 크기 제한 검증
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (!files || !config?.maxSize) return;
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (file.size > config.maxSize * 1024 * 1024) {
|
||||
// MB to bytes
|
||||
alert(`파일 크기가 ${config.maxSize}MB를 초과합니다: ${file.name}`);
|
||||
e.target.value = ""; // 파일 선택 초기화
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
type="file"
|
||||
disabled={readonly}
|
||||
required={required}
|
||||
multiple={config?.multiple}
|
||||
accept={config?.accept}
|
||||
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"
|
||||
}`}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,15 +64,42 @@ export const CheckboxTypeConfigPanel: React.FC<CheckboxTypeConfigPanelProps> = (
|
|||
setLocalValues((prev) => ({ ...prev, [key]: value }));
|
||||
}
|
||||
|
||||
// 실제 config 업데이트
|
||||
const newConfig = { ...safeConfig, [key]: value };
|
||||
// 실제 config 업데이트 - 현재 로컬 상태를 기반으로 새 객체 생성 (safeConfig 기본값 덮어쓰기 방지)
|
||||
const currentValues = {
|
||||
defaultChecked: key === "defaultChecked" ? value : localValues.defaultChecked,
|
||||
labelPosition: key === "labelPosition" ? value : localValues.labelPosition,
|
||||
checkboxText: key === "checkboxText" ? value : localValues.checkboxText,
|
||||
trueValue:
|
||||
key === "trueValue"
|
||||
? value
|
||||
: localValues.trueValue === "true"
|
||||
? true
|
||||
: localValues.trueValue === "false"
|
||||
? false
|
||||
: localValues.trueValue,
|
||||
falseValue:
|
||||
key === "falseValue"
|
||||
? value
|
||||
: localValues.falseValue === "true"
|
||||
? true
|
||||
: localValues.falseValue === "false"
|
||||
? false
|
||||
: localValues.falseValue,
|
||||
indeterminate: key === "indeterminate" ? value : localValues.indeterminate,
|
||||
};
|
||||
|
||||
const newConfig = JSON.parse(JSON.stringify(currentValues));
|
||||
console.log("☑️ CheckboxTypeConfig 업데이트:", {
|
||||
key,
|
||||
value,
|
||||
oldConfig: safeConfig,
|
||||
newConfig,
|
||||
localValues,
|
||||
});
|
||||
onConfigChange(newConfig);
|
||||
|
||||
setTimeout(() => {
|
||||
onConfigChange(newConfig);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -73,13 +73,27 @@ export const NumberTypeConfigPanel: React.FC<NumberTypeConfigPanelProps> = ({ co
|
|||
setLocalValues((prev) => ({ ...prev, [key]: value }));
|
||||
}
|
||||
|
||||
// 실제 config 업데이트 - 깊은 복사로 새 객체 보장
|
||||
const newConfig = JSON.parse(JSON.stringify({ ...safeConfig, [key]: value }));
|
||||
// 실제 config 업데이트 - 현재 로컬 상태를 기반으로 새 객체 생성 (safeConfig 기본값 덮어쓰기 방지)
|
||||
const currentValues = {
|
||||
format: key === "format" ? value : localValues.format,
|
||||
min: key === "min" ? value : localValues.min ? Number(localValues.min) : undefined,
|
||||
max: key === "max" ? value : localValues.max ? Number(localValues.max) : undefined,
|
||||
step: key === "step" ? value : localValues.step ? Number(localValues.step) : undefined,
|
||||
decimalPlaces:
|
||||
key === "decimalPlaces" ? value : localValues.decimalPlaces ? Number(localValues.decimalPlaces) : undefined,
|
||||
thousandSeparator: key === "thousandSeparator" ? value : localValues.thousandSeparator,
|
||||
prefix: key === "prefix" ? value : localValues.prefix,
|
||||
suffix: key === "suffix" ? value : localValues.suffix,
|
||||
placeholder: key === "placeholder" ? value : localValues.placeholder,
|
||||
};
|
||||
|
||||
const newConfig = JSON.parse(JSON.stringify(currentValues));
|
||||
console.log("🔢 NumberTypeConfig 업데이트:", {
|
||||
key,
|
||||
value,
|
||||
oldConfig: safeConfig,
|
||||
newConfig,
|
||||
localValues,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -62,15 +62,28 @@ export const TextTypeConfigPanel: React.FC<TextTypeConfigPanelProps> = ({ config
|
|||
setLocalValues((prev) => ({ ...prev, [key]: value }));
|
||||
}
|
||||
|
||||
// 실제 config 업데이트
|
||||
const newConfig = { ...safeConfig, [key]: value };
|
||||
// 실제 config 업데이트 - 현재 로컬 상태를 기반으로 새 객체 생성 (safeConfig 기본값 덮어쓰기 방지)
|
||||
const currentValues = {
|
||||
minLength: key === "minLength" ? value : localValues.minLength ? Number(localValues.minLength) : undefined,
|
||||
maxLength: key === "maxLength" ? value : localValues.maxLength ? Number(localValues.maxLength) : undefined,
|
||||
pattern: key === "pattern" ? value : localValues.pattern,
|
||||
format: key === "format" ? value : localValues.format,
|
||||
placeholder: key === "placeholder" ? value : localValues.placeholder,
|
||||
multiline: key === "multiline" ? value : localValues.multiline,
|
||||
};
|
||||
|
||||
const newConfig = JSON.parse(JSON.stringify(currentValues));
|
||||
console.log("📝 TextTypeConfig 업데이트:", {
|
||||
key,
|
||||
value,
|
||||
oldConfig: safeConfig,
|
||||
newConfig,
|
||||
localValues,
|
||||
});
|
||||
onConfigChange(newConfig);
|
||||
|
||||
setTimeout(() => {
|
||||
onConfigChange(newConfig);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -65,15 +65,30 @@ export const TextareaTypeConfigPanel: React.FC<TextareaTypeConfigPanelProps> = (
|
|||
setLocalValues((prev) => ({ ...prev, [key]: value }));
|
||||
}
|
||||
|
||||
// 실제 config 업데이트
|
||||
const newConfig = { ...safeConfig, [key]: value };
|
||||
// 실제 config 업데이트 - 현재 로컬 상태를 기반으로 새 객체 생성 (safeConfig 기본값 덮어쓰기 방지)
|
||||
const currentValues = {
|
||||
rows: key === "rows" ? value : localValues.rows ? Number(localValues.rows) : undefined,
|
||||
maxLength: key === "maxLength" ? value : localValues.maxLength ? Number(localValues.maxLength) : undefined,
|
||||
minLength: key === "minLength" ? value : localValues.minLength ? Number(localValues.minLength) : undefined,
|
||||
placeholder: key === "placeholder" ? value : localValues.placeholder,
|
||||
defaultValue: key === "defaultValue" ? value : localValues.defaultValue,
|
||||
resizable: key === "resizable" ? value : localValues.resizable,
|
||||
autoResize: key === "autoResize" ? value : localValues.autoResize,
|
||||
wordWrap: key === "wordWrap" ? value : localValues.wordWrap,
|
||||
};
|
||||
|
||||
const newConfig = JSON.parse(JSON.stringify(currentValues));
|
||||
console.log("📄 TextareaTypeConfig 업데이트:", {
|
||||
key,
|
||||
value,
|
||||
oldConfig: safeConfig,
|
||||
newConfig,
|
||||
localValues,
|
||||
});
|
||||
onConfigChange(newConfig);
|
||||
|
||||
setTimeout(() => {
|
||||
onConfigChange(newConfig);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
Loading…
Reference in New Issue