엔티티타입 연쇄관계관리 설정 추가
This commit is contained in:
parent
0832e7b6eb
commit
3d287bb883
|
|
@ -958,6 +958,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
|||
characterMaximumLength: col.characterMaximumLength || col.character_maximum_length,
|
||||
codeCategory: col.codeCategory || col.code_category,
|
||||
codeValue: col.codeValue || col.code_value,
|
||||
// 엔티티 타입용 참조 테이블 정보
|
||||
referenceTable: col.referenceTable || col.reference_table,
|
||||
referenceColumn: col.referenceColumn || col.reference_column,
|
||||
displayColumn: col.displayColumn || col.display_column,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,18 +6,10 @@ 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 { Database, Search, Info } 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;
|
||||
}
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
|
||||
export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
||||
component,
|
||||
|
|
@ -27,16 +19,31 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
const widget = component as WidgetComponent;
|
||||
const config = (widget.webTypeConfig as EntityTypeConfig) || {};
|
||||
|
||||
// 로컬 상태
|
||||
// 테이블 타입 관리에서 설정된 참조 테이블 정보
|
||||
const [referenceInfo, setReferenceInfo] = useState<{
|
||||
referenceTable: string;
|
||||
referenceColumn: string;
|
||||
displayColumn: string;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}>({
|
||||
referenceTable: "",
|
||||
referenceColumn: "",
|
||||
displayColumn: "",
|
||||
isLoading: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
// 로컬 상태 (UI 관련 설정만)
|
||||
const [localConfig, setLocalConfig] = useState<EntityTypeConfig>({
|
||||
entityType: config.entityType || "",
|
||||
displayFields: config.displayFields || [],
|
||||
searchFields: config.searchFields || [],
|
||||
valueField: config.valueField || "id",
|
||||
labelField: config.labelField || "name",
|
||||
valueField: config.valueField || "",
|
||||
labelField: config.labelField || "",
|
||||
multiple: config.multiple || false,
|
||||
searchable: config.searchable !== false, // 기본값 true
|
||||
placeholder: config.placeholder || "엔티티를 선택하세요",
|
||||
searchable: config.searchable !== false,
|
||||
placeholder: config.placeholder || "항목을 선택하세요",
|
||||
emptyMessage: config.emptyMessage || "검색 결과가 없습니다",
|
||||
pageSize: config.pageSize || 20,
|
||||
minSearchLength: config.minSearchLength || 1,
|
||||
|
|
@ -47,10 +54,95 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
filters: config.filters || {},
|
||||
});
|
||||
|
||||
// 새 필드 추가용 상태
|
||||
const [newFieldName, setNewFieldName] = useState("");
|
||||
const [newFieldLabel, setNewFieldLabel] = useState("");
|
||||
const [newFieldType, setNewFieldType] = useState("string");
|
||||
// 테이블 타입 관리에서 설정된 참조 테이블 정보 로드
|
||||
useEffect(() => {
|
||||
const loadReferenceInfo = async () => {
|
||||
// 컴포넌트의 테이블명과 컬럼명이 있는 경우에만 조회
|
||||
const tableName = widget.tableName;
|
||||
const columnName = widget.columnName;
|
||||
|
||||
if (!tableName || !columnName) {
|
||||
setReferenceInfo({
|
||||
referenceTable: "",
|
||||
referenceColumn: "",
|
||||
displayColumn: "",
|
||||
isLoading: false,
|
||||
error: "테이블 또는 컬럼 정보가 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 테이블 타입 관리에서 컬럼 정보 조회
|
||||
const columns = await tableTypeApi.getColumns(tableName);
|
||||
const columnInfo = columns.find((col: any) =>
|
||||
(col.columnName || col.column_name) === columnName
|
||||
);
|
||||
|
||||
if (columnInfo) {
|
||||
const refTable = columnInfo.referenceTable || columnInfo.reference_table || "";
|
||||
const refColumn = columnInfo.referenceColumn || columnInfo.reference_column || "";
|
||||
const dispColumn = columnInfo.displayColumn || columnInfo.display_column || "";
|
||||
|
||||
// detailSettings에서도 정보 확인 (JSON 파싱)
|
||||
let detailSettings: any = {};
|
||||
if (columnInfo.detailSettings) {
|
||||
try {
|
||||
if (typeof columnInfo.detailSettings === 'string') {
|
||||
detailSettings = JSON.parse(columnInfo.detailSettings);
|
||||
} else {
|
||||
detailSettings = columnInfo.detailSettings;
|
||||
}
|
||||
} catch {
|
||||
// JSON 파싱 실패 시 무시
|
||||
}
|
||||
}
|
||||
|
||||
const finalRefTable = refTable || detailSettings.referenceTable || "";
|
||||
const finalRefColumn = refColumn || detailSettings.referenceColumn || "";
|
||||
const finalDispColumn = dispColumn || detailSettings.displayColumn || "";
|
||||
|
||||
setReferenceInfo({
|
||||
referenceTable: finalRefTable,
|
||||
referenceColumn: finalRefColumn,
|
||||
displayColumn: finalDispColumn,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
// webTypeConfig에 참조 테이블 정보 자동 설정
|
||||
if (finalRefTable) {
|
||||
const newConfig = {
|
||||
...localConfig,
|
||||
valueField: finalRefColumn || "id",
|
||||
labelField: finalDispColumn || "name",
|
||||
};
|
||||
setLocalConfig(newConfig);
|
||||
onUpdateProperty("webTypeConfig", newConfig);
|
||||
}
|
||||
} else {
|
||||
setReferenceInfo({
|
||||
referenceTable: "",
|
||||
referenceColumn: "",
|
||||
displayColumn: "",
|
||||
isLoading: false,
|
||||
error: "컬럼 정보를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("참조 테이블 정보 로드 실패:", error);
|
||||
setReferenceInfo({
|
||||
referenceTable: "",
|
||||
referenceColumn: "",
|
||||
displayColumn: "",
|
||||
isLoading: false,
|
||||
error: "참조 테이블 정보 로드 실패",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadReferenceInfo();
|
||||
}, [widget.tableName, widget.columnName]);
|
||||
|
||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||
useEffect(() => {
|
||||
|
|
@ -59,11 +151,11 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
entityType: currentConfig.entityType || "",
|
||||
displayFields: currentConfig.displayFields || [],
|
||||
searchFields: currentConfig.searchFields || [],
|
||||
valueField: currentConfig.valueField || "id",
|
||||
labelField: currentConfig.labelField || "name",
|
||||
valueField: currentConfig.valueField || referenceInfo.referenceColumn || "",
|
||||
labelField: currentConfig.labelField || referenceInfo.displayColumn || "",
|
||||
multiple: currentConfig.multiple || false,
|
||||
searchable: currentConfig.searchable !== false,
|
||||
placeholder: currentConfig.placeholder || "엔티티를 선택하세요",
|
||||
placeholder: currentConfig.placeholder || "항목을 선택하세요",
|
||||
emptyMessage: currentConfig.emptyMessage || "검색 결과가 없습니다",
|
||||
pageSize: currentConfig.pageSize || 20,
|
||||
minSearchLength: currentConfig.minSearchLength || 1,
|
||||
|
|
@ -73,7 +165,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
apiEndpoint: currentConfig.apiEndpoint || "",
|
||||
filters: currentConfig.filters || {},
|
||||
});
|
||||
}, [widget.webTypeConfig]);
|
||||
}, [widget.webTypeConfig, referenceInfo.referenceColumn, referenceInfo.displayColumn]);
|
||||
|
||||
// 설정 업데이트 핸들러 (즉시 부모에게 전달 - 드롭다운, 체크박스 등)
|
||||
const updateConfig = (field: keyof EntityTypeConfig, value: any) => {
|
||||
|
|
@ -92,89 +184,6 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
onUpdateProperty("webTypeConfig", localConfig);
|
||||
};
|
||||
|
||||
// 필드 추가
|
||||
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);
|
||||
};
|
||||
|
||||
// 필드 업데이트 (입력 중) - 로컬 상태만 업데이트
|
||||
const updateDisplayField = (index: number, field: keyof EntityField, value: any) => {
|
||||
const newFields = [...localConfig.displayFields];
|
||||
newFields[index] = { ...newFields[index], [field]: value };
|
||||
setLocalConfig({ ...localConfig, displayFields: newFields });
|
||||
};
|
||||
|
||||
// 필드 업데이트 완료 (onBlur) - 부모에게 전달
|
||||
const handleFieldBlur = () => {
|
||||
onUpdateProperty("webTypeConfig", localConfig);
|
||||
};
|
||||
|
||||
// 검색 필드 토글
|
||||
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>
|
||||
|
|
@ -182,12 +191,70 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
<Database className="h-4 w-4" />
|
||||
엔티티 설정
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">데이터베이스 엔티티 선택 필드의 설정을 관리합니다.</CardDescription>
|
||||
<CardDescription className="text-xs">
|
||||
데이터베이스 엔티티 선택 필드의 설정을 관리합니다.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* 기본 설정 */}
|
||||
{/* 참조 테이블 정보 (테이블 타입 관리에서 설정된 값 - 읽기 전용) */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-medium">기본 설정</h4>
|
||||
<h4 className="text-sm font-medium flex items-center gap-2">
|
||||
참조 테이블 정보
|
||||
<span className="bg-muted text-muted-foreground px-1.5 py-0.5 rounded text-[10px]">
|
||||
테이블 타입 관리에서 설정
|
||||
</span>
|
||||
</h4>
|
||||
|
||||
{referenceInfo.isLoading ? (
|
||||
<div className="bg-muted/50 rounded-md border p-3">
|
||||
<p className="text-xs text-muted-foreground">참조 테이블 정보 로딩 중...</p>
|
||||
</div>
|
||||
) : referenceInfo.error ? (
|
||||
<div className="bg-destructive/10 rounded-md border border-destructive/20 p-3">
|
||||
<p className="text-xs text-destructive flex items-center gap-1">
|
||||
<Info className="h-3 w-3" />
|
||||
{referenceInfo.error}
|
||||
</p>
|
||||
<p className="text-[10px] text-muted-foreground mt-1">
|
||||
테이블 타입 관리에서 이 컬럼의 참조 테이블을 먼저 설정해주세요.
|
||||
</p>
|
||||
</div>
|
||||
) : !referenceInfo.referenceTable ? (
|
||||
<div className="bg-amber-500/10 rounded-md border border-amber-500/20 p-3">
|
||||
<p className="text-xs text-amber-700 flex items-center gap-1">
|
||||
<Info className="h-3 w-3" />
|
||||
참조 테이블이 설정되지 않았습니다.
|
||||
</p>
|
||||
<p className="text-[10px] text-muted-foreground mt-1">
|
||||
테이블 타입 관리에서 이 컬럼의 참조 테이블을 먼저 설정해주세요.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-muted/50 rounded-md border p-3 space-y-2">
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div>
|
||||
<span className="text-muted-foreground">참조 테이블:</span>
|
||||
<div className="font-medium">{referenceInfo.referenceTable}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">참조 컬럼:</span>
|
||||
<div className="font-medium">{referenceInfo.referenceColumn || "-"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">표시 컬럼:</span>
|
||||
<div className="font-medium">{referenceInfo.displayColumn || "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
이 정보는 테이블 타입 관리에서 변경할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* UI 모드 설정 */}
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-medium">UI 설정</h4>
|
||||
|
||||
{/* UI 모드 선택 */}
|
||||
<div className="space-y-2">
|
||||
|
|
@ -216,208 +283,6 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="entityType" className="text-xs">
|
||||
엔티티 타입
|
||||
</Label>
|
||||
<Input
|
||||
id="entityType"
|
||||
value={localConfig.entityType || ""}
|
||||
onChange={(e) => updateConfigLocal("entityType", e.target.value)}
|
||||
onBlur={handleInputBlur}
|
||||
placeholder="user, product, department..."
|
||||
className="text-xs"
|
||||
/>
|
||||
</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)}
|
||||
className="text-xs"
|
||||
>
|
||||
{entity.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="apiEndpoint" className="text-xs">
|
||||
API 엔드포인트
|
||||
</Label>
|
||||
<Input
|
||||
id="apiEndpoint"
|
||||
value={localConfig.apiEndpoint || ""}
|
||||
onChange={(e) => updateConfigLocal("apiEndpoint", e.target.value)}
|
||||
onBlur={handleInputBlur}
|
||||
placeholder="/api/entities/user"
|
||||
className="text-xs"
|
||||
/>
|
||||
</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 || ""}
|
||||
onChange={(e) => updateConfigLocal("valueField", e.target.value)}
|
||||
onBlur={handleInputBlur}
|
||||
placeholder="id"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="labelField" className="text-xs">
|
||||
라벨 필드
|
||||
</Label>
|
||||
<Input
|
||||
id="labelField"
|
||||
value={localConfig.labelField || ""}
|
||||
onChange={(e) => updateConfigLocal("labelField", e.target.value)}
|
||||
onBlur={handleInputBlur}
|
||||
placeholder="name"
|
||||
className="text-xs"
|
||||
/>
|
||||
</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="필드명"
|
||||
className="flex-1 text-xs"
|
||||
/>
|
||||
<Input
|
||||
value={newFieldLabel}
|
||||
onChange={(e) => setNewFieldLabel(e.target.value)}
|
||||
placeholder="라벨"
|
||||
className="flex-1 text-xs"
|
||||
/>
|
||||
<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()}
|
||||
className="text-xs"
|
||||
>
|
||||
<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) => (
|
||||
<div key={`${field.name}-${index}`} className="flex items-center gap-2 rounded border p-2">
|
||||
<Switch
|
||||
checked={field.visible}
|
||||
onCheckedChange={(checked) => {
|
||||
const newFields = [...localConfig.displayFields];
|
||||
newFields[index] = { ...newFields[index], visible: checked };
|
||||
const newConfig = { ...localConfig, displayFields: newFields };
|
||||
setLocalConfig(newConfig);
|
||||
onUpdateProperty("webTypeConfig", newConfig);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
value={field.name}
|
||||
onChange={(e) => updateDisplayField(index, "name", e.target.value)}
|
||||
onBlur={handleFieldBlur}
|
||||
placeholder="필드명"
|
||||
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"
|
||||
/>
|
||||
<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);
|
||||
}}
|
||||
>
|
||||
<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)}
|
||||
className="p-1 text-xs"
|
||||
title={localConfig.searchFields.includes(field.name) ? "검색 필드에서 제거" : "검색 필드로 추가"}
|
||||
>
|
||||
<Search className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => removeDisplayField(index)}
|
||||
className="p-1 text-xs"
|
||||
>
|
||||
<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">
|
||||
플레이스홀더
|
||||
|
|
@ -427,7 +292,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
value={localConfig.placeholder || ""}
|
||||
onChange={(e) => updateConfigLocal("placeholder", e.target.value)}
|
||||
onBlur={handleInputBlur}
|
||||
placeholder="엔티티를 선택하세요"
|
||||
placeholder="항목을 선택하세요"
|
||||
className="text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -445,6 +310,11 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
className="text-xs"
|
||||
/>
|
||||
</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">
|
||||
|
|
@ -483,7 +353,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
<Label htmlFor="searchable" className="text-xs">
|
||||
검색 가능
|
||||
</Label>
|
||||
<p className="text-muted-foreground text-xs">엔티티를 검색할 수 있습니다.</p>
|
||||
<p className="text-muted-foreground text-xs">항목을 검색할 수 있습니다.</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="searchable"
|
||||
|
|
@ -497,7 +367,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
<Label htmlFor="multiple" className="text-xs">
|
||||
다중 선택
|
||||
</Label>
|
||||
<p className="text-muted-foreground text-xs">여러 엔티티를 선택할 수 있습니다.</p>
|
||||
<p className="text-muted-foreground text-xs">여러 항목을 선택할 수 있습니다.</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="multiple"
|
||||
|
|
@ -507,33 +377,6 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
</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"}'
|
||||
className="font-mono text-xs"
|
||||
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>
|
||||
|
|
@ -543,7 +386,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
<Label htmlFor="required" className="text-xs">
|
||||
필수 선택
|
||||
</Label>
|
||||
<p className="text-muted-foreground text-xs">엔티티가 반드시 선택되어야 합니다.</p>
|
||||
<p className="text-muted-foreground text-xs">반드시 항목을 선택해야 합니다.</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="required"
|
||||
|
|
@ -557,7 +400,7 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
<Label htmlFor="readonly" className="text-xs">
|
||||
읽기 전용
|
||||
</Label>
|
||||
<p className="text-muted-foreground text-xs">엔티티를 변경할 수 없습니다.</p>
|
||||
<p className="text-muted-foreground text-xs">값을 변경할 수 없습니다.</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="readonly"
|
||||
|
|
@ -574,31 +417,18 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
<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" />
|
||||
<span className="flex-1 text-xs text-muted-foreground">{localConfig.placeholder || "엔티티를 선택하세요"}</span>
|
||||
<span className="flex-1 text-xs text-muted-foreground">
|
||||
{localConfig.placeholder || "항목을 선택하세요"}
|
||||
</span>
|
||||
{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>테이블: {referenceInfo.referenceTable || "미설정"}</div>
|
||||
<div>값 필드: {localConfig.valueField || referenceInfo.referenceColumn || "-"}</div>
|
||||
<div>표시 필드: {localConfig.labelField || referenceInfo.displayColumn || "-"}</div>
|
||||
{localConfig.multiple && <span> / 다중선택</span>}
|
||||
{localConfig.required && <span> / 필수</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -609,5 +439,3 @@ export const EntityConfigPanel: React.FC<WebTypeConfigPanelProps> = ({
|
|||
};
|
||||
|
||||
EntityConfigPanel.displayName = "EntityConfigPanel";
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ interface DetailSettingsPanelProps {
|
|||
currentTableName?: string; // 현재 화면의 테이블명
|
||||
tables?: TableInfo[]; // 전체 테이블 목록
|
||||
currentScreenCompanyCode?: string; // 현재 편집 중인 화면의 회사 코드
|
||||
components?: ComponentData[]; // 현재 화면의 모든 컴포넌트 (연쇄관계 부모 필드 선택용)
|
||||
}
|
||||
|
||||
export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
|
|
@ -55,6 +56,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
|||
currentTableName,
|
||||
tables = [], // 기본값 빈 배열
|
||||
currentScreenCompanyCode,
|
||||
components = [], // 기본값 빈 배열
|
||||
}) => {
|
||||
// 데이터베이스에서 입력 가능한 웹타입들을 동적으로 가져오기
|
||||
const { webTypes } = useWebTypes({ active: "Y" });
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search, X, Check, ChevronsUpDown } from "lucide-react";
|
||||
|
|
@ -10,6 +10,7 @@ import { cn } from "@/lib/utils";
|
|||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
|
||||
import { dynamicFormApi } from "@/lib/api/dynamicForm";
|
||||
import { cascadingRelationApi } from "@/lib/api/cascadingRelation";
|
||||
|
||||
export function EntitySearchInputComponent({
|
||||
tableName,
|
||||
|
|
@ -29,6 +30,11 @@ export function EntitySearchInputComponent({
|
|||
additionalFields = [],
|
||||
className,
|
||||
style,
|
||||
// 연쇄관계 props
|
||||
cascadingRelationCode,
|
||||
parentValue: parentValueProp,
|
||||
parentFieldId,
|
||||
formData,
|
||||
// 🆕 추가 props
|
||||
component,
|
||||
isInteractive,
|
||||
|
|
@ -38,10 +44,21 @@ export function EntitySearchInputComponent({
|
|||
component?: any;
|
||||
isInteractive?: boolean;
|
||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||
webTypeConfig?: any; // 웹타입 설정 (연쇄관계 등)
|
||||
}) {
|
||||
// uiMode가 있으면 우선 사용, 없으면 modeProp 사용, 기본값 "combo"
|
||||
const mode = (uiMode || modeProp || "combo") as "select" | "modal" | "combo" | "autocomplete";
|
||||
|
||||
// 연쇄관계 설정 추출 (webTypeConfig 또는 component.componentConfig에서)
|
||||
const config = component?.componentConfig || {};
|
||||
const effectiveCascadingRelationCode = cascadingRelationCode || config.cascadingRelationCode;
|
||||
const effectiveParentFieldId = parentFieldId || config.parentFieldId;
|
||||
const effectiveCascadingRole = config.cascadingRole; // "parent" | "child" | undefined
|
||||
|
||||
// 부모 역할이면 연쇄관계 로직 적용 안함 (자식만 부모 값에 따라 필터링됨)
|
||||
const isChildRole = effectiveCascadingRole === "child";
|
||||
const shouldApplyCascading = effectiveCascadingRelationCode && isChildRole;
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [selectOpen, setSelectOpen] = useState(false);
|
||||
const [displayValue, setDisplayValue] = useState("");
|
||||
|
|
@ -50,16 +67,82 @@ export function EntitySearchInputComponent({
|
|||
const [isLoadingOptions, setIsLoadingOptions] = useState(false);
|
||||
const [optionsLoaded, setOptionsLoaded] = useState(false);
|
||||
|
||||
// 연쇄관계 상태
|
||||
const [cascadingOptions, setCascadingOptions] = useState<EntitySearchResult[]>([]);
|
||||
const [isCascadingLoading, setIsCascadingLoading] = useState(false);
|
||||
const previousParentValue = useRef<any>(null);
|
||||
|
||||
// 부모 필드 값 결정 (직접 전달 또는 formData에서 추출) - 자식 역할일 때만 필요
|
||||
const parentValue = isChildRole
|
||||
? (parentValueProp ?? (effectiveParentFieldId && formData ? formData[effectiveParentFieldId] : undefined))
|
||||
: undefined;
|
||||
|
||||
// filterCondition을 문자열로 변환하여 비교 (객체 참조 문제 해결)
|
||||
const filterConditionKey = JSON.stringify(filterCondition || {});
|
||||
|
||||
// select 모드일 때 옵션 로드 (한 번만)
|
||||
// 연쇄관계가 설정된 경우: 부모 값이 변경되면 자식 옵션 로드 (자식 역할일 때만)
|
||||
useEffect(() => {
|
||||
if (mode === "select" && tableName && !optionsLoaded) {
|
||||
const loadCascadingOptions = async () => {
|
||||
if (!shouldApplyCascading) return;
|
||||
|
||||
// 부모 값이 없으면 옵션 초기화
|
||||
if (!parentValue) {
|
||||
setCascadingOptions([]);
|
||||
// 부모 값이 변경되면 현재 값도 초기화
|
||||
if (previousParentValue.current !== null && previousParentValue.current !== parentValue) {
|
||||
handleClear();
|
||||
}
|
||||
previousParentValue.current = parentValue;
|
||||
return;
|
||||
}
|
||||
|
||||
// 부모 값이 동일하면 스킵
|
||||
if (previousParentValue.current === parentValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
previousParentValue.current = parentValue;
|
||||
setIsCascadingLoading(true);
|
||||
|
||||
try {
|
||||
console.log("🔗 연쇄관계 옵션 로드:", { effectiveCascadingRelationCode, parentValue });
|
||||
const response = await cascadingRelationApi.getOptions(effectiveCascadingRelationCode, String(parentValue));
|
||||
|
||||
if (response.success && response.data) {
|
||||
// 옵션을 EntitySearchResult 형태로 변환
|
||||
const formattedOptions = response.data.map((opt: any) => ({
|
||||
[valueField]: opt.value,
|
||||
[displayField]: opt.label,
|
||||
...opt, // 추가 필드도 포함
|
||||
}));
|
||||
setCascadingOptions(formattedOptions);
|
||||
console.log("✅ 연쇄관계 옵션 로드 완료:", formattedOptions.length, "개");
|
||||
|
||||
// 현재 선택된 값이 새 옵션에 없으면 초기화
|
||||
if (value && !formattedOptions.find((opt: any) => opt[valueField] === value)) {
|
||||
handleClear();
|
||||
}
|
||||
} else {
|
||||
setCascadingOptions([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ 연쇄관계 옵션 로드 실패:", error);
|
||||
setCascadingOptions([]);
|
||||
} finally {
|
||||
setIsCascadingLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadCascadingOptions();
|
||||
}, [shouldApplyCascading, effectiveCascadingRelationCode, parentValue, valueField, displayField]);
|
||||
|
||||
// select 모드일 때 옵션 로드 (연쇄관계가 없거나 부모 역할인 경우)
|
||||
useEffect(() => {
|
||||
if (mode === "select" && tableName && !optionsLoaded && !shouldApplyCascading) {
|
||||
loadOptions();
|
||||
setOptionsLoaded(true);
|
||||
}
|
||||
}, [mode, tableName, filterConditionKey, optionsLoaded]);
|
||||
}, [mode, tableName, filterConditionKey, optionsLoaded, shouldApplyCascading]);
|
||||
|
||||
const loadOptions = async () => {
|
||||
if (!tableName) return;
|
||||
|
|
@ -82,15 +165,19 @@ export function EntitySearchInputComponent({
|
|||
}
|
||||
};
|
||||
|
||||
// 실제 사용할 옵션 목록 (자식 역할이고 연쇄관계가 있으면 연쇄 옵션 사용)
|
||||
const effectiveOptions = shouldApplyCascading ? cascadingOptions : options;
|
||||
const isLoading = shouldApplyCascading ? isCascadingLoading : isLoadingOptions;
|
||||
|
||||
// value가 변경되면 표시값 업데이트 (외래키 값으로 데이터 조회)
|
||||
useEffect(() => {
|
||||
const loadDisplayValue = async () => {
|
||||
if (value && selectedData) {
|
||||
// 이미 selectedData가 있으면 표시값만 업데이트
|
||||
setDisplayValue(selectedData[displayField] || "");
|
||||
} else if (value && mode === "select" && options.length > 0) {
|
||||
} else if (value && mode === "select" && effectiveOptions.length > 0) {
|
||||
// select 모드에서 value가 있고 options가 로드된 경우
|
||||
const found = options.find((opt) => opt[valueField] === value);
|
||||
const found = effectiveOptions.find((opt) => opt[valueField] === value);
|
||||
if (found) {
|
||||
setSelectedData(found);
|
||||
setDisplayValue(found[displayField] || "");
|
||||
|
|
@ -142,7 +229,7 @@ export function EntitySearchInputComponent({
|
|||
};
|
||||
|
||||
loadDisplayValue();
|
||||
}, [value, displayField, options, mode, valueField, tableName, selectedData]);
|
||||
}, [value, displayField, effectiveOptions, mode, valueField, tableName, selectedData]);
|
||||
|
||||
const handleSelect = (newValue: any, fullData: EntitySearchResult) => {
|
||||
setSelectedData(fullData);
|
||||
|
|
@ -200,7 +287,7 @@ export function EntitySearchInputComponent({
|
|||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={selectOpen}
|
||||
disabled={disabled || isLoadingOptions}
|
||||
disabled={disabled || isLoading || Boolean(shouldApplyCascading && !parentValue)}
|
||||
className={cn(
|
||||
"w-full justify-between font-normal",
|
||||
!componentHeight && "h-8 text-xs sm:h-10 sm:text-sm",
|
||||
|
|
@ -208,7 +295,11 @@ export function EntitySearchInputComponent({
|
|||
)}
|
||||
style={inputStyle}
|
||||
>
|
||||
{isLoadingOptions ? "로딩 중..." : displayValue || placeholder}
|
||||
{isLoading
|
||||
? "로딩 중..."
|
||||
: shouldApplyCascading && !parentValue
|
||||
? "상위 항목을 먼저 선택하세요"
|
||||
: displayValue || placeholder}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
|
@ -218,7 +309,7 @@ export function EntitySearchInputComponent({
|
|||
<CommandList>
|
||||
<CommandEmpty className="py-4 text-center text-xs sm:text-sm">항목을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option, index) => (
|
||||
{effectiveOptions.map((option, index) => (
|
||||
<CommandItem
|
||||
key={option[valueField] || index}
|
||||
value={`${option[displayField] || ""}-${option[valueField] || ""}`}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
|
@ -8,19 +8,27 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
|
|||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, X, Check, ChevronsUpDown } from "lucide-react";
|
||||
import { Plus, X, Check, ChevronsUpDown, Database, Info, Link2, ExternalLink } from "lucide-react";
|
||||
// allComponents는 현재 사용되지 않지만 향후 확장을 위해 props에 유지
|
||||
import { EntitySearchInputConfig } from "./config";
|
||||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||||
import { tableTypeApi } from "@/lib/api/screen";
|
||||
import { cascadingRelationApi, CascadingRelation } from "@/lib/api/cascadingRelation";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
interface EntitySearchInputConfigPanelProps {
|
||||
config: EntitySearchInputConfig;
|
||||
onConfigChange: (config: EntitySearchInputConfig) => void;
|
||||
currentComponent?: any; // 테이블 패널에서 드래그한 컴포넌트 정보
|
||||
allComponents?: any[]; // 현재 화면의 모든 컴포넌트 (연쇄 드롭다운 부모 감지용)
|
||||
}
|
||||
|
||||
export function EntitySearchInputConfigPanel({
|
||||
config,
|
||||
onConfigChange,
|
||||
currentComponent,
|
||||
allComponents = [],
|
||||
}: EntitySearchInputConfigPanelProps) {
|
||||
const [localConfig, setLocalConfig] = useState(config);
|
||||
const [allTables, setAllTables] = useState<any[]>([]);
|
||||
|
|
@ -31,7 +39,151 @@ export function EntitySearchInputConfigPanel({
|
|||
const [openDisplayFieldCombo, setOpenDisplayFieldCombo] = useState(false);
|
||||
const [openValueFieldCombo, setOpenValueFieldCombo] = useState(false);
|
||||
|
||||
// 전체 테이블 목록 로드
|
||||
// 연쇄 드롭다운 설정 상태 (SelectBasicConfigPanel과 동일)
|
||||
const [cascadingEnabled, setCascadingEnabled] = useState(!!config.cascadingRelationCode);
|
||||
|
||||
// 연쇄관계 목록
|
||||
const [relationList, setRelationList] = useState<CascadingRelation[]>([]);
|
||||
const [loadingRelations, setLoadingRelations] = useState(false);
|
||||
|
||||
// 테이블 타입 관리에서 설정된 참조 테이블 정보
|
||||
const [referenceInfo, setReferenceInfo] = useState<{
|
||||
referenceTable: string;
|
||||
referenceColumn: string;
|
||||
displayColumn: string;
|
||||
isLoading: boolean;
|
||||
isAutoLoaded: boolean; // 자동 로드되었는지 여부
|
||||
error: string | null;
|
||||
}>({
|
||||
referenceTable: "",
|
||||
referenceColumn: "",
|
||||
displayColumn: "",
|
||||
isLoading: false,
|
||||
isAutoLoaded: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
// 자동 설정 완료 여부 (중복 방지)
|
||||
const autoConfigApplied = useRef(false);
|
||||
|
||||
// 테이블 패널에서 드래그한 컴포넌트인 경우, 참조 테이블 정보 자동 로드
|
||||
useEffect(() => {
|
||||
const loadReferenceInfo = async () => {
|
||||
// currentComponent에서 소스 테이블/컬럼 정보 추출
|
||||
const sourceTableName = currentComponent?.tableName || currentComponent?.sourceTableName;
|
||||
const sourceColumnName = currentComponent?.columnName || currentComponent?.sourceColumnName;
|
||||
|
||||
if (!sourceTableName || !sourceColumnName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 이미 config에 테이블명이 설정되어 있고, 자동 로드가 완료되었다면 스킵
|
||||
if (config.tableName && autoConfigApplied.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setReferenceInfo(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
// 테이블 타입 관리에서 컬럼 정보 조회
|
||||
const columns = await tableTypeApi.getColumns(sourceTableName);
|
||||
const columnInfo = columns.find((col: any) =>
|
||||
(col.columnName || col.column_name) === sourceColumnName
|
||||
);
|
||||
|
||||
if (columnInfo) {
|
||||
const refTable = columnInfo.referenceTable || columnInfo.reference_table || "";
|
||||
const refColumn = columnInfo.referenceColumn || columnInfo.reference_column || "";
|
||||
const dispColumn = columnInfo.displayColumn || columnInfo.display_column || "";
|
||||
|
||||
// detailSettings에서도 정보 확인 (JSON 파싱)
|
||||
let detailSettings: any = {};
|
||||
if (columnInfo.detailSettings) {
|
||||
try {
|
||||
if (typeof columnInfo.detailSettings === 'string') {
|
||||
detailSettings = JSON.parse(columnInfo.detailSettings);
|
||||
} else {
|
||||
detailSettings = columnInfo.detailSettings;
|
||||
}
|
||||
} catch {
|
||||
// JSON 파싱 실패 시 무시
|
||||
}
|
||||
}
|
||||
|
||||
const finalRefTable = refTable || detailSettings.referenceTable || "";
|
||||
const finalRefColumn = refColumn || detailSettings.referenceColumn || "id";
|
||||
const finalDispColumn = dispColumn || detailSettings.displayColumn || "name";
|
||||
|
||||
setReferenceInfo({
|
||||
referenceTable: finalRefTable,
|
||||
referenceColumn: finalRefColumn,
|
||||
displayColumn: finalDispColumn,
|
||||
isLoading: false,
|
||||
isAutoLoaded: true,
|
||||
error: null,
|
||||
});
|
||||
|
||||
// 참조 테이블 정보로 config 자동 설정 (config에 아직 설정이 없는 경우만)
|
||||
if (finalRefTable && !config.tableName) {
|
||||
autoConfigApplied.current = true;
|
||||
const newConfig: EntitySearchInputConfig = {
|
||||
...localConfig,
|
||||
tableName: finalRefTable,
|
||||
valueField: finalRefColumn,
|
||||
displayField: finalDispColumn,
|
||||
};
|
||||
setLocalConfig(newConfig);
|
||||
onConfigChange(newConfig);
|
||||
}
|
||||
} else {
|
||||
setReferenceInfo({
|
||||
referenceTable: "",
|
||||
referenceColumn: "",
|
||||
displayColumn: "",
|
||||
isLoading: false,
|
||||
isAutoLoaded: false,
|
||||
error: "컬럼 정보를 찾을 수 없습니다.",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("참조 테이블 정보 로드 실패:", error);
|
||||
setReferenceInfo({
|
||||
referenceTable: "",
|
||||
referenceColumn: "",
|
||||
displayColumn: "",
|
||||
isLoading: false,
|
||||
isAutoLoaded: false,
|
||||
error: "참조 테이블 정보 로드 실패",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadReferenceInfo();
|
||||
}, [currentComponent?.tableName, currentComponent?.columnName, currentComponent?.sourceTableName, currentComponent?.sourceColumnName]);
|
||||
|
||||
// 연쇄 관계 목록 로드
|
||||
useEffect(() => {
|
||||
if (cascadingEnabled && relationList.length === 0) {
|
||||
loadRelationList();
|
||||
}
|
||||
}, [cascadingEnabled]);
|
||||
|
||||
// 연쇄 관계 목록 로드 함수
|
||||
const loadRelationList = async () => {
|
||||
setLoadingRelations(true);
|
||||
try {
|
||||
const response = await cascadingRelationApi.getList("Y");
|
||||
if (response.success && response.data) {
|
||||
setRelationList(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("연쇄 관계 목록 로드 실패:", error);
|
||||
} finally {
|
||||
setLoadingRelations(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 전체 테이블 목록 로드 (수동 선택을 위해)
|
||||
useEffect(() => {
|
||||
const loadTables = async () => {
|
||||
setIsLoadingTables(true);
|
||||
|
|
@ -73,8 +225,11 @@ export function EntitySearchInputConfigPanel({
|
|||
loadColumns();
|
||||
}, [localConfig.tableName]);
|
||||
|
||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||
useEffect(() => {
|
||||
setLocalConfig(config);
|
||||
// 연쇄 드롭다운 설정 동기화
|
||||
setCascadingEnabled(!!config.cascadingRelationCode);
|
||||
}, [config]);
|
||||
|
||||
const updateConfig = (updates: Partial<EntitySearchInputConfig>) => {
|
||||
|
|
@ -83,6 +238,71 @@ export function EntitySearchInputConfigPanel({
|
|||
onConfigChange(newConfig);
|
||||
};
|
||||
|
||||
// 연쇄 드롭다운 활성화/비활성화
|
||||
const handleCascadingToggle = (enabled: boolean) => {
|
||||
setCascadingEnabled(enabled);
|
||||
|
||||
if (!enabled) {
|
||||
// 비활성화 시 관계 설정 제거
|
||||
const newConfig = {
|
||||
...localConfig,
|
||||
cascadingRelationCode: undefined,
|
||||
cascadingRole: undefined,
|
||||
cascadingParentField: undefined,
|
||||
};
|
||||
setLocalConfig(newConfig);
|
||||
onConfigChange(newConfig);
|
||||
} else {
|
||||
// 활성화 시 관계 목록 로드
|
||||
loadRelationList();
|
||||
}
|
||||
};
|
||||
|
||||
// 연쇄 관계 선택 (역할은 별도 선택)
|
||||
const handleRelationSelect = (code: string) => {
|
||||
const newConfig = {
|
||||
...localConfig,
|
||||
cascadingRelationCode: code || undefined,
|
||||
cascadingRole: undefined, // 역할은 별도로 선택
|
||||
cascadingParentField: undefined,
|
||||
};
|
||||
setLocalConfig(newConfig);
|
||||
onConfigChange(newConfig);
|
||||
};
|
||||
|
||||
// 역할 변경 핸들러
|
||||
const handleRoleChange = (role: "parent" | "child") => {
|
||||
const selectedRel = relationList.find(r => r.relation_code === localConfig.cascadingRelationCode);
|
||||
|
||||
if (role === "parent" && selectedRel) {
|
||||
// 부모 역할: 부모 테이블 정보로 설정
|
||||
const newConfig = {
|
||||
...localConfig,
|
||||
cascadingRole: role,
|
||||
tableName: selectedRel.parent_table,
|
||||
valueField: selectedRel.parent_value_column,
|
||||
displayField: selectedRel.parent_label_column || selectedRel.parent_value_column,
|
||||
cascadingParentField: undefined, // 부모 역할이면 부모 필드 필요 없음
|
||||
};
|
||||
setLocalConfig(newConfig);
|
||||
onConfigChange(newConfig);
|
||||
} else if (role === "child" && selectedRel) {
|
||||
// 자식 역할: 자식 테이블 정보로 설정
|
||||
const newConfig = {
|
||||
...localConfig,
|
||||
cascadingRole: role,
|
||||
tableName: selectedRel.child_table,
|
||||
valueField: selectedRel.child_value_column,
|
||||
displayField: selectedRel.child_label_column || selectedRel.child_value_column,
|
||||
};
|
||||
setLocalConfig(newConfig);
|
||||
onConfigChange(newConfig);
|
||||
}
|
||||
};
|
||||
|
||||
// 선택된 관계 정보
|
||||
const selectedRelation = relationList.find(r => r.relation_code === localConfig.cascadingRelationCode);
|
||||
|
||||
const addSearchField = () => {
|
||||
const fields = localConfig.searchFields || [];
|
||||
updateConfig({ searchFields: [...fields, ""] });
|
||||
|
|
@ -134,10 +354,213 @@ export function EntitySearchInputConfigPanel({
|
|||
updateConfig({ additionalFields: fields });
|
||||
};
|
||||
|
||||
// 자동 로드된 참조 테이블 정보가 있는지 확인
|
||||
const hasAutoReference = referenceInfo.isAutoLoaded && referenceInfo.referenceTable;
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-4">
|
||||
{/* 연쇄 드롭다운 설정 - SelectConfigPanel과 동일한 패턴 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link2 className="h-4 w-4" />
|
||||
<h4 className="text-sm font-medium">연쇄 드롭다운</h4>
|
||||
</div>
|
||||
<Switch
|
||||
checked={cascadingEnabled}
|
||||
onCheckedChange={handleCascadingToggle}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
다른 필드의 값에 따라 옵션이 동적으로 변경됩니다. (예: 창고 선택 → 해당 창고의 위치만 표시)
|
||||
</p>
|
||||
|
||||
{cascadingEnabled && (
|
||||
<div className="space-y-3 rounded-md border p-3 bg-muted/30">
|
||||
{/* 관계 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">연쇄 관계 선택</Label>
|
||||
<Select
|
||||
value={localConfig.cascadingRelationCode || ""}
|
||||
onValueChange={handleRelationSelect}
|
||||
>
|
||||
<SelectTrigger className="text-xs">
|
||||
<SelectValue placeholder={loadingRelations ? "로딩 중..." : "관계 선택"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{relationList.map((relation) => (
|
||||
<SelectItem key={relation.relation_code} value={relation.relation_code}>
|
||||
<div className="flex flex-col">
|
||||
<span>{relation.relation_name}</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{relation.parent_table} → {relation.child_table}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 역할 선택 */}
|
||||
{localConfig.cascadingRelationCode && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">역할 선택</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={localConfig.cascadingRole === "parent" ? "default" : "outline"}
|
||||
className="flex-1 text-xs"
|
||||
onClick={() => handleRoleChange("parent")}
|
||||
>
|
||||
부모 (상위 선택)
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={localConfig.cascadingRole === "child" ? "default" : "outline"}
|
||||
className="flex-1 text-xs"
|
||||
onClick={() => handleRoleChange("child")}
|
||||
>
|
||||
자식 (하위 선택)
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{localConfig.cascadingRole === "parent"
|
||||
? "이 필드가 상위 선택 역할을 합니다. (예: 창고 선택)"
|
||||
: localConfig.cascadingRole === "child"
|
||||
? "이 필드는 상위 필드 값에 따라 옵션이 변경됩니다. (예: 위치 선택)"
|
||||
: "이 필드의 역할을 선택하세요."}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 부모 필드 설정 (자식 역할일 때만) */}
|
||||
{localConfig.cascadingRelationCode && localConfig.cascadingRole === "child" && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">부모 필드명</Label>
|
||||
<Input
|
||||
value={localConfig.cascadingParentField || ""}
|
||||
onChange={(e) => updateConfig({ cascadingParentField: e.target.value || undefined })}
|
||||
placeholder="예: warehouse_code"
|
||||
className="text-xs"
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
이 드롭다운의 옵션을 결정할 부모 필드의 컬럼명을 입력하세요.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 선택된 관계 정보 표시 */}
|
||||
{selectedRelation && localConfig.cascadingRole && (
|
||||
<div className="bg-background space-y-1 rounded-md p-2 text-xs">
|
||||
{localConfig.cascadingRole === "parent" ? (
|
||||
<>
|
||||
<div className="font-medium text-blue-600">부모 역할 (상위 선택)</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">데이터 소스:</span>{" "}
|
||||
<span className="font-medium">{selectedRelation.parent_table}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">저장 값:</span>{" "}
|
||||
<span className="font-medium">{selectedRelation.parent_value_column}</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="font-medium text-green-600">자식 역할 (하위 선택)</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">데이터 소스:</span>{" "}
|
||||
<span className="font-medium">{selectedRelation.child_table}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">저장 값:</span>{" "}
|
||||
<span className="font-medium">{selectedRelation.child_value_column}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">필터 컬럼:</span>{" "}
|
||||
<span className="font-medium">{selectedRelation.child_filter_column}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 관계 관리 페이지 링크 */}
|
||||
<div className="flex justify-end">
|
||||
<Link href="/admin/cascading-relations" target="_blank">
|
||||
<Button variant="link" size="sm" className="h-auto p-0 text-xs">
|
||||
<ExternalLink className="mr-1 h-3 w-3" />
|
||||
관계 관리
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 구분선 - 연쇄 드롭다운 비활성화 시에만 표시 */}
|
||||
{!cascadingEnabled && (
|
||||
<div className="border-t pt-4">
|
||||
<p className="text-[10px] text-muted-foreground mb-4">
|
||||
아래에서 직접 테이블/필드를 설정하세요.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 참조 테이블 자동 로드 정보 표시 */}
|
||||
{referenceInfo.isLoading && (
|
||||
<div className="bg-muted/50 rounded-md border p-3">
|
||||
<p className="text-xs text-muted-foreground">참조 테이블 정보 로딩 중...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasAutoReference && !cascadingEnabled && (
|
||||
<div className="bg-primary/5 rounded-md border border-primary/20 p-3 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-4 w-4 text-primary" />
|
||||
<span className="text-xs font-medium text-primary">테이블 타입에서 자동 설정됨</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div>
|
||||
<span className="text-muted-foreground">참조 테이블:</span>
|
||||
<div className="font-medium">{referenceInfo.referenceTable}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">값 필드:</span>
|
||||
<div className="font-medium">{referenceInfo.referenceColumn || "id"}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">표시 필드:</span>
|
||||
<div className="font-medium">{referenceInfo.displayColumn || "name"}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
소스: {currentComponent?.tableName}.{currentComponent?.columnName}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{referenceInfo.error && !hasAutoReference && !cascadingEnabled && (
|
||||
<div className="bg-amber-500/10 rounded-md border border-amber-500/20 p-3">
|
||||
<p className="text-xs text-amber-700 flex items-center gap-1">
|
||||
<Info className="h-3 w-3" />
|
||||
{referenceInfo.error}
|
||||
</p>
|
||||
<p className="text-[10px] text-muted-foreground mt-1">
|
||||
테이블을 수동으로 선택하세요.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs sm:text-sm">테이블명 *</Label>
|
||||
<Label className="text-xs sm:text-sm">
|
||||
테이블명 *
|
||||
{hasAutoReference && (
|
||||
<span className="text-[10px] text-muted-foreground ml-2">(자동 설정됨)</span>
|
||||
)}
|
||||
</Label>
|
||||
<Popover open={openTableCombo} onOpenChange={setOpenTableCombo}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -10,5 +10,10 @@ export interface EntitySearchInputConfig {
|
|||
modalColumns?: string[];
|
||||
showAdditionalInfo?: boolean;
|
||||
additionalFields?: string[];
|
||||
|
||||
// 연쇄관계 설정 (cascading_relation 테이블과 연동)
|
||||
cascadingRelationCode?: string; // 연쇄관계 코드 (WAREHOUSE_LOCATION 등)
|
||||
cascadingRole?: "parent" | "child"; // 역할 (부모/자식)
|
||||
cascadingParentField?: string; // 부모 필드의 컬럼명 (자식 역할일 때만 사용)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,13 @@ export interface EntitySearchInputProps {
|
|||
filterCondition?: Record<string, any>; // 추가 WHERE 조건
|
||||
companyCode?: string; // 멀티테넌시
|
||||
|
||||
// 연쇄관계 설정
|
||||
cascadingRelationCode?: string; // 연쇄관계 코드 (cascading_relation 테이블)
|
||||
cascadingRole?: "parent" | "child"; // 역할 (부모/자식)
|
||||
parentFieldId?: string; // 부모 필드의 컬럼명 (자식 역할일 때, formData에서 값 추출용)
|
||||
parentValue?: any; // 부모 필드의 현재 값 (직접 전달)
|
||||
formData?: Record<string, any>; // 전체 폼 데이터 (부모 값 추출용)
|
||||
|
||||
// 선택된 값
|
||||
value?: any;
|
||||
onChange?: (value: any, fullData?: any) => void;
|
||||
|
|
|
|||
|
|
@ -16,9 +16,16 @@ declare global {
|
|||
masterData: Record<string, any> | null;
|
||||
config: RelatedDataButtonsConfig | null;
|
||||
};
|
||||
// 🆕 RelatedDataButtons가 대상으로 하는 테이블 목록 (전역 레지스트리)
|
||||
__relatedButtonsTargetTables?: Set<string>;
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 레지스트리 초기화
|
||||
if (typeof window !== "undefined" && !window.__relatedButtonsTargetTables) {
|
||||
window.__relatedButtonsTargetTables = new Set();
|
||||
}
|
||||
|
||||
interface RelatedDataButtonsComponentProps {
|
||||
config: RelatedDataButtonsConfig;
|
||||
className?: string;
|
||||
|
|
@ -59,11 +66,54 @@ export const RelatedDataButtonsComponent: React.FC<RelatedDataButtonsComponentPr
|
|||
setMasterData(null);
|
||||
setButtons([]);
|
||||
setSelectedId(null);
|
||||
setSelectedItem(null);
|
||||
|
||||
// 🆕 좌측 데이터가 없을 때 대상 테이블에 빈 상태 알림
|
||||
if (config.events?.targetTable) {
|
||||
window.dispatchEvent(new CustomEvent("related-button-select", {
|
||||
detail: {
|
||||
targetTable: config.events.targetTable,
|
||||
filterColumn: config.events.targetFilterColumn,
|
||||
filterValue: null, // null로 설정하여 빈 상태 표시
|
||||
selectedData: null,
|
||||
},
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setMasterData(splitPanelContext.selectedLeftData);
|
||||
}, [splitPanelContext?.selectedLeftData]);
|
||||
}, [splitPanelContext?.selectedLeftData, config.events]);
|
||||
|
||||
// 🆕 컴포넌트 마운트 시 대상 테이블에 필터 필요 알림
|
||||
useEffect(() => {
|
||||
if (config.events?.targetTable) {
|
||||
// 전역 레지스트리에 등록
|
||||
window.__relatedButtonsTargetTables?.add(config.events.targetTable);
|
||||
|
||||
// 이벤트도 발생 (이미 마운트된 테이블 컴포넌트를 위해)
|
||||
window.dispatchEvent(new CustomEvent("related-button-register", {
|
||||
detail: {
|
||||
targetTable: config.events.targetTable,
|
||||
filterColumn: config.events.targetFilterColumn,
|
||||
},
|
||||
}));
|
||||
console.log("📝 [RelatedDataButtons] 대상 테이블에 필터 등록:", config.events.targetTable);
|
||||
}
|
||||
|
||||
return () => {
|
||||
// 컴포넌트 언마운트 시 등록 해제
|
||||
if (config.events?.targetTable) {
|
||||
window.__relatedButtonsTargetTables?.delete(config.events.targetTable);
|
||||
|
||||
window.dispatchEvent(new CustomEvent("related-button-unregister", {
|
||||
detail: {
|
||||
targetTable: config.events.targetTable,
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
}, [config.events?.targetTable, config.events?.targetFilterColumn]);
|
||||
|
||||
// 버튼 데이터 로드
|
||||
const loadButtons = useCallback(async () => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ import { codeCache } from "@/lib/caching/codeCache";
|
|||
import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization";
|
||||
import { getFullImageUrl } from "@/lib/api/client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
// 🆕 RelatedDataButtons 전역 레지스트리 타입 선언
|
||||
declare global {
|
||||
interface Window {
|
||||
__relatedButtonsTargetTables?: Set<string>;
|
||||
}
|
||||
}
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
|
|
@ -310,6 +317,15 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
filterValue: any;
|
||||
} | null>(null);
|
||||
|
||||
// 🆕 RelatedDataButtons가 이 테이블을 대상으로 등록되어 있는지 여부
|
||||
const [isRelatedButtonTarget, setIsRelatedButtonTarget] = useState(() => {
|
||||
// 초기값: 전역 레지스트리에서 확인
|
||||
if (typeof window !== "undefined" && window.__relatedButtonsTargetTables) {
|
||||
return window.__relatedButtonsTargetTables.has(tableConfig.selectedTable || "");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// TableOptions Context
|
||||
const { registerTable, unregisterTable, updateTableDataCount } = useTableOptions();
|
||||
const [filters, setFilters] = useState<TableFilter[]>([]);
|
||||
|
|
@ -1554,6 +1570,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
// 🆕 RelatedDataButtons 대상이지만 아직 버튼이 선택되지 않은 경우
|
||||
// → 빈 데이터 표시 (모든 데이터를 보여주지 않음)
|
||||
if (isRelatedButtonTarget && !relatedButtonFilter) {
|
||||
console.log("⚠️ [TableList] RelatedDataButtons 대상이지만 버튼 미선택 → 빈 데이터 표시");
|
||||
setData([]);
|
||||
setTotalItems(0);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 🆕 RelatedDataButtons 필터 값 준비
|
||||
let relatedButtonFilterValues: Record<string, any> = {};
|
||||
if (relatedButtonFilter) {
|
||||
|
|
@ -1767,6 +1793,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
splitPanelContext?.selectedLeftData,
|
||||
// 🆕 RelatedDataButtons 필터 추가
|
||||
relatedButtonFilter,
|
||||
isRelatedButtonTarget,
|
||||
]);
|
||||
|
||||
const fetchTableDataDebounced = useCallback(
|
||||
|
|
@ -4783,6 +4810,45 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
};
|
||||
}, [tableConfig.selectedTable, isDesignMode]);
|
||||
|
||||
// 🆕 테이블명 변경 시 전역 레지스트리에서 확인
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined" && window.__relatedButtonsTargetTables && tableConfig.selectedTable) {
|
||||
const isTarget = window.__relatedButtonsTargetTables.has(tableConfig.selectedTable);
|
||||
if (isTarget) {
|
||||
console.log("📝 [TableList] 전역 레지스트리에서 RelatedDataButtons 대상 확인:", tableConfig.selectedTable);
|
||||
setIsRelatedButtonTarget(true);
|
||||
}
|
||||
}
|
||||
}, [tableConfig.selectedTable]);
|
||||
|
||||
// 🆕 RelatedDataButtons 등록/해제 이벤트 리스너
|
||||
useEffect(() => {
|
||||
const handleRelatedButtonRegister = (event: CustomEvent) => {
|
||||
const { targetTable } = event.detail || {};
|
||||
if (targetTable === tableConfig.selectedTable) {
|
||||
console.log("📝 [TableList] RelatedDataButtons 대상으로 등록됨:", tableConfig.selectedTable);
|
||||
setIsRelatedButtonTarget(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRelatedButtonUnregister = (event: CustomEvent) => {
|
||||
const { targetTable } = event.detail || {};
|
||||
if (targetTable === tableConfig.selectedTable) {
|
||||
console.log("📝 [TableList] RelatedDataButtons 대상에서 해제됨:", tableConfig.selectedTable);
|
||||
setIsRelatedButtonTarget(false);
|
||||
setRelatedButtonFilter(null);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("related-button-register" as any, handleRelatedButtonRegister);
|
||||
window.addEventListener("related-button-unregister" as any, handleRelatedButtonUnregister);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("related-button-register" as any, handleRelatedButtonRegister);
|
||||
window.removeEventListener("related-button-unregister" as any, handleRelatedButtonUnregister);
|
||||
};
|
||||
}, [tableConfig.selectedTable]);
|
||||
|
||||
// 🆕 RelatedDataButtons 선택 이벤트 리스너 (버튼 선택 시 테이블 필터링)
|
||||
useEffect(() => {
|
||||
const handleRelatedButtonSelect = (event: CustomEvent) => {
|
||||
|
|
@ -4790,12 +4856,20 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
// 이 테이블이 대상 테이블인지 확인
|
||||
if (targetTable === tableConfig.selectedTable) {
|
||||
console.log("📌 [TableList] RelatedDataButtons 필터 적용:", {
|
||||
tableName: tableConfig.selectedTable,
|
||||
filterColumn,
|
||||
filterValue,
|
||||
});
|
||||
setRelatedButtonFilter({ filterColumn, filterValue });
|
||||
// filterValue가 null이면 선택 해제 (빈 상태)
|
||||
if (filterValue === null || filterValue === undefined) {
|
||||
console.log("📌 [TableList] RelatedDataButtons 선택 해제 (빈 상태):", tableConfig.selectedTable);
|
||||
setRelatedButtonFilter(null);
|
||||
setIsRelatedButtonTarget(true); // 대상으로 등록은 유지
|
||||
} else {
|
||||
console.log("📌 [TableList] RelatedDataButtons 필터 적용:", {
|
||||
tableName: tableConfig.selectedTable,
|
||||
filterColumn,
|
||||
filterValue,
|
||||
});
|
||||
setRelatedButtonFilter({ filterColumn, filterValue });
|
||||
setIsRelatedButtonTarget(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -4808,8 +4882,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
// 🆕 relatedButtonFilter 변경 시 데이터 다시 로드
|
||||
useEffect(() => {
|
||||
if (relatedButtonFilter && !isDesignMode) {
|
||||
console.log("🔄 [TableList] RelatedDataButtons 필터 변경으로 데이터 새로고침:", relatedButtonFilter);
|
||||
if (!isDesignMode) {
|
||||
// relatedButtonFilter가 있으면 데이터 로드, null이면 빈 상태 (setRefreshTrigger로 트리거)
|
||||
console.log("🔄 [TableList] RelatedDataButtons 상태 변경:", {
|
||||
relatedButtonFilter,
|
||||
isRelatedButtonTarget
|
||||
});
|
||||
setRefreshTrigger((prev) => prev + 1);
|
||||
}
|
||||
}, [relatedButtonFilter, isDesignMode]);
|
||||
|
|
|
|||
|
|
@ -397,7 +397,6 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
|||
// 🆕 수주 등록 관련 컴포넌트들은 간단한 인터페이스 사용
|
||||
const isSimpleConfigPanel = [
|
||||
"autocomplete-search-input",
|
||||
"entity-search-input",
|
||||
"modal-repeater-table",
|
||||
"conditional-container",
|
||||
].includes(componentId);
|
||||
|
|
@ -406,6 +405,19 @@ export const DynamicComponentConfigPanel: React.FC<ComponentConfigPanelProps> =
|
|||
return <ConfigPanelComponent config={config} onConfigChange={onChange} />;
|
||||
}
|
||||
|
||||
// entity-search-input은 currentComponent 정보 필요 (참조 테이블 자동 로드용)
|
||||
// 그리고 allComponents 필요 (연쇄관계 부모 필드 선택용)
|
||||
if (componentId === "entity-search-input") {
|
||||
return (
|
||||
<ConfigPanelComponent
|
||||
config={config}
|
||||
onConfigChange={onChange}
|
||||
currentComponent={currentComponent}
|
||||
allComponents={allComponents}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 🆕 selected-items-detail-input은 특별한 props 사용
|
||||
if (componentId === "selected-items-detail-input") {
|
||||
return (
|
||||
|
|
|
|||
Loading…
Reference in New Issue