diff --git a/frontend/app/(main)/screens/[screenId]/page.tsx b/frontend/app/(main)/screens/[screenId]/page.tsx index dffbd75b..dd9fcc3a 100644 --- a/frontend/app/(main)/screens/[screenId]/page.tsx +++ b/frontend/app/(main)/screens/[screenId]/page.tsx @@ -23,6 +23,7 @@ import { TableSearchWidgetHeightProvider, useTableSearchWidgetHeight } from "@/c import { ScreenContextProvider } from "@/contexts/ScreenContext"; // 컴포넌트 간 통신 import { SplitPanelProvider } from "@/lib/registry/components/split-panel-layout/SplitPanelContext"; // 분할 패널 리사이즈 import { ActiveTabProvider } from "@/contexts/ActiveTabContext"; // 활성 탭 관리 +import { evaluateConditional } from "@/lib/utils/conditionalEvaluator"; // 조건부 표시 평가 function ScreenViewPage() { const params = useParams(); @@ -218,6 +219,67 @@ function ScreenViewPage() { initAutoFill(); }, [layout, user]); + // 🆕 조건부 비활성화/숨김 시 해당 필드 값 초기화 + // 조건 필드들의 값을 추적하여 변경 시에만 실행 + const conditionalFieldValues = useMemo(() => { + if (!layout?.components) return ""; + + // 조건부 설정에 사용되는 필드들의 현재 값을 JSON 문자열로 만들어 비교 + const conditionFields = new Set(); + layout.components.forEach((component) => { + const conditional = (component as any).conditional; + if (conditional?.enabled && conditional.field) { + conditionFields.add(conditional.field); + } + }); + + const values: Record = {}; + conditionFields.forEach((field) => { + values[field] = (formData as Record)[field]; + }); + + return JSON.stringify(values); + }, [layout?.components, formData]); + + useEffect(() => { + if (!layout?.components) return; + + const fieldsToReset: string[] = []; + + layout.components.forEach((component) => { + const conditional = (component as any).conditional; + if (!conditional?.enabled) return; + + const conditionalResult = evaluateConditional( + conditional, + formData as Record, + layout.components, + ); + + // 숨김 또는 비활성화 상태인 경우 + if (!conditionalResult.visible || conditionalResult.disabled) { + const fieldName = (component as any).columnName || component.id; + const currentValue = (formData as Record)[fieldName]; + + // 값이 있으면 초기화 대상에 추가 + if (currentValue !== undefined && currentValue !== "" && currentValue !== null) { + fieldsToReset.push(fieldName); + } + } + }); + + // 초기화할 필드가 있으면 한 번에 처리 + if (fieldsToReset.length > 0) { + setFormData((prev) => { + const updated = { ...prev }; + fieldsToReset.forEach((fieldName) => { + updated[fieldName] = ""; + }); + return updated; + }); + } + }, [conditionalFieldValues, layout?.components]); + // 캔버스 비율 조정 (사용자 화면에 맞게 자동 스케일) - 초기 로딩 시에만 계산 // 브라우저 배율 조정 시 메뉴와 화면이 함께 축소/확대되도록 resize 이벤트는 감지하지 않음 useEffect(() => { @@ -469,9 +531,30 @@ function ScreenViewPage() { <> {/* 일반 컴포넌트들 */} {adjustedComponents.map((component) => { + // 조건부 표시 설정이 있는 경우에만 평가 + const conditional = (component as any).conditional; + let conditionalDisabled = false; + + if (conditional?.enabled) { + const conditionalResult = evaluateConditional( + conditional, + formData as Record, + layout?.components || [], + ); + + // 조건에 따라 숨김 처리 + if (!conditionalResult.visible) { + return null; + } + + // 조건에 따라 비활성화 처리 + conditionalDisabled = conditionalResult.disabled; + } + // 화면 관리 해상도를 사용하므로 위치 조정 불필요 return ( , - allComponents: ComponentData[], -): { visible: boolean; disabled: boolean } { - if (!conditional || !conditional.enabled) { - return { visible: true, disabled: false }; - } - - const { field, operator, value, action } = conditional; - - // 참조 필드의 현재 값 가져오기 - // 필드 ID로 컴포넌트를 찾아 columnName 또는 id로 formData에서 값 조회 - const refComponent = allComponents.find((c) => c.id === field); - const fieldName = (refComponent as any)?.columnName || field; - const fieldValue = formData[fieldName]; - - // 조건 평가 - let conditionMet = false; - switch (operator) { - case "=": - conditionMet = fieldValue === value || String(fieldValue) === String(value); - break; - case "!=": - conditionMet = fieldValue !== value && String(fieldValue) !== String(value); - break; - case ">": - conditionMet = Number(fieldValue) > Number(value); - break; - case "<": - conditionMet = Number(fieldValue) < Number(value); - break; - case "in": - if (Array.isArray(value)) { - conditionMet = value.includes(fieldValue) || value.map(String).includes(String(fieldValue)); - } - break; - case "notIn": - if (Array.isArray(value)) { - conditionMet = !value.includes(fieldValue) && !value.map(String).includes(String(fieldValue)); - } else { - conditionMet = true; - } - break; - case "isEmpty": - conditionMet = - fieldValue === null || - fieldValue === undefined || - fieldValue === "" || - (Array.isArray(fieldValue) && fieldValue.length === 0); - break; - case "isNotEmpty": - conditionMet = - fieldValue !== null && - fieldValue !== undefined && - fieldValue !== "" && - !(Array.isArray(fieldValue) && fieldValue.length === 0); - break; - default: - conditionMet = true; - } - - // 액션에 따른 결과 반환 - switch (action) { - case "show": - return { visible: conditionMet, disabled: false }; - case "hide": - return { visible: !conditionMet, disabled: false }; - case "enable": - return { visible: true, disabled: !conditionMet }; - case "disable": - return { visible: true, disabled: conditionMet }; - default: - return { visible: true, disabled: false }; - } -} +import { evaluateConditional } from "@/lib/utils/conditionalEvaluator"; // 컴포넌트 렌더러들을 강제로 로드하여 레지스트리에 등록 import "@/lib/registry/components/ButtonRenderer"; diff --git a/frontend/components/screen/RealtimePreviewDynamic.tsx b/frontend/components/screen/RealtimePreviewDynamic.tsx index a2537ce0..222de998 100644 --- a/frontend/components/screen/RealtimePreviewDynamic.tsx +++ b/frontend/components/screen/RealtimePreviewDynamic.tsx @@ -64,6 +64,9 @@ interface RealtimePreviewProps { // 🆕 조건부 컨테이너 높이 변화 콜백 onHeightChange?: (componentId: string, newHeight: number) => void; + + // 🆕 조건부 비활성화 상태 + conditionalDisabled?: boolean; } // 동적 위젯 타입 아이콘 (레지스트리에서 조회) @@ -93,7 +96,7 @@ const getWidgetIcon = (widgetType: WebType | undefined): React.ReactNode => { return iconMap[widgetType] || ; }; -export const RealtimePreviewDynamic: React.FC = ({ +const RealtimePreviewDynamicComponent: React.FC = ({ component, isSelected = false, isDesignMode = true, // 기본값은 편집 모드 @@ -128,6 +131,7 @@ export const RealtimePreviewDynamic: React.FC = ({ formData, onFormDataChange, onHeightChange, // 🆕 조건부 컨테이너 높이 변화 콜백 + conditionalDisabled, // 🆕 조건부 비활성화 상태 }) => { const [actualHeight, setActualHeight] = React.useState(null); const contentRef = React.useRef(null); @@ -509,6 +513,7 @@ export const RealtimePreviewDynamic: React.FC = ({ sortOrder={sortOrder} columnOrder={columnOrder} onHeightChange={onHeightChange} + conditionalDisabled={conditionalDisabled} /> @@ -532,6 +537,12 @@ export const RealtimePreviewDynamic: React.FC = ({ ); }; +// React.memo로 래핑하여 불필요한 리렌더링 방지 +export const RealtimePreviewDynamic = React.memo(RealtimePreviewDynamicComponent); + +// displayName 설정 (디버깅용) +RealtimePreviewDynamic.displayName = "RealtimePreviewDynamic"; + // 기존 RealtimePreview와의 호환성을 위한 export export { RealtimePreviewDynamic as RealtimePreview }; export default RealtimePreviewDynamic; diff --git a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx index 83dde425..35be3808 100644 --- a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx +++ b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx @@ -1548,18 +1548,67 @@ export const UnifiedPropertiesPanel: React.FC = ({ action: "show", } } - onChange={(newConfig: ConditionalConfig) => { + onChange={(newConfig: ConditionalConfig | undefined) => { handleUpdate("conditional", newConfig); }} availableFields={ allComponents - ?.filter((c) => c.type === "widget" && c.id !== selectedComponent.id) - .map((c) => ({ - id: (c as any).columnName || c.id, - label: (c as any).label || c.id, - type: (c as any).widgetType || "text", - })) || [] + ?.filter((c) => { + // 자기 자신 제외 + if (c.id === selectedComponent.id) return false; + // widget 타입 또는 component 타입 (Unified 컴포넌트 포함) + return c.type === "widget" || c.type === "component"; + }) + .map((c) => { + const widgetType = (c as any).widgetType || (c as any).componentType || "text"; + const config = (c as any).componentConfig || (c as any).webTypeConfig || {}; + const detailSettings = (c as any).detailSettings || {}; + + // 정적 옵션 추출 (select, dropdown, radio, entity 등) + let options: Array<{ value: string; label: string }> | undefined; + + // Unified 컴포넌트의 경우 + if (config.options && Array.isArray(config.options)) { + options = config.options; + } + // 레거시 컴포넌트의 경우 + else if ((c as any).options && Array.isArray((c as any).options)) { + options = (c as any).options; + } + + // 엔티티 정보 추출 (config > detailSettings > 직접 속성 순으로 우선순위) + const entityTable = + config.entityTable || + detailSettings.referenceTable || + (c as any).entityTable || + (c as any).referenceTable; + const entityValueColumn = + config.entityValueColumn || + detailSettings.referenceColumn || + (c as any).entityValueColumn || + (c as any).referenceColumn; + const entityLabelColumn = + config.entityLabelColumn || + detailSettings.displayColumn || + (c as any).entityLabelColumn || + (c as any).displayColumn; + + // 공통코드 정보 추출 + const codeGroup = config.codeGroup || detailSettings.codeGroup || (c as any).codeGroup; + + return { + id: (c as any).columnName || c.id, + label: (c as any).label || config.label || c.id, + type: widgetType, + options, + entityTable, + entityValueColumn, + entityLabelColumn, + codeGroup, + }; + }) || [] } + currentComponentId={selectedComponent.id} /> diff --git a/frontend/components/unified/ConditionalConfigPanel.tsx b/frontend/components/unified/ConditionalConfigPanel.tsx index ba6dee1a..314c2ac8 100644 --- a/frontend/components/unified/ConditionalConfigPanel.tsx +++ b/frontend/components/unified/ConditionalConfigPanel.tsx @@ -17,16 +17,24 @@ import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; -import { Zap, Plus, Trash2, HelpCircle } from "lucide-react"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { Zap, Plus, Trash2, HelpCircle, Check, ChevronsUpDown } from "lucide-react"; import { ConditionalConfig } from "@/types/unified-components"; +import { cn } from "@/lib/utils"; // ===== 타입 정의 ===== interface FieldOption { id: string; label: string; - type?: string; // text, number, select, checkbox 등 + type?: string; // text, number, select, checkbox, entity, code 등 options?: Array<{ value: string; label: string }>; // select 타입일 경우 옵션들 + // 동적 옵션 로드를 위한 정보 + entityTable?: string; + entityValueColumn?: string; + entityLabelColumn?: string; + codeGroup?: string; } interface ConditionalConfigPanelProps { @@ -85,6 +93,86 @@ export function ConditionalConfigPanel({ return selectableFields.find((f) => f.id === field); }, [selectableFields, field]); + // 동적 옵션 로드 상태 + const [dynamicOptions, setDynamicOptions] = useState>([]); + const [loadingOptions, setLoadingOptions] = useState(false); + + // Combobox 열림 상태 + const [comboboxOpen, setComboboxOpen] = useState(false); + + // 엔티티/공통코드 필드 선택 시 동적으로 옵션 로드 + useEffect(() => { + const loadDynamicOptions = async () => { + if (!selectedField) { + setDynamicOptions([]); + return; + } + + // 정적 옵션이 있으면 사용 + if (selectedField.options && selectedField.options.length > 0) { + setDynamicOptions([]); + return; + } + + // 엔티티 타입 (타입이 entity이거나, entityTable이 있으면 엔티티로 간주) + if (selectedField.entityTable) { + setLoadingOptions(true); + try { + const { apiClient } = await import("@/lib/api/client"); + const valueCol = selectedField.entityValueColumn || "id"; + const labelCol = selectedField.entityLabelColumn || "name"; + const response = await apiClient.get(`/entity/${selectedField.entityTable}/options`, { + params: { value: valueCol, label: labelCol }, + }); + if (response.data.success && response.data.data) { + setDynamicOptions(response.data.data); + } + } catch (error) { + console.error("엔티티 옵션 로드 실패:", error); + setDynamicOptions([]); + } finally { + setLoadingOptions(false); + } + return; + } + + // 공통코드 타입 (타입이 code이거나, codeGroup이 있으면 공통코드로 간주) + if (selectedField.codeGroup) { + setLoadingOptions(true); + try { + const { apiClient } = await import("@/lib/api/client"); + const response = await apiClient.get(`/common-codes/${selectedField.codeGroup}/items`); + if (response.data.success && response.data.data) { + setDynamicOptions( + response.data.data.map((item: { code: string; codeName: string }) => ({ + value: item.code, + label: item.codeName, + })) + ); + } + } catch (error) { + console.error("공통코드 옵션 로드 실패:", error); + setDynamicOptions([]); + } finally { + setLoadingOptions(false); + } + return; + } + + setDynamicOptions([]); + }; + + loadDynamicOptions(); + }, [selectedField?.id, selectedField?.entityTable, selectedField?.entityValueColumn, selectedField?.entityLabelColumn, selectedField?.codeGroup]); + + // 최종 옵션 (정적 + 동적) + const fieldOptions = useMemo(() => { + if (selectedField?.options && selectedField.options.length > 0) { + return selectedField.options; + } + return dynamicOptions; + }, [selectedField?.options, dynamicOptions]); + // config prop 변경 시 로컬 상태 동기화 useEffect(() => { setEnabled(config?.enabled ?? false); @@ -171,21 +259,66 @@ export function ConditionalConfigPanel({ ); } - // 선택된 필드에 옵션이 있으면 Select로 표시 - if (selectedField?.options && selectedField.options.length > 0) { + // 옵션 로딩 중 + if (loadingOptions) { return ( - +
+ 옵션 로딩 중... +
+ ); + } + + // 옵션이 있으면 검색 가능한 Combobox로 표시 + if (fieldOptions.length > 0) { + const selectedOption = fieldOptions.find((opt) => opt.value === value); + + return ( + + + + + + + + + + 검색 결과가 없습니다 + + + {fieldOptions.map((opt) => ( + { + handleValueChange(opt.value); + setComboboxOpen(false); + }} + className="text-xs" + > + + {opt.label} + + ))} + + + + + ); } diff --git a/frontend/components/unified/UnifiedSelect.tsx b/frontend/components/unified/UnifiedSelect.tsx index b0026b35..39da5d13 100644 --- a/frontend/components/unified/UnifiedSelect.tsx +++ b/frontend/components/unified/UnifiedSelect.tsx @@ -460,24 +460,31 @@ export const UnifiedSelect = forwardRef( const [options, setOptions] = useState(config.options || []); const [loading, setLoading] = useState(false); + const [optionsLoaded, setOptionsLoaded] = useState(false); - // 데이터 소스에 따른 옵션 로딩 + // 옵션 로딩에 필요한 값들만 추출 (객체 참조 대신 원시값 사용) + const source = config.source; + const entityTable = config.entityTable; + const entityValueColumn = config.entityValueColumn || config.entityValueField; + const entityLabelColumn = config.entityLabelColumn || config.entityLabelField; + const codeGroup = config.codeGroup; + const table = config.table; + const valueColumn = config.valueColumn; + const labelColumn = config.labelColumn; + const apiEndpoint = config.apiEndpoint; + const staticOptions = config.options; + + // 데이터 소스에 따른 옵션 로딩 (원시값 의존성만 사용) useEffect(() => { - const loadOptions = async () => { - console.log("🎯 UnifiedSelect 전체 props:", props); - console.log("🎯 UnifiedSelect config:", config); - console.log("🎯 UnifiedSelect loadOptions 호출:", { - source: config.source, - entityTable: config.entityTable, - entityValueColumn: config.entityValueColumn, - entityLabelColumn: config.entityLabelColumn, - codeGroup: config.codeGroup, - table: config.table, - config, - }); + // 이미 로드된 경우 스킵 (static 제외) + if (optionsLoaded && source !== "static") { + return; + } - if (config.source === "static") { - setOptions(config.options || []); + const loadOptions = async () => { + if (source === "static") { + setOptions(staticOptions || []); + setOptionsLoaded(true); return; } @@ -485,9 +492,9 @@ export const UnifiedSelect = forwardRef( try { let fetchedOptions: SelectOption[] = []; - if (config.source === "code" && config.codeGroup) { + if (source === "code" && codeGroup) { // 공통코드에서 로드 - const response = await apiClient.get(`/common-codes/${config.codeGroup}/items`); + const response = await apiClient.get(`/common-codes/${codeGroup}/items`); const data = response.data; if (data.success && data.data) { fetchedOptions = data.data.map((item: { code: string; codeName: string }) => ({ @@ -495,37 +502,35 @@ export const UnifiedSelect = forwardRef( label: item.codeName, })); } - } else if (config.source === "db" && config.table) { + } else if (source === "db" && table) { // DB 테이블에서 로드 - const response = await apiClient.get(`/entity/${config.table}/options`, { + const response = await apiClient.get(`/entity/${table}/options`, { params: { - value: config.valueColumn || "id", - label: config.labelColumn || "name", + value: valueColumn || "id", + label: labelColumn || "name", }, }); const data = response.data; if (data.success && data.data) { fetchedOptions = data.data; } - } else if (config.source === "entity" && config.entityTable) { + } else if (source === "entity" && entityTable) { // 엔티티(참조 테이블)에서 로드 - const valueCol = config.entityValueColumn || config.entityValueField || "id"; - const labelCol = config.entityLabelColumn || config.entityLabelField || "name"; - console.log("🔍 Entity 옵션 API 호출:", `/entity/${config.entityTable}/options`, { value: valueCol, label: labelCol }); - const response = await apiClient.get(`/entity/${config.entityTable}/options`, { + const valueCol = entityValueColumn || "id"; + const labelCol = entityLabelColumn || "name"; + const response = await apiClient.get(`/entity/${entityTable}/options`, { params: { value: valueCol, label: labelCol, }, }); const data = response.data; - console.log("🔍 Entity 옵션 API 응답:", data); if (data.success && data.data) { fetchedOptions = data.data; } - } else if (config.source === "api" && config.apiEndpoint) { + } else if (source === "api" && apiEndpoint) { // 외부 API에서 로드 - const response = await apiClient.get(config.apiEndpoint); + const response = await apiClient.get(apiEndpoint); const data = response.data; if (Array.isArray(data)) { fetchedOptions = data; @@ -533,6 +538,7 @@ export const UnifiedSelect = forwardRef( } setOptions(fetchedOptions); + setOptionsLoaded(true); } catch (error) { console.error("옵션 로딩 실패:", error); setOptions([]); @@ -542,7 +548,7 @@ export const UnifiedSelect = forwardRef( }; loadOptions(); - }, [config]); + }, [source, entityTable, entityValueColumn, entityLabelColumn, codeGroup, table, valueColumn, labelColumn, apiEndpoint, staticOptions, optionsLoaded]); // 모드별 컴포넌트 렌더링 const renderSelect = () => { diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index f54bd4bc..4de9d4f7 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -148,6 +148,8 @@ export interface DynamicComponentRendererProps { // 탭 관련 정보 (탭 내부의 컴포넌트에서 사용) parentTabId?: string; // 부모 탭 ID parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID + // 🆕 조건부 비활성화 상태 + conditionalDisabled?: boolean; [key: string]: any; } @@ -183,7 +185,8 @@ export const DynamicComponentRenderer: React.FC = label: (component as any).label, required: (component as any).required, readonly: (component as any).readonly, - disabled: (component as any).disabled || props.disabledFields?.includes(fieldName), + // conditionalDisabled가 true이면 비활성화 + disabled: (component as any).disabled || props.disabledFields?.includes(fieldName) || props.conditionalDisabled, value: currentValue, onChange: handleChange, tableName: (component as any).tableName || props.tableName, diff --git a/frontend/lib/utils/conditionalEvaluator.ts b/frontend/lib/utils/conditionalEvaluator.ts new file mode 100644 index 00000000..ffe39e75 --- /dev/null +++ b/frontend/lib/utils/conditionalEvaluator.ts @@ -0,0 +1,118 @@ +/** + * 조건부 표시 평가 유틸리티 + * 컴포넌트의 조건부 표시 설정을 평가하여 visible/disabled 상태를 반환합니다. + */ + +import { ComponentData } from "@/types/screen"; + +export interface ConditionalResult { + visible: boolean; + disabled: boolean; +} + +export interface ConditionalConfig { + enabled: boolean; + field: string; + operator: "=" | "!=" | ">" | "<" | "in" | "notIn" | "isEmpty" | "isNotEmpty"; + value: string | string[]; + action: "show" | "hide" | "enable" | "disable"; +} + +/** + * 조건부 표시를 평가합니다. + * @param conditional - 컴포넌트의 조건부 설정 + * @param formData - 현재 폼 데이터 + * @param allComponents - 화면의 모든 컴포넌트 (필드 참조용) + * @returns visible/disabled 상태 + */ +export function evaluateConditional( + conditional: ConditionalConfig | undefined, + formData: Record, + allComponents: ComponentData[], +): ConditionalResult { + // 조건부 설정이 없거나 비활성화된 경우 기본값 반환 + if (!conditional || !conditional.enabled) { + return { visible: true, disabled: false }; + } + + const { field, operator, value, action } = conditional; + + // 필드가 설정되지 않은 경우 기본값 반환 + if (!field) { + console.warn("[evaluateConditional] 조건 필드가 설정되지 않음"); + return { visible: true, disabled: false }; + } + + // 참조 필드의 현재 값 가져오기 + // field 값은 columnName 또는 id일 수 있으므로 양쪽으로 찾기 + const refComponent = allComponents.find((c) => { + const columnName = (c as any)?.columnName; + return c.id === field || columnName === field; + }); + + // formData에서 값 조회: columnName 우선, 없으면 field 값 직접 사용 + const fieldName = (refComponent as any)?.columnName || field; + const fieldValue = formData[fieldName]; + + // 조건 평가 + let conditionMet = false; + switch (operator) { + case "=": + conditionMet = fieldValue === value || String(fieldValue) === String(value); + break; + case "!=": + conditionMet = fieldValue !== value && String(fieldValue) !== String(value); + break; + case ">": + conditionMet = Number(fieldValue) > Number(value); + break; + case "<": + conditionMet = Number(fieldValue) < Number(value); + break; + case "in": + if (Array.isArray(value)) { + conditionMet = value.includes(fieldValue) || value.map(String).includes(String(fieldValue)); + } + break; + case "notIn": + if (Array.isArray(value)) { + conditionMet = !value.includes(fieldValue) && !value.map(String).includes(String(fieldValue)); + } else { + conditionMet = true; + } + break; + case "isEmpty": + conditionMet = + fieldValue === null || + fieldValue === undefined || + fieldValue === "" || + (Array.isArray(fieldValue) && fieldValue.length === 0); + break; + case "isNotEmpty": + conditionMet = + fieldValue !== null && + fieldValue !== undefined && + fieldValue !== "" && + !(Array.isArray(fieldValue) && fieldValue.length === 0); + break; + default: + conditionMet = true; + } + + // 액션에 따른 결과 반환 + switch (action) { + case "show": + // 조건이 참이면 표시, 거짓이면 숨김 + return { visible: conditionMet, disabled: false }; + case "hide": + // 조건이 참이면 숨김, 거짓이면 표시 + return { visible: !conditionMet, disabled: false }; + case "enable": + return { visible: true, disabled: !conditionMet }; + case "disable": + return { visible: true, disabled: conditionMet }; + default: + return { visible: true, disabled: false }; + } +} +