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

534 lines
22 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { Check, ChevronsUpDown, Search } from "lucide-react";
import { cn } from "@/lib/utils";
import { ComponentData } from "@/types/screen";
import { apiClient } from "@/lib/api/client";
import { ButtonDataflowConfigPanel } from "./ButtonDataflowConfigPanel";
interface ButtonConfigPanelProps {
component: ComponentData;
onUpdateProperty: (path: string, value: any) => void;
}
interface ScreenOption {
id: number;
name: string;
description?: string;
}
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component, onUpdateProperty }) => {
const config = component.componentConfig || {};
const [screens, setScreens] = useState<ScreenOption[]>([]);
const [screensLoading, setScreensLoading] = useState(false);
const [modalScreenOpen, setModalScreenOpen] = useState(false);
const [navScreenOpen, setNavScreenOpen] = useState(false);
const [modalSearchTerm, setModalSearchTerm] = useState("");
const [navSearchTerm, setNavSearchTerm] = useState("");
// 화면 목록 가져오기
useEffect(() => {
const fetchScreens = async () => {
try {
setScreensLoading(true);
console.log("🔍 화면 목록 API 호출 시작");
const response = await apiClient.get("/screen-management/screens");
console.log("✅ 화면 목록 API 응답:", response.data);
if (response.data.success && Array.isArray(response.data.data)) {
const screenList = response.data.data.map((screen: any) => ({
id: screen.screenId,
name: screen.screenName,
description: screen.description,
}));
setScreens(screenList);
console.log("✅ 화면 목록 설정 완료:", screenList.length, "개");
}
} catch (error) {
console.error("❌ 화면 목록 로딩 실패:", error);
} finally {
setScreensLoading(false);
}
};
fetchScreens();
}, []);
// 검색 필터링 함수
const filterScreens = (searchTerm: string) => {
if (!searchTerm.trim()) return screens;
return screens.filter(
(screen) =>
screen.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(screen.description && screen.description.toLowerCase().includes(searchTerm.toLowerCase())),
);
};
console.log("🔧 config-panels/ButtonConfigPanel 렌더링:", {
component,
config,
action: config.action,
actionType: config.action?.type,
screensCount: screens.length,
});
return (
<div className="space-y-4">
<div>
<Label htmlFor="button-text"> </Label>
<Input
id="button-text"
value={config.text || "버튼"}
onChange={(e) => onUpdateProperty("componentConfig.text", e.target.value)}
placeholder="버튼 텍스트를 입력하세요"
/>
</div>
<div>
<Label htmlFor="button-variant"> </Label>
<Select
value={config.variant || "default"}
onValueChange={(value) => onUpdateProperty("componentConfig.variant", value)}
>
<SelectTrigger>
<SelectValue placeholder="버튼 스타일 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="primary"> (Primary)</SelectItem>
<SelectItem value="secondary"> (Secondary)</SelectItem>
<SelectItem value="danger"> (Danger)</SelectItem>
<SelectItem value="success"> (Success)</SelectItem>
<SelectItem value="outline"> (Outline)</SelectItem>
<SelectItem value="ghost"> (Ghost)</SelectItem>
<SelectItem value="link"> (Link)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="button-size"> </Label>
<Select
value={config.size || "default"}
onValueChange={(value) => onUpdateProperty("componentConfig.size", value)}
>
<SelectTrigger>
<SelectValue placeholder="버튼 크기 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="small"> (Small)</SelectItem>
<SelectItem value="default"> (Default)</SelectItem>
<SelectItem value="large"> (Large)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="button-action"> </Label>
<Select
value={config.action?.type || "save"}
defaultValue="save"
onValueChange={(value) => {
// 액션 설정 업데이트
onUpdateProperty("componentConfig.action", { type: value });
// 액션에 따른 라벨 색상 자동 설정
if (value === 'delete') {
// 삭제 액션일 때 빨간색으로 설정
onUpdateProperty("style", {
...component.style,
labelColor: '#ef4444'
});
} else {
// 다른 액션일 때 기본 파란색으로 리셋
onUpdateProperty("style", {
...component.style,
labelColor: '#3b83f6'
});
}
}}
>
<SelectTrigger>
<SelectValue placeholder="버튼 액션 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="save"></SelectItem>
<SelectItem value="cancel"></SelectItem>
<SelectItem value="delete"></SelectItem>
<SelectItem value="edit"></SelectItem>
<SelectItem value="add"></SelectItem>
<SelectItem value="search"></SelectItem>
<SelectItem value="reset"></SelectItem>
<SelectItem value="submit"></SelectItem>
<SelectItem value="close"></SelectItem>
<SelectItem value="modal"> </SelectItem>
<SelectItem value="navigate"> </SelectItem>
<SelectItem value="control"> ( )</SelectItem>
</SelectContent>
</Select>
</div>
{/* 모달 열기 액션 설정 */}
{config.action?.type === "modal" && (
<div className="mt-4 space-y-4 rounded-lg border bg-gray-50 p-4">
<h4 className="text-sm font-medium text-gray-700"> </h4>
<div>
<Label htmlFor="modal-title"> </Label>
<Input
id="modal-title"
placeholder="모달 제목을 입력하세요"
value={config.action?.modalTitle || ""}
onChange={(e) =>
onUpdateProperty("componentConfig.action", {
...config.action,
modalTitle: e.target.value,
})
}
/>
</div>
<div>
<Label htmlFor="modal-size"> </Label>
<Select
value={config.action?.modalSize || "md"}
onValueChange={(value) =>
onUpdateProperty("componentConfig.action", {
...config.action,
modalSize: value,
})
}
>
<SelectTrigger>
<SelectValue placeholder="모달 크기 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sm"> (Small)</SelectItem>
<SelectItem value="md"> (Medium)</SelectItem>
<SelectItem value="lg"> (Large)</SelectItem>
<SelectItem value="xl"> (Extra Large)</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="target-screen-modal"> </Label>
<Popover open={modalScreenOpen} onOpenChange={setModalScreenOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={modalScreenOpen}
className="h-10 w-full justify-between"
disabled={screensLoading}
>
{config.action?.targetScreenId
? screens.find((screen) => screen.id === config.action?.targetScreenId)?.name ||
"화면을 선택하세요..."
: "화면을 선택하세요..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
<div className="flex flex-col">
{/* 검색 입력 */}
<div className="flex items-center border-b px-3 py-2">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<Input
placeholder="화면 검색..."
value={modalSearchTerm}
onChange={(e) => setModalSearchTerm(e.target.value)}
className="border-0 p-0 focus-visible:ring-0"
/>
</div>
{/* 검색 결과 */}
<div className="max-h-[200px] overflow-auto">
{(() => {
const filteredScreens = filterScreens(modalSearchTerm);
if (screensLoading) {
return <div className="p-3 text-sm text-gray-500"> ...</div>;
}
if (filteredScreens.length === 0) {
return <div className="p-3 text-sm text-gray-500"> .</div>;
}
return filteredScreens.map((screen, index) => (
<div
key={`modal-screen-${screen.id}-${index}`}
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
onClick={() => {
onUpdateProperty("componentConfig.action", {
...config.action,
targetScreenId: screen.id,
});
setModalScreenOpen(false);
setModalSearchTerm("");
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
config.action?.targetScreenId === screen.id ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{screen.name}</span>
{screen.description && <span className="text-xs text-gray-500">{screen.description}</span>}
</div>
</div>
));
})()}
</div>
</div>
</PopoverContent>
</Popover>
</div>
</div>
)}
{/* 수정 액션 설정 */}
{config.action?.type === "edit" && (
<div className="mt-4 space-y-4 rounded-lg border bg-green-50 p-4">
<h4 className="text-sm font-medium text-gray-700"> </h4>
<div>
<Label htmlFor="edit-screen"> </Label>
<Popover open={modalScreenOpen} onOpenChange={setModalScreenOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={modalScreenOpen}
className="h-10 w-full justify-between"
disabled={screensLoading}
>
{config.action?.targetScreenId
? screens.find((screen) => screen.id === config.action?.targetScreenId)?.name ||
"수정 폼 화면을 선택하세요..."
: "수정 폼 화면을 선택하세요..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
<div className="flex flex-col">
{/* 검색 입력 */}
<div className="flex items-center border-b px-3 py-2">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<Input
placeholder="화면 검색..."
value={modalSearchTerm}
onChange={(e) => setModalSearchTerm(e.target.value)}
className="border-0 p-0 focus-visible:ring-0"
/>
</div>
{/* 검색 결과 */}
<div className="max-h-[200px] overflow-auto">
{(() => {
const filteredScreens = filterScreens(modalSearchTerm);
if (screensLoading) {
return <div className="p-3 text-sm text-gray-500"> ...</div>;
}
if (filteredScreens.length === 0) {
return <div className="p-3 text-sm text-gray-500"> .</div>;
}
return filteredScreens.map((screen, index) => (
<div
key={`edit-screen-${screen.id}-${index}`}
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
onClick={() => {
onUpdateProperty("componentConfig.action", {
...config.action,
targetScreenId: screen.id,
});
setModalScreenOpen(false);
setModalSearchTerm("");
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
config.action?.targetScreenId === screen.id ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{screen.name}</span>
{screen.description && <span className="text-xs text-gray-500">{screen.description}</span>}
</div>
</div>
));
})()}
</div>
</div>
</PopoverContent>
</Popover>
<p className="mt-1 text-xs text-gray-500">
</p>
</div>
<div>
<Label htmlFor="edit-mode"> </Label>
<Select
value={config.action?.editMode || "modal"}
onValueChange={(value) =>
onUpdateProperty("componentConfig.action", {
...config.action,
editMode: value,
})
}
>
<SelectTrigger>
<SelectValue placeholder="수정 모드 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="modal"> </SelectItem>
<SelectItem value="navigate"> </SelectItem>
<SelectItem value="inline"> </SelectItem>
</SelectContent>
</Select>
</div>
{config.action?.editMode === "modal" && (
<div>
<Label htmlFor="edit-modal-size"> </Label>
<Select
value={config.action?.modalSize || "lg"}
onValueChange={(value) =>
onUpdateProperty("componentConfig.action", {
...config.action,
modalSize: value,
})
}
>
<SelectTrigger>
<SelectValue placeholder="모달 크기 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="sm"> (Small)</SelectItem>
<SelectItem value="md"> (Medium)</SelectItem>
<SelectItem value="lg"> (Large)</SelectItem>
<SelectItem value="xl"> (Extra Large)</SelectItem>
<SelectItem value="full"> (Full)</SelectItem>
</SelectContent>
</Select>
</div>
)}
</div>
)}
{/* 페이지 이동 액션 설정 */}
{config.action?.type === "navigate" && (
<div className="mt-4 space-y-4 rounded-lg border bg-gray-50 p-4">
<h4 className="text-sm font-medium text-gray-700"> </h4>
<div>
<Label htmlFor="target-screen-nav"> </Label>
<Popover open={navScreenOpen} onOpenChange={setNavScreenOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={navScreenOpen}
className="h-10 w-full justify-between"
disabled={screensLoading}
>
{config.action?.targetScreenId
? screens.find((screen) => screen.id === config.action?.targetScreenId)?.name ||
"화면을 선택하세요..."
: "화면을 선택하세요..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start" style={{ width: "var(--radix-popover-trigger-width)" }}>
<div className="flex flex-col">
{/* 검색 입력 */}
<div className="flex items-center border-b px-3 py-2">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<Input
placeholder="화면 검색..."
value={navSearchTerm}
onChange={(e) => setNavSearchTerm(e.target.value)}
className="border-0 p-0 focus-visible:ring-0"
/>
</div>
{/* 검색 결과 */}
<div className="max-h-[200px] overflow-auto">
{(() => {
const filteredScreens = filterScreens(navSearchTerm);
if (screensLoading) {
return <div className="p-3 text-sm text-gray-500"> ...</div>;
}
if (filteredScreens.length === 0) {
return <div className="p-3 text-sm text-gray-500"> .</div>;
}
return filteredScreens.map((screen, index) => (
<div
key={`navigate-screen-${screen.id}-${index}`}
className="flex cursor-pointer items-center px-3 py-2 hover:bg-gray-100"
onClick={() => {
onUpdateProperty("componentConfig.action", {
...config.action,
targetScreenId: screen.id,
});
setNavScreenOpen(false);
setNavSearchTerm("");
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
config.action?.targetScreenId === screen.id ? "opacity-100" : "opacity-0",
)}
/>
<div className="flex flex-col">
<span className="font-medium">{screen.name}</span>
{screen.description && <span className="text-xs text-gray-500">{screen.description}</span>}
</div>
</div>
));
})()}
</div>
</div>
</PopoverContent>
</Popover>
<p className="mt-1 text-xs text-gray-500">
/screens/{"{"}ID{"}"}
</p>
</div>
<div>
<Label htmlFor="target-url"> URL ()</Label>
<Input
id="target-url"
placeholder="예: /admin/users 또는 https://example.com"
value={config.action?.targetUrl || ""}
onChange={(e) =>
onUpdateProperty("componentConfig.action", {
...config.action,
targetUrl: e.target.value,
})
}
/>
<p className="mt-1 text-xs text-gray-500">URL을 </p>
</div>
</div>
)}
{/* 🔥 NEW: 제어관리 기능 섹션 */}
<div className="mt-8 border-t border-gray-200 pt-6">
<div className="mb-4">
<h3 className="text-lg font-medium text-gray-900">🔧 </h3>
<p className="mt-1 text-sm text-gray-600"> </p>
</div>
<ButtonDataflowConfigPanel component={component} onUpdateProperty={onUpdateProperty} />
</div>
</div>
);
};