675 lines
25 KiB
TypeScript
675 lines
25 KiB
TypeScript
|
|
"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;
|
||
|
|
|