ERP-node/frontend/components/unified/ConditionalConfigPanel.tsx

360 lines
13 KiB
TypeScript

"use client";
/**
* ConditionalConfigPanel
*
* 비개발자도 쉽게 조건부 표시/숨김/활성화/비활성화를 설정할 수 있는 UI
*
* 사용처:
* - 화면관리 > 상세설정 패널
* - 화면관리 > 속성 패널
*/
import React, { useState, useEffect, useMemo } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
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 { ConditionalConfig } from "@/types/unified-components";
// ===== 타입 정의 =====
interface FieldOption {
id: string;
label: string;
type?: string; // text, number, select, checkbox 등
options?: Array<{ value: string; label: string }>; // select 타입일 경우 옵션들
}
interface ConditionalConfigPanelProps {
/** 현재 조건부 설정 */
config?: ConditionalConfig;
/** 설정 변경 콜백 */
onChange: (config: ConditionalConfig | undefined) => void;
/** 같은 화면에 있는 다른 필드들 (조건 필드로 선택 가능) */
availableFields: FieldOption[];
/** 현재 컴포넌트 ID (자기 자신은 조건 필드에서 제외) */
currentComponentId?: string;
}
// 연산자 옵션
const OPERATORS: Array<{ value: ConditionalConfig["operator"]; label: string; description: string }> = [
{ value: "=", label: "같음", description: "값이 정확히 일치할 때" },
{ value: "!=", label: "다름", description: "값이 일치하지 않을 때" },
{ value: ">", label: "보다 큼", description: "값이 더 클 때 (숫자)" },
{ value: "<", label: "보다 작음", description: "값이 더 작을 때 (숫자)" },
{ value: "in", label: "포함됨", description: "여러 값 중 하나일 때" },
{ value: "notIn", label: "포함 안됨", description: "여러 값 중 아무것도 아닐 때" },
{ value: "isEmpty", label: "비어있음", description: "값이 없을 때" },
{ value: "isNotEmpty", label: "값이 있음", description: "값이 있을 때" },
];
// 동작 옵션
const ACTIONS: Array<{ value: ConditionalConfig["action"]; label: string; description: string }> = [
{ value: "show", label: "표시", description: "조건 만족 시 이 필드를 표시" },
{ value: "hide", label: "숨김", description: "조건 만족 시 이 필드를 숨김" },
{ value: "enable", label: "활성화", description: "조건 만족 시 이 필드를 활성화" },
{ value: "disable", label: "비활성화", description: "조건 만족 시 이 필드를 비활성화" },
];
// ===== 컴포넌트 =====
export function ConditionalConfigPanel({
config,
onChange,
availableFields,
currentComponentId,
}: ConditionalConfigPanelProps) {
// 로컬 상태
const [enabled, setEnabled] = useState(config?.enabled ?? false);
const [field, setField] = useState(config?.field ?? "");
const [operator, setOperator] = useState<ConditionalConfig["operator"]>(config?.operator ?? "=");
const [value, setValue] = useState<string>(String(config?.value ?? ""));
const [action, setAction] = useState<ConditionalConfig["action"]>(config?.action ?? "show");
// 자기 자신을 제외한 필드 목록
const selectableFields = useMemo(() => {
return availableFields.filter((f) => f.id !== currentComponentId);
}, [availableFields, currentComponentId]);
// 선택된 필드 정보
const selectedField = useMemo(() => {
return selectableFields.find((f) => f.id === field);
}, [selectableFields, field]);
// config prop 변경 시 로컬 상태 동기화
useEffect(() => {
setEnabled(config?.enabled ?? false);
setField(config?.field ?? "");
setOperator(config?.operator ?? "=");
setValue(String(config?.value ?? ""));
setAction(config?.action ?? "show");
}, [config]);
// 설정 변경 시 부모에게 알림
const updateConfig = (updates: Partial<ConditionalConfig>) => {
const newConfig: ConditionalConfig = {
enabled: updates.enabled ?? enabled,
field: updates.field ?? field,
operator: updates.operator ?? operator,
value: updates.value ?? value,
action: updates.action ?? action,
};
// enabled가 false이면 undefined 반환 (설정 제거)
if (!newConfig.enabled) {
onChange(undefined);
} else {
onChange(newConfig);
}
};
// 활성화 토글
const handleEnabledChange = (checked: boolean) => {
setEnabled(checked);
updateConfig({ enabled: checked });
};
// 조건 필드 변경
const handleFieldChange = (newField: string) => {
setField(newField);
setValue(""); // 필드 변경 시 값 초기화
updateConfig({ field: newField, value: "" });
};
// 연산자 변경
const handleOperatorChange = (newOperator: ConditionalConfig["operator"]) => {
setOperator(newOperator);
// 비어있음/값이있음 연산자는 value 필요 없음
if (newOperator === "isEmpty" || newOperator === "isNotEmpty") {
setValue("");
updateConfig({ operator: newOperator, value: "" });
} else {
updateConfig({ operator: newOperator });
}
};
// 값 변경
const handleValueChange = (newValue: string) => {
setValue(newValue);
// 타입에 따라 적절한 값으로 변환
let parsedValue: unknown = newValue;
if (selectedField?.type === "number") {
parsedValue = Number(newValue);
} else if (newValue === "true") {
parsedValue = true;
} else if (newValue === "false") {
parsedValue = false;
}
updateConfig({ value: parsedValue });
};
// 동작 변경
const handleActionChange = (newAction: ConditionalConfig["action"]) => {
setAction(newAction);
updateConfig({ action: newAction });
};
// 값 입력 필드 렌더링 (필드 타입에 따라 다르게)
const renderValueInput = () => {
// 비어있음/값이있음은 값 입력 불필요
if (operator === "isEmpty" || operator === "isNotEmpty") {
return (
<div className="text-xs text-muted-foreground italic">
( )
</div>
);
}
// 선택된 필드에 옵션이 있으면 Select로 표시
if (selectedField?.options && selectedField.options.length > 0) {
return (
<Select value={value} onValueChange={handleValueChange}>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="값 선택" />
</SelectTrigger>
<SelectContent>
{selectedField.options.map((opt) => (
<SelectItem key={opt.value} value={opt.value} className="text-xs">
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
// 체크박스 타입이면 true/false Select
if (selectedField?.type === "checkbox" || selectedField?.type === "boolean") {
return (
<Select value={value} onValueChange={handleValueChange}>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="값 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="true" className="text-xs"></SelectItem>
<SelectItem value="false" className="text-xs"> </SelectItem>
</SelectContent>
</Select>
);
}
// 숫자 타입
if (selectedField?.type === "number") {
return (
<Input
type="number"
value={value}
onChange={(e) => handleValueChange(e.target.value)}
placeholder="숫자 입력"
className="h-8 text-xs"
/>
);
}
// 기본: 텍스트 입력
return (
<Input
value={value}
onChange={(e) => handleValueChange(e.target.value)}
placeholder="값 입력"
className="h-8 text-xs"
/>
);
};
return (
<div className="space-y-4">
{/* 헤더 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Zap className="h-4 w-4 text-orange-500" />
<span className="text-sm font-medium"> </span>
<span
className="text-muted-foreground cursor-help"
title="다른 필드의 값에 따라 이 필드를 표시/숨김/활성화/비활성화할 수 있습니다."
>
<HelpCircle className="h-3 w-3" />
</span>
</div>
<Switch
checked={enabled}
onCheckedChange={handleEnabledChange}
aria-label="조건부 표시 활성화"
/>
</div>
{/* 조건 설정 영역 */}
{enabled && (
<div className="space-y-3 rounded-lg border bg-muted/30 p-3">
{/* 조건 필드 선택 */}
<div className="space-y-1.5">
<Label className="text-xs font-medium"> </Label>
<Select value={field} onValueChange={handleFieldChange}>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{selectableFields.length === 0 ? (
<div className="p-2 text-xs text-muted-foreground">
</div>
) : (
selectableFields.map((f) => (
<SelectItem key={f.id} value={f.id} className="text-xs">
{f.label || f.id}
</SelectItem>
))
)}
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground">
</p>
</div>
{/* 연산자 선택 */}
<div className="space-y-1.5">
<Label className="text-xs font-medium"></Label>
<Select value={operator} onValueChange={handleOperatorChange}>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{OPERATORS.map((op) => (
<SelectItem key={op.value} value={op.value} className="text-xs">
<div className="flex flex-col">
<span>{op.label}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 값 입력 */}
<div className="space-y-1.5">
<Label className="text-xs font-medium"></Label>
{renderValueInput()}
</div>
{/* 동작 선택 */}
<div className="space-y-1.5">
<Label className="text-xs font-medium"></Label>
<Select value={action} onValueChange={handleActionChange}>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{ACTIONS.map((act) => (
<SelectItem key={act.value} value={act.value} className="text-xs">
<div className="flex flex-col">
<span>{act.label}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground">
{ACTIONS.find(a => a.value === action)?.label}
</p>
</div>
{/* 미리보기 */}
{field && (
<div className="mt-3 rounded bg-slate-100 p-2">
<p className="text-[10px] font-medium text-slate-600"> :</p>
<p className="text-[11px] text-slate-800">
"{selectableFields.find(f => f.id === field)?.label || field}" {" "}
<span className="font-medium">
{operator === "isEmpty" ? "비어있으면" :
operator === "isNotEmpty" ? "값이 있으면" :
`"${value}"${operator === "=" ? "이면" :
operator === "!=" ? "이 아니면" :
operator === ">" ? "보다 크면" :
operator === "<" ? "보다 작으면" :
operator === "in" ? "에 포함되면" : "에 포함되지 않으면"}`}
</span>{" "}
{" "}
<span className="font-medium text-orange-600">
{action === "show" ? "표시" :
action === "hide" ? "숨김" :
action === "enable" ? "활성화" : "비활성화"}
</span>
</p>
</div>
)}
</div>
)}
</div>
);
}
export default ConditionalConfigPanel;