538 lines
21 KiB
TypeScript
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>
|
|
);
|
|
};
|