실제 화면에 상세설정 적용

This commit is contained in:
kjs
2025-09-03 11:50:42 +09:00
parent f82d18575e
commit f2bdf5356a
6 changed files with 721 additions and 120 deletions

View File

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