ui개선
This commit is contained in:
@@ -107,17 +107,17 @@ export function ComponentsPanel({ className }: ComponentsPanelProps) {
|
||||
e.currentTarget.style.opacity = "1";
|
||||
e.currentTarget.style.transform = "none";
|
||||
}}
|
||||
className="group cursor-grab rounded-lg border border-gray-200/40 bg-white/90 p-4 shadow-sm backdrop-blur-sm transition-all duration-300 hover:-translate-y-1 hover:scale-[1.02] hover:border-purple-300/60 hover:bg-white hover:shadow-lg hover:shadow-purple-500/15 active:translate-y-0 active:scale-[0.98] active:cursor-grabbing"
|
||||
className="group bg-card hover:border-primary/50 cursor-grab rounded-lg border p-3 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md active:translate-y-0 active:scale-[0.98] active:cursor-grabbing"
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-purple-100 text-purple-700 shadow-md transition-all duration-300 group-hover:scale-110 group-hover:shadow-lg">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-primary/10 text-primary group-hover:bg-primary/20 flex h-10 w-10 items-center justify-center rounded-md transition-all duration-200">
|
||||
{getCategoryIcon(component.category)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="mb-1 text-sm leading-tight font-semibold text-gray-900">{component.name}</h4>
|
||||
<p className="mb-2 line-clamp-2 text-xs leading-relaxed text-gray-500">{component.description}</p>
|
||||
<div className="flex items-center space-x-2 text-xs text-gray-400">
|
||||
<span className="rounded-full bg-purple-100 px-2 py-0.5 font-medium text-purple-700">
|
||||
<h4 className="mb-1 text-xs leading-tight font-semibold">{component.name}</h4>
|
||||
<p className="text-muted-foreground mb-1.5 line-clamp-2 text-xs leading-relaxed">{component.description}</p>
|
||||
<div className="flex items-center">
|
||||
<span className="bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs font-medium">
|
||||
{component.defaultSize.width}×{component.defaultSize.height}
|
||||
</span>
|
||||
</div>
|
||||
@@ -128,80 +128,80 @@ export function ComponentsPanel({ className }: ComponentsPanelProps) {
|
||||
|
||||
// 빈 상태 렌더링
|
||||
const renderEmptyState = () => (
|
||||
<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-muted-foreground text-sm font-medium">컴포넌트를 찾을 수 없습니다</p>
|
||||
<p className="mt-1 text-xs text-gray-400">검색어를 조정해보세요</p>
|
||||
<div className="flex h-32 items-center justify-center text-center">
|
||||
<div className="p-6">
|
||||
<Package className="text-muted-foreground/40 mx-auto mb-2 h-10 w-10" />
|
||||
<p className="text-muted-foreground text-xs font-medium">컴포넌트를 찾을 수 없습니다</p>
|
||||
<p className="text-muted-foreground/60 mt-1 text-xs">검색어를 조정해보세요</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`flex h-full flex-col border-r border-gray-200/60 bg-slate-50 p-6 shadow-sm ${className}`}>
|
||||
<div className={`bg-background flex h-full flex-col p-4 ${className}`}>
|
||||
{/* 헤더 */}
|
||||
<div className="mb-4">
|
||||
<h2 className="mb-1 text-lg font-semibold text-gray-900">컴포넌트</h2>
|
||||
<p className="text-sm text-gray-500">{allComponents.length}개의 사용 가능한 컴포넌트</p>
|
||||
<div className="mb-3">
|
||||
<h2 className="mb-0.5 text-sm font-semibold">컴포넌트</h2>
|
||||
<p className="text-muted-foreground text-xs">{allComponents.length}개 사용 가능</p>
|
||||
</div>
|
||||
|
||||
{/* 검색 */}
|
||||
<div className="mb-4">
|
||||
<div className="mb-3">
|
||||
<div className="relative">
|
||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
||||
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-3.5 w-3.5 -translate-y-1/2" />
|
||||
<Input
|
||||
placeholder="컴포넌트 검색..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="border-0 bg-white/80 pl-10 shadow-sm backdrop-blur-sm transition-colors focus:bg-white"
|
||||
className="h-8 pl-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 카테고리 탭 */}
|
||||
<Tabs defaultValue="input" className="flex flex-1 flex-col">
|
||||
<TabsList className="mb-4 grid w-full grid-cols-4 bg-white/80 p-1">
|
||||
<TabsTrigger value="input" className="flex items-center gap-1 text-xs">
|
||||
<TabsList className="mb-3 grid h-8 w-full grid-cols-4">
|
||||
<TabsTrigger value="input" className="flex items-center gap-1 px-1 text-xs">
|
||||
<Edit3 className="h-3 w-3" />
|
||||
입력
|
||||
<span className="hidden sm:inline">입력</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="action" className="flex items-center gap-1 text-xs">
|
||||
<TabsTrigger value="action" className="flex items-center gap-1 px-1 text-xs">
|
||||
<Zap className="h-3 w-3" />
|
||||
액션
|
||||
<span className="hidden sm:inline">액션</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="display" className="flex items-center gap-1 text-xs">
|
||||
<TabsTrigger value="display" className="flex items-center gap-1 px-1 text-xs">
|
||||
<BarChart3 className="h-3 w-3" />
|
||||
표시
|
||||
<span className="hidden sm:inline">표시</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="layout" className="flex items-center gap-1 text-xs">
|
||||
<TabsTrigger value="layout" className="flex items-center gap-1 px-1 text-xs">
|
||||
<Layers className="h-3 w-3" />
|
||||
레이아웃
|
||||
<span className="hidden sm:inline">레이아웃</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 입력 컴포넌트 */}
|
||||
<TabsContent value="input" className="mt-0 flex-1 space-y-3 overflow-y-auto">
|
||||
<TabsContent value="input" className="mt-0 flex-1 space-y-2 overflow-y-auto">
|
||||
{getFilteredComponents("input").length > 0
|
||||
? getFilteredComponents("input").map(renderComponentCard)
|
||||
: renderEmptyState()}
|
||||
</TabsContent>
|
||||
|
||||
{/* 액션 컴포넌트 */}
|
||||
<TabsContent value="action" className="mt-0 flex-1 space-y-3 overflow-y-auto">
|
||||
<TabsContent value="action" className="mt-0 flex-1 space-y-2 overflow-y-auto">
|
||||
{getFilteredComponents("action").length > 0
|
||||
? getFilteredComponents("action").map(renderComponentCard)
|
||||
: renderEmptyState()}
|
||||
</TabsContent>
|
||||
|
||||
{/* 표시 컴포넌트 */}
|
||||
<TabsContent value="display" className="mt-0 flex-1 space-y-3 overflow-y-auto">
|
||||
<TabsContent value="display" className="mt-0 flex-1 space-y-2 overflow-y-auto">
|
||||
{getFilteredComponents("display").length > 0
|
||||
? getFilteredComponents("display").map(renderComponentCard)
|
||||
: renderEmptyState()}
|
||||
</TabsContent>
|
||||
|
||||
{/* 레이아웃 컴포넌트 */}
|
||||
<TabsContent value="layout" className="mt-0 flex-1 space-y-3 overflow-y-auto">
|
||||
<TabsContent value="layout" className="mt-0 flex-1 space-y-2 overflow-y-auto">
|
||||
{getFilteredComponents("layout").length > 0
|
||||
? getFilteredComponents("layout").map(renderComponentCard)
|
||||
: renderEmptyState()}
|
||||
@@ -209,14 +209,12 @@ export function ComponentsPanel({ className }: ComponentsPanelProps) {
|
||||
</Tabs>
|
||||
|
||||
{/* 도움말 */}
|
||||
<div className="mt-4 rounded-xl border border-purple-100/60 bg-gradient-to-r from-purple-50 to-pink-50 p-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<MousePointer className="mt-0.5 h-4 w-4 flex-shrink-0 text-purple-600" />
|
||||
<div className="flex-1">
|
||||
<p className="text-xs leading-relaxed text-gray-700">
|
||||
컴포넌트를 <span className="font-semibold text-purple-700">드래그</span>하여 화면에 추가하세요
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-primary/20 bg-primary/5 mt-3 rounded-lg border p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<MousePointer className="text-primary mt-0.5 h-3.5 w-3.5 flex-shrink-0" />
|
||||
<p className="text-muted-foreground text-xs leading-relaxed">
|
||||
컴포넌트를 <span className="text-foreground font-semibold">드래그</span>하여 화면에 추가하세요
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -81,30 +81,30 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* 헤더 */}
|
||||
<div className="border-b border-gray-200 p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="border-b p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="text-muted-foreground h-4 w-4" />
|
||||
<h3 className="font-medium text-gray-900">레이아웃 설정</h3>
|
||||
<h3 className="text-sm font-semibold">레이아웃 설정</h3>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center space-x-2">
|
||||
<span className="text-muted-foreground text-sm">타입:</span>
|
||||
<span className="rounded bg-purple-100 px-2 py-1 text-xs font-medium text-purple-800">
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs">타입:</span>
|
||||
<span className="bg-primary/10 text-primary rounded-md px-2 py-0.5 text-xs font-medium">
|
||||
{layoutComponent.layoutType}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-500">ID: {layoutComponent.id}</div>
|
||||
<div className="text-muted-foreground mt-1 text-xs">ID: {layoutComponent.id}</div>
|
||||
</div>
|
||||
|
||||
{/* 레이아웃 설정 영역 */}
|
||||
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
||||
<div className="flex-1 space-y-3 overflow-y-auto p-4">
|
||||
{/* 기본 정보 */}
|
||||
<div>
|
||||
<label className="mb-2 block text-sm font-medium text-gray-700">레이아웃 이름</label>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-medium">레이아웃 이름</label>
|
||||
<input
|
||||
type="text"
|
||||
value={layoutComponent.label || ""}
|
||||
onChange={(e) => onUpdateProperty(layoutComponent.id, "label", e.target.value)}
|
||||
className="focus:border-primary w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500"
|
||||
className="border-input bg-background focus-visible:ring-ring h-8 w-full rounded-md border px-3 text-xs focus-visible:ring-1 focus-visible:outline-none"
|
||||
placeholder="레이아웃 이름을 입력하세요"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -55,44 +55,44 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* 헤더 */}
|
||||
<div className="border-b border-gray-200 p-4">
|
||||
<div className="border-b p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Grid3X3 className="h-4 w-4 text-muted-foreground" />
|
||||
<h3 className="font-medium text-gray-900">격자 설정</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Grid3X3 className="text-muted-foreground h-4 w-4" />
|
||||
<h3 className="text-sm font-semibold">격자 설정</h3>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{onForceGridUpdate && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onForceGridUpdate}
|
||||
className="flex items-center space-x-1"
|
||||
className="h-7 px-2 text-xs"
|
||||
title="현재 해상도에 맞게 모든 컴포넌트를 격자에 재정렬합니다"
|
||||
>
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
<span>재정렬</span>
|
||||
<RefreshCw className="mr-1 h-3 w-3" />
|
||||
재정렬
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button size="sm" variant="outline" onClick={onResetGrid} className="flex items-center space-x-1">
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
<span>초기화</span>
|
||||
<Button size="sm" variant="outline" onClick={onResetGrid} className="h-7 px-2 text-xs">
|
||||
<RotateCcw className="mr-1 h-3 w-3" />
|
||||
초기화
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 주요 토글들 */}
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{gridSettings.showGrid ? (
|
||||
<Eye className="h-4 w-4 text-primary" />
|
||||
<Eye className="text-primary h-3.5 w-3.5" />
|
||||
) : (
|
||||
<EyeOff className="h-4 w-4 text-gray-400" />
|
||||
<EyeOff className="text-muted-foreground h-3.5 w-3.5" />
|
||||
)}
|
||||
<Label htmlFor="showGrid" className="text-sm font-medium">
|
||||
<Label htmlFor="showGrid" className="text-xs font-medium">
|
||||
격자 표시
|
||||
</Label>
|
||||
</div>
|
||||
@@ -104,9 +104,9 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Zap className="h-4 w-4 text-green-600" />
|
||||
<Label htmlFor="snapToGrid" className="text-sm font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="text-primary h-3.5 w-3.5" />
|
||||
<Label htmlFor="snapToGrid" className="text-xs font-medium">
|
||||
격자 스냅
|
||||
</Label>
|
||||
</div>
|
||||
@@ -120,14 +120,14 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 설정 영역 */}
|
||||
<div className="flex-1 space-y-6 overflow-y-auto p-4">
|
||||
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
||||
{/* 격자 구조 */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-medium text-gray-900">격자 구조</h4>
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-xs font-semibold">격자 구조</h4>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="columns" className="mb-2 block text-sm font-medium">
|
||||
컬럼 수: {gridSettings.columns}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="columns" className="text-xs font-medium">
|
||||
컬럼 수: <span className="text-primary">{gridSettings.columns}</span>
|
||||
</Label>
|
||||
<Slider
|
||||
id="columns"
|
||||
@@ -138,15 +138,15 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||
onValueChange={([value]) => updateSetting("columns", value)}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="mt-1 flex justify-between text-xs text-gray-500">
|
||||
<div className="text-muted-foreground flex justify-between text-xs">
|
||||
<span>1</span>
|
||||
<span>24</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="gap" className="mb-2 block text-sm font-medium">
|
||||
간격: {gridSettings.gap}px
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gap" className="text-xs font-medium">
|
||||
간격: <span className="text-primary">{gridSettings.gap}px</span>
|
||||
</Label>
|
||||
<Slider
|
||||
id="gap"
|
||||
@@ -157,15 +157,15 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||
onValueChange={([value]) => updateSetting("gap", value)}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="mt-1 flex justify-between text-xs text-gray-500">
|
||||
<div className="text-muted-foreground flex justify-between text-xs">
|
||||
<span>0px</span>
|
||||
<span>40px</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="padding" className="mb-2 block text-sm font-medium">
|
||||
여백: {gridSettings.padding}px
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="padding" className="text-xs font-medium">
|
||||
여백: <span className="text-primary">{gridSettings.padding}px</span>
|
||||
</Label>
|
||||
<Slider
|
||||
id="padding"
|
||||
@@ -176,7 +176,7 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||
onValueChange={([value]) => updateSetting("padding", value)}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="mt-1 flex justify-between text-xs text-gray-500">
|
||||
<div className="text-muted-foreground flex justify-between text-xs">
|
||||
<span>0px</span>
|
||||
<span>60px</span>
|
||||
</div>
|
||||
@@ -248,8 +248,8 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||
opacity: gridSettings.gridOpacity || 0.5,
|
||||
}}
|
||||
>
|
||||
<div className="flex h-16 items-center justify-center rounded border-2 border-dashed border-blue-300 bg-primary/20">
|
||||
<span className="text-xs text-primary">컴포넌트 예시</span>
|
||||
<div className="bg-primary/20 flex h-16 items-center justify-center rounded border-2 border-dashed border-blue-300">
|
||||
<span className="text-primary text-xs">컴포넌트 예시</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -257,7 +257,7 @@ export const GridPanel: React.FC<GridPanelProps> = ({
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="border-t border-gray-200 bg-gray-50 p-3">
|
||||
<div className="text-xs text-muted-foreground">💡 격자 설정은 실시간으로 캔버스에 반영됩니다 </div>
|
||||
<div className="text-muted-foreground text-xs">💡 격자 설정은 실시간으로 캔버스에 반영됩니다 </div>
|
||||
|
||||
{/* 해상도 및 격자 정보 */}
|
||||
{screenResolution && actualGridInfo && (
|
||||
|
||||
@@ -481,9 +481,13 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||
if (!selectedComponent) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center p-6 text-center">
|
||||
<Settings className="mb-4 h-12 w-12 text-gray-400" />
|
||||
<h3 className="mb-2 text-lg font-medium text-gray-900">컴포넌트를 선택하세요</h3>
|
||||
<p className="text-sm text-gray-500">캔버스에서 컴포넌트를 클릭하면 속성을 편집할 수 있습니다.</p>
|
||||
<Settings className="text-muted-foreground mb-3 h-10 w-10" />
|
||||
<h3 className="mb-2 text-sm font-semibold">컴포넌트를 선택하세요</h3>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
캔버스에서 컴포넌트를 클릭하면
|
||||
<br />
|
||||
속성을 편집할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -535,58 +539,58 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||
<div className="flex h-full flex-col">
|
||||
{/* 헤더 */}
|
||||
<div className="border-b border-gray-200 p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Settings className="text-muted-foreground h-4 w-4" />
|
||||
<h3 className="font-medium text-gray-900">속성 편집</h3>
|
||||
<h3 className="text-sm font-semibold">속성 편집</h3>
|
||||
</div>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<Badge variant="secondary" className="text-xs font-medium">
|
||||
{selectedComponent.type}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* 액션 버튼들 */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button size="sm" variant="outline" onClick={onCopyComponent} className="flex items-center space-x-1">
|
||||
<Copy className="h-3 w-3" />
|
||||
<span>복사</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<Button size="sm" variant="outline" onClick={onCopyComponent} className="h-8 px-2.5 text-xs">
|
||||
<Copy className="mr-1 h-3 w-3" />
|
||||
복사
|
||||
</Button>
|
||||
|
||||
{canGroup && (
|
||||
<Button size="sm" variant="outline" onClick={onGroupComponents} className="flex items-center space-x-1">
|
||||
<Group className="h-3 w-3" />
|
||||
<span>그룹</span>
|
||||
<Button size="sm" variant="outline" onClick={onGroupComponents} className="h-8 px-2.5 text-xs">
|
||||
<Group className="mr-1 h-3 w-3" />
|
||||
그룹
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{canUngroup && (
|
||||
<Button size="sm" variant="outline" onClick={onUngroupComponents} className="flex items-center space-x-1">
|
||||
<Ungroup className="h-3 w-3" />
|
||||
<span>해제</span>
|
||||
<Button size="sm" variant="outline" onClick={onUngroupComponents} className="h-8 px-2.5 text-xs">
|
||||
<Ungroup className="mr-1 h-3 w-3" />
|
||||
해제
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button size="sm" variant="destructive" onClick={onDeleteComponent} className="flex items-center space-x-1">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<span>삭제</span>
|
||||
<Button size="sm" variant="destructive" onClick={onDeleteComponent} className="h-8 px-2.5 text-xs">
|
||||
<Trash2 className="mr-1 h-3 w-3" />
|
||||
삭제
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 속성 편집 영역 */}
|
||||
<div className="flex-1 space-y-6 overflow-y-auto p-4">
|
||||
<div className="flex-1 space-y-4 overflow-y-auto p-4">
|
||||
{/* 기본 정보 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Type className="text-muted-foreground h-4 w-4" />
|
||||
<h4 className="font-medium text-gray-900">기본 정보</h4>
|
||||
<h4 className="text-sm font-semibold">기본 정보</h4>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{selectedComponent.type === "widget" && (
|
||||
<>
|
||||
<div>
|
||||
<Label htmlFor="columnName" className="text-sm font-medium">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="columnName" className="text-xs font-medium">
|
||||
컬럼명 (읽기 전용)
|
||||
</Label>
|
||||
<Input
|
||||
@@ -594,21 +598,20 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||
value={selectedComponent.columnName || ""}
|
||||
readOnly
|
||||
placeholder="데이터베이스 컬럼명"
|
||||
className="text-muted-foreground mt-1 bg-gray-50"
|
||||
className="bg-muted/50 text-muted-foreground h-8"
|
||||
title="컬럼명은 변경할 수 없습니다"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="inputType" className="text-sm font-medium">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="inputType" className="text-xs font-medium">
|
||||
입력 타입
|
||||
</Label>
|
||||
<select
|
||||
className="focus:border-primary mt-1 w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
className="border-input bg-background focus-visible:ring-ring flex h-8 w-full rounded-md border px-3 py-1 text-xs shadow-sm transition-colors focus-visible:ring-1 focus-visible:outline-none"
|
||||
value={getBaseInputType(localInputs.widgetType)}
|
||||
onChange={(e) => {
|
||||
const selectedInputType = e.target.value as BaseInputType;
|
||||
// 입력 타입에 맞는 기본 세부 타입 설정
|
||||
const defaultWebType = getDefaultDetailType(selectedInputType);
|
||||
setLocalInputs((prev) => ({ ...prev, widgetType: defaultWebType }));
|
||||
onUpdateProperty("widgetType", defaultWebType);
|
||||
@@ -620,11 +623,11 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-gray-500">세부 타입은 "상세 설정" 패널에서 선택하세요</p>
|
||||
<p className="text-muted-foreground text-xs">세부 타입은 "상세 설정" 패널에서 선택하세요</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="placeholder" className="text-sm font-medium">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="placeholder" className="text-xs font-medium">
|
||||
플레이스홀더
|
||||
</Label>
|
||||
<Input
|
||||
@@ -632,12 +635,11 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||
value={localInputs.placeholder}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
// console.log("🔄 placeholder 변경:", newValue);
|
||||
setLocalInputs((prev) => ({ ...prev, placeholder: newValue }));
|
||||
onUpdateProperty("placeholder", newValue);
|
||||
}}
|
||||
placeholder="입력 힌트 텍스트"
|
||||
className="mt-1"
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const getWidgetIcon = (widgetType: WebType) => {
|
||||
case "text":
|
||||
case "email":
|
||||
case "tel":
|
||||
return <Type className="h-3 w-3 text-primary" />;
|
||||
return <Type className="text-primary h-3 w-3" />;
|
||||
case "number":
|
||||
case "decimal":
|
||||
return <Hash className="h-3 w-3 text-green-600" />;
|
||||
@@ -49,9 +49,9 @@ const getWidgetIcon = (widgetType: WebType) => {
|
||||
return <AlignLeft className="h-3 w-3 text-indigo-600" />;
|
||||
case "boolean":
|
||||
case "checkbox":
|
||||
return <CheckSquare className="h-3 w-3 text-primary" />;
|
||||
return <CheckSquare className="text-primary h-3 w-3" />;
|
||||
case "code":
|
||||
return <Code className="h-3 w-3 text-muted-foreground" />;
|
||||
return <Code className="text-muted-foreground h-3 w-3" />;
|
||||
case "entity":
|
||||
return <Building className="h-3 w-3 text-cyan-600" />;
|
||||
case "file":
|
||||
@@ -89,55 +89,55 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* 헤더 */}
|
||||
<div className="border-b border-gray-200 p-4">
|
||||
<div className="border-b p-4">
|
||||
{selectedTableName && (
|
||||
<div className="mb-3 rounded-md bg-accent p-3">
|
||||
<div className="text-sm font-medium text-blue-900">선택된 테이블</div>
|
||||
<div className="mt-1 flex items-center space-x-2">
|
||||
<Database className="h-3 w-3 text-primary" />
|
||||
<span className="font-mono text-xs text-blue-800">{selectedTableName}</span>
|
||||
<div className="border-primary/20 bg-primary/5 mb-3 rounded-lg border p-3">
|
||||
<div className="text-xs font-semibold">선택된 테이블</div>
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<Database className="text-primary h-3 w-3" />
|
||||
<span className="font-mono text-xs font-medium">{selectedTableName}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 검색 */}
|
||||
<div className="relative">
|
||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
||||
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-3.5 w-3.5 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="테이블명, 컬럼명으로 검색..."
|
||||
placeholder="테이블명, 컬럼명 검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="w-full rounded-md border border-gray-300 py-2 pr-3 pl-10 focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
||||
className="border-input bg-background focus-visible:ring-ring h-8 w-full rounded-md border px-3 pl-8 text-xs focus-visible:ring-1 focus-visible:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 text-xs text-muted-foreground">총 {filteredTables.length}개 테이블</div>
|
||||
<div className="text-muted-foreground mt-2 text-xs">총 {filteredTables.length}개</div>
|
||||
</div>
|
||||
|
||||
{/* 테이블 목록 */}
|
||||
<div className="scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100 flex-1 overflow-y-auto">
|
||||
<div className="space-y-1 p-2">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="space-y-1.5 p-3">
|
||||
{filteredTables.map((table) => {
|
||||
const isExpanded = expandedTables.has(table.tableName);
|
||||
|
||||
return (
|
||||
<div key={table.tableName} className="rounded-md border border-gray-200">
|
||||
<div key={table.tableName} className="bg-card rounded-lg border">
|
||||
{/* 테이블 헤더 */}
|
||||
<div
|
||||
className="flex cursor-pointer items-center justify-between p-3 hover:bg-gray-50"
|
||||
className="hover:bg-accent/50 flex cursor-pointer items-center justify-between p-2.5 transition-colors"
|
||||
onClick={() => toggleTable(table.tableName)}
|
||||
>
|
||||
<div className="flex flex-1 items-center space-x-2">
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4 text-gray-500" />
|
||||
<ChevronDown className="text-muted-foreground h-3.5 w-3.5" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-gray-500" />
|
||||
<ChevronRight className="text-muted-foreground h-3.5 w-3.5" />
|
||||
)}
|
||||
<Database className="h-4 w-4 text-primary" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium">{table.tableLabel || table.tableName}</div>
|
||||
<div className="text-xs text-gray-500">{table.columns.length}개 컬럼</div>
|
||||
<Database className="text-primary h-3.5 w-3.5" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-xs font-semibold">{table.tableLabel || table.tableName}</div>
|
||||
<div className="text-muted-foreground text-xs">{table.columns.length}개</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -146,7 +146,7 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||
variant="ghost"
|
||||
draggable
|
||||
onDragStart={(e) => onDragStart(e, table)}
|
||||
className="ml-2 text-xs"
|
||||
className="h-6 px-2 text-xs"
|
||||
>
|
||||
드래그
|
||||
</Button>
|
||||
@@ -154,43 +154,33 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||
|
||||
{/* 컬럼 목록 */}
|
||||
{isExpanded && (
|
||||
<div className="border-t border-gray-200 bg-gray-50">
|
||||
<div
|
||||
className={`${
|
||||
table.columns.length > 8
|
||||
? "scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100 max-h-64 overflow-y-auto"
|
||||
: ""
|
||||
}`}
|
||||
style={{
|
||||
scrollbarWidth: "thin",
|
||||
scrollbarColor: "#cbd5e1 #f1f5f9",
|
||||
}}
|
||||
>
|
||||
<div className="bg-muted/30 border-t">
|
||||
<div className={`${table.columns.length > 8 ? "max-h-64 overflow-y-auto" : ""}`}>
|
||||
{table.columns.map((column, index) => (
|
||||
<div
|
||||
key={column.columnName}
|
||||
className={`flex cursor-pointer items-center justify-between p-2 hover:bg-white ${
|
||||
index < table.columns.length - 1 ? "border-b border-gray-100" : ""
|
||||
className={`hover:bg-accent/50 flex cursor-grab items-center justify-between p-2 transition-colors ${
|
||||
index < table.columns.length - 1 ? "border-border/50 border-b" : ""
|
||||
}`}
|
||||
draggable
|
||||
onDragStart={(e) => onDragStart(e, table, column)}
|
||||
>
|
||||
<div className="flex flex-1 items-center space-x-2">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
{getWidgetIcon(column.widgetType)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm font-medium">
|
||||
<div className="truncate text-xs font-semibold">
|
||||
{column.columnLabel || column.columnName}
|
||||
</div>
|
||||
<div className="truncate text-xs text-gray-500">{column.dataType}</div>
|
||||
<div className="text-muted-foreground truncate text-xs">{column.dataType}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-shrink-0 items-center space-x-1">
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<div className="flex flex-shrink-0 items-center gap-1">
|
||||
<Badge variant="secondary" className="h-4 px-1.5 text-xs">
|
||||
{column.widgetType}
|
||||
</Badge>
|
||||
{column.required && (
|
||||
<Badge variant="destructive" className="text-xs">
|
||||
<Badge variant="destructive" className="h-4 px-1.5 text-xs">
|
||||
필수
|
||||
</Badge>
|
||||
)}
|
||||
@@ -200,8 +190,8 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||
|
||||
{/* 컬럼 수가 많을 때 안내 메시지 */}
|
||||
{table.columns.length > 8 && (
|
||||
<div className="sticky bottom-0 bg-gray-100 p-2 text-center">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<div className="bg-muted sticky bottom-0 p-2 text-center">
|
||||
<div className="text-muted-foreground text-xs">
|
||||
📜 총 {table.columns.length}개 컬럼 (스크롤하여 더 보기)
|
||||
</div>
|
||||
</div>
|
||||
@@ -217,7 +207,7 @@ export const TablesPanel: React.FC<TablesPanelProps> = ({
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="border-t border-gray-200 bg-gray-50 p-3">
|
||||
<div className="text-xs text-muted-foreground">💡 테이블이나 컬럼을 캔버스로 드래그하세요</div>
|
||||
<div className="text-muted-foreground text-xs">💡 테이블이나 컬럼을 캔버스로 드래그하세요</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user