조건부 설정 구현

This commit is contained in:
kjs
2025-12-22 10:44:22 +09:00
parent a717f97b34
commit ac526c8578
8 changed files with 459 additions and 133 deletions

View File

@@ -19,84 +19,7 @@ import { FlowVisibilityConfig } from "@/types/control-management";
import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils";
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
// 조건부 표시 평가 함수
function evaluateConditional(
conditional: ComponentData["conditional"],
formData: Record<string, any>,
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";

View File

@@ -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] || <Type className="h-3 w-3" />;
};
export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
const RealtimePreviewDynamicComponent: React.FC<RealtimePreviewProps> = ({
component,
isSelected = false,
isDesignMode = true, // 기본값은 편집 모드
@@ -128,6 +131,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
formData,
onFormDataChange,
onHeightChange, // 🆕 조건부 컨테이너 높이 변화 콜백
conditionalDisabled, // 🆕 조건부 비활성화 상태
}) => {
const [actualHeight, setActualHeight] = React.useState<number | null>(null);
const contentRef = React.useRef<HTMLDivElement>(null);
@@ -509,6 +513,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
sortOrder={sortOrder}
columnOrder={columnOrder}
onHeightChange={onHeightChange}
conditionalDisabled={conditionalDisabled}
/>
</div>
@@ -532,6 +537,12 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
);
};
// React.memo로 래핑하여 불필요한 리렌더링 방지
export const RealtimePreviewDynamic = React.memo(RealtimePreviewDynamicComponent);
// displayName 설정 (디버깅용)
RealtimePreviewDynamic.displayName = "RealtimePreviewDynamic";
// 기존 RealtimePreview와의 호환성을 위한 export
export { RealtimePreviewDynamic as RealtimePreview };
export default RealtimePreviewDynamic;

View File

@@ -1548,18 +1548,67 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
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}
/>
</div>
</div>