엔티티타입 연쇄관계관리 설정 추가
This commit is contained in:
@@ -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[]>([]);
|
||||
@@ -30,8 +38,152 @@ export function EntitySearchInputConfigPanel({
|
||||
const [openTableCombo, setOpenTableCombo] = useState(false);
|
||||
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>) => {
|
||||
@@ -82,6 +237,71 @@ export function EntitySearchInputConfigPanel({
|
||||
setLocalConfig(newConfig);
|
||||
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 || [];
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user