엔티티타입 연쇄관계관리 설정 추가

This commit is contained in:
kjs
2025-12-17 11:48:05 +09:00
parent 0832e7b6eb
commit 3d287bb883
10 changed files with 890 additions and 390 deletions

View File

@@ -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] || ""}`}