엔티티타입 연쇄관계관리 설정 추가
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 { 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] || ""}`}
|
||||
|
||||
Reference in New Issue
Block a user