ui 수정 및 시현할 기능 업데이트

This commit is contained in:
leeheejin
2025-10-02 14:34:15 +09:00
parent 2c0dca08b4
commit 3fa410cbe4
168 changed files with 1545 additions and 1066 deletions

View File

@@ -1,14 +1,12 @@
"use client";
import React, { useState, useMemo } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ComponentRegistry } from "@/lib/registry/ComponentRegistry";
import { ComponentDefinition, ComponentCategory } from "@/types/component";
import { Search, Package, Grid, Layers, Palette, Zap, RotateCcw } from "lucide-react";
import { Search, Package, Grid, Layers, Palette, Zap, MousePointer } from "lucide-react";
interface ComponentsPanelProps {
className?: string;
@@ -16,136 +14,93 @@ interface ComponentsPanelProps {
export function ComponentsPanel({ className }: ComponentsPanelProps) {
const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState<ComponentCategory | "all">("all");
const [selectedCategory, setSelectedCategory] = useState<"all" | "display" | "action" | "layout" | "utility">("all");
// 레지스트리에서 모든 컴포넌트 조회
const allComponents = useMemo(() => {
const components = ComponentRegistry.getAllComponents();
// console.log("🔍 ComponentsPanel - 로드된 컴포넌트:", components.map(c => ({ id: c.id, name: c.name, category: c.category })));
// 수동으로 table-list 컴포넌트 추가 (임시)
const hasTableList = components.some(c => c.id === 'table-list');
if (!hasTableList) {
// console.log("⚠️ table-list 컴포넌트가 없어서 수동 추가");
components.push({
id: "table-list",
name: "테이블 리스트",
nameEng: "TableList Component",
description: "데이터베이스 테이블의 데이터를 목록으로 표시하는 컴포넌트",
category: "display",
webType: "text",
defaultConfig: {},
defaultSize: { width: 800, height: 400 },
icon: "Table",
tags: ["테이블", "데이터", "목록", "그리드"],
version: "1.0.0",
author: "개발팀",
});
id: 'table-list',
name: '데이터 테이블 v2',
description: '검색, 필터링, 페이징, CRUD 기능이 포함된 완전한 데이터 테이블 컴포넌트',
category: 'display',
tags: ['table', 'data', 'crud'],
defaultSize: { width: 1000, height: 680 },
} as ComponentDefinition);
}
return components;
}, []);
// 카테고리별 분류 (input 카테고리 제외)
// 카테고리별 컴포넌트 그룹화
const componentsByCategory = useMemo(() => {
// input 카테고리 컴포넌트들을 제외한 컴포넌트만 필터링
const filteredComponents = allComponents.filter((component) => component.category !== "input");
const categories: Record<ComponentCategory | "all", ComponentDefinition[]> = {
all: filteredComponents, // input 카테고리 제외된 컴포넌트들만 포함
input: [], // 빈 배열로 유지 (사용되지 않음)
display: [],
action: [],
layout: [],
utility: [],
return {
all: allComponents,
display: allComponents.filter((c) => c.category === "display"),
action: allComponents.filter((c) => c.category === "action"),
layout: allComponents.filter((c) => c.category === "layout"),
utility: allComponents.filter((c) => c.category === "utility"),
};
filteredComponents.forEach((component) => {
if (categories[component.category]) {
categories[component.category].push(component);
}
});
return categories;
}, [allComponents]);
// 검색 및 필터링된 컴포넌트
const filteredComponents = useMemo(() => {
let components = componentsByCategory[selectedCategory] || [];
let components = selectedCategory === "all" ? componentsByCategory.all : componentsByCategory[selectedCategory as keyof typeof componentsByCategory];
if (searchQuery.trim()) {
if (searchQuery) {
const query = searchQuery.toLowerCase();
components = components.filter(
(component) =>
(component: ComponentDefinition) =>
component.name.toLowerCase().includes(query) ||
component.description.toLowerCase().includes(query) ||
component.tags?.some((tag) => tag.toLowerCase().includes(query)),
component.tags?.some((tag: string) => tag.toLowerCase().includes(query))
);
}
return components;
}, [componentsByCategory, selectedCategory, searchQuery]);
// 카테고리 아이콘 매핑
const getCategoryIcon = (category: ComponentCategory) => {
switch (category) {
case "display":
return <Palette className="h-6 w-6" />;
case "action":
return <Zap className="h-6 w-6" />;
case "layout":
return <Layers className="h-6 w-6" />;
case "utility":
return <Package className="h-6 w-6" />;
default:
return <Grid className="h-6 w-6" />;
}
};
// 드래그 시작 핸들러
const handleDragStart = (e: React.DragEvent, component: ComponentDefinition) => {
const handleDragStart = (e: React.DragEvent<HTMLDivElement>, component: ComponentDefinition) => {
const dragData = {
type: "component",
component: component,
};
// console.log("🚀 컴포넌트 드래그 시작:", component.name, dragData);
e.dataTransfer.setData("application/json", JSON.stringify(dragData));
e.dataTransfer.effectAllowed = "copy";
};
// 카테고리별 아이콘
const getCategoryIcon = (category: ComponentCategory | "all") => {
switch (category) {
case "input":
return <Grid className="h-4 w-4" />;
case "display":
return <Palette className="h-4 w-4" />;
case "action":
return <Zap className="h-4 w-4" />;
case "layout":
return <Layers className="h-4 w-4" />;
case "utility":
return <Package className="h-4 w-4" />;
default:
return <Package className="h-4 w-4" />;
}
};
// 컴포넌트 새로고침
const handleRefresh = () => {
// Hot Reload 트리거 (개발 모드에서만)
if (process.env.NODE_ENV === "development") {
ComponentRegistry.refreshComponents?.();
}
window.location.reload();
};
return (
<div className={`h-full bg-gradient-to-br from-slate-50 to-purple-50/30 border-r border-gray-200/60 shadow-sm ${className}`}>
<div className="p-6">
{/* 헤더 */}
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-1"></h2>
<p className="text-sm text-gray-500">{componentsByCategory.all.length} </p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleRefresh}
title="컴포넌트 새로고침"
className="bg-white/60 border-gray-200/60 hover:bg-white hover:border-gray-300"
>
<RotateCcw className="h-4 w-4" />
</Button>
</div>
<div className={`flex h-full flex-col bg-slate-50 p-6 border-r border-gray-200/60 shadow-sm ${className}`}>
{/* 헤더 */}
<div className="mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-1"></h2>
<p className="text-sm text-gray-500">7 </p>
</div>
{/* 검색 */}
<div className="relative mb-6">
{/* 검색 */}
<div className="space-y-4">
<div className="relative">
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Input
placeholder="컴포넌트 검색..."
@@ -154,166 +109,125 @@ export function ComponentsPanel({ className }: ComponentsPanelProps) {
className="pl-10 border-0 bg-white/80 backdrop-blur-sm shadow-sm focus:bg-white transition-colors"
/>
</div>
{/* 카테고리 필터 */}
<div className="flex flex-wrap gap-2">
<Button
variant={selectedCategory === "all" ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory("all")}
className="flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all"
>
<Package className="h-3 w-3" />
<span></span>
</Button>
<Button
variant={selectedCategory === "display" ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory("display")}
className="flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all"
>
<Palette className="h-3 w-3" />
<span></span>
</Button>
<Button
variant={selectedCategory === "action" ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory("action")}
className="flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all"
>
<Zap className="h-3 w-3" />
<span></span>
</Button>
<Button
variant={selectedCategory === "layout" ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory("layout")}
className="flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all"
>
<Layers className="h-3 w-3" />
<span></span>
</Button>
<Button
variant={selectedCategory === "utility" ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory("utility")}
className="flex items-center space-x-1.5 h-8 px-3 text-xs font-medium rounded-full transition-all"
>
<Package className="h-3 w-3" />
<span></span>
</Button>
</div>
</div>
<div className="px-6">
<Tabs
value={selectedCategory}
onValueChange={(value) => setSelectedCategory(value as ComponentCategory | "all")}
>
{/* 카테고리 탭 (input 카테고리 제외) */}
<TabsList className="grid w-full grid-cols-3 lg:grid-cols-5 bg-white/60 backdrop-blur-sm border-0 p-1">
<TabsTrigger value="all" className="flex items-center text-xs data-[state=active]:bg-purple-600 data-[state=active]:text-white">
<Package className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="display" className="flex items-center text-xs data-[state=active]:bg-purple-600 data-[state=active]:text-white">
<Palette className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="action" className="flex items-center text-xs data-[state=active]:bg-purple-600 data-[state=active]:text-white">
<Zap className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="layout" className="flex items-center text-xs data-[state=active]:bg-purple-600 data-[state=active]:text-white">
<Layers className="mr-1 h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="utility" className="flex items-center text-xs data-[state=active]:bg-purple-600 data-[state=active]:text-white">
<Package className="mr-1 h-3 w-3" />
</TabsTrigger>
</TabsList>
{/* 주황색 강조 영역 */}
<div className="mt-4 mb-4 p-4 bg-gradient-to-r from-orange-50 to-amber-50 border border-orange-200 rounded-lg">
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-orange-500 rounded-full"></div>
<span className="text-sm font-medium text-orange-800"> </span>
</div>
</div>
{/* 컴포넌트 목록 */}
<div className="mt-6">
<TabsContent value={selectedCategory} className="space-y-3">
{filteredComponents.length > 0 ? (
<div className="grid max-h-96 grid-cols-1 gap-3 overflow-y-auto pr-2">
{filteredComponents.map((component) => (
<div
key={component.id}
draggable
onDragStart={(e) => {
handleDragStart(e, component);
// 드래그 시작 시 시각적 피드백
e.currentTarget.style.opacity = '0.5';
e.currentTarget.style.transform = 'rotate(-3deg) scale(0.95)';
}}
onDragEnd={(e) => {
// 드래그 종료 시 원래 상태로 복원
e.currentTarget.style.opacity = '1';
e.currentTarget.style.transform = 'none';
}}
className="group cursor-grab rounded-lg border border-gray-200/40 bg-white/90 backdrop-blur-sm p-6 shadow-sm transition-all duration-300 hover:bg-white hover:shadow-lg hover:shadow-purple-500/15 hover:scale-[1.02] hover:border-purple-300/60 hover:-translate-y-1 active:cursor-grabbing active:scale-[0.98] active:translate-y-0"
title={component.description}
>
<div className="flex items-start space-x-4">
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-gradient-to-br from-purple-500 to-purple-600 text-white shadow-md group-hover:shadow-lg group-hover:scale-110 transition-all duration-300">
{getCategoryIcon(component.category)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between mb-2">
<h4 className="font-semibold text-gray-900 text-sm leading-tight">{component.name}</h4>
<Badge variant="default" className="bg-gradient-to-r from-emerald-500 to-emerald-600 text-white text-xs border-0 ml-2 px-2 py-1 rounded-full font-medium shadow-sm">
</Badge>
</div>
<p className="text-xs text-gray-500 line-clamp-2 leading-relaxed mb-3">{component.description}</p>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center space-x-2 text-xs text-gray-400">
<span className="bg-gradient-to-r from-gray-100 to-gray-200 px-3 py-1 rounded-full font-medium text-gray-600">
{component.defaultSize.width}×{component.defaultSize.height}
</span>
</div>
<span className="text-xs font-medium text-purple-600 capitalize bg-gradient-to-r from-purple-50 to-indigo-50 px-3 py-1 rounded-full border border-purple-200/50">
{component.category}
</span>
</div>
{/* 태그 */}
{component.tags && component.tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{component.tags.slice(0, 2).map((tag, index) => (
<Badge key={index} variant="outline" className="text-xs bg-gradient-to-r from-gray-50 to-gray-100 text-gray-600 border-gray-200/50 rounded-full px-2 py-1">
{tag}
</Badge>
))}
{component.tags.length > 2 && (
<Badge variant="outline" className="text-xs bg-gradient-to-r from-gray-50 to-gray-100 text-gray-600 border-gray-200/50 rounded-full px-2 py-1">
+{component.tags.length - 2}
</Badge>
)}
</div>
)}
</div>
</div>
</div>
))}
{/* 컴포넌트 목록 */}
<div className="flex-1 space-y-3 overflow-y-auto mt-6">
{filteredComponents.length > 0 ? (
filteredComponents.map((component) => (
<div
key={component.id}
draggable
onDragStart={(e) => {
handleDragStart(e, component);
// 드래그 시작 시 시각적 피드백
e.currentTarget.style.opacity = '0.6';
e.currentTarget.style.transform = 'rotate(2deg) scale(0.98)';
}}
onDragEnd={(e) => {
// 드래그 종료 시 원래 상태로 복원
e.currentTarget.style.opacity = '1';
e.currentTarget.style.transform = 'none';
}}
className="group cursor-grab rounded-lg border border-gray-200/40 bg-white/90 backdrop-blur-sm p-6 shadow-sm transition-all duration-300 hover:bg-white hover:shadow-lg hover:shadow-purple-500/15 hover:scale-[1.02] hover:border-purple-300/60 hover:-translate-y-1 active:cursor-grabbing active:scale-[0.98] active:translate-y-0"
>
<div className="flex items-start space-x-4">
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-purple-100 text-purple-700 shadow-md group-hover:shadow-lg group-hover:scale-110 transition-all duration-300">
{getCategoryIcon(component.category)}
</div>
) : (
<div className="py-12 text-center text-gray-500">
<div className="p-8">
<Package className="mx-auto mb-3 h-12 w-12 text-gray-300" />
<p className="text-sm font-medium text-gray-600">
{searchQuery
? `"${searchQuery}"에 대한 컴포넌트를 찾을 수 없습니다`
: "이 카테고리에 컴포넌트가 없습니다"}
</p>
<p className="text-xs text-gray-400 mt-1"> </p>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between mb-2">
<h4 className="font-semibold text-gray-900 text-sm leading-tight">{component.name}</h4>
<Badge variant="secondary" className="text-xs bg-purple-50 text-purple-600 border-0 ml-2 px-2 py-1 rounded-full font-medium">
</Badge>
</div>
<p className="text-xs text-gray-500 line-clamp-2 leading-relaxed mb-3">{component.description}</p>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2 text-xs text-gray-400">
<span className="bg-purple-100 px-3 py-1 rounded-full font-medium text-purple-700 shadow-sm">
{component.defaultSize.width}×{component.defaultSize.height}
</span>
</div>
<span className="text-xs font-medium text-primary capitalize bg-gradient-to-r from-purple-50 to-indigo-50 px-3 py-1 rounded-full border border-primary/20/50">
{component.category}
</span>
</div>
</div>
)}
</TabsContent>
</div>
</Tabs>
{/* 통계 정보 */}
<div className="mt-6 rounded-xl bg-gradient-to-r from-purple-50 to-pink-50 border border-purple-100/60 p-4">
<div className="grid grid-cols-2 gap-4 text-center">
<div>
<div className="text-lg font-bold text-emerald-600">{filteredComponents.length}</div>
<div className="text-xs text-gray-500"></div>
</div>
</div>
<div>
<div className="text-lg font-bold text-purple-600">{allComponents.length}</div>
<div className="text-xs text-gray-500"></div>
</div>
</div>
</div>
{/* 개발 정보 (개발 모드에서만) */}
{process.env.NODE_ENV === "development" && (
<div className="mt-4 rounded-xl bg-gradient-to-r from-gray-50 to-slate-50 border border-gray-100/60 p-4">
<div className="space-y-1 text-xs text-gray-600">
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-blue-500 rounded-full"></span>
<span> </span>
</div>
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
<span>Hot Reload </span>
</div>
<div className="flex items-center space-x-2">
<span className="w-2 h-2 bg-purple-500 rounded-full"></span>
<span> </span>
</div>
))
) : (
<div className="flex h-32 items-center justify-center text-center text-gray-500">
<div className="p-8">
<Package className="mx-auto mb-3 h-12 w-12 text-gray-300" />
<p className="text-sm font-medium text-muted-foreground"> </p>
<p className="text-xs text-gray-400 mt-1"> </p>
</div>
</div>
)}
</div>
{/* 도움말 */}
<div className="rounded-xl bg-gradient-to-r from-purple-50 to-pink-50 border border-purple-100/60 p-4 mt-6">
<div className="flex items-start space-x-3">
<MousePointer className="h-4 w-4 text-purple-600 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-xs text-gray-700 leading-relaxed">
<span className="font-semibold text-purple-700"></span>
</p>
</div>
</div>
</div>
</div>
);
}
export default ComponentsPanel;