ERP-node/frontend/components/screen/panels/ButtonConfigPanel.tsx

538 lines
21 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, ScreenDefinition } from "@/types/screen";
import { screenApi } from "@/lib/api/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: "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>(() => {
const defaultConfig = {
actionType: "custom" as ButtonActionType,
variant: "default" as ButtonVariant,
};
return {
...defaultConfig,
...config, // 저장된 값이 기본값을 덮어씀
};
});
// 화면 목록 상태
const [screens, setScreens] = useState<ScreenDefinition[]>([]);
const [screensLoading, setScreensLoading] = useState(false);
// 화면 목록 로드 함수
const loadScreens = async () => {
try {
setScreensLoading(true);
const response = await screenApi.getScreens({ size: 1000 }); // 모든 화면 가져오기
setScreens(response.data);
} catch (error) {
console.error("화면 목록 로드 실패:", error);
} finally {
setScreensLoading(false);
}
};
// 모달 또는 네비게이션 액션 타입일 때 화면 목록 로드
useEffect(() => {
if (localConfig.actionType === "popup" || localConfig.actionType === "navigate") {
loadScreens();
}
}, [localConfig.actionType]);
// 컴포넌트 변경 시 로컬 상태 동기화
useEffect(() => {
const newConfig = (component.webTypeConfig as ButtonTypeConfig) || {};
// 기본값 설정 (실제 값이 있으면 덮어쓰지 않음)
const defaultConfig = {
actionType: "custom" as ButtonActionType,
variant: "default" as ButtonVariant,
};
// 실제 저장된 값이 우선순위를 가지도록 설정
setLocalConfig({
...defaultConfig,
...newConfig, // 저장된 값이 기본값을 덮어씀
});
console.log("🔄 ButtonConfigPanel 로컬 상태 동기화:", {
componentId: component.id,
savedConfig: newConfig,
finalConfig: { ...defaultConfig, ...newConfig },
});
}, [component.webTypeConfig, component.id]);
// 설정 업데이트 함수
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 "close":
updates.variant = "outline";
updates.backgroundColor = "transparent";
updates.textColor = "#6b7280";
onUpdateComponent({
label: "닫기",
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.navigateType = "url";
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;
}
// 로컬 상태 업데이트 후 webTypeConfig도 함께 업데이트
const newConfig = { ...localConfig, ...updates };
setLocalConfig(newConfig);
// webTypeConfig를 마지막에 다시 업데이트하여 확실히 저장되도록 함
setTimeout(() => {
onUpdateComponent({
webTypeConfig: newConfig,
});
console.log("🎯 ButtonActionType webTypeConfig 최종 업데이트:", {
actionType,
newConfig,
componentId: component.id,
});
}, 0);
};
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>
{/* 아이콘 설정 */}
<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>
<Select
value={localConfig.popupScreenId?.toString() || "none"}
onValueChange={(value) =>
updateConfig({
popupScreenId: value === "none" ? undefined : parseInt(value),
})
}
disabled={screensLoading}
>
<SelectTrigger className="h-8">
<SelectValue placeholder={screensLoading ? "로딩 중..." : "화면을 선택하세요"} />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> </SelectItem>
{screens.map((screen) => (
<SelectItem key={screen.screenId} value={screen.screenId.toString()}>
{screen.screenName} ({screen.screenCode})
</SelectItem>
))}
</SelectContent>
</Select>
{localConfig.popupScreenId && <p className="text-xs text-gray-500"> </p>}
</div>
<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>
{!localConfig.popupScreenId && (
<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"
/>
<p className="text-xs text-gray-500"> </p>
</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"> </Label>
<Select
value={localConfig.navigateType || "url"}
onValueChange={(value) => updateConfig({ navigateType: value as "url" | "screen" })}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="url">URL </SelectItem>
<SelectItem value="screen"> </SelectItem>
</SelectContent>
</Select>
</div>
{(localConfig.navigateType || "url") === "url" ? (
<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.navigateScreenId?.toString() || ""}
onValueChange={(value) => updateConfig({ navigateScreenId: value ? parseInt(value) : undefined })}
>
<SelectTrigger className="h-8">
<SelectValue placeholder="화면을 선택하세요" />
</SelectTrigger>
<SelectContent>
{screensLoading ? (
<SelectItem value="" disabled>
...
</SelectItem>
) : screens.length === 0 ? (
<SelectItem value="" disabled>
</SelectItem>
) : (
screens.map((screen) => (
<SelectItem key={screen.screenId} value={screen.screenId.toString()}>
{screen.screenName} ({screen.screenCode})
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
)}
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Select
value={localConfig.navigateTarget || "_self"}
onValueChange={(value) => updateConfig({ navigateTarget: value as "_self" | "_blank" })}
>
<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>
);
};