feat(pop-button): 제어 실행 기능 추가 + 연결 데이터 UX 개선 + 필터 UI 개선
POP 버튼 컴포넌트에서 백엔드 제어관리(node_flows)를 직접 실행할 수 있도록 "제어 실행" 작업 유형을 추가하고, 데이터 수정 시 연결된 컴포넌트 기반으로 필드를 선택하는 UX로 개선한다. [제어 실행 (custom-event 확장)] - ButtonTask에 flowId/flowName 필드 추가 - ControlFlowTaskForm: Combobox(Popover+Command)로 검색/선택 UI - executePopAction: flowId 기반 POST /dataflow/node-flows/:flowId/execute - 기존 eventName 발행 메커니즘은 폴백으로 유지 [연결 데이터 UX 개선] - extractCardFields -> extractConnectedFields 리팩토링 (connections 기반 연결 컴포넌트에서만 필드 추출) - pop-card-list/pop-field/pop-search 타입별 필드 추출 로직 - 시스템 필드(__cart_quantity 등)에 한글 라벨 부여 - UI 라벨: "화면 데이터" -> "연결된 데이터" [pop-card-list 필터 UI] - 필터 조건 레이아웃을 가로 -> 세로 스택으로 변경 - 조건 번호 표시 + 입력 필드 높이 확대 [버그 수정] - apiClient baseURL 이중 /api 경로 수정 - 응답 필드명 camelCase 통일
This commit is contained in:
parent
20ad1d6829
commit
62e11127a7
|
|
@ -322,7 +322,9 @@ export async function executeTaskList(
|
||||||
}
|
}
|
||||||
|
|
||||||
case "custom-event":
|
case "custom-event":
|
||||||
if (task.eventName) {
|
if (task.flowId) {
|
||||||
|
await apiClient.post(`/dataflow/node-flows/${task.flowId}/execute`, {});
|
||||||
|
} else if (task.eventName) {
|
||||||
publish(task.eventName, task.eventPayload ?? {});
|
publish(task.eventName, task.eventPayload ?? {});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,19 @@ import {
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/components/ui/command";
|
||||||
import { PopComponentRegistry } from "@/lib/registry/PopComponentRegistry";
|
import { PopComponentRegistry } from "@/lib/registry/PopComponentRegistry";
|
||||||
import { usePopAction } from "@/hooks/pop/usePopAction";
|
import { usePopAction } from "@/hooks/pop/usePopAction";
|
||||||
import { executeTaskList, type CollectedPayload } from "@/hooks/pop/executePopAction";
|
import { executeTaskList, type CollectedPayload } from "@/hooks/pop/executePopAction";
|
||||||
|
|
@ -51,6 +64,7 @@ import {
|
||||||
PackageCheck,
|
PackageCheck,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
GripVertical,
|
GripVertical,
|
||||||
|
ChevronsUpDown,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
@ -227,9 +241,11 @@ export interface ButtonTask {
|
||||||
apiEndpoint?: string;
|
apiEndpoint?: string;
|
||||||
apiMethod?: "GET" | "POST" | "PUT" | "DELETE";
|
apiMethod?: "GET" | "POST" | "PUT" | "DELETE";
|
||||||
|
|
||||||
// custom-event
|
// custom-event (제어 실행)
|
||||||
eventName?: string;
|
eventName?: string;
|
||||||
eventPayload?: Record<string, unknown>;
|
eventPayload?: Record<string, unknown>;
|
||||||
|
flowId?: number;
|
||||||
|
flowName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 빠른 시작 템플릿 */
|
/** 빠른 시작 템플릿 */
|
||||||
|
|
@ -345,7 +361,7 @@ export const TASK_TYPE_LABELS: Record<ButtonTaskType, string> = {
|
||||||
"close-modal": "모달 닫기",
|
"close-modal": "모달 닫기",
|
||||||
"refresh": "새로고침",
|
"refresh": "새로고침",
|
||||||
"api-call": "API 호출",
|
"api-call": "API 호출",
|
||||||
"custom-event": "커스텀 이벤트",
|
"custom-event": "제어 실행",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 빠른 시작 템플릿별 기본 작업 목록 + 외형 */
|
/** 빠른 시작 템플릿별 기본 작업 목록 + 외형 */
|
||||||
|
|
@ -1267,39 +1283,80 @@ interface PopButtonConfigPanelProps {
|
||||||
componentId?: string;
|
componentId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 화면 내 카드 컴포넌트에서 사용 가능한 필드 목록 추출 */
|
/** 연결된 컴포넌트에서 사용 가능한 필드 목록 추출 (연결 기반) */
|
||||||
function extractCardFields(
|
function extractConnectedFields(
|
||||||
|
componentId?: string,
|
||||||
|
connections?: PopButtonConfigPanelProps["connections"],
|
||||||
allComponents?: PopButtonConfigPanelProps["allComponents"],
|
allComponents?: PopButtonConfigPanelProps["allComponents"],
|
||||||
): { value: string; label: string; source: string }[] {
|
): { value: string; label: string; source: string }[] {
|
||||||
if (!allComponents) return [];
|
if (!componentId || !connections || !allComponents) return [];
|
||||||
|
|
||||||
|
const targetIds = connections
|
||||||
|
.filter((c) => c.sourceComponent === componentId || c.targetComponent === componentId)
|
||||||
|
.map((c) => (c.sourceComponent === componentId ? c.targetComponent : c.sourceComponent));
|
||||||
|
const uniqueIds = [...new Set(targetIds)];
|
||||||
|
if (uniqueIds.length === 0) return [];
|
||||||
|
|
||||||
const fields: { value: string; label: string; source: string }[] = [];
|
const fields: { value: string; label: string; source: string }[] = [];
|
||||||
|
|
||||||
for (const comp of allComponents) {
|
for (const tid of uniqueIds) {
|
||||||
if (comp.type !== "pop-card-list" || !comp.config) continue;
|
const comp = allComponents.find((c) => c.id === tid);
|
||||||
const tpl = (comp.config as Record<string, unknown>).cardTemplate as
|
if (!comp?.config) continue;
|
||||||
|
const cfg = comp.config as Record<string, unknown>;
|
||||||
|
const compLabel = (comp as Record<string, unknown>).label as string || comp.type || tid;
|
||||||
|
|
||||||
|
if (comp.type === "pop-card-list") {
|
||||||
|
const tpl = cfg.cardTemplate as
|
||||||
| { header?: Record<string, unknown>; body?: { fields?: { id?: string; label?: string; valueType?: string; columnName?: string }[] } }
|
| { header?: Record<string, unknown>; body?: { fields?: { id?: string; label?: string; valueType?: string; columnName?: string }[] } }
|
||||||
| undefined;
|
| undefined;
|
||||||
if (!tpl) continue;
|
if (tpl) {
|
||||||
|
|
||||||
if (tpl.header?.codeField) {
|
if (tpl.header?.codeField) {
|
||||||
fields.push({ value: String(tpl.header.codeField), label: String(tpl.header.codeField), source: "헤더 코드" });
|
fields.push({ value: String(tpl.header.codeField), label: String(tpl.header.codeField), source: compLabel });
|
||||||
}
|
}
|
||||||
if (tpl.header?.titleField) {
|
if (tpl.header?.titleField) {
|
||||||
fields.push({ value: String(tpl.header.titleField), label: String(tpl.header.titleField), source: "헤더 제목" });
|
fields.push({ value: String(tpl.header.titleField), label: String(tpl.header.titleField), source: compLabel });
|
||||||
}
|
}
|
||||||
for (const f of tpl.body?.fields ?? []) {
|
for (const f of tpl.body?.fields ?? []) {
|
||||||
if (f.valueType === "column" && f.columnName) {
|
if (f.valueType === "column" && f.columnName) {
|
||||||
fields.push({ value: f.columnName, label: f.label || f.columnName, source: "본문" });
|
fields.push({ value: f.columnName, label: f.label || f.columnName, source: compLabel });
|
||||||
} else if (f.valueType === "formula" && f.label) {
|
} else if (f.valueType === "formula" && f.label) {
|
||||||
const formulaKey = `__formula_${f.id || f.label}`;
|
fields.push({ value: `__formula_${f.id || f.label}`, label: f.label, source: `${compLabel} (수식)` });
|
||||||
fields.push({ value: formulaKey, label: f.label, source: "수식" });
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fields.push({ value: "__cart_quantity", label: "사용자 입력 수량", source: `${compLabel} (장바구니)` });
|
||||||
|
fields.push({ value: "__cart_row_key", label: "선택한 카드의 원본 키", source: `${compLabel} (장바구니)` });
|
||||||
|
fields.push({ value: "__cart_id", label: "장바구니 항목 ID", source: `${compLabel} (장바구니)` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comp.type === "pop-field") {
|
||||||
|
const sections = cfg.sections as Array<{
|
||||||
|
fields?: Array<{ id: string; fieldName?: string; labelText?: string }>;
|
||||||
|
}> | undefined;
|
||||||
|
if (Array.isArray(sections)) {
|
||||||
|
for (const section of sections) {
|
||||||
|
for (const f of section.fields ?? []) {
|
||||||
|
if (f.fieldName) {
|
||||||
|
fields.push({ value: f.fieldName, label: f.labelText || f.fieldName, source: compLabel });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 시스템 필드 추가
|
if (comp.type === "pop-search") {
|
||||||
fields.push({ value: "__cart_quantity", label: "수량 (장바구니)", source: "시스템" });
|
const filterCols = cfg.filterColumns as string[] | undefined;
|
||||||
fields.push({ value: "__cart_row_key", label: "원본 키", source: "시스템" });
|
const modalCfg = cfg.modalConfig as { valueField?: string } | undefined;
|
||||||
fields.push({ value: "__cart_id", label: "카드 항목 ID", source: "시스템" });
|
if (Array.isArray(filterCols) && filterCols.length > 0) {
|
||||||
|
for (const col of filterCols) {
|
||||||
|
fields.push({ value: col, label: col, source: compLabel });
|
||||||
|
}
|
||||||
|
} else if (modalCfg?.valueField) {
|
||||||
|
fields.push({ value: modalCfg.valueField, label: modalCfg.valueField, source: compLabel });
|
||||||
|
} else if (cfg.fieldName && typeof cfg.fieldName === "string") {
|
||||||
|
fields.push({ value: cfg.fieldName, label: (cfg.placeholder as string) || cfg.fieldName, source: compLabel });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
|
|
@ -1309,9 +1366,14 @@ export function PopButtonConfigPanel({
|
||||||
config,
|
config,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
allComponents,
|
allComponents,
|
||||||
|
connections,
|
||||||
|
componentId,
|
||||||
}: PopButtonConfigPanelProps) {
|
}: PopButtonConfigPanelProps) {
|
||||||
const v2 = useMemo(() => migrateButtonConfig(config), [config]);
|
const v2 = useMemo(() => migrateButtonConfig(config), [config]);
|
||||||
const cardFields = useMemo(() => extractCardFields(allComponents), [allComponents]);
|
const cardFields = useMemo(
|
||||||
|
() => extractConnectedFields(componentId, connections, allComponents),
|
||||||
|
[componentId, connections, allComponents],
|
||||||
|
);
|
||||||
|
|
||||||
const updateV2 = useCallback(
|
const updateV2 = useCallback(
|
||||||
(partial: Partial<PopButtonConfigV2>) => {
|
(partial: Partial<PopButtonConfigV2>) => {
|
||||||
|
|
@ -1560,7 +1622,7 @@ function buildTaskSummary(task: ButtonTask): string {
|
||||||
case "api-call":
|
case "api-call":
|
||||||
return task.apiEndpoint || "";
|
return task.apiEndpoint || "";
|
||||||
case "custom-event":
|
case "custom-event":
|
||||||
return task.eventName || "";
|
return task.flowName || task.eventName || "";
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
@ -1829,15 +1891,10 @@ function TaskDetailForm({
|
||||||
|
|
||||||
case "custom-event":
|
case "custom-event":
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<ControlFlowTaskForm
|
||||||
<Label className="text-xs font-medium">이벤트명</Label>
|
task={task}
|
||||||
<Input
|
onUpdate={onUpdate}
|
||||||
value={task.eventName || ""}
|
|
||||||
onChange={(e) => onUpdate({ eventName: e.target.value })}
|
|
||||||
placeholder="예: data-saved, item-selected"
|
|
||||||
className="h-8 text-xs"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
case "refresh":
|
case "refresh":
|
||||||
|
|
@ -1897,7 +1954,7 @@ function buildUpdateSummaryText(task: ButtonTask): string | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
const val = task.valueSource === "linked"
|
const val = task.valueSource === "linked"
|
||||||
? `화면 데이터(${task.sourceField || "?"})`
|
? `연결 데이터(${task.sourceField || "?"})`
|
||||||
: `"${task.fixedValue || "?"}"`;
|
: `"${task.fixedValue || "?"}"`;
|
||||||
return `[${task.targetTable}].${task.targetColumn} ${opLabel} ${val}`;
|
return `[${task.targetTable}].${task.targetColumn} ${opLabel} ${val}`;
|
||||||
}
|
}
|
||||||
|
|
@ -2003,7 +2060,7 @@ function DataUpdateTaskForm({
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="fixed" className="text-xs">직접 입력</SelectItem>
|
<SelectItem value="fixed" className="text-xs">직접 입력</SelectItem>
|
||||||
<SelectItem value="linked" className="text-xs">화면 데이터에서 가져오기</SelectItem>
|
<SelectItem value="linked" className="text-xs">연결된 데이터 가져오기</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2022,19 +2079,19 @@ function DataUpdateTaskForm({
|
||||||
|
|
||||||
{task.valueSource === "linked" && (
|
{task.valueSource === "linked" && (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-xs font-medium">어떤 화면 데이터를 사용할까요?</Label>
|
<Label className="text-xs font-medium">어떤 데이터를 사용할까요?</Label>
|
||||||
{cardFields.length > 0 ? (
|
{cardFields.length > 0 ? (
|
||||||
<Select
|
<Select
|
||||||
value={task.sourceField || ""}
|
value={task.sourceField || ""}
|
||||||
onValueChange={(v) => onUpdate({ sourceField: v })}
|
onValueChange={(v) => onUpdate({ sourceField: v })}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 text-xs">
|
<SelectTrigger className="h-8 text-xs">
|
||||||
<SelectValue placeholder="필드 선택" />
|
<SelectValue placeholder="데이터 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{cardFields.map((f) => (
|
{cardFields.map((f) => (
|
||||||
<SelectItem key={f.value} value={f.value} className="text-xs">
|
<SelectItem key={f.value} value={f.value} className="text-xs">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-col">
|
||||||
<span>{f.label}</span>
|
<span>{f.label}</span>
|
||||||
<span className="text-[10px] text-muted-foreground">{f.source}</span>
|
<span className="text-[10px] text-muted-foreground">{f.source}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2052,8 +2109,8 @@ function DataUpdateTaskForm({
|
||||||
)}
|
)}
|
||||||
<p className="text-[11px] text-muted-foreground">
|
<p className="text-[11px] text-muted-foreground">
|
||||||
{cardFields.length > 0
|
{cardFields.length > 0
|
||||||
? "카드에 표시되는 데이터 중 하나를 선택합니다"
|
? "연결된 컴포넌트의 데이터 중 하나를 선택합니다"
|
||||||
: "카드 컴포넌트가 없으면 직접 입력해주세요"}
|
: "연결된 컴포넌트가 없습니다. 연결 탭에서 먼저 연결해주세요"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -2924,6 +2981,107 @@ function SingleRuleEditor({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// 제어 실행 작업 폼 (custom-event -> 제어 플로우)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
function ControlFlowTaskForm({
|
||||||
|
task,
|
||||||
|
onUpdate,
|
||||||
|
}: {
|
||||||
|
task: ButtonTask;
|
||||||
|
onUpdate: (partial: Partial<ButtonTask>) => void;
|
||||||
|
}) {
|
||||||
|
const [flows, setFlows] = useState<{ flowId: number; flowName: string; flowDescription?: string }[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
apiClient
|
||||||
|
.get("/dataflow/node-flows")
|
||||||
|
.then((res) => {
|
||||||
|
const data = res.data?.data ?? res.data ?? [];
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
setFlows(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => setFlows([]))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const selectedFlow = flows.find((f) => f.flowId === task.flowId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-xs font-medium">제어 플로우 선택</Label>
|
||||||
|
{loading ? (
|
||||||
|
<p className="text-[11px] text-muted-foreground">목록 불러오는 중...</p>
|
||||||
|
) : (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={open}
|
||||||
|
className="h-8 w-full justify-between text-xs"
|
||||||
|
>
|
||||||
|
{selectedFlow ? selectedFlow.flowName : "플로우를 선택하세요"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="p-0"
|
||||||
|
style={{ width: "var(--radix-popover-trigger-width)" }}
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="플로우 검색..." className="text-xs" />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty className="p-2 text-center text-xs">
|
||||||
|
검색 결과 없음
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{flows.map((f) => (
|
||||||
|
<CommandItem
|
||||||
|
key={f.flowId}
|
||||||
|
value={f.flowName}
|
||||||
|
onSelect={() => {
|
||||||
|
onUpdate({
|
||||||
|
flowId: f.flowId,
|
||||||
|
flowName: f.flowName,
|
||||||
|
eventName: `__node_flow_${f.flowId}`,
|
||||||
|
});
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-3 w-3",
|
||||||
|
task.flowId === f.flowId ? "opacity-100" : "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span>{f.flowName}</span>
|
||||||
|
{f.flowDescription && (
|
||||||
|
<span className="text-[10px] text-muted-foreground">
|
||||||
|
{f.flowDescription}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 레지스트리 등록
|
// 레지스트리 등록
|
||||||
PopComponentRegistry.registerComponent({
|
PopComponentRegistry.registerComponent({
|
||||||
id: "pop-button",
|
id: "pop-button",
|
||||||
|
|
|
||||||
|
|
@ -2039,16 +2039,29 @@ function FilterSettingsSection({
|
||||||
{filters.map((filter, index) => (
|
{filters.map((filter, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-center gap-1 rounded-md border bg-card p-1.5"
|
className="space-y-1.5 rounded-md border bg-card p-2"
|
||||||
>
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-[10px] font-medium text-muted-foreground">
|
||||||
|
조건 {index + 1}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 text-destructive"
|
||||||
|
onClick={() => deleteFilter(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<Select
|
<Select
|
||||||
value={filter.column || ""}
|
value={filter.column || ""}
|
||||||
onValueChange={(val) =>
|
onValueChange={(val) =>
|
||||||
updateFilter(index, { ...filter, column: val })
|
updateFilter(index, { ...filter, column: val })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-7 text-xs flex-1">
|
<SelectTrigger className="h-8 text-xs">
|
||||||
<SelectValue placeholder="컬럼" />
|
<SelectValue placeholder="컬럼 선택" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{columns.map((col) => (
|
{columns.map((col) => (
|
||||||
|
|
@ -2058,7 +2071,7 @@ function FilterSettingsSection({
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<Select
|
<Select
|
||||||
value={filter.operator}
|
value={filter.operator}
|
||||||
onValueChange={(val) =>
|
onValueChange={(val) =>
|
||||||
|
|
@ -2068,7 +2081,7 @@ function FilterSettingsSection({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-7 w-16 text-xs">
|
<SelectTrigger className="h-8 w-20 shrink-0 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -2079,24 +2092,15 @@ function FilterSettingsSection({
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
value={filter.value}
|
value={filter.value}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateFilter(index, { ...filter, value: e.target.value })
|
updateFilter(index, { ...filter, value: e.target.value })
|
||||||
}
|
}
|
||||||
placeholder="값"
|
placeholder="값 입력"
|
||||||
className="h-7 flex-1 text-xs"
|
className="h-8 flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-7 w-7 shrink-0 text-destructive"
|
|
||||||
onClick={() => deleteFilter(index)}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2663,22 +2667,34 @@ function FilterCriteriaSection({
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{filters.map((filter, index) => (
|
{filters.map((filter, index) => (
|
||||||
<div key={index} className="flex items-center gap-1 rounded-md border bg-card p-1.5">
|
<div key={index} className="space-y-1.5 rounded-md border bg-card p-2">
|
||||||
<div className="flex-1">
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-[10px] font-medium text-muted-foreground">
|
||||||
|
조건 {index + 1}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 text-destructive"
|
||||||
|
onClick={() => deleteFilter(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<GroupedColumnSelect
|
<GroupedColumnSelect
|
||||||
columnGroups={columnGroups}
|
columnGroups={columnGroups}
|
||||||
value={filter.column || undefined}
|
value={filter.column || undefined}
|
||||||
onValueChange={(val) => updateFilter(index, { ...filter, column: val || "" })}
|
onValueChange={(val) => updateFilter(index, { ...filter, column: val || "" })}
|
||||||
placeholder="컬럼 선택"
|
placeholder="컬럼 선택"
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
<Select
|
<Select
|
||||||
value={filter.operator}
|
value={filter.operator}
|
||||||
onValueChange={(val) =>
|
onValueChange={(val) =>
|
||||||
updateFilter(index, { ...filter, operator: val as FilterOperator })
|
updateFilter(index, { ...filter, operator: val as FilterOperator })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-7 w-16 text-xs">
|
<SelectTrigger className="h-8 w-20 shrink-0 text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -2692,17 +2708,10 @@ function FilterCriteriaSection({
|
||||||
<Input
|
<Input
|
||||||
value={filter.value}
|
value={filter.value}
|
||||||
onChange={(e) => updateFilter(index, { ...filter, value: e.target.value })}
|
onChange={(e) => updateFilter(index, { ...filter, value: e.target.value })}
|
||||||
placeholder="값"
|
placeholder="값 입력"
|
||||||
className="h-7 flex-1 text-xs"
|
className="h-8 flex-1 text-xs"
|
||||||
/>
|
/>
|
||||||
<Button
|
</div>
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-7 w-7 shrink-0 text-destructive"
|
|
||||||
onClick={() => deleteFilter(index)}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue