편집기 인풋 오류 수정 및 탭 컴포넌트 완성

This commit is contained in:
kjs
2025-11-25 13:04:58 +09:00
parent 5e2392c417
commit a0180d66a2
40 changed files with 342 additions and 325 deletions

View File

@@ -51,37 +51,52 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
const [newFieldName, setNewFieldName] = useState("");
const [newFieldLabel, setNewFieldLabel] = useState("");
const [newFieldType, setNewFieldType] = useState("string");
const [isUserEditing, setIsUserEditing] = useState(false);
// 컴포넌트 변경 시 로컬 상태 동기화
// 컴포넌트 변경 시 로컬 상태 동기화 (사용자가 입력 중이 아닐 때만)
useEffect(() => {
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]);
if (!isUserEditing) {
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, isUserEditing]);
// 설정 업데이트 핸들러
// 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등)
const updateConfig = (field: keyof EntityTypeConfig, value: any) => {
const newConfig = { ...localConfig, [field]: value };
setLocalConfig(newConfig);
onUpdateProperty("webTypeConfig", newConfig);
};
// 입력 필드용 업데이트 (로컬 상태만)
const updateConfigLocal = (field: keyof EntityTypeConfig, value: any) => {
setIsUserEditing(true);
setLocalConfig({ ...localConfig, [field]: value });
};
// 입력 완료 시 부모에게 전달
const handleInputBlur = () => {
setIsUserEditing(false);
onUpdateProperty("webTypeConfig", localConfig);
};
// 필드 추가
const addDisplayField = () => {
if (!newFieldName.trim() || !newFieldLabel.trim()) return;
@@ -106,11 +121,18 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
updateConfig("displayFields", newFields);
};
// 필드 업데이트
// 필드 업데이트 (입력 중)
const updateDisplayField = (index: number, field: keyof EntityField, value: any) => {
setIsUserEditing(true);
const newFields = [...localConfig.displayFields];
newFields[index] = { ...newFields[index], [field]: value };
updateConfig("displayFields", newFields);
setLocalConfig({ ...localConfig, displayFields: newFields });
};
// 필드 업데이트 완료 (onBlur)
const handleFieldBlur = () => {
setIsUserEditing(false);
onUpdateProperty("webTypeConfig", localConfig);
};
// 검색 필드 토글
@@ -163,7 +185,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-xs" style={{ fontSize: "12px" }}>
<CardTitle className="flex items-center gap-2 text-xs">
<Database className="h-4 w-4" />
</CardTitle>
@@ -181,9 +203,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input
id="entityType"
value={localConfig.entityType || ""}
onChange={(e) => updateConfig("entityType", e.target.value)}
onChange={(e) => updateConfigLocal("entityType", e.target.value)}
onBlur={handleInputBlur}
placeholder="user, product, department..."
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -196,7 +219,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
size="sm"
variant="outline"
onClick={() => applyEntityType(entity.value)}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
>
{entity.label}
</Button>
@@ -211,9 +234,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input
id="apiEndpoint"
value={localConfig.apiEndpoint || ""}
onChange={(e) => updateConfig("apiEndpoint", e.target.value)}
onChange={(e) => updateConfigLocal("apiEndpoint", e.target.value)}
onBlur={handleInputBlur}
placeholder="/api/entities/user"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
</div>
@@ -230,9 +254,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input
id="valueField"
value={localConfig.valueField || ""}
onChange={(e) => updateConfig("valueField", e.target.value)}
onChange={(e) => updateConfigLocal("valueField", e.target.value)}
onBlur={handleInputBlur}
placeholder="id"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -243,9 +268,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input
id="labelField"
value={localConfig.labelField || ""}
onChange={(e) => updateConfig("labelField", e.target.value)}
onChange={(e) => updateConfigLocal("labelField", e.target.value)}
onBlur={handleInputBlur}
placeholder="name"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
</div>
@@ -263,13 +289,13 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
value={newFieldName}
onChange={(e) => setNewFieldName(e.target.value)}
placeholder="필드명"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Input
value={newFieldLabel}
onChange={(e) => setNewFieldLabel(e.target.value)}
placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Select value={newFieldType} onValueChange={setNewFieldType}>
<SelectTrigger className="w-24 text-xs">
@@ -287,7 +313,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
size="sm"
onClick={addDisplayField}
disabled={!newFieldName.trim() || !newFieldLabel.trim()}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
>
<Plus className="h-3 w-3" />
</Button>
@@ -302,19 +328,24 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<div key={index} className="flex items-center gap-2 rounded border p-2">
<Switch
checked={field.visible}
onCheckedChange={(checked) => updateDisplayField(index, "visible", checked)}
onCheckedChange={(checked) => {
updateDisplayField(index, "visible", checked);
handleFieldBlur();
}}
/>
<Input
value={field.name}
onChange={(e) => updateDisplayField(index, "name", e.target.value)}
onBlur={handleFieldBlur}
placeholder="필드명"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Input
value={field.label}
onChange={(e) => updateDisplayField(index, "label", e.target.value)}
onBlur={handleFieldBlur}
placeholder="라벨"
className="flex-1 text-xs" style={{ fontSize: "12px" }}
className="flex-1 text-xs"
/>
<Select value={field.type} onValueChange={(value) => updateDisplayField(index, "type", value)}>
<SelectTrigger className="w-24 text-xs">
@@ -332,7 +363,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
size="sm"
variant={localConfig.searchFields.includes(field.name) ? "default" : "outline"}
onClick={() => toggleSearchField(field.name)}
className="p-1 text-xs" style={{ fontSize: "12px" }}
className="p-1 text-xs"
title={localConfig.searchFields.includes(field.name) ? "검색 필드에서 제거" : "검색 필드로 추가"}
>
<Search className="h-3 w-3" />
@@ -341,7 +372,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
size="sm"
variant="destructive"
onClick={() => removeDisplayField(index)}
className="p-1 text-xs" style={{ fontSize: "12px" }}
className="p-1 text-xs"
>
<Trash2 className="h-3 w-3" />
</Button>
@@ -362,9 +393,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input
id="placeholder"
value={localConfig.placeholder || ""}
onChange={(e) => updateConfig("placeholder", e.target.value)}
onChange={(e) => updateConfigLocal("placeholder", e.target.value)}
onBlur={handleInputBlur}
placeholder="엔티티를 선택하세요"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -375,9 +407,10 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
<Input
id="emptyMessage"
value={localConfig.emptyMessage || ""}
onChange={(e) => updateConfig("emptyMessage", e.target.value)}
onChange={(e) => updateConfigLocal("emptyMessage", e.target.value)}
onBlur={handleInputBlur}
placeholder="검색 결과가 없습니다"
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -393,7 +426,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("minSearchLength", parseInt(e.target.value))}
min={0}
max={10}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
@@ -408,7 +441,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
onChange={(e) => updateConfig("pageSize", parseInt(e.target.value))}
min={5}
max={100}
className="text-xs" style={{ fontSize: "12px" }}
className="text-xs"
/>
</div>
</div>
@@ -462,7 +495,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
}
}}
placeholder='{"status": "active", "department": "IT"}'
className="font-mono text-xs" style={{ fontSize: "12px" }}
className="font-mono text-xs"
rows={3}
/>
<p className="text-muted-foreground text-xs">API JSON .</p>