385 lines
16 KiB
TypeScript
385 lines
16 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 })}
|
|
>
|
|
<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>
|
|
</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 === "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>
|
|
);
|
|
};
|