From 2a313c5ca20e7e0196a5b9aebc3da1cde210dfc1 Mon Sep 17 00:00:00 2001 From: hyeonsu Date: Thu, 18 Sep 2025 18:18:18 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/(main)/admin/components/page.tsx | 361 ----------- .../components/admin/ComponentFormModal.tsx | 565 ------------------ .../hooks/admin/useComponentDuplicateCheck.ts | 16 - 3 files changed, 942 deletions(-) delete mode 100644 frontend/app/(main)/admin/components/page.tsx delete mode 100644 frontend/components/admin/ComponentFormModal.tsx delete mode 100644 frontend/hooks/admin/useComponentDuplicateCheck.ts diff --git a/frontend/app/(main)/admin/components/page.tsx b/frontend/app/(main)/admin/components/page.tsx deleted file mode 100644 index dbd63be8..00000000 --- a/frontend/app/(main)/admin/components/page.tsx +++ /dev/null @@ -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("all"); - const [sortBy, setSortBy] = useState("sort_order"); - const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); - const [selectedComponent, setSelectedComponent] = useState(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 = {}; - 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 ( -
-
- -

컴포넌트 목록을 불러오는 중...

-
-
- ); - } - - if (error) { - return ( -
-
- -

컴포넌트 목록을 불러오는데 실패했습니다.

- -
-
- ); - } - - return ( -
- {/* 헤더 */} -
-
-
-

컴포넌트 관리

-

화면 설계에 사용되는 컴포넌트들을 관리합니다

-
-
- -
-
-
- - {/* 카테고리 통계 */} -
- {COMPONENT_CATEGORIES.map((category) => { - const count = categoryStats[category.id] || 0; - return ( - setSelectedCategory(category.id)} - > - -
{count}
-
{category.name}
-
-
- ); - })} -
- - {/* 검색 및 필터 */} - - -
- {/* 검색 */} -
- - setSearchTerm(e.target.value)} - className="pl-9" - /> -
- - {/* 카테고리 필터 */} - - - {/* 정렬 */} - - - - - -
-
-
- - {/* 컴포넌트 목록 테이블 */} - - - - 컴포넌트 목록 ({components.length}개) - - - -
- - - - 컴포넌트 이름 - 컴포넌트 코드 - 카테고리 - 타입 - 상태 - 수정일 - 작업 - - - - {components.map((component) => { - const categoryInfo = getCategoryInfo(component.category || "other"); - - return ( - - -
-
{component.component_name}
- {component.component_name_eng && ( -
{component.component_name_eng}
- )} -
-
- - {component.component_code} - - - - {categoryInfo.name} - - - - {component.component_config ? ( - - {component.component_config.type || component.component_code} - - ) : ( - 없음 - )} - - - - {component.is_active === "Y" ? "활성" : "비활성"} - - - - {component.updated_date ? new Date(component.updated_date).toLocaleDateString() : "-"} - - -
- - -
-
-
- ); - })} -
-
-
-
-
- - {/* 삭제 확인 모달 */} - setShowDeleteModal(false)} - onConfirm={handleDelete} - type="warning" - title="컴포넌트 삭제" - message={`정말로 "${selectedComponent?.component_name}" 컴포넌트를 삭제하시겠습니까?`} - confirmText="삭제" - /> - - {/* 새 컴포넌트 추가 모달 */} - setShowNewComponentModal(false)} - onSubmit={handleCreate} - mode="create" - /> - - {/* 컴포넌트 편집 모달 */} - { - setShowEditComponentModal(false); - setSelectedComponent(null); - }} - onSubmit={handleUpdate} - initialData={selectedComponent} - mode="edit" - /> -
- ); -} diff --git a/frontend/components/admin/ComponentFormModal.tsx b/frontend/components/admin/ComponentFormModal.tsx deleted file mode 100644 index c64b62c9..00000000 --- a/frontend/components/admin/ComponentFormModal.tsx +++ /dev/null @@ -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; - initialData?: any; - mode?: "create" | "edit"; -} - -export const ComponentFormModal: React.FC = ({ - isOpen, - onClose, - onSubmit, - initialData, - mode = "create", -}) => { - const [formData, setFormData] = useState({ - 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 ( - - - - {mode === "create" ? "새 컴포넌트 추가" : "컴포넌트 편집"} - - {mode === "create" - ? "화면 설계에 사용할 새로운 컴포넌트를 추가합니다." - : "선택한 컴포넌트의 정보를 수정합니다."} - - - -
- {/* 기본 정보 */} - - - 기본 정보 - - -
-
- - handleChange("component_name", e.target.value)} - placeholder="예: 정보 알림" - /> -
-
- -
- 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 && ( -
- {duplicateCheck.data.isDuplicate ? ( - - ) : ( - - )} -
- )} -
- {mode === "create" && formData.component_code && duplicateCheck.data && ( - - - {duplicateCheck.data.isDuplicate - ? "⚠️ 이미 사용 중인 컴포넌트 코드입니다." - : "✅ 사용 가능한 컴포넌트 코드입니다."} - - - )} -
-
- -
- -