추가모달 상세설정 구현
This commit is contained in:
@@ -84,6 +84,12 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
|
||||
// 필터별 로컬 입력 상태
|
||||
const [localFilterInputs, setLocalFilterInputs] = useState<Record<string, string>>({});
|
||||
|
||||
// 컬럼별 상세 설정 상태
|
||||
const [localColumnDetailSettings, setLocalColumnDetailSettings] = useState<Record<string, any>>({});
|
||||
|
||||
// 컬럼별 상세 설정 확장/축소 상태
|
||||
const [isColumnDetailOpen, setIsColumnDetailOpen] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 모달 설정 확장/축소 상태
|
||||
const [isModalConfigOpen, setIsModalConfigOpen] = useState<Record<string, boolean>>({});
|
||||
|
||||
@@ -379,6 +385,347 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
|
||||
[component.columns, onUpdateComponent],
|
||||
);
|
||||
|
||||
// 컬럼 상세 설정 업데이트 (테이블 타입 관리에도 반영)
|
||||
const updateColumnDetailSettings = useCallback(
|
||||
async (columnId: string, webTypeConfig: any) => {
|
||||
// 1. 먼저 화면 컴포넌트의 컬럼 설정 업데이트
|
||||
const updatedColumns = component.columns.map((col) => (col.id === columnId ? { ...col, webTypeConfig } : col));
|
||||
console.log("🔄 컬럼 상세 설정 업데이트:", { columnId, webTypeConfig, updatedColumns });
|
||||
onUpdateComponent({ columns: updatedColumns });
|
||||
|
||||
// 2. 테이블 타입 관리에도 반영 (라디오 타입인 경우)
|
||||
const targetColumn = component.columns.find((col) => col.id === columnId);
|
||||
if (targetColumn && targetColumn.widgetType === "radio" && selectedTable) {
|
||||
try {
|
||||
// TODO: 테이블 타입 관리 API 호출하여 웹 타입과 상세 설정 업데이트
|
||||
console.log("📡 테이블 타입 관리 업데이트 필요:", {
|
||||
tableName: component.tableName,
|
||||
columnName: targetColumn.columnName,
|
||||
webType: "radio",
|
||||
detailSettings: JSON.stringify(webTypeConfig),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("테이블 타입 관리 업데이트 실패:", error);
|
||||
}
|
||||
}
|
||||
},
|
||||
[component.columns, component.tableName, selectedTable, onUpdateComponent],
|
||||
);
|
||||
|
||||
// 컬럼의 현재 웹 타입 가져오기 (테이블 타입 관리에서 설정된 값)
|
||||
const getColumnWebType = useCallback(
|
||||
(column: DataTableColumn) => {
|
||||
// 테이블 타입 관리에서 설정된 웹 타입 찾기
|
||||
if (!selectedTable) return "text";
|
||||
|
||||
const tableColumn = selectedTable.columns.find((col) => col.columnName === column.columnName);
|
||||
return (
|
||||
tableColumn?.webType ||
|
||||
getWidgetTypeFromColumn(tableColumn || { columnName: column.columnName, dataType: "text" })
|
||||
);
|
||||
},
|
||||
[selectedTable],
|
||||
);
|
||||
|
||||
// 컬럼의 현재 상세 설정 가져오기
|
||||
const getColumnCurrentDetailSettings = useCallback((column: DataTableColumn) => {
|
||||
return column.webTypeConfig || {};
|
||||
}, []);
|
||||
|
||||
// 웹 타입별 상세 설정 렌더링
|
||||
const renderColumnDetailSettings = useCallback(
|
||||
(column: DataTableColumn) => {
|
||||
const webType = getColumnWebType(column);
|
||||
const currentSettings = getColumnCurrentDetailSettings(column);
|
||||
const localSettings = localColumnDetailSettings[column.id] || currentSettings;
|
||||
|
||||
const updateSettings = (newSettings: any) => {
|
||||
const merged = { ...localSettings, ...newSettings };
|
||||
setLocalColumnDetailSettings((prev) => ({
|
||||
...prev,
|
||||
[column.id]: merged,
|
||||
}));
|
||||
updateColumnDetailSettings(column.id, merged);
|
||||
};
|
||||
|
||||
switch (webType) {
|
||||
case "select":
|
||||
case "dropdown":
|
||||
case "radio":
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">옵션 목록</Label>
|
||||
<div className="space-y-2">
|
||||
{(localSettings.options || []).map((option: any, index: number) => {
|
||||
// 안전한 값 추출
|
||||
const currentLabel =
|
||||
typeof option === "object" && option !== null
|
||||
? option.label || option.value || ""
|
||||
: String(option || "");
|
||||
|
||||
return (
|
||||
<div key={index} className="flex items-center space-x-2">
|
||||
<Input
|
||||
value={currentLabel}
|
||||
onChange={(e) => {
|
||||
const newOptions = [...(localSettings.options || [])];
|
||||
newOptions[index] = { label: e.target.value, value: e.target.value };
|
||||
updateSettings({ options: newOptions });
|
||||
}}
|
||||
placeholder="옵션명"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newOptions = (localSettings.options || []).filter((_: any, i: number) => i !== index);
|
||||
updateSettings({ options: newOptions });
|
||||
}}
|
||||
className="h-7 w-7 p-0"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newOption = { label: "", value: "" };
|
||||
updateSettings({ options: [...(localSettings.options || []), newOption] });
|
||||
}}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
옵션 추가
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{webType === "radio" ? (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">기본값 설정</Label>
|
||||
<Select
|
||||
value={localSettings.defaultValue || "__NONE__"}
|
||||
onValueChange={(value) => updateSettings({ defaultValue: value === "__NONE__" ? "" : value })}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs">
|
||||
<SelectValue placeholder="기본값 선택..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__NONE__">선택 안함</SelectItem>
|
||||
{(localSettings.options || []).map((option: any, index: number) => {
|
||||
// 안전한 문자열 변환
|
||||
const getStringValue = (val: any): string => {
|
||||
if (typeof val === "string") return val;
|
||||
if (typeof val === "number") return String(val);
|
||||
if (typeof val === "object" && val !== null) {
|
||||
return val.label || val.value || val.name || JSON.stringify(val);
|
||||
}
|
||||
return String(val || "");
|
||||
};
|
||||
|
||||
const optionValue = getStringValue(option.value || option.label || option) || `option-${index}`;
|
||||
const optionLabel =
|
||||
getStringValue(option.label || option.value || option) || `옵션 ${index + 1}`;
|
||||
|
||||
return (
|
||||
<SelectItem key={index} value={optionValue}>
|
||||
{optionLabel}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={localSettings.multiple || false}
|
||||
onCheckedChange={(checked) => updateSettings({ multiple: checked })}
|
||||
/>
|
||||
<Label className="text-xs">다중 선택 허용</Label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case "number":
|
||||
case "decimal":
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">최소값</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={localSettings.min || ""}
|
||||
onChange={(e) => updateSettings({ min: e.target.value ? Number(e.target.value) : undefined })}
|
||||
placeholder="최소값"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">최대값</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={localSettings.max || ""}
|
||||
onChange={(e) => updateSettings({ max: e.target.value ? Number(e.target.value) : undefined })}
|
||||
placeholder="최대값"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{webType === "decimal" && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">단계 (step)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={localSettings.step || "0.01"}
|
||||
onChange={(e) => updateSettings({ step: e.target.value })}
|
||||
placeholder="0.01"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case "date":
|
||||
case "datetime":
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">최소 날짜</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={localSettings.minDate || ""}
|
||||
onChange={(e) => updateSettings({ minDate: e.target.value })}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">최대 날짜</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={localSettings.maxDate || ""}
|
||||
onChange={(e) => updateSettings({ maxDate: e.target.value })}
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{webType === "datetime" && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={localSettings.showSeconds || false}
|
||||
onCheckedChange={(checked) => updateSettings({ showSeconds: checked })}
|
||||
/>
|
||||
<Label className="text-xs">초 단위까지 표시</Label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case "text":
|
||||
case "email":
|
||||
case "tel":
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">최대 길이</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={localSettings.maxLength || ""}
|
||||
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
|
||||
placeholder="최대 문자 수"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">플레이스홀더</Label>
|
||||
<Input
|
||||
value={localSettings.placeholder || ""}
|
||||
onChange={(e) => updateSettings({ placeholder: e.target.value })}
|
||||
placeholder="입력 안내 텍스트"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "textarea":
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">행 수</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={localSettings.rows || "3"}
|
||||
onChange={(e) => updateSettings({ rows: Number(e.target.value) })}
|
||||
placeholder="3"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">최대 길이</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={localSettings.maxLength || ""}
|
||||
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
|
||||
placeholder="최대 문자 수"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "file":
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">허용 파일 형식</Label>
|
||||
<Input
|
||||
value={localSettings.accept || ""}
|
||||
onChange={(e) => updateSettings({ accept: e.target.value })}
|
||||
placeholder=".jpg,.png,.pdf"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">최대 파일 크기 (MB)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={localSettings.maxSize ? localSettings.maxSize / 1024 / 1024 : "10"}
|
||||
onChange={(e) => updateSettings({ maxSize: Number(e.target.value) * 1024 * 1024 })}
|
||||
placeholder="10"
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={localSettings.multiple || false}
|
||||
onCheckedChange={(checked) => updateSettings({ multiple: checked })}
|
||||
/>
|
||||
<Label className="text-xs">다중 파일 허용</Label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return <div className="py-2 text-xs text-gray-500">이 웹 타입({webType})에 대한 상세 설정이 없습니다.</div>;
|
||||
}
|
||||
},
|
||||
[getColumnWebType, getColumnCurrentDetailSettings, localColumnDetailSettings, updateColumnDetailSettings],
|
||||
);
|
||||
|
||||
// 컬럼 삭제
|
||||
const removeColumn = useCallback(
|
||||
(columnId: string) => {
|
||||
@@ -1060,19 +1407,39 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{column.columnName}
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{getColumnWebType(column)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsColumnDetailOpen((prev) => ({
|
||||
...prev,
|
||||
[column.id]: !prev[column.id],
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<Settings className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
removeColumn(column.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
removeColumn(column.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
@@ -1162,6 +1529,19 @@ export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ comp
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 웹 타입 상세 설정 */}
|
||||
{isColumnDetailOpen[column.id] && (
|
||||
<div className="border-t pt-2">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Label className="text-xs font-medium">웹 타입 상세 설정</Label>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{getColumnWebType(column)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="pl-2">{renderColumnDetailSettings(column)}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 모달 전용 설정 */}
|
||||
{component.enableAdd && (
|
||||
<div className="border-t pt-2">
|
||||
|
||||
Reference in New Issue
Block a user