불필요한 파일 삭제
This commit is contained in:
parent
532a532e0a
commit
2a313c5ca2
|
|
@ -1,361 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState, useMemo } from "react";
|
|
||||||
import { Search, Plus, Edit, Trash2, RefreshCw, Package, Filter } from "lucide-react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { AlertModal } from "@/components/common/AlertModal";
|
|
||||||
import {
|
|
||||||
useComponents,
|
|
||||||
useComponentCategories,
|
|
||||||
useComponentStatistics,
|
|
||||||
useDeleteComponent,
|
|
||||||
useCreateComponent,
|
|
||||||
useUpdateComponent,
|
|
||||||
} from "@/hooks/admin/useComponents";
|
|
||||||
import { ComponentFormModal } from "@/components/admin/ComponentFormModal";
|
|
||||||
|
|
||||||
// 컴포넌트 카테고리 정의
|
|
||||||
const COMPONENT_CATEGORIES = [
|
|
||||||
{ id: "input", name: "입력", color: "blue" },
|
|
||||||
{ id: "action", name: "액션", color: "green" },
|
|
||||||
{ id: "display", name: "표시", color: "purple" },
|
|
||||||
{ id: "layout", name: "레이아웃", color: "orange" },
|
|
||||||
{ id: "other", name: "기타", color: "gray" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function ComponentManagementPage() {
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string>("all");
|
|
||||||
const [sortBy, setSortBy] = useState<string>("sort_order");
|
|
||||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");
|
|
||||||
const [selectedComponent, setSelectedComponent] = useState<any>(null);
|
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
||||||
const [showNewComponentModal, setShowNewComponentModal] = useState(false);
|
|
||||||
const [showEditComponentModal, setShowEditComponentModal] = useState(false);
|
|
||||||
|
|
||||||
// 컴포넌트 데이터 가져오기
|
|
||||||
const {
|
|
||||||
data: componentsData,
|
|
||||||
isLoading: loading,
|
|
||||||
error,
|
|
||||||
refetch,
|
|
||||||
} = useComponents({
|
|
||||||
category: selectedCategory === "all" ? undefined : selectedCategory,
|
|
||||||
active: "Y",
|
|
||||||
search: searchTerm,
|
|
||||||
sort: sortBy,
|
|
||||||
order: sortOrder,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 카테고리와 통계 데이터
|
|
||||||
const { data: categories } = useComponentCategories();
|
|
||||||
const { data: statistics } = useComponentStatistics();
|
|
||||||
|
|
||||||
// 뮤테이션
|
|
||||||
const deleteComponentMutation = useDeleteComponent();
|
|
||||||
const createComponentMutation = useCreateComponent();
|
|
||||||
const updateComponentMutation = useUpdateComponent();
|
|
||||||
|
|
||||||
// 컴포넌트 목록 (이미 필터링과 정렬이 적용된 상태)
|
|
||||||
const components = componentsData?.components || [];
|
|
||||||
|
|
||||||
// 카테고리별 통계 (백엔드에서 가져온 데이터 사용)
|
|
||||||
const categoryStats = useMemo(() => {
|
|
||||||
if (!statistics?.byCategory) return {};
|
|
||||||
|
|
||||||
const stats: Record<string, number> = {};
|
|
||||||
statistics.byCategory.forEach(({ category, count }) => {
|
|
||||||
stats[category] = count;
|
|
||||||
});
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}, [statistics]);
|
|
||||||
|
|
||||||
// 카테고리 이름 및 색상 가져오기
|
|
||||||
const getCategoryInfo = (categoryId: string) => {
|
|
||||||
const category = COMPONENT_CATEGORIES.find((cat) => cat.id === categoryId);
|
|
||||||
return category || { id: "other", name: "기타", color: "gray" };
|
|
||||||
};
|
|
||||||
|
|
||||||
// 삭제 처리
|
|
||||||
const handleDelete = async () => {
|
|
||||||
if (!selectedComponent) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await deleteComponentMutation.mutateAsync(selectedComponent.component_code);
|
|
||||||
setShowDeleteModal(false);
|
|
||||||
setSelectedComponent(null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("컴포넌트 삭제 실패:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 컴포넌트 생성 처리
|
|
||||||
const handleCreate = async (data: any) => {
|
|
||||||
await createComponentMutation.mutateAsync(data);
|
|
||||||
setShowNewComponentModal(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 컴포넌트 수정 처리
|
|
||||||
const handleUpdate = async (data: any) => {
|
|
||||||
if (!selectedComponent) return;
|
|
||||||
await updateComponentMutation.mutateAsync({
|
|
||||||
component_code: selectedComponent.component_code,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
setShowEditComponentModal(false);
|
|
||||||
setSelectedComponent(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<RefreshCw className="mx-auto h-8 w-8 animate-spin text-gray-400" />
|
|
||||||
<p className="mt-2 text-sm text-gray-500">컴포넌트 목록을 불러오는 중...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<Package className="mx-auto h-8 w-8 text-red-400" />
|
|
||||||
<p className="mt-2 text-sm text-red-600">컴포넌트 목록을 불러오는데 실패했습니다.</p>
|
|
||||||
<Button variant="outline" size="sm" onClick={() => refetch()} className="mt-4">
|
|
||||||
<RefreshCw className="mr-2 h-4 w-4" />
|
|
||||||
다시 시도
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full flex-col p-6">
|
|
||||||
{/* 헤더 */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900">컴포넌트 관리</h1>
|
|
||||||
<p className="text-sm text-gray-500">화면 설계에 사용되는 컴포넌트들을 관리합니다</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button size="sm" onClick={() => setShowNewComponentModal(true)}>
|
|
||||||
<Plus className="mr-2 h-4 w-4" />새 컴포넌트
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 카테고리 통계 */}
|
|
||||||
<div className="mb-6 grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-6">
|
|
||||||
{COMPONENT_CATEGORIES.map((category) => {
|
|
||||||
const count = categoryStats[category.id] || 0;
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={category.id}
|
|
||||||
className="cursor-pointer hover:shadow-md"
|
|
||||||
onClick={() => setSelectedCategory(category.id)}
|
|
||||||
>
|
|
||||||
<CardContent className="p-4 text-center">
|
|
||||||
<div className={`mb-2 text-2xl font-bold text-${category.color}-600`}>{count}</div>
|
|
||||||
<div className="text-sm text-gray-600">{category.name}</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 검색 및 필터 */}
|
|
||||||
<Card className="mb-6">
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div className="flex flex-col space-y-4 md:flex-row md:items-center md:space-y-0 md:space-x-4">
|
|
||||||
{/* 검색 */}
|
|
||||||
<div className="relative flex-1">
|
|
||||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
|
||||||
<Input
|
|
||||||
placeholder="컴포넌트 이름, 타입, 설명으로 검색..."
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
className="pl-9"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 카테고리 필터 */}
|
|
||||||
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
|
|
||||||
<SelectTrigger className="w-40">
|
|
||||||
<Filter className="mr-2 h-4 w-4" />
|
|
||||||
<SelectValue placeholder="카테고리" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">전체 카테고리</SelectItem>
|
|
||||||
{COMPONENT_CATEGORIES.map((category) => (
|
|
||||||
<SelectItem key={category.id} value={category.id}>
|
|
||||||
{category.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
{/* 정렬 */}
|
|
||||||
<Select value={sortBy} onValueChange={setSortBy}>
|
|
||||||
<SelectTrigger className="w-32">
|
|
||||||
<SelectValue placeholder="정렬" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="sort_order">순서</SelectItem>
|
|
||||||
<SelectItem value="type_name">이름</SelectItem>
|
|
||||||
<SelectItem value="web_type">타입</SelectItem>
|
|
||||||
<SelectItem value="category">카테고리</SelectItem>
|
|
||||||
<SelectItem value="updated_date">수정일</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setSortOrder(sortOrder === "asc" ? "desc" : "asc")}>
|
|
||||||
{sortOrder === "asc" ? "↑" : "↓"}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button variant="outline" size="sm" onClick={() => refetch()}>
|
|
||||||
<RefreshCw className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 컴포넌트 목록 테이블 */}
|
|
||||||
<Card className="flex-1">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-center justify-between">
|
|
||||||
<span>컴포넌트 목록 ({components.length}개)</span>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-0">
|
|
||||||
<div className="overflow-auto">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>컴포넌트 이름</TableHead>
|
|
||||||
<TableHead>컴포넌트 코드</TableHead>
|
|
||||||
<TableHead>카테고리</TableHead>
|
|
||||||
<TableHead>타입</TableHead>
|
|
||||||
<TableHead>상태</TableHead>
|
|
||||||
<TableHead>수정일</TableHead>
|
|
||||||
<TableHead className="w-24">작업</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{components.map((component) => {
|
|
||||||
const categoryInfo = getCategoryInfo(component.category || "other");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow key={component.component_code}>
|
|
||||||
<TableCell>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">{component.component_name}</div>
|
|
||||||
{component.component_name_eng && (
|
|
||||||
<div className="text-xs text-gray-500">{component.component_name_eng}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<code className="rounded bg-gray-100 px-2 py-1 text-xs">{component.component_code}</code>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={`text-${categoryInfo.color}-600 border-${categoryInfo.color}-200`}
|
|
||||||
>
|
|
||||||
{categoryInfo.name}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{component.component_config ? (
|
|
||||||
<code className="text-xs text-blue-600">
|
|
||||||
{component.component_config.type || component.component_code}
|
|
||||||
</code>
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-gray-400">없음</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant={component.is_active === "Y" ? "default" : "secondary"}>
|
|
||||||
{component.is_active === "Y" ? "활성" : "비활성"}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-xs text-gray-500">
|
|
||||||
{component.updated_date ? new Date(component.updated_date).toLocaleDateString() : "-"}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedComponent(component);
|
|
||||||
setShowEditComponentModal(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Edit className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedComponent(component);
|
|
||||||
setShowDeleteModal(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 삭제 확인 모달 */}
|
|
||||||
<AlertModal
|
|
||||||
isOpen={showDeleteModal}
|
|
||||||
onClose={() => setShowDeleteModal(false)}
|
|
||||||
onConfirm={handleDelete}
|
|
||||||
type="warning"
|
|
||||||
title="컴포넌트 삭제"
|
|
||||||
message={`정말로 "${selectedComponent?.component_name}" 컴포넌트를 삭제하시겠습니까?`}
|
|
||||||
confirmText="삭제"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 새 컴포넌트 추가 모달 */}
|
|
||||||
<ComponentFormModal
|
|
||||||
isOpen={showNewComponentModal}
|
|
||||||
onClose={() => setShowNewComponentModal(false)}
|
|
||||||
onSubmit={handleCreate}
|
|
||||||
mode="create"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 컴포넌트 편집 모달 */}
|
|
||||||
<ComponentFormModal
|
|
||||||
isOpen={showEditComponentModal}
|
|
||||||
onClose={() => {
|
|
||||||
setShowEditComponentModal(false);
|
|
||||||
setSelectedComponent(null);
|
|
||||||
}}
|
|
||||||
onSubmit={handleUpdate}
|
|
||||||
initialData={selectedComponent}
|
|
||||||
mode="edit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,565 +0,0 @@
|
||||||
"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 { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Plus, X, Save, RotateCcw, AlertTriangle, CheckCircle } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { useComponentDuplicateCheck } from "@/hooks/admin/useComponentDuplicateCheck";
|
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
||||||
|
|
||||||
// 컴포넌트 카테고리 정의
|
|
||||||
const COMPONENT_CATEGORIES = [
|
|
||||||
{ id: "input", name: "입력", description: "사용자 입력을 받는 컴포넌트" },
|
|
||||||
{ id: "action", name: "액션", description: "사용자 액션을 처리하는 컴포넌트" },
|
|
||||||
{ id: "display", name: "표시", description: "정보를 표시하는 컴포넌트" },
|
|
||||||
{ id: "layout", name: "레이아웃", description: "레이아웃을 구성하는 컴포넌트" },
|
|
||||||
{ id: "other", name: "기타", description: "기타 컴포넌트" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 컴포넌트 타입 정의
|
|
||||||
const COMPONENT_TYPES = [
|
|
||||||
{ id: "widget", name: "위젯", description: "입력 양식 위젯" },
|
|
||||||
{ id: "button", name: "버튼", description: "액션 버튼" },
|
|
||||||
{ id: "card", name: "카드", description: "카드 컨테이너" },
|
|
||||||
{ id: "container", name: "컨테이너", description: "일반 컨테이너" },
|
|
||||||
{ id: "dashboard", name: "대시보드", description: "대시보드 그리드" },
|
|
||||||
{ id: "alert", name: "알림", description: "알림 메시지" },
|
|
||||||
{ id: "badge", name: "배지", description: "상태 배지" },
|
|
||||||
{ id: "progress", name: "진행률", description: "진행률 표시" },
|
|
||||||
{ id: "chart", name: "차트", description: "데이터 차트" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 웹타입 정의 (위젯인 경우만)
|
|
||||||
const WEB_TYPES = [
|
|
||||||
"text",
|
|
||||||
"number",
|
|
||||||
"decimal",
|
|
||||||
"date",
|
|
||||||
"datetime",
|
|
||||||
"select",
|
|
||||||
"dropdown",
|
|
||||||
"textarea",
|
|
||||||
"boolean",
|
|
||||||
"checkbox",
|
|
||||||
"radio",
|
|
||||||
"code",
|
|
||||||
"entity",
|
|
||||||
"file",
|
|
||||||
"email",
|
|
||||||
"tel",
|
|
||||||
"color",
|
|
||||||
"range",
|
|
||||||
"time",
|
|
||||||
"week",
|
|
||||||
"month",
|
|
||||||
];
|
|
||||||
|
|
||||||
interface ComponentFormData {
|
|
||||||
component_code: string;
|
|
||||||
component_name: string;
|
|
||||||
description: string;
|
|
||||||
category: string;
|
|
||||||
component_config: {
|
|
||||||
type: string;
|
|
||||||
webType?: string;
|
|
||||||
config_panel?: string;
|
|
||||||
};
|
|
||||||
default_size: {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
icon_name: string;
|
|
||||||
active: string;
|
|
||||||
sort_order: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ComponentFormModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onSubmit: (data: ComponentFormData) => Promise<void>;
|
|
||||||
initialData?: any;
|
|
||||||
mode?: "create" | "edit";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ComponentFormModal: React.FC<ComponentFormModalProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
onSubmit,
|
|
||||||
initialData,
|
|
||||||
mode = "create",
|
|
||||||
}) => {
|
|
||||||
const [formData, setFormData] = useState<ComponentFormData>({
|
|
||||||
component_code: "",
|
|
||||||
component_name: "",
|
|
||||||
description: "",
|
|
||||||
category: "other",
|
|
||||||
component_config: {
|
|
||||||
type: "widget",
|
|
||||||
},
|
|
||||||
default_size: {
|
|
||||||
width: 200,
|
|
||||||
height: 40,
|
|
||||||
},
|
|
||||||
icon_name: "",
|
|
||||||
is_active: "Y",
|
|
||||||
sort_order: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [shouldCheckDuplicate, setShouldCheckDuplicate] = useState(false);
|
|
||||||
|
|
||||||
// 중복 체크 쿼리 (생성 모드에서만 활성화)
|
|
||||||
const duplicateCheck = useComponentDuplicateCheck(
|
|
||||||
formData.component_code,
|
|
||||||
mode === "create" && shouldCheckDuplicate && formData.component_code.length > 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 초기 데이터 설정
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
if (mode === "edit" && initialData) {
|
|
||||||
setFormData({
|
|
||||||
component_code: initialData.component_code || "",
|
|
||||||
component_name: initialData.component_name || "",
|
|
||||||
description: initialData.description || "",
|
|
||||||
category: initialData.category || "other",
|
|
||||||
component_config: initialData.component_config || { type: "widget" },
|
|
||||||
default_size: initialData.default_size || { width: 200, height: 40 },
|
|
||||||
icon_name: initialData.icon_name || "",
|
|
||||||
is_active: initialData.is_active || "Y",
|
|
||||||
sort_order: initialData.sort_order || 100,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 새 컴포넌트 생성 시 초기값
|
|
||||||
setFormData({
|
|
||||||
component_code: "",
|
|
||||||
component_name: "",
|
|
||||||
description: "",
|
|
||||||
category: "other",
|
|
||||||
component_config: {
|
|
||||||
type: "widget",
|
|
||||||
},
|
|
||||||
default_size: {
|
|
||||||
width: 200,
|
|
||||||
height: 40,
|
|
||||||
},
|
|
||||||
icon_name: "",
|
|
||||||
is_active: "Y",
|
|
||||||
sort_order: 100,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [isOpen, mode, initialData]);
|
|
||||||
|
|
||||||
// 컴포넌트 코드 자동 생성
|
|
||||||
const generateComponentCode = (name: string, type: string) => {
|
|
||||||
if (!name) return "";
|
|
||||||
|
|
||||||
// 한글을 영문으로 매핑
|
|
||||||
const koreanToEnglish: { [key: string]: string } = {
|
|
||||||
도움말: "help",
|
|
||||||
툴팁: "tooltip",
|
|
||||||
안내: "guide",
|
|
||||||
알림: "alert",
|
|
||||||
버튼: "button",
|
|
||||||
카드: "card",
|
|
||||||
대시보드: "dashboard",
|
|
||||||
패널: "panel",
|
|
||||||
입력: "input",
|
|
||||||
텍스트: "text",
|
|
||||||
선택: "select",
|
|
||||||
체크: "check",
|
|
||||||
라디오: "radio",
|
|
||||||
파일: "file",
|
|
||||||
이미지: "image",
|
|
||||||
테이블: "table",
|
|
||||||
리스트: "list",
|
|
||||||
폼: "form",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 한글을 영문으로 변환
|
|
||||||
let englishName = name;
|
|
||||||
Object.entries(koreanToEnglish).forEach(([korean, english]) => {
|
|
||||||
englishName = englishName.replace(new RegExp(korean, "g"), english);
|
|
||||||
});
|
|
||||||
|
|
||||||
const cleanName = englishName
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9\s]/g, "")
|
|
||||||
.replace(/\s+/g, "-")
|
|
||||||
.replace(/-+/g, "-")
|
|
||||||
.replace(/^-|-$/g, "");
|
|
||||||
|
|
||||||
// 빈 문자열이거나 숫자로 시작하는 경우 기본값 설정
|
|
||||||
const finalName = cleanName || "component";
|
|
||||||
const validName = /^[0-9]/.test(finalName) ? `comp-${finalName}` : finalName;
|
|
||||||
|
|
||||||
return type === "widget" ? validName : `${validName}-${type}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 폼 필드 변경 처리
|
|
||||||
const handleChange = (field: string, value: any) => {
|
|
||||||
setFormData((prev) => {
|
|
||||||
const newData = { ...prev };
|
|
||||||
|
|
||||||
if (field.includes(".")) {
|
|
||||||
const [parent, child] = field.split(".");
|
|
||||||
newData[parent as keyof ComponentFormData] = {
|
|
||||||
...(newData[parent as keyof ComponentFormData] as any),
|
|
||||||
[child]: value,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
(newData as any)[field] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 컴포넌트 이름이 변경되면 코드 자동 생성
|
|
||||||
if (field === "component_name" || field === "component_config.type") {
|
|
||||||
const name = field === "component_name" ? value : newData.component_name;
|
|
||||||
const type = field === "component_config.type" ? value : newData.component_config.type;
|
|
||||||
|
|
||||||
if (name && mode === "create") {
|
|
||||||
newData.component_code = generateComponentCode(name, type);
|
|
||||||
// 자동 생성된 코드에 대해서도 중복 체크 활성화
|
|
||||||
setShouldCheckDuplicate(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 컴포넌트 코드가 직접 변경되면 중복 체크 활성화
|
|
||||||
if (field === "component_code" && mode === "create") {
|
|
||||||
setShouldCheckDuplicate(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newData;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 폼 제출
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
// 유효성 검사
|
|
||||||
if (!formData.component_code || !formData.component_name) {
|
|
||||||
toast.error("컴포넌트 코드와 이름은 필수입니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.component_config.type) {
|
|
||||||
toast.error("컴포넌트 타입을 선택해주세요.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 생성 모드에서 중복 체크
|
|
||||||
if (mode === "create" && duplicateCheck.data?.isDuplicate) {
|
|
||||||
toast.error("이미 사용 중인 컴포넌트 코드입니다. 다른 코드를 사용해주세요.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
try {
|
|
||||||
await onSubmit(formData);
|
|
||||||
toast.success(mode === "create" ? "컴포넌트가 생성되었습니다." : "컴포넌트가 수정되었습니다.");
|
|
||||||
onClose();
|
|
||||||
} catch (error) {
|
|
||||||
toast.error(mode === "create" ? "컴포넌트 생성에 실패했습니다." : "컴포넌트 수정에 실패했습니다.");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 폼 초기화
|
|
||||||
const handleReset = () => {
|
|
||||||
if (mode === "edit" && initialData) {
|
|
||||||
setFormData({
|
|
||||||
component_code: initialData.component_code || "",
|
|
||||||
component_name: initialData.component_name || "",
|
|
||||||
description: initialData.description || "",
|
|
||||||
category: initialData.category || "other",
|
|
||||||
component_config: initialData.component_config || { type: "widget" },
|
|
||||||
default_size: initialData.default_size || { width: 200, height: 40 },
|
|
||||||
icon_name: initialData.icon_name || "",
|
|
||||||
is_active: initialData.is_active || "Y",
|
|
||||||
sort_order: initialData.sort_order || 100,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setFormData({
|
|
||||||
component_code: "",
|
|
||||||
component_name: "",
|
|
||||||
description: "",
|
|
||||||
category: "other",
|
|
||||||
component_config: {
|
|
||||||
type: "widget",
|
|
||||||
},
|
|
||||||
default_size: {
|
|
||||||
width: 200,
|
|
||||||
height: 40,
|
|
||||||
},
|
|
||||||
icon_name: "",
|
|
||||||
is_active: "Y",
|
|
||||||
sort_order: 100,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
||||||
<DialogContent className="max-h-[90vh] max-w-2xl overflow-y-auto">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{mode === "create" ? "새 컴포넌트 추가" : "컴포넌트 편집"}</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{mode === "create"
|
|
||||||
? "화면 설계에 사용할 새로운 컴포넌트를 추가합니다."
|
|
||||||
: "선택한 컴포넌트의 정보를 수정합니다."}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-6 py-4">
|
|
||||||
{/* 기본 정보 */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">기본 정보</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="component_name">컴포넌트 이름 *</Label>
|
|
||||||
<Input
|
|
||||||
id="component_name"
|
|
||||||
value={formData.component_name}
|
|
||||||
onChange={(e) => handleChange("component_name", e.target.value)}
|
|
||||||
placeholder="예: 정보 알림"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="component_code">컴포넌트 코드 *</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
id="component_code"
|
|
||||||
value={formData.component_code}
|
|
||||||
onChange={(e) => handleChange("component_code", e.target.value)}
|
|
||||||
placeholder="예: alert-info"
|
|
||||||
disabled={mode === "edit"}
|
|
||||||
className={
|
|
||||||
mode === "create" && duplicateCheck.data?.isDuplicate
|
|
||||||
? "border-red-500 pr-10"
|
|
||||||
: mode === "create" && duplicateCheck.data && !duplicateCheck.data.isDuplicate
|
|
||||||
? "border-green-500 pr-10"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{mode === "create" && formData.component_code && duplicateCheck.data && (
|
|
||||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
|
|
||||||
{duplicateCheck.data.isDuplicate ? (
|
|
||||||
<AlertTriangle className="h-4 w-4 text-red-500" />
|
|
||||||
) : (
|
|
||||||
<CheckCircle className="h-4 w-4 text-green-500" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{mode === "create" && formData.component_code && duplicateCheck.data && (
|
|
||||||
<Alert
|
|
||||||
className={`mt-2 ${duplicateCheck.data.isDuplicate ? "border-red-200 bg-red-50" : "border-green-200 bg-green-50"}`}
|
|
||||||
>
|
|
||||||
<AlertDescription className={duplicateCheck.data.isDuplicate ? "text-red-700" : "text-green-700"}>
|
|
||||||
{duplicateCheck.data.isDuplicate
|
|
||||||
? "⚠️ 이미 사용 중인 컴포넌트 코드입니다."
|
|
||||||
: "✅ 사용 가능한 컴포넌트 코드입니다."}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="description">설명</Label>
|
|
||||||
<Textarea
|
|
||||||
id="description"
|
|
||||||
value={formData.description}
|
|
||||||
onChange={(e) => handleChange("description", e.target.value)}
|
|
||||||
placeholder="컴포넌트에 대한 설명을 입력하세요"
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label>카테고리</Label>
|
|
||||||
<Select value={formData.category} onValueChange={(value) => handleChange("category", value)}>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{COMPONENT_CATEGORIES.map((category) => (
|
|
||||||
<SelectItem key={category.id} value={category.id}>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">{category.name}</div>
|
|
||||||
<div className="text-xs text-gray-500">{category.description}</div>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="icon_name">아이콘 이름</Label>
|
|
||||||
<Input
|
|
||||||
id="icon_name"
|
|
||||||
value={formData.icon_name}
|
|
||||||
onChange={(e) => handleChange("icon_name", e.target.value)}
|
|
||||||
placeholder="예: info, alert-triangle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 컴포넌트 설정 */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">컴포넌트 설정</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Label>컴포넌트 타입</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.component_config.type}
|
|
||||||
onValueChange={(value) => handleChange("component_config.type", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{COMPONENT_TYPES.map((type) => (
|
|
||||||
<SelectItem key={type.id} value={type.id}>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">{type.name}</div>
|
|
||||||
<div className="text-xs text-gray-500">{type.description}</div>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 위젯인 경우 웹타입 선택 */}
|
|
||||||
{formData.component_config.type === "widget" && (
|
|
||||||
<div>
|
|
||||||
<Label>웹타입</Label>
|
|
||||||
<Select
|
|
||||||
value={formData.component_config.webType || ""}
|
|
||||||
onValueChange={(value) => handleChange("component_config.webType", value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="웹타입을 선택하세요" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{WEB_TYPES.map((webType) => (
|
|
||||||
<SelectItem key={webType} value={webType}>
|
|
||||||
{webType}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="config_panel">설정 패널</Label>
|
|
||||||
<Input
|
|
||||||
id="config_panel"
|
|
||||||
value={formData.component_config.config_panel || ""}
|
|
||||||
onChange={(e) => handleChange("component_config.config_panel", e.target.value)}
|
|
||||||
placeholder="예: AlertConfigPanel"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 기본 크기 및 기타 설정 */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">기본 크기 및 기타</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="width">기본 너비 (px)</Label>
|
|
||||||
<Input
|
|
||||||
id="width"
|
|
||||||
type="number"
|
|
||||||
value={formData.default_size.width}
|
|
||||||
onChange={(e) => handleChange("default_size.width", parseInt(e.target.value))}
|
|
||||||
min="1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="height">기본 높이 (px)</Label>
|
|
||||||
<Input
|
|
||||||
id="height"
|
|
||||||
type="number"
|
|
||||||
value={formData.default_size.height}
|
|
||||||
onChange={(e) => handleChange("default_size.height", parseInt(e.target.value))}
|
|
||||||
min="1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="sort_order">정렬 순서</Label>
|
|
||||||
<Input
|
|
||||||
id="sort_order"
|
|
||||||
type="number"
|
|
||||||
value={formData.sort_order}
|
|
||||||
onChange={(e) => handleChange("sort_order", parseInt(e.target.value))}
|
|
||||||
min="0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Switch
|
|
||||||
id="active"
|
|
||||||
checked={formData.is_active === "Y"}
|
|
||||||
onCheckedChange={(checked) => handleChange("is_active", checked ? "Y" : "N")}
|
|
||||||
/>
|
|
||||||
<Label htmlFor="active">활성화</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={handleReset} disabled={isSubmitting}>
|
|
||||||
<RotateCcw className="mr-2 h-4 w-4" />
|
|
||||||
초기화
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" onClick={onClose} disabled={isSubmitting}>
|
|
||||||
취소
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={isSubmitting || (mode === "create" && duplicateCheck.data?.isDuplicate)}
|
|
||||||
>
|
|
||||||
<Save className="mr-2 h-4 w-4" />
|
|
||||||
{isSubmitting ? "저장 중..." : mode === "create" ? "생성" : "수정"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { checkComponentDuplicate } from "@/lib/api/componentApi";
|
|
||||||
|
|
||||||
export const useComponentDuplicateCheck = (componentCode: string, enabled: boolean = true) => {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["componentDuplicateCheck", componentCode],
|
|
||||||
queryFn: async () => {
|
|
||||||
const result = await checkComponentDuplicate(componentCode);
|
|
||||||
console.log(`🔍 중복 체크 응답 데이터:`, { componentCode, result, isDuplicate: result.isDuplicate });
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
enabled: enabled && !!componentCode && componentCode.length > 0,
|
|
||||||
staleTime: 0, // 항상 최신 데이터 확인
|
|
||||||
retry: false, // 실패 시 재시도 안함
|
|
||||||
});
|
|
||||||
};
|
|
||||||
Loading…
Reference in New Issue