2025-09-09 14:29:04 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import { Switch } from "@/components/ui/switch";
|
|
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
|
|
|
import { Database, Search, Plus, Trash2 } from "lucide-react";
|
|
|
|
|
import { WebTypeConfigPanelProps } from "@/lib/registry/types";
|
|
|
|
|
import { WidgetComponent, EntityTypeConfig } from "@/types/screen";
|
|
|
|
|
|
|
|
|
|
interface EntityField {
|
|
|
|
|
name: string;
|
|
|
|
|
label: string;
|
|
|
|
|
type: string;
|
|
|
|
|
visible: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|
|
|
|
component,
|
|
|
|
|
onUpdateComponent,
|
|
|
|
|
onUpdateProperty,
|
|
|
|
|
}) => {
|
|
|
|
|
const widget = component as WidgetComponent;
|
|
|
|
|
const config = (widget.webTypeConfig as EntityTypeConfig) || {};
|
|
|
|
|
|
|
|
|
|
// 로컬 상태
|
|
|
|
|
const [localConfig, setLocalConfig] = useState<EntityTypeConfig>({
|
|
|
|
|
entityType: config.entityType || "",
|
|
|
|
|
displayFields: config.displayFields || [],
|
|
|
|
|
searchFields: config.searchFields || [],
|
|
|
|
|
valueField: config.valueField || "id",
|
|
|
|
|
labelField: config.labelField || "name",
|
|
|
|
|
multiple: config.multiple || false,
|
|
|
|
|
searchable: config.searchable !== false, // 기본값 true
|
|
|
|
|
placeholder: config.placeholder || "엔티티를 선택하세요",
|
|
|
|
|
emptyMessage: config.emptyMessage || "검색 결과가 없습니다",
|
|
|
|
|
pageSize: config.pageSize || 20,
|
|
|
|
|
minSearchLength: config.minSearchLength || 1,
|
|
|
|
|
defaultValue: config.defaultValue || "",
|
|
|
|
|
required: config.required || false,
|
|
|
|
|
readonly: config.readonly || false,
|
|
|
|
|
apiEndpoint: config.apiEndpoint || "",
|
|
|
|
|
filters: config.filters || {},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 새 필드 추가용 상태
|
|
|
|
|
const [newFieldName, setNewFieldName] = useState("");
|
|
|
|
|
const [newFieldLabel, setNewFieldLabel] = useState("");
|
|
|
|
|
const [newFieldType, setNewFieldType] = useState("string");
|
|
|
|
|
|
2025-11-26 14:44:49 +09:00
|
|
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
2025-09-09 14:29:04 +09:00
|
|
|
useEffect(() => {
|
2025-11-26 14:44:49 +09:00
|
|
|
const currentConfig = (widget.webTypeConfig as EntityTypeConfig) || {};
|
|
|
|
|
setLocalConfig({
|
|
|
|
|
entityType: currentConfig.entityType || "",
|
|
|
|
|
displayFields: currentConfig.displayFields || [],
|
|
|
|
|
searchFields: currentConfig.searchFields || [],
|
|
|
|
|
valueField: currentConfig.valueField || "id",
|
|
|
|
|
labelField: currentConfig.labelField || "name",
|
|
|
|
|
multiple: currentConfig.multiple || false,
|
|
|
|
|
searchable: currentConfig.searchable !== false,
|
|
|
|
|
placeholder: currentConfig.placeholder || "엔티티를 선택하세요",
|
|
|
|
|
emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다",
|
|
|
|
|
pageSize: currentConfig.pageSize || 20,
|
|
|
|
|
minSearchLength: currentConfig.minSearchLength || 1,
|
|
|
|
|
defaultValue: currentConfig.defaultValue || "",
|
|
|
|
|
required: currentConfig.required || false,
|
|
|
|
|
readonly: currentConfig.readonly || false,
|
|
|
|
|
apiEndpoint: currentConfig.apiEndpoint || "",
|
|
|
|
|
filters: currentConfig.filters || {},
|
|
|
|
|
});
|
|
|
|
|
}, [widget.webTypeConfig]);
|
2025-11-25 13:04:58 +09:00
|
|
|
|
|
|
|
|
// 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등)
|
2025-09-09 14:29:04 +09:00
|
|
|
const updateConfig = (field: keyof EntityTypeConfig, value: any) => {
|
|
|
|
|
const newConfig = { ...localConfig, [field]: value };
|
|
|
|
|
setLocalConfig(newConfig);
|
|
|
|
|
onUpdateProperty("webTypeConfig", newConfig);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-25 13:04:58 +09:00
|
|
|
// 입력 필드용 업데이트 (로컬 상태만)
|
|
|
|
|
const updateConfigLocal = (field: keyof EntityTypeConfig, value: any) => {
|
|
|
|
|
setLocalConfig({ ...localConfig, [field]: value });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 입력 완료 시 부모에게 전달
|
|
|
|
|
const handleInputBlur = () => {
|
|
|
|
|
onUpdateProperty("webTypeConfig", localConfig);
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
// 필드 추가
|
|
|
|
|
const addDisplayField = () => {
|
|
|
|
|
if (!newFieldName.trim() || !newFieldLabel.trim()) return;
|
|
|
|
|
|
|
|
|
|
const newField: EntityField = {
|
|
|
|
|
name: newFieldName.trim(),
|
|
|
|
|
label: newFieldLabel.trim(),
|
|
|
|
|
type: newFieldType,
|
|
|
|
|
visible: true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const newFields = [...localConfig.displayFields, newField];
|
|
|
|
|
updateConfig("displayFields", newFields);
|
|
|
|
|
setNewFieldName("");
|
|
|
|
|
setNewFieldLabel("");
|
|
|
|
|
setNewFieldType("string");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 필드 제거
|
|
|
|
|
const removeDisplayField = (index: number) => {
|
|
|
|
|
const newFields = localConfig.displayFields.filter((_, i) => i !== index);
|
|
|
|
|
updateConfig("displayFields", newFields);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 14:44:49 +09:00
|
|
|
// 필드 업데이트 (입력 중) - 로컬 상태만 업데이트
|
2025-09-09 14:29:04 +09:00
|
|
|
const updateDisplayField = (index: number, field: keyof EntityField, value: any) => {
|
|
|
|
|
const newFields = [...localConfig.displayFields];
|
|
|
|
|
newFields[index] = { ...newFields[index], [field]: value };
|
2025-11-25 13:04:58 +09:00
|
|
|
setLocalConfig({ ...localConfig, displayFields: newFields });
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 14:44:49 +09:00
|
|
|
// 필드 업데이트 완료 (onBlur) - 부모에게 전달
|
2025-11-25 13:04:58 +09:00
|
|
|
const handleFieldBlur = () => {
|
|
|
|
|
onUpdateProperty("webTypeConfig", localConfig);
|
2025-09-09 14:29:04 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 검색 필드 토글
|
|
|
|
|
const toggleSearchField = (fieldName: string) => {
|
|
|
|
|
const currentSearchFields = localConfig.searchFields || [];
|
|
|
|
|
const newSearchFields = currentSearchFields.includes(fieldName)
|
|
|
|
|
? currentSearchFields.filter((f) => f !== fieldName)
|
|
|
|
|
: [...currentSearchFields, fieldName];
|
|
|
|
|
updateConfig("searchFields", newSearchFields);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 기본 엔티티 타입들
|
|
|
|
|
const commonEntityTypes = [
|
|
|
|
|
{ value: "user", label: "사용자", fields: ["id", "name", "email", "department"] },
|
|
|
|
|
{ value: "department", label: "부서", fields: ["id", "name", "code", "parentId"] },
|
|
|
|
|
{ value: "product", label: "제품", fields: ["id", "name", "code", "category", "price"] },
|
|
|
|
|
{ value: "customer", label: "고객", fields: ["id", "name", "company", "contact"] },
|
|
|
|
|
{ value: "project", label: "프로젝트", fields: ["id", "name", "status", "manager", "startDate"] },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 기본 엔티티 타입 적용
|
|
|
|
|
const applyEntityType = (entityType: string) => {
|
|
|
|
|
const entityConfig = commonEntityTypes.find((e) => e.value === entityType);
|
|
|
|
|
if (!entityConfig) return;
|
|
|
|
|
|
|
|
|
|
updateConfig("entityType", entityType);
|
|
|
|
|
updateConfig("apiEndpoint", `/api/entities/${entityType}`);
|
|
|
|
|
|
|
|
|
|
const defaultFields: EntityField[] = entityConfig.fields.map((field) => ({
|
|
|
|
|
name: field,
|
|
|
|
|
label: field.charAt(0).toUpperCase() + field.slice(1),
|
|
|
|
|
type: field.includes("Date") ? "date" : field.includes("price") || field.includes("Id") ? "number" : "string",
|
|
|
|
|
visible: true,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
updateConfig("displayFields", defaultFields);
|
|
|
|
|
updateConfig("searchFields", [entityConfig.fields[1] || "name"]); // 두 번째 필드를 기본 검색 필드로
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 필드 타입 옵션
|
|
|
|
|
const fieldTypes = [
|
|
|
|
|
{ value: "string", label: "문자열" },
|
|
|
|
|
{ value: "number", label: "숫자" },
|
|
|
|
|
{ value: "date", label: "날짜" },
|
|
|
|
|
{ value: "boolean", label: "불린" },
|
|
|
|
|
{ value: "email", label: "이메일" },
|
|
|
|
|
{ value: "url", label: "URL" },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
2025-11-25 13:04:58 +09:00
|
|
|
<CardTitle className="flex items-center gap-2 text-xs">
|
2025-09-09 14:29:04 +09:00
|
|
|
<Database className="h-4 w-4" />
|
|
|
|
|
엔티티 설정
|
|
|
|
|
</CardTitle>
|
|
|
|
|
<CardDescription className="text-xs">데이터베이스 엔티티 선택 필드의 설정을 관리합니다.</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
{/* 기본 설정 */}
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<h4 className="text-sm font-medium">기본 설정</h4>
|
|
|
|
|
|
2025-12-16 14:38:03 +09:00
|
|
|
{/* UI 모드 선택 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="uiMode" className="text-xs">
|
|
|
|
|
UI 모드
|
|
|
|
|
</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={(localConfig as any).uiMode || "combo"}
|
|
|
|
|
onValueChange={(value) => updateConfig("uiMode" as any, value)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="text-xs">
|
|
|
|
|
<SelectValue placeholder="모드 선택" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="select">드롭다운 (Select)</SelectItem>
|
|
|
|
|
<SelectItem value="modal">모달 팝업 (Modal)</SelectItem>
|
|
|
|
|
<SelectItem value="combo">입력 + 모달 버튼 (Combo)</SelectItem>
|
|
|
|
|
<SelectItem value="autocomplete">자동완성 (Autocomplete)</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<p className="text-[10px] text-muted-foreground">
|
|
|
|
|
{(localConfig as any).uiMode === "select" && "검색 가능한 드롭다운 형태로 표시됩니다."}
|
|
|
|
|
{(localConfig as any).uiMode === "modal" && "모달 팝업에서 데이터를 검색하고 선택합니다."}
|
|
|
|
|
{((localConfig as any).uiMode === "combo" || !(localConfig as any).uiMode) && "입력 필드와 검색 버튼이 함께 표시됩니다."}
|
|
|
|
|
{(localConfig as any).uiMode === "autocomplete" && "입력하면서 자동완성 목록이 표시됩니다."}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="entityType" className="text-xs">
|
|
|
|
|
엔티티 타입
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="entityType"
|
|
|
|
|
value={localConfig.entityType || ""}
|
2025-11-25 13:04:58 +09:00
|
|
|
onChange={(e) => updateConfigLocal("entityType", e.target.value)}
|
|
|
|
|
onBlur={handleInputBlur}
|
2025-09-09 14:29:04 +09:00
|
|
|
placeholder="user, product, department..."
|
2025-11-25 13:04:58 +09:00
|
|
|
className="text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label className="text-xs">기본 엔티티 타입</Label>
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
{commonEntityTypes.map((entity) => (
|
|
|
|
|
<Button
|
|
|
|
|
key={entity.value}
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => applyEntityType(entity.value)}
|
2025-11-25 13:04:58 +09:00
|
|
|
className="text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
>
|
|
|
|
|
{entity.label}
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="apiEndpoint" className="text-xs">
|
|
|
|
|
API 엔드포인트
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="apiEndpoint"
|
|
|
|
|
value={localConfig.apiEndpoint || ""}
|
2025-11-25 13:04:58 +09:00
|
|
|
onChange={(e) => updateConfigLocal("apiEndpoint", e.target.value)}
|
|
|
|
|
onBlur={handleInputBlur}
|
2025-09-09 14:29:04 +09:00
|
|
|
placeholder="/api/entities/user"
|
2025-11-25 13:04:58 +09:00
|
|
|
className="text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 필드 매핑 */}
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<h4 className="text-sm font-medium">필드 매핑</h4>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="valueField" className="text-xs">
|
|
|
|
|
값 필드
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="valueField"
|
|
|
|
|
value={localConfig.valueField || ""}
|
2025-11-25 13:04:58 +09:00
|
|
|
onChange={(e) => updateConfigLocal("valueField", e.target.value)}
|
|
|
|
|
onBlur={handleInputBlur}
|
2025-09-09 14:29:04 +09:00
|
|
|
placeholder="id"
|
2025-11-25 13:04:58 +09:00
|
|
|
className="text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="labelField" className="text-xs">
|
|
|
|
|
라벨 필드
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="labelField"
|
|
|
|
|
value={localConfig.labelField || ""}
|
2025-11-25 13:04:58 +09:00
|
|
|
onChange={(e) => updateConfigLocal("labelField", e.target.value)}
|
|
|
|
|
onBlur={handleInputBlur}
|
2025-09-09 14:29:04 +09:00
|
|
|
placeholder="name"
|
2025-11-25 13:04:58 +09:00
|
|
|
className="text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 표시 필드 관리 */}
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<h4 className="text-sm font-medium">표시 필드</h4>
|
|
|
|
|
|
|
|
|
|
{/* 새 필드 추가 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label className="text-xs">필드 추가</Label>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Input
|
|
|
|
|
value={newFieldName}
|
|
|
|
|
onChange={(e) => setNewFieldName(e.target.value)}
|
|
|
|
|
placeholder="필드명"
|
2025-11-25 13:04:58 +09:00
|
|
|
className="flex-1 text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
value={newFieldLabel}
|
|
|
|
|
onChange={(e) => setNewFieldLabel(e.target.value)}
|
|
|
|
|
placeholder="라벨"
|
2025-11-25 13:04:58 +09:00
|
|
|
className="flex-1 text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
/>
|
|
|
|
|
<Select value={newFieldType} onValueChange={setNewFieldType}>
|
|
|
|
|
<SelectTrigger className="w-24 text-xs">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{fieldTypes.map((type) => (
|
|
|
|
|
<SelectItem key={type.value} value={type.value}>
|
|
|
|
|
{type.label}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={addDisplayField}
|
|
|
|
|
disabled={!newFieldName.trim() || !newFieldLabel.trim()}
|
2025-11-25 13:04:58 +09:00
|
|
|
className="text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
>
|
|
|
|
|
<Plus className="h-3 w-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 현재 필드 목록 */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label className="text-xs">표시 필드 ({localConfig.displayFields.length}개)</Label>
|
|
|
|
|
<div className="max-h-40 space-y-2 overflow-y-auto">
|
|
|
|
|
{localConfig.displayFields.map((field, index) => (
|
2025-11-26 14:44:49 +09:00
|
|
|
<div key={`${field.name}-${index}`} className="flex items-center gap-2 rounded border p-2">
|
2025-09-09 14:29:04 +09:00
|
|
|
<Switch
|
|
|
|
|
checked={field.visible}
|
2025-11-25 13:04:58 +09:00
|
|
|
onCheckedChange={(checked) => {
|
2025-11-26 14:44:49 +09:00
|
|
|
const newFields = [...localConfig.displayFields];
|
|
|
|
|
newFields[index] = { ...newFields[index], visible: checked };
|
|
|
|
|
const newConfig = { ...localConfig, displayFields: newFields };
|
|
|
|
|
setLocalConfig(newConfig);
|
|
|
|
|
onUpdateProperty("webTypeConfig", newConfig);
|
2025-11-25 13:04:58 +09:00
|
|
|
}}
|
2025-09-09 14:29:04 +09:00
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
value={field.name}
|
|
|
|
|
onChange={(e) => updateDisplayField(index, "name", e.target.value)}
|
2025-11-25 13:04:58 +09:00
|
|
|
onBlur={handleFieldBlur}
|
2025-09-09 14:29:04 +09:00
|
|
|
placeholder="필드명"
|
2025-11-25 13:04:58 +09:00
|
|
|
className="flex-1 text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
value={field.label}
|
|
|
|
|
onChange={(e) => updateDisplayField(index, "label", e.target.value)}
|
2025-11-25 13:04:58 +09:00
|
|
|
onBlur={handleFieldBlur}
|
2025-09-09 14:29:04 +09:00
|
|
|
placeholder="라벨"
|
2025-11-25 13:04:58 +09:00
|
|
|
className="flex-1 text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
/>
|
2025-11-26 14:44:49 +09:00
|
|
|
<Select
|
|
|
|
|
value={field.type}
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
const newFields = [...localConfig.displayFields];
|
|
|
|
|
newFields[index] = { ...newFields[index], type: value };
|
|
|
|
|
const newConfig = { ...localConfig, displayFields: newFields };
|
|
|
|
|
setLocalConfig(newConfig);
|
|
|
|
|
onUpdateProperty("webTypeConfig", newConfig);
|
|
|
|
|
}}
|
|
|
|
|
>
|
2025-09-09 14:29:04 +09:00
|
|
|
<SelectTrigger className="w-24 text-xs">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{fieldTypes.map((type) => (
|
|
|
|
|
<SelectItem key={type.value} value={type.value}>
|
|
|
|
|
{type.label}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant={localConfig.searchFields.includes(field.name) ? "default" : "outline"}
|
|
|
|
|
onClick={() => toggleSearchField(field.name)}
|
2025-11-25 13:04:58 +09:00
|
|
|
className="p-1 text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
title={localConfig.searchFields.includes(field.name) ? "검색 필드에서 제거" : "검색 필드로 추가"}
|
|
|
|
|
>
|
|
|
|
|
<Search className="h-3 w-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="destructive"
|
|
|
|
|
onClick={() => removeDisplayField(index)}
|
2025-11-25 13:04:58 +09:00
|
|
|
className="p-1 text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 검색 설정 */}
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<h4 className="text-sm font-medium">검색 설정</h4>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="placeholder" className="text-xs">
|
|
|
|
|
플레이스홀더
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="placeholder"
|
|
|
|
|
value={localConfig.placeholder || ""}
|
2025-11-25 13:04:58 +09:00
|
|
|
onChange={(e) => updateConfigLocal("placeholder", e.target.value)}
|
|
|
|
|
onBlur={handleInputBlur}
|
2025-09-09 14:29:04 +09:00
|
|
|
placeholder="엔티티를 선택하세요"
|
2025-11-25 13:04:58 +09:00
|
|
|
className="text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="emptyMessage" className="text-xs">
|
|
|
|
|
빈 결과 메시지
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="emptyMessage"
|
|
|
|
|
value={localConfig.emptyMessage || ""}
|
2025-11-25 13:04:58 +09:00
|
|
|
onChange={(e) => updateConfigLocal("emptyMessage", e.target.value)}
|
|
|
|
|
onBlur={handleInputBlur}
|
2025-09-09 14:29:04 +09:00
|
|
|
placeholder="검색 결과가 없습니다"
|
2025-11-25 13:04:58 +09:00
|
|
|
className="text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="minSearchLength" className="text-xs">
|
|
|
|
|
최소 검색 길이
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="minSearchLength"
|
|
|
|
|
type="number"
|
|
|
|
|
value={localConfig.minSearchLength || 1}
|
|
|
|
|
onChange={(e) => updateConfig("minSearchLength", parseInt(e.target.value))}
|
|
|
|
|
min={0}
|
|
|
|
|
max={10}
|
2025-11-25 13:04:58 +09:00
|
|
|
className="text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="pageSize" className="text-xs">
|
|
|
|
|
페이지 크기
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="pageSize"
|
|
|
|
|
type="number"
|
|
|
|
|
value={localConfig.pageSize || 20}
|
|
|
|
|
onChange={(e) => updateConfig("pageSize", parseInt(e.target.value))}
|
|
|
|
|
min={5}
|
|
|
|
|
max={100}
|
2025-11-25 13:04:58 +09:00
|
|
|
className="text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label htmlFor="searchable" className="text-xs">
|
|
|
|
|
검색 가능
|
|
|
|
|
</Label>
|
|
|
|
|
<p className="text-muted-foreground text-xs">엔티티를 검색할 수 있습니다.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Switch
|
|
|
|
|
id="searchable"
|
|
|
|
|
checked={localConfig.searchable !== false}
|
|
|
|
|
onCheckedChange={(checked) => updateConfig("searchable", checked)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label htmlFor="multiple" className="text-xs">
|
|
|
|
|
다중 선택
|
|
|
|
|
</Label>
|
|
|
|
|
<p className="text-muted-foreground text-xs">여러 엔티티를 선택할 수 있습니다.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Switch
|
|
|
|
|
id="multiple"
|
|
|
|
|
checked={localConfig.multiple || false}
|
|
|
|
|
onCheckedChange={(checked) => updateConfig("multiple", checked)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 필터 설정 */}
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<h4 className="text-sm font-medium">추가 필터</h4>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="filters" className="text-xs">
|
|
|
|
|
JSON 필터
|
|
|
|
|
</Label>
|
|
|
|
|
<Textarea
|
|
|
|
|
id="filters"
|
|
|
|
|
value={JSON.stringify(localConfig.filters || {}, null, 2)}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
try {
|
|
|
|
|
const parsed = JSON.parse(e.target.value);
|
|
|
|
|
updateConfig("filters", parsed);
|
|
|
|
|
} catch {
|
|
|
|
|
// 유효하지 않은 JSON은 무시
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
placeholder='{"status": "active", "department": "IT"}'
|
2025-11-25 13:04:58 +09:00
|
|
|
className="font-mono text-xs"
|
2025-09-09 14:29:04 +09:00
|
|
|
rows={3}
|
|
|
|
|
/>
|
|
|
|
|
<p className="text-muted-foreground text-xs">API 요청에 추가될 필터 조건을 JSON 형태로 입력하세요.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 상태 설정 */}
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<h4 className="text-sm font-medium">상태 설정</h4>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label htmlFor="required" className="text-xs">
|
|
|
|
|
필수 선택
|
|
|
|
|
</Label>
|
|
|
|
|
<p className="text-muted-foreground text-xs">엔티티가 반드시 선택되어야 합니다.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Switch
|
|
|
|
|
id="required"
|
|
|
|
|
checked={localConfig.required || false}
|
|
|
|
|
onCheckedChange={(checked) => updateConfig("required", checked)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Label htmlFor="readonly" className="text-xs">
|
|
|
|
|
읽기 전용
|
|
|
|
|
</Label>
|
|
|
|
|
<p className="text-muted-foreground text-xs">엔티티를 변경할 수 없습니다.</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Switch
|
|
|
|
|
id="readonly"
|
|
|
|
|
checked={localConfig.readonly || false}
|
|
|
|
|
onCheckedChange={(checked) => updateConfig("readonly", checked)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 미리보기 */}
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<h4 className="text-sm font-medium">미리보기</h4>
|
|
|
|
|
<div className="bg-muted/50 rounded-md border p-3">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div className="flex items-center gap-2 rounded border bg-white p-2">
|
|
|
|
|
<Database className="h-4 w-4 text-gray-400" />
|
2025-10-02 14:34:15 +09:00
|
|
|
<span className="flex-1 text-xs text-muted-foreground">{localConfig.placeholder || "엔티티를 선택하세요"}</span>
|
2025-09-09 14:29:04 +09:00
|
|
|
{localConfig.searchable && <Search className="h-4 w-4 text-gray-400" />}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{localConfig.displayFields.length > 0 && (
|
|
|
|
|
<div className="text-muted-foreground text-xs">
|
|
|
|
|
<div className="font-medium">표시 필드:</div>
|
|
|
|
|
<div className="mt-1 flex flex-wrap gap-1">
|
|
|
|
|
{localConfig.displayFields
|
|
|
|
|
.filter((f) => f.visible)
|
|
|
|
|
.map((field, index) => (
|
|
|
|
|
<span key={index} className="rounded bg-gray-100 px-2 py-1">
|
|
|
|
|
{field.label}
|
|
|
|
|
{localConfig.searchFields.includes(field.name) && " 🔍"}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="text-muted-foreground text-xs">
|
|
|
|
|
타입: {localConfig.entityType || "미정"}• 값 필드: {localConfig.valueField}• 라벨 필드:{" "}
|
|
|
|
|
{localConfig.labelField}
|
|
|
|
|
{localConfig.multiple && " • 다중선택"}
|
|
|
|
|
{localConfig.required && " • 필수"}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
EntityConfigPanel.displayName = "EntityConfigPanel";
|
|
|
|
|
|
|
|
|
|
|