feat(split-panel-layout2): 그룹핑, 탭 필터링, 설정 모달 기능 추가
- types.ts: GroupingConfig, TabConfig, ColumnDisplayConfig 등 타입 확장 - Component: groupData, generateTabs, filterDataByTab 함수 추가 - ConfigPanel: SearchableColumnSelect, 설정 모달 상태 관리 추가 - 신규 모달: ActionButtonConfigModal, ColumnConfigModal, DataTransferConfigModal - UniversalFormModal: 연결필드 소스 테이블 Combobox로 변경
This commit is contained in:
@@ -0,0 +1,674 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
useSortable,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { Plus, GripVertical, Settings, X, Check, ChevronsUpDown } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import type { ActionButtonConfig, ModalParamMapping, ColumnConfig } from "./types";
|
||||
|
||||
interface ScreenInfo {
|
||||
screen_id: number;
|
||||
screen_name: string;
|
||||
screen_code: string;
|
||||
}
|
||||
|
||||
// 정렬 가능한 버튼 아이템
|
||||
const SortableButtonItem: React.FC<{
|
||||
id: string;
|
||||
button: ActionButtonConfig;
|
||||
index: number;
|
||||
onSettingsClick: () => void;
|
||||
onRemove: () => void;
|
||||
}> = ({ id, button, index, onSettingsClick, onRemove }) => {
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
const getVariantColor = (variant?: string) => {
|
||||
switch (variant) {
|
||||
case "destructive":
|
||||
return "bg-destructive/10 text-destructive";
|
||||
case "outline":
|
||||
return "bg-background border";
|
||||
case "ghost":
|
||||
return "bg-muted/50";
|
||||
case "secondary":
|
||||
return "bg-secondary text-secondary-foreground";
|
||||
default:
|
||||
return "bg-primary/10 text-primary";
|
||||
}
|
||||
};
|
||||
|
||||
const getActionLabel = (action?: string) => {
|
||||
switch (action) {
|
||||
case "add":
|
||||
return "추가";
|
||||
case "edit":
|
||||
return "수정";
|
||||
case "delete":
|
||||
return "삭제";
|
||||
case "bulk-delete":
|
||||
return "일괄삭제";
|
||||
case "api":
|
||||
return "API";
|
||||
case "custom":
|
||||
return "커스텀";
|
||||
default:
|
||||
return "추가";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cn("flex items-center gap-2 rounded-md border bg-card p-3", isDragging && "opacity-50 shadow-lg")}
|
||||
>
|
||||
{/* 드래그 핸들 */}
|
||||
<div {...attributes} {...listeners} className="cursor-grab touch-none text-muted-foreground hover:text-foreground">
|
||||
<GripVertical className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
{/* 버튼 정보 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn("px-2 py-0.5 rounded text-xs font-medium", getVariantColor(button.variant))}>
|
||||
{button.label || `버튼 ${index + 1}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
<Badge variant="outline" className="text-[10px] h-4">
|
||||
{getActionLabel(button.action)}
|
||||
</Badge>
|
||||
{button.icon && (
|
||||
<Badge variant="secondary" className="text-[10px] h-4">
|
||||
{button.icon}
|
||||
</Badge>
|
||||
)}
|
||||
{button.showCondition && button.showCondition !== "always" && (
|
||||
<Badge variant="secondary" className="text-[10px] h-4">
|
||||
{button.showCondition === "selected" ? "선택시만" : "미선택시만"}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 액션 버튼들 */}
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button size="sm" variant="ghost" className="h-7 w-7 p-0" onClick={onSettingsClick} title="세부설정">
|
||||
<Settings className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-7 w-7 p-0 text-destructive hover:text-destructive"
|
||||
onClick={onRemove}
|
||||
title="삭제"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ActionButtonConfigModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
actionButtons: ActionButtonConfig[];
|
||||
displayColumns?: ColumnConfig[]; // 모달 파라미터 매핑용
|
||||
onSave: (buttons: ActionButtonConfig[]) => void;
|
||||
side: "left" | "right";
|
||||
}
|
||||
|
||||
export const ActionButtonConfigModal: React.FC<ActionButtonConfigModalProps> = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
actionButtons: initialButtons,
|
||||
displayColumns = [],
|
||||
onSave,
|
||||
side,
|
||||
}) => {
|
||||
// 로컬 상태
|
||||
const [buttons, setButtons] = useState<ActionButtonConfig[]>([]);
|
||||
|
||||
// 버튼 세부설정 모달
|
||||
const [detailModalOpen, setDetailModalOpen] = useState(false);
|
||||
const [editingButtonIndex, setEditingButtonIndex] = useState<number | null>(null);
|
||||
const [editingButton, setEditingButton] = useState<ActionButtonConfig | null>(null);
|
||||
|
||||
// 화면 목록
|
||||
const [screens, setScreens] = useState<ScreenInfo[]>([]);
|
||||
const [screensLoading, setScreensLoading] = useState(false);
|
||||
const [screenSelectOpen, setScreenSelectOpen] = useState(false);
|
||||
|
||||
// 드래그 센서
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
// 초기값 설정
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setButtons(initialButtons || []);
|
||||
}
|
||||
}, [open, initialButtons]);
|
||||
|
||||
// 화면 목록 로드
|
||||
const loadScreens = useCallback(async () => {
|
||||
setScreensLoading(true);
|
||||
try {
|
||||
const response = await apiClient.get("/screen-management/screens?size=1000");
|
||||
|
||||
let screenList: any[] = [];
|
||||
if (response.data?.success && Array.isArray(response.data?.data)) {
|
||||
screenList = response.data.data;
|
||||
} else if (Array.isArray(response.data?.data)) {
|
||||
screenList = response.data.data;
|
||||
}
|
||||
|
||||
const transformedScreens = screenList.map((s: any) => ({
|
||||
screen_id: s.screenId ?? s.screen_id ?? s.id,
|
||||
screen_name: s.screenName ?? s.screen_name ?? s.name ?? `화면 ${s.screenId || s.screen_id || s.id}`,
|
||||
screen_code: s.screenCode ?? s.screen_code ?? s.code ?? "",
|
||||
}));
|
||||
|
||||
setScreens(transformedScreens);
|
||||
} catch (error) {
|
||||
console.error("화면 목록 로드 실패:", error);
|
||||
setScreens([]);
|
||||
} finally {
|
||||
setScreensLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
loadScreens();
|
||||
}
|
||||
}, [open, loadScreens]);
|
||||
|
||||
// 드래그 종료 핸들러
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = buttons.findIndex((btn) => btn.id === active.id);
|
||||
const newIndex = buttons.findIndex((btn) => btn.id === over.id);
|
||||
|
||||
if (oldIndex !== -1 && newIndex !== -1) {
|
||||
setButtons(arrayMove(buttons, oldIndex, newIndex));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 버튼 추가
|
||||
const handleAddButton = () => {
|
||||
const newButton: ActionButtonConfig = {
|
||||
id: `btn-${Date.now()}`,
|
||||
label: "새 버튼",
|
||||
variant: "default",
|
||||
action: "add",
|
||||
showCondition: "always",
|
||||
};
|
||||
setButtons([...buttons, newButton]);
|
||||
};
|
||||
|
||||
// 버튼 삭제
|
||||
const handleRemoveButton = (index: number) => {
|
||||
setButtons(buttons.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
// 버튼 업데이트
|
||||
const handleUpdateButton = (index: number, updates: Partial<ActionButtonConfig>) => {
|
||||
const newButtons = [...buttons];
|
||||
newButtons[index] = { ...newButtons[index], ...updates };
|
||||
setButtons(newButtons);
|
||||
};
|
||||
|
||||
// 버튼 세부설정 열기
|
||||
const handleOpenDetailSettings = (index: number) => {
|
||||
setEditingButtonIndex(index);
|
||||
setEditingButton({ ...buttons[index] });
|
||||
setDetailModalOpen(true);
|
||||
};
|
||||
|
||||
// 버튼 세부설정 저장
|
||||
const handleSaveDetailSettings = () => {
|
||||
if (editingButtonIndex !== null && editingButton) {
|
||||
handleUpdateButton(editingButtonIndex, editingButton);
|
||||
}
|
||||
setDetailModalOpen(false);
|
||||
setEditingButtonIndex(null);
|
||||
setEditingButton(null);
|
||||
};
|
||||
|
||||
// 저장
|
||||
const handleSave = () => {
|
||||
onSave(buttons);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
// 선택된 화면 정보
|
||||
const getScreenInfo = (screenId?: number) => {
|
||||
return screens.find((s) => s.screen_id === screenId);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[600px] max-h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{side === "left" ? "좌측" : "우측"} 패널 액션 버튼 설정
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
버튼을 추가하고 순서를 드래그로 변경할 수 있습니다.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-hidden flex flex-col">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Label className="text-sm font-medium">
|
||||
액션 버튼 ({buttons.length}개)
|
||||
</Label>
|
||||
<Button size="sm" variant="outline" onClick={handleAddButton}>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
버튼 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 pr-4">
|
||||
{buttons.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<p className="text-muted-foreground text-sm mb-2">
|
||||
액션 버튼이 없습니다
|
||||
</p>
|
||||
<Button size="sm" variant="outline" onClick={handleAddButton}>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
첫 번째 버튼 추가
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={buttons.map((btn) => btn.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{buttons.map((btn, index) => (
|
||||
<SortableButtonItem
|
||||
key={btn.id}
|
||||
id={btn.id}
|
||||
button={btn}
|
||||
index={index}
|
||||
onSettingsClick={() => handleOpenDetailSettings(index)}
|
||||
onRemove={() => handleRemoveButton(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave}>저장</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 버튼 세부설정 모달 */}
|
||||
<Dialog open={detailModalOpen} onOpenChange={setDetailModalOpen}>
|
||||
<DialogContent className="max-w-[95vw] sm:max-w-[500px] max-h-[85vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>버튼 세부설정</DialogTitle>
|
||||
<DialogDescription>
|
||||
{editingButton?.label || "버튼"}의 동작을 설정합니다.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{editingButton && (
|
||||
<ScrollArea className="max-h-[60vh]">
|
||||
<div className="space-y-4 pr-4">
|
||||
{/* 기본 설정 */}
|
||||
<div className="space-y-3 p-3 border rounded-lg">
|
||||
<h4 className="text-sm font-medium">기본 설정</h4>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">버튼 라벨</Label>
|
||||
<Input
|
||||
value={editingButton.label}
|
||||
onChange={(e) =>
|
||||
setEditingButton({ ...editingButton, label: e.target.value })
|
||||
}
|
||||
placeholder="버튼 라벨"
|
||||
className="mt-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">버튼 스타일</Label>
|
||||
<Select
|
||||
value={editingButton.variant || "default"}
|
||||
onValueChange={(value) =>
|
||||
setEditingButton({
|
||||
...editingButton,
|
||||
variant: value as ActionButtonConfig["variant"],
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="default">기본 (Primary)</SelectItem>
|
||||
<SelectItem value="secondary">보조 (Secondary)</SelectItem>
|
||||
<SelectItem value="outline">외곽선</SelectItem>
|
||||
<SelectItem value="ghost">투명</SelectItem>
|
||||
<SelectItem value="destructive">삭제 (빨간색)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">아이콘</Label>
|
||||
<Select
|
||||
value={editingButton.icon || "none"}
|
||||
onValueChange={(value) =>
|
||||
setEditingButton({
|
||||
...editingButton,
|
||||
icon: value === "none" ? undefined : value,
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">없음</SelectItem>
|
||||
<SelectItem value="Plus">+ (추가)</SelectItem>
|
||||
<SelectItem value="Edit">수정</SelectItem>
|
||||
<SelectItem value="Trash2">삭제</SelectItem>
|
||||
<SelectItem value="Download">다운로드</SelectItem>
|
||||
<SelectItem value="Upload">업로드</SelectItem>
|
||||
<SelectItem value="RefreshCw">새로고침</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">표시 조건</Label>
|
||||
<Select
|
||||
value={editingButton.showCondition || "always"}
|
||||
onValueChange={(value) =>
|
||||
setEditingButton({
|
||||
...editingButton,
|
||||
showCondition: value as ActionButtonConfig["showCondition"],
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="always">항상 표시</SelectItem>
|
||||
<SelectItem value="selected">선택 시만 표시</SelectItem>
|
||||
<SelectItem value="notSelected">미선택 시만 표시</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 동작 설정 */}
|
||||
<div className="space-y-3 p-3 border rounded-lg">
|
||||
<h4 className="text-sm font-medium">동작 설정</h4>
|
||||
|
||||
<div>
|
||||
<Label className="text-xs">동작 유형</Label>
|
||||
<Select
|
||||
value={editingButton.action || "add"}
|
||||
onValueChange={(value) =>
|
||||
setEditingButton({
|
||||
...editingButton,
|
||||
action: value as ActionButtonConfig["action"],
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="add">추가 (모달 열기)</SelectItem>
|
||||
<SelectItem value="edit">수정 (선택 항목)</SelectItem>
|
||||
<SelectItem value="delete">삭제 (선택 항목)</SelectItem>
|
||||
<SelectItem value="bulk-delete">일괄 삭제 (체크된 항목)</SelectItem>
|
||||
<SelectItem value="api">API 호출</SelectItem>
|
||||
<SelectItem value="custom">커스텀 액션</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 모달 설정 (add, edit 액션) */}
|
||||
{(editingButton.action === "add" || editingButton.action === "edit") && (
|
||||
<div>
|
||||
<Label className="text-xs">모달 화면</Label>
|
||||
<Popover open={screenSelectOpen} onOpenChange={setScreenSelectOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className="mt-1 h-9 w-full justify-between"
|
||||
disabled={screensLoading}
|
||||
>
|
||||
{screensLoading
|
||||
? "로딩 중..."
|
||||
: editingButton.modalScreenId
|
||||
? getScreenInfo(editingButton.modalScreenId)?.screen_name ||
|
||||
`화면 ${editingButton.modalScreenId}`
|
||||
: "화면 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="화면 검색..." className="h-9" />
|
||||
<CommandList>
|
||||
<CommandEmpty>검색 결과가 없습니다</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{screens.map((screen) => (
|
||||
<CommandItem
|
||||
key={screen.screen_id}
|
||||
value={`${screen.screen_id}-${screen.screen_name}`}
|
||||
onSelect={() => {
|
||||
setEditingButton({
|
||||
...editingButton,
|
||||
modalScreenId: screen.screen_id,
|
||||
});
|
||||
setScreenSelectOpen(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
editingButton.modalScreenId === screen.screen_id
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
<span className="flex flex-col">
|
||||
<span>{screen.screen_name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{screen.screen_code}
|
||||
</span>
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* API 설정 */}
|
||||
{editingButton.action === "api" && (
|
||||
<>
|
||||
<div>
|
||||
<Label className="text-xs">API 엔드포인트</Label>
|
||||
<Input
|
||||
value={editingButton.apiEndpoint || ""}
|
||||
onChange={(e) =>
|
||||
setEditingButton({
|
||||
...editingButton,
|
||||
apiEndpoint: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="/api/example"
|
||||
className="mt-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">HTTP 메서드</Label>
|
||||
<Select
|
||||
value={editingButton.apiMethod || "POST"}
|
||||
onValueChange={(value) =>
|
||||
setEditingButton({
|
||||
...editingButton,
|
||||
apiMethod: value as ActionButtonConfig["apiMethod"],
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="mt-1 h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
<SelectItem value="PUT">PUT</SelectItem>
|
||||
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 확인 메시지 (삭제 계열) */}
|
||||
{(editingButton.action === "delete" ||
|
||||
editingButton.action === "bulk-delete" ||
|
||||
(editingButton.action === "api" && editingButton.apiMethod === "DELETE")) && (
|
||||
<div>
|
||||
<Label className="text-xs">확인 메시지</Label>
|
||||
<Input
|
||||
value={editingButton.confirmMessage || ""}
|
||||
onChange={(e) =>
|
||||
setEditingButton({
|
||||
...editingButton,
|
||||
confirmMessage: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="정말 삭제하시겠습니까?"
|
||||
className="mt-1 h-9"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 커스텀 액션 ID */}
|
||||
{editingButton.action === "custom" && (
|
||||
<div>
|
||||
<Label className="text-xs">커스텀 액션 ID</Label>
|
||||
<Input
|
||||
value={editingButton.customActionId || ""}
|
||||
onChange={(e) =>
|
||||
setEditingButton({
|
||||
...editingButton,
|
||||
customActionId: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="customAction1"
|
||||
className="mt-1 h-9"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
커스텀 이벤트 핸들러에서 이 ID로 버튼을 구분합니다
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setDetailModalOpen(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSaveDetailSettings}>적용</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionButtonConfigModal;
|
||||
|
||||
Reference in New Issue
Block a user