438 lines
17 KiB
TypeScript
438 lines
17 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
Save,
|
|
X,
|
|
Trash2,
|
|
Edit,
|
|
Plus,
|
|
RotateCcw,
|
|
Send,
|
|
ExternalLink,
|
|
MousePointer,
|
|
Settings,
|
|
AlertTriangle,
|
|
} from "lucide-react";
|
|
import { ButtonActionType, ButtonTypeConfig, WidgetComponent } from "@/types/screen";
|
|
|
|
interface ButtonConfigPanelProps {
|
|
component: WidgetComponent;
|
|
onUpdateComponent: (updates: Partial<WidgetComponent>) => void;
|
|
}
|
|
|
|
const actionTypeOptions: { value: ButtonActionType; label: string; icon: React.ReactNode; color: string }[] = [
|
|
{ value: "save", label: "저장", icon: <Save className="h-4 w-4" />, color: "#3b82f6" },
|
|
{ value: "cancel", label: "취소", icon: <X className="h-4 w-4" />, color: "#6b7280" },
|
|
{ value: "delete", label: "삭제", icon: <Trash2 className="h-4 w-4" />, color: "#ef4444" },
|
|
{ value: "edit", label: "수정", icon: <Edit className="h-4 w-4" />, color: "#f59e0b" },
|
|
{ value: "add", label: "추가", icon: <Plus className="h-4 w-4" />, color: "#10b981" },
|
|
{ value: "search", label: "검색", icon: <MousePointer className="h-4 w-4" />, color: "#8b5cf6" },
|
|
{ value: "reset", label: "초기화", icon: <RotateCcw className="h-4 w-4" />, color: "#6b7280" },
|
|
{ value: "submit", label: "제출", icon: <Send className="h-4 w-4" />, color: "#059669" },
|
|
{ value: "close", label: "닫기", icon: <X className="h-4 w-4" />, color: "#6b7280" },
|
|
{ value: "popup", label: "팝업 열기", icon: <ExternalLink className="h-4 w-4" />, color: "#8b5cf6" },
|
|
{ value: "navigate", label: "페이지 이동", icon: <ExternalLink className="h-4 w-4" />, color: "#0ea5e9" },
|
|
{ value: "custom", label: "사용자 정의", icon: <Settings className="h-4 w-4" />, color: "#64748b" },
|
|
];
|
|
|
|
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component, onUpdateComponent }) => {
|
|
const config = (component.webTypeConfig as ButtonTypeConfig) || {};
|
|
|
|
// 로컬 상태 관리
|
|
const [localConfig, setLocalConfig] = useState<ButtonTypeConfig>({
|
|
actionType: "custom",
|
|
variant: "default",
|
|
size: "sm",
|
|
...config,
|
|
});
|
|
|
|
// 컴포넌트 변경 시 로컬 상태 동기화
|
|
useEffect(() => {
|
|
const newConfig = (component.webTypeConfig as ButtonTypeConfig) || {};
|
|
setLocalConfig({
|
|
actionType: "custom",
|
|
variant: "default",
|
|
size: "sm",
|
|
...newConfig,
|
|
});
|
|
}, [component.webTypeConfig]);
|
|
|
|
// 설정 업데이트 함수
|
|
const updateConfig = (updates: Partial<ButtonTypeConfig>) => {
|
|
const newConfig = { ...localConfig, ...updates };
|
|
setLocalConfig(newConfig);
|
|
|
|
// 스타일 업데이트도 함께 적용
|
|
const styleUpdates: any = {};
|
|
if (updates.backgroundColor) styleUpdates.backgroundColor = updates.backgroundColor;
|
|
if (updates.textColor) styleUpdates.color = updates.textColor;
|
|
if (updates.borderColor) styleUpdates.borderColor = updates.borderColor;
|
|
|
|
onUpdateComponent({
|
|
webTypeConfig: newConfig,
|
|
...(Object.keys(styleUpdates).length > 0 && {
|
|
style: { ...component.style, ...styleUpdates },
|
|
}),
|
|
});
|
|
};
|
|
|
|
// 액션 타입 변경 시 기본값 설정
|
|
const handleActionTypeChange = (actionType: ButtonActionType) => {
|
|
const actionOption = actionTypeOptions.find((opt) => opt.value === actionType);
|
|
const updates: Partial<ButtonTypeConfig> = { actionType };
|
|
|
|
// 액션 타입에 따른 기본 설정
|
|
switch (actionType) {
|
|
case "save":
|
|
updates.variant = "default";
|
|
updates.backgroundColor = "#3b82f6";
|
|
updates.textColor = "#ffffff";
|
|
// 버튼 라벨과 스타일도 업데이트
|
|
onUpdateComponent({
|
|
label: "저장",
|
|
style: { ...component.style, backgroundColor: "#3b82f6", color: "#ffffff" },
|
|
});
|
|
break;
|
|
case "cancel":
|
|
case "close":
|
|
updates.variant = "outline";
|
|
updates.backgroundColor = "transparent";
|
|
updates.textColor = "#6b7280";
|
|
onUpdateComponent({
|
|
label: actionType === "cancel" ? "취소" : "닫기",
|
|
style: { ...component.style, backgroundColor: "transparent", color: "#6b7280", border: "1px solid #d1d5db" },
|
|
});
|
|
break;
|
|
case "delete":
|
|
updates.variant = "destructive";
|
|
updates.backgroundColor = "#ef4444";
|
|
updates.textColor = "#ffffff";
|
|
updates.confirmMessage = "정말로 삭제하시겠습니까?";
|
|
onUpdateComponent({
|
|
label: "삭제",
|
|
style: { ...component.style, backgroundColor: "#ef4444", color: "#ffffff" },
|
|
});
|
|
break;
|
|
case "edit":
|
|
updates.backgroundColor = "#f59e0b";
|
|
updates.textColor = "#ffffff";
|
|
onUpdateComponent({
|
|
label: "수정",
|
|
style: { ...component.style, backgroundColor: "#f59e0b", color: "#ffffff" },
|
|
});
|
|
break;
|
|
case "add":
|
|
updates.backgroundColor = "#10b981";
|
|
updates.textColor = "#ffffff";
|
|
onUpdateComponent({
|
|
label: "추가",
|
|
style: { ...component.style, backgroundColor: "#10b981", color: "#ffffff" },
|
|
});
|
|
break;
|
|
case "search":
|
|
updates.backgroundColor = "#8b5cf6";
|
|
updates.textColor = "#ffffff";
|
|
onUpdateComponent({
|
|
label: "검색",
|
|
style: { ...component.style, backgroundColor: "#8b5cf6", color: "#ffffff" },
|
|
});
|
|
break;
|
|
case "reset":
|
|
updates.variant = "outline";
|
|
updates.backgroundColor = "transparent";
|
|
updates.textColor = "#6b7280";
|
|
onUpdateComponent({
|
|
label: "초기화",
|
|
style: { ...component.style, backgroundColor: "transparent", color: "#6b7280", border: "1px solid #d1d5db" },
|
|
});
|
|
break;
|
|
case "submit":
|
|
updates.backgroundColor = "#059669";
|
|
updates.textColor = "#ffffff";
|
|
onUpdateComponent({
|
|
label: "제출",
|
|
style: { ...component.style, backgroundColor: "#059669", color: "#ffffff" },
|
|
});
|
|
break;
|
|
case "popup":
|
|
updates.backgroundColor = "#8b5cf6";
|
|
updates.textColor = "#ffffff";
|
|
updates.popupTitle = "상세 정보";
|
|
updates.popupContent = "여기에 팝업 내용을 입력하세요.";
|
|
updates.popupSize = "md";
|
|
onUpdateComponent({
|
|
label: "상세보기",
|
|
style: { ...component.style, backgroundColor: "#8b5cf6", color: "#ffffff" },
|
|
});
|
|
break;
|
|
case "navigate":
|
|
updates.backgroundColor = "#0ea5e9";
|
|
updates.textColor = "#ffffff";
|
|
updates.navigateUrl = "/";
|
|
updates.navigateTarget = "_self";
|
|
onUpdateComponent({
|
|
label: "이동",
|
|
style: { ...component.style, backgroundColor: "#0ea5e9", color: "#ffffff" },
|
|
});
|
|
break;
|
|
case "custom":
|
|
updates.backgroundColor = "#64748b";
|
|
updates.textColor = "#ffffff";
|
|
onUpdateComponent({
|
|
label: "버튼",
|
|
style: { ...component.style, backgroundColor: "#64748b", color: "#ffffff" },
|
|
});
|
|
break;
|
|
}
|
|
|
|
updateConfig(updates);
|
|
};
|
|
|
|
const selectedActionOption = actionTypeOptions.find((opt) => opt.value === localConfig.actionType);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
|
<Settings className="h-4 w-4" />
|
|
버튼 기능 설정
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* 액션 타입 선택 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">버튼 기능</Label>
|
|
<Select value={localConfig.actionType} onValueChange={handleActionTypeChange}>
|
|
<SelectTrigger className="h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{actionTypeOptions.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
<div className="flex items-center gap-2">
|
|
{option.icon}
|
|
<span>{option.label}</span>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{selectedActionOption && (
|
|
<div className="flex items-center gap-2 text-xs text-gray-500">
|
|
{selectedActionOption.icon}
|
|
<span>{selectedActionOption.label}</span>
|
|
<Badge
|
|
variant="outline"
|
|
style={{ backgroundColor: selectedActionOption.color + "20", color: selectedActionOption.color }}
|
|
>
|
|
{selectedActionOption.value}
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* 기본 설정 */}
|
|
<div className="space-y-3">
|
|
<Label className="text-xs font-medium">기본 설정</Label>
|
|
|
|
{/* 버튼 텍스트 */}
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">버튼 텍스트</Label>
|
|
<Input
|
|
value={component.label || ""}
|
|
onChange={(e) => {
|
|
const newValue = e.target.value;
|
|
onUpdateComponent({ label: newValue });
|
|
}}
|
|
placeholder="버튼에 표시될 텍스트"
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 버튼 스타일 */}
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">스타일</Label>
|
|
<Select value={localConfig.variant} onValueChange={(value) => updateConfig({ variant: value as any })}>
|
|
<SelectTrigger className="h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="default">기본</SelectItem>
|
|
<SelectItem value="destructive">위험</SelectItem>
|
|
<SelectItem value="outline">외곽선</SelectItem>
|
|
<SelectItem value="secondary">보조</SelectItem>
|
|
<SelectItem value="ghost">투명</SelectItem>
|
|
<SelectItem value="link">링크</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">크기</Label>
|
|
<Select value={localConfig.size} onValueChange={(value) => updateConfig({ size: value as any })}>
|
|
<SelectTrigger className="h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="sm">작음</SelectItem>
|
|
<SelectItem value="default">기본</SelectItem>
|
|
<SelectItem value="lg">큼</SelectItem>
|
|
<SelectItem value="icon">아이콘</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 아이콘 설정 */}
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">아이콘 (Lucide 아이콘 이름)</Label>
|
|
<Input
|
|
value={localConfig.icon || ""}
|
|
onChange={(e) => updateConfig({ icon: e.target.value })}
|
|
placeholder="예: Save, Edit, Trash2"
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* 액션별 세부 설정 */}
|
|
{localConfig.actionType === "delete" && (
|
|
<div className="space-y-3">
|
|
<Label className="flex items-center gap-1 text-xs font-medium">
|
|
<AlertTriangle className="h-3 w-3 text-red-500" />
|
|
삭제 확인 설정
|
|
</Label>
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">확인 메시지</Label>
|
|
<Input
|
|
value={localConfig.confirmMessage || ""}
|
|
onChange={(e) => updateConfig({ confirmMessage: e.target.value })}
|
|
placeholder="정말로 삭제하시겠습니까?"
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{localConfig.actionType === "popup" && (
|
|
<div className="space-y-3">
|
|
<Label className="flex items-center gap-1 text-xs font-medium">
|
|
<ExternalLink className="h-3 w-3 text-purple-500" />
|
|
팝업 설정
|
|
</Label>
|
|
<div className="space-y-2">
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">팝업 제목</Label>
|
|
<Input
|
|
value={localConfig.popupTitle || ""}
|
|
onChange={(e) => updateConfig({ popupTitle: e.target.value })}
|
|
placeholder="상세 정보"
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">팝업 크기</Label>
|
|
<Select
|
|
value={localConfig.popupSize}
|
|
onValueChange={(value) => updateConfig({ popupSize: value as any })}
|
|
>
|
|
<SelectTrigger className="h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="sm">작음</SelectItem>
|
|
<SelectItem value="md">보통</SelectItem>
|
|
<SelectItem value="lg">큼</SelectItem>
|
|
<SelectItem value="xl">매우 큼</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">팝업 내용</Label>
|
|
<Textarea
|
|
value={localConfig.popupContent || ""}
|
|
onChange={(e) => updateConfig({ popupContent: e.target.value })}
|
|
placeholder="여기에 팝업 내용을 입력하세요."
|
|
className="h-16 resize-none text-xs"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{localConfig.actionType === "navigate" && (
|
|
<div className="space-y-3">
|
|
<Label className="flex items-center gap-1 text-xs font-medium">
|
|
<ExternalLink className="h-3 w-3 text-blue-500" />
|
|
페이지 이동 설정
|
|
</Label>
|
|
<div className="space-y-2">
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">이동할 URL</Label>
|
|
<Input
|
|
value={localConfig.navigateUrl || ""}
|
|
onChange={(e) => updateConfig({ navigateUrl: e.target.value })}
|
|
placeholder="/admin/users"
|
|
className="h-8 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label className="text-xs">열기 방식</Label>
|
|
<Select
|
|
value={localConfig.navigateTarget}
|
|
onValueChange={(value) => updateConfig({ navigateTarget: value as any })}
|
|
>
|
|
<SelectTrigger className="h-8">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="_self">현재 창</SelectItem>
|
|
<SelectItem value="_blank">새 창</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{localConfig.actionType === "custom" && (
|
|
<div className="space-y-3">
|
|
<Label className="flex items-center gap-1 text-xs font-medium">
|
|
<Settings className="h-3 w-3 text-gray-500" />
|
|
사용자 정의 액션
|
|
</Label>
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">JavaScript 코드</Label>
|
|
<Textarea
|
|
value={localConfig.customAction || ""}
|
|
onChange={(e) => updateConfig({ customAction: e.target.value })}
|
|
placeholder="alert('버튼이 클릭되었습니다!');"
|
|
className="h-16 resize-none font-mono text-xs"
|
|
/>
|
|
<div className="text-xs text-gray-500">
|
|
JavaScript 코드를 입력하세요. 예: alert(), console.log(), 함수 호출 등
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|