패널들 좌측으로 이동
This commit is contained in:
parent
980dc8125b
commit
4cb967fea6
|
|
@ -17,6 +17,9 @@ export default function ScreenManagementPage() {
|
|||
const [selectedScreen, setSelectedScreen] = useState<ScreenDefinition | null>(null);
|
||||
const [stepHistory, setStepHistory] = useState<Step[]>(["list"]);
|
||||
|
||||
// 화면 설계 모드일 때는 전체 화면 사용
|
||||
const isDesignMode = currentStep === "design";
|
||||
|
||||
// 단계별 제목과 설명
|
||||
const stepConfig = {
|
||||
list: {
|
||||
|
|
@ -65,11 +68,16 @@ export default function ScreenManagementPage() {
|
|||
// 현재 단계가 마지막 단계인지 확인
|
||||
const isLastStep = currentStep === "template";
|
||||
|
||||
// 화면 설계 모드일 때는 레이아웃 없이 전체 화면 사용
|
||||
if (isDesignMode) {
|
||||
return <ScreenDesigner selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="w-full max-w-none px-4 py-8 space-y-6">
|
||||
<div className="w-full max-w-none space-y-6 px-4 py-8">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between rounded-lg border bg-white p-6 shadow-sm">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">화면 관리</h1>
|
||||
<p className="mt-2 text-gray-600">화면을 설계하고 템플릿을 관리합니다</p>
|
||||
|
|
@ -81,40 +89,27 @@ export default function ScreenManagementPage() {
|
|||
{/* 화면 목록 단계 */}
|
||||
{currentStep === "list" && (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
|
||||
<div className="flex items-center justify-between rounded-lg border bg-white p-4 shadow-sm">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.list.title}</h2>
|
||||
<Button variant="default" className="shadow-sm" onClick={() => goToNextStep("design")}>
|
||||
화면 설계하기 <ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<ScreenList
|
||||
onScreenSelect={setSelectedScreen}
|
||||
selectedScreen={selectedScreen}
|
||||
onDesignScreen={(screen) => {
|
||||
setSelectedScreen(screen);
|
||||
goToNextStep("design");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 화면 설계 단계 */}
|
||||
{currentStep === "design" && (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.design.title}</h2>
|
||||
<Button variant="outline" className="shadow-sm" onClick={() => goToStep("list")}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />목록으로 돌아가기
|
||||
</Button>
|
||||
</div>
|
||||
<ScreenDesigner selectedScreen={selectedScreen} onBackToList={() => goToStep("list")} />
|
||||
<ScreenList
|
||||
onScreenSelect={setSelectedScreen}
|
||||
selectedScreen={selectedScreen}
|
||||
onDesignScreen={(screen) => {
|
||||
setSelectedScreen(screen);
|
||||
goToNextStep("design");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 템플릿 관리 단계 */}
|
||||
{currentStep === "template" && (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-4">
|
||||
<div className="flex items-center justify-between rounded-lg border bg-white p-4 shadow-sm">
|
||||
<h2 className="text-xl font-semibold text-gray-800">{stepConfig.template.title}</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="shadow-sm" onClick={goToPreviousStep}>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -18,10 +18,10 @@ interface StyleEditorProps {
|
|||
}
|
||||
|
||||
export default function StyleEditor({ style, onStyleChange, className }: StyleEditorProps) {
|
||||
const [localStyle, setLocalStyle] = useState<ComponentStyle>(style);
|
||||
const [localStyle, setLocalStyle] = useState<ComponentStyle>(style || {});
|
||||
|
||||
useEffect(() => {
|
||||
setLocalStyle(style);
|
||||
setLocalStyle(style || {});
|
||||
}, [style]);
|
||||
|
||||
const handleStyleChange = (property: keyof ComponentStyle, value: any) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,735 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { ChevronDown, Settings, Info, Database, Trash2, Copy } from "lucide-react";
|
||||
import {
|
||||
ComponentData,
|
||||
WebType,
|
||||
WidgetComponent,
|
||||
GroupComponent,
|
||||
DataTableComponent,
|
||||
TableInfo,
|
||||
LayoutComponent,
|
||||
FileComponent,
|
||||
AreaComponent,
|
||||
} from "@/types/screen";
|
||||
import { ColumnSpanPreset, COLUMN_SPAN_PRESETS } from "@/lib/constants/columnSpans";
|
||||
|
||||
// 컬럼 스팬 숫자 배열 (1~12)
|
||||
const COLUMN_NUMBERS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
|
||||
import { cn } from "@/lib/utils";
|
||||
import DataTableConfigPanel from "./DataTableConfigPanel";
|
||||
import { WebTypeConfigPanel } from "./WebTypeConfigPanel";
|
||||
import { FileComponentConfigPanel } from "./FileComponentConfigPanel";
|
||||
import { useWebTypes } from "@/hooks/admin/useWebTypes";
|
||||
import { isFileComponent } from "@/lib/utils/componentTypeUtils";
|
||||
import {
|
||||
BaseInputType,
|
||||
BASE_INPUT_TYPE_OPTIONS,
|
||||
getBaseInputType,
|
||||
getDefaultDetailType,
|
||||
getDetailTypes,
|
||||
DetailTypeOption,
|
||||
} from "@/types/input-type-mapping";
|
||||
|
||||
// 새로운 컴포넌트 설정 패널들
|
||||
import { ButtonConfigPanel } from "../config-panels/ButtonConfigPanel";
|
||||
import { CardConfigPanel } from "../config-panels/CardConfigPanel";
|
||||
import { DashboardConfigPanel } from "../config-panels/DashboardConfigPanel";
|
||||
import { StatsCardConfigPanel } from "../config-panels/StatsCardConfigPanel";
|
||||
import { ProgressBarConfigPanel } from "../config-panels/ProgressBarConfigPanel";
|
||||
import { ChartConfigPanel } from "../config-panels/ChartConfigPanel";
|
||||
import { AlertConfigPanel } from "../config-panels/AlertConfigPanel";
|
||||
import { BadgeConfigPanel } from "../config-panels/BadgeConfigPanel";
|
||||
import { DynamicComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel";
|
||||
|
||||
interface UnifiedPropertiesPanelProps {
|
||||
selectedComponent?: ComponentData;
|
||||
tables: TableInfo[];
|
||||
onUpdateProperty: (componentId: string, path: string, value: any) => void;
|
||||
onDeleteComponent?: (componentId: string) => void;
|
||||
onCopyComponent?: (componentId: string) => void;
|
||||
currentTable?: TableInfo;
|
||||
currentTableName?: string;
|
||||
dragState?: any;
|
||||
}
|
||||
|
||||
export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
selectedComponent,
|
||||
tables,
|
||||
onUpdateProperty,
|
||||
onDeleteComponent,
|
||||
onCopyComponent,
|
||||
currentTable,
|
||||
currentTableName,
|
||||
dragState,
|
||||
}) => {
|
||||
const { webTypes } = useWebTypes({ active: "Y" });
|
||||
const [activeTab, setActiveTab] = useState("basic");
|
||||
const [localComponentDetailType, setLocalComponentDetailType] = useState<string>("");
|
||||
|
||||
// 새로운 컴포넌트 시스템의 webType 동기화
|
||||
useEffect(() => {
|
||||
if (selectedComponent?.type === "component") {
|
||||
const webType = selectedComponent.componentConfig?.webType;
|
||||
if (webType) {
|
||||
setLocalComponentDetailType(webType);
|
||||
}
|
||||
}
|
||||
}, [selectedComponent?.type, selectedComponent?.componentConfig?.webType, selectedComponent?.id]);
|
||||
|
||||
// 컴포넌트가 선택되지 않았을 때
|
||||
if (!selectedComponent) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center p-8 text-center">
|
||||
<Settings className="mb-4 h-12 w-12 text-gray-300" />
|
||||
<p className="text-sm text-gray-500">컴포넌트를 선택하여</p>
|
||||
<p className="text-sm text-gray-500">속성을 편집하세요</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleUpdate = (path: string, value: any) => {
|
||||
onUpdateProperty(selectedComponent.id, path, value);
|
||||
};
|
||||
|
||||
// 드래그 중일 때 실시간 위치 표시
|
||||
const currentPosition =
|
||||
dragState?.isDragging && dragState?.draggedComponent?.id === selectedComponent.id
|
||||
? dragState.currentPosition
|
||||
: selectedComponent.position;
|
||||
|
||||
// 컴포넌트별 설정 패널 렌더링 함수 (DetailSettingsPanel의 로직)
|
||||
const renderComponentConfigPanel = () => {
|
||||
if (!selectedComponent) return null;
|
||||
|
||||
const componentType = selectedComponent.componentConfig?.type || selectedComponent.type;
|
||||
|
||||
const handleUpdateProperty = (path: string, value: any) => {
|
||||
onUpdateProperty(selectedComponent.id, path, value);
|
||||
};
|
||||
|
||||
switch (componentType) {
|
||||
case "button":
|
||||
case "button-primary":
|
||||
case "button-secondary":
|
||||
return <ButtonConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||
|
||||
case "card":
|
||||
return <CardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||
|
||||
case "dashboard":
|
||||
return <DashboardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||
|
||||
case "stats":
|
||||
case "stats-card":
|
||||
return <StatsCardConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||
|
||||
case "progress":
|
||||
case "progress-bar":
|
||||
return <ProgressBarConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||
|
||||
case "chart":
|
||||
case "chart-basic":
|
||||
return <ChartConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||
|
||||
case "alert":
|
||||
case "alert-info":
|
||||
return <AlertConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||
|
||||
case "badge":
|
||||
case "badge-status":
|
||||
return <BadgeConfigPanel component={selectedComponent} onUpdateProperty={handleUpdateProperty} />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 기본 정보 탭
|
||||
const renderBasicTab = () => {
|
||||
const widget = selectedComponent as WidgetComponent;
|
||||
const group = selectedComponent as GroupComponent;
|
||||
const area = selectedComponent as AreaComponent;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 컴포넌트 정보 */}
|
||||
<div className="rounded-lg bg-slate-50 p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Info className="h-4 w-4 text-slate-500" />
|
||||
<span className="text-sm font-medium text-slate-700">컴포넌트 정보</span>
|
||||
</div>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{selectedComponent.type}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mt-2 space-y-1 text-xs text-slate-600">
|
||||
<div>ID: {selectedComponent.id}</div>
|
||||
{widget.widgetType && <div>위젯: {widget.widgetType}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 라벨 */}
|
||||
<div>
|
||||
<Label>라벨</Label>
|
||||
<Input
|
||||
value={widget.label || ""}
|
||||
onChange={(e) => handleUpdate("label", e.target.value)}
|
||||
placeholder="컴포넌트 라벨"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Placeholder (widget만) */}
|
||||
{selectedComponent.type === "widget" && (
|
||||
<div>
|
||||
<Label>Placeholder</Label>
|
||||
<Input
|
||||
value={widget.placeholder || ""}
|
||||
onChange={(e) => handleUpdate("placeholder", e.target.value)}
|
||||
placeholder="입력 안내 텍스트"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Title (group/area) */}
|
||||
{(selectedComponent.type === "group" || selectedComponent.type === "area") && (
|
||||
<div>
|
||||
<Label>제목</Label>
|
||||
<Input
|
||||
value={group.title || area.title || ""}
|
||||
onChange={(e) => handleUpdate("title", e.target.value)}
|
||||
placeholder="제목"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description (area만) */}
|
||||
{selectedComponent.type === "area" && (
|
||||
<div>
|
||||
<Label>설명</Label>
|
||||
<Input
|
||||
value={area.description || ""}
|
||||
onChange={(e) => handleUpdate("description", e.target.value)}
|
||||
placeholder="설명"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 크기 */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label>너비 (px)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={widget.width || 0}
|
||||
onChange={(e) => handleUpdate("width", parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>높이 (px)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={widget.height || 0}
|
||||
onChange={(e) => handleUpdate("height", parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 컬럼 스팬 */}
|
||||
{widget.columnSpan !== undefined && (
|
||||
<div>
|
||||
<Label>컬럼 스팬</Label>
|
||||
<Select
|
||||
value={widget.columnSpan?.toString() || "12"}
|
||||
onValueChange={(value) => handleUpdate("columnSpan", parseInt(value))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{COLUMN_NUMBERS.map((span) => (
|
||||
<SelectItem key={span} value={span.toString()}>
|
||||
{span} 컬럼 ({Math.round((span / 12) * 100)}%)
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Grid Columns */}
|
||||
{(selectedComponent as any).gridColumns !== undefined && (
|
||||
<div>
|
||||
<Label>Grid Columns</Label>
|
||||
<Select
|
||||
value={((selectedComponent as any).gridColumns || 12).toString()}
|
||||
onValueChange={(value) => handleUpdate("gridColumns", parseInt(value))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{COLUMN_NUMBERS.map((span) => (
|
||||
<SelectItem key={span} value={span.toString()}>
|
||||
{span} 컬럼 ({Math.round((span / 12) * 100)}%)
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 위치 */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div>
|
||||
<Label>X {dragState?.isDragging && <Badge variant="secondary">드래그중</Badge>}</Label>
|
||||
<Input type="number" value={Math.round(currentPosition.x || 0)} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Y</Label>
|
||||
<Input type="number" value={Math.round(currentPosition.y || 0)} disabled />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Z</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={currentPosition.z || 1}
|
||||
onChange={(e) => handleUpdate("position.z", parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 라벨 스타일 */}
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-lg bg-slate-50 p-2 text-sm font-medium hover:bg-slate-100">
|
||||
라벨 스타일
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-2 space-y-2">
|
||||
<div>
|
||||
<Label>라벨 텍스트</Label>
|
||||
<Input
|
||||
value={selectedComponent.style?.labelText || selectedComponent.label || ""}
|
||||
onChange={(e) => handleUpdate("style.labelText", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<Label>폰트 크기</Label>
|
||||
<Input
|
||||
value={selectedComponent.style?.labelFontSize || "12px"}
|
||||
onChange={(e) => handleUpdate("style.labelFontSize", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>색상</Label>
|
||||
<Input
|
||||
type="color"
|
||||
value={selectedComponent.style?.labelColor || "#212121"}
|
||||
onChange={(e) => handleUpdate("style.labelColor", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>하단 여백</Label>
|
||||
<Input
|
||||
value={selectedComponent.style?.labelMarginBottom || "4px"}
|
||||
onChange={(e) => handleUpdate("style.labelMarginBottom", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={selectedComponent.style?.labelDisplay !== false}
|
||||
onCheckedChange={(checked) => handleUpdate("style.labelDisplay", checked)}
|
||||
/>
|
||||
<Label>라벨 표시</Label>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* 옵션 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={widget.visible !== false}
|
||||
onCheckedChange={(checked) => handleUpdate("visible", checked)}
|
||||
/>
|
||||
<Label>표시</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={widget.disabled === true}
|
||||
onCheckedChange={(checked) => handleUpdate("disabled", checked)}
|
||||
/>
|
||||
<Label>비활성화</Label>
|
||||
</div>
|
||||
{widget.required !== undefined && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={widget.required === true}
|
||||
onCheckedChange={(checked) => handleUpdate("required", checked)}
|
||||
/>
|
||||
<Label>필수 입력</Label>
|
||||
</div>
|
||||
)}
|
||||
{widget.readonly !== undefined && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={widget.readonly === true}
|
||||
onCheckedChange={(checked) => handleUpdate("readonly", checked)}
|
||||
/>
|
||||
<Label>읽기 전용</Label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 액션 버튼 */}
|
||||
<Separator />
|
||||
<div className="flex gap-2">
|
||||
{onCopyComponent && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onCopyComponent(selectedComponent.id)}
|
||||
className="flex-1"
|
||||
>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
복사
|
||||
</Button>
|
||||
)}
|
||||
{onDeleteComponent && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onDeleteComponent(selectedComponent.id)}
|
||||
className="flex-1 text-red-600 hover:bg-red-50 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
삭제
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 상세 설정 탭 (DetailSettingsPanel의 전체 로직 통합)
|
||||
const renderDetailTab = () => {
|
||||
// 1. DataTable 컴포넌트
|
||||
if (selectedComponent.type === "datatable") {
|
||||
return (
|
||||
<DataTableConfigPanel
|
||||
component={selectedComponent as DataTableComponent}
|
||||
tables={tables}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
onUpdateComponent={(updates) => {
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
handleUpdate(key, value);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 파일 컴포넌트
|
||||
if (isFileComponent(selectedComponent)) {
|
||||
return (
|
||||
<FileComponentConfigPanel
|
||||
component={selectedComponent as FileComponent}
|
||||
onUpdateProperty={onUpdateProperty}
|
||||
currentTable={currentTable}
|
||||
currentTableName={currentTableName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 4. 새로운 컴포넌트 시스템 (button, card 등)
|
||||
const componentType = selectedComponent.componentConfig?.type || selectedComponent.type;
|
||||
const hasNewConfigPanel =
|
||||
componentType &&
|
||||
[
|
||||
"button",
|
||||
"button-primary",
|
||||
"button-secondary",
|
||||
"card",
|
||||
"dashboard",
|
||||
"stats",
|
||||
"stats-card",
|
||||
"progress",
|
||||
"progress-bar",
|
||||
"chart",
|
||||
"chart-basic",
|
||||
"alert",
|
||||
"alert-info",
|
||||
"badge",
|
||||
"badge-status",
|
||||
].includes(componentType);
|
||||
|
||||
if (hasNewConfigPanel) {
|
||||
const configPanel = renderComponentConfigPanel();
|
||||
if (configPanel) {
|
||||
return <div className="space-y-4">{configPanel}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 새로운 컴포넌트 시스템 (type: "component")
|
||||
if (selectedComponent.type === "component") {
|
||||
const componentId = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type;
|
||||
const webType = selectedComponent.componentConfig?.webType;
|
||||
|
||||
if (!componentId) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-8 text-center">
|
||||
<p className="text-sm text-gray-500">컴포넌트 ID가 설정되지 않았습니다</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 현재 웹타입의 기본 입력 타입 추출
|
||||
const currentBaseInputType = webType ? getBaseInputType(webType as any) : null;
|
||||
|
||||
// 선택 가능한 세부 타입 목록
|
||||
const availableDetailTypes = currentBaseInputType ? getDetailTypes(currentBaseInputType) : [];
|
||||
|
||||
// 세부 타입 변경 핸들러
|
||||
const handleDetailTypeChange = (newDetailType: string) => {
|
||||
setLocalComponentDetailType(newDetailType);
|
||||
handleUpdate("componentConfig.webType", newDetailType);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 컴포넌트 정보 */}
|
||||
<div className="rounded-lg bg-green-50 p-3">
|
||||
<span className="text-sm font-medium text-green-900">컴포넌트: {componentId}</span>
|
||||
{webType && currentBaseInputType && (
|
||||
<div className="mt-1 text-xs text-green-700">입력 타입: {currentBaseInputType}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 세부 타입 선택 */}
|
||||
{webType && availableDetailTypes.length > 1 && (
|
||||
<div>
|
||||
<Label>세부 타입 선택</Label>
|
||||
<Select value={localComponentDetailType || webType} onValueChange={handleDetailTypeChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="세부 타입 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableDetailTypes.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<div>
|
||||
<div className="font-medium">{option.label}</div>
|
||||
<div className="text-xs text-gray-500">{option.description}</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="mt-1 text-xs text-gray-500">입력 타입 "{currentBaseInputType}"의 세부 형태를 선택하세요</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* DynamicComponentConfigPanel */}
|
||||
<DynamicComponentConfigPanel
|
||||
componentId={componentId}
|
||||
config={selectedComponent.componentConfig || {}}
|
||||
screenTableName={selectedComponent.tableName || currentTable?.tableName || currentTableName}
|
||||
tableColumns={currentTable?.columns || []}
|
||||
onChange={(newConfig) => {
|
||||
Object.entries(newConfig).forEach(([key, value]) => {
|
||||
handleUpdate(`componentConfig.${key}`, value);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 웹타입별 특화 설정 */}
|
||||
{webType && (
|
||||
<div className="border-t pt-4">
|
||||
<h4 className="mb-2 text-sm font-semibold">웹타입 설정</h4>
|
||||
<WebTypeConfigPanel
|
||||
webType={webType as any}
|
||||
config={selectedComponent.componentConfig || {}}
|
||||
onUpdateConfig={(newConfig) => {
|
||||
Object.entries(newConfig).forEach(([key, value]) => {
|
||||
handleUpdate(`componentConfig.${key}`, value);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 6. Widget 컴포넌트
|
||||
if (selectedComponent.type === "widget") {
|
||||
const widget = selectedComponent as WidgetComponent;
|
||||
|
||||
// Widget에 webType이 있는 경우
|
||||
if (widget.webType) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* WebType 선택 */}
|
||||
<div>
|
||||
<Label>입력 타입</Label>
|
||||
<Select value={widget.webType} onValueChange={(value) => handleUpdate("webType", value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{webTypes.map((wt) => (
|
||||
<SelectItem key={wt.web_type} value={wt.web_type}>
|
||||
{wt.web_type_name_kor || wt.web_type}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* WebType 설정 패널 */}
|
||||
<WebTypeConfigPanel
|
||||
webType={widget.webType as any}
|
||||
config={widget.webTypeConfig || {}}
|
||||
onUpdateConfig={(newConfig) => {
|
||||
Object.entries(newConfig).forEach(([key, value]) => {
|
||||
handleUpdate(`webTypeConfig.${key}`, value);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 새로운 컴포넌트 시스템 (widgetType이 button, card 등)
|
||||
if (
|
||||
widget.widgetType &&
|
||||
["button", "card", "dashboard", "stats-card", "progress-bar", "chart", "alert", "badge"].includes(
|
||||
widget.widgetType,
|
||||
)
|
||||
) {
|
||||
return (
|
||||
<DynamicComponentConfigPanel
|
||||
componentId={widget.widgetType}
|
||||
config={widget.componentConfig || {}}
|
||||
screenTableName={widget.tableName || currentTable?.tableName || currentTableName}
|
||||
tableColumns={currentTable?.columns || []}
|
||||
onChange={(newConfig) => {
|
||||
Object.entries(newConfig).forEach(([key, value]) => {
|
||||
handleUpdate(`componentConfig.${key}`, value);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 기본 메시지
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-8 text-center">
|
||||
<p className="text-sm text-gray-500">이 컴포넌트는 추가 설정이 없습니다</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 데이터 바인딩 탭
|
||||
const renderDataTab = () => {
|
||||
if (selectedComponent.type !== "widget") {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-8 text-center">
|
||||
<p className="text-sm text-gray-500">이 컴포넌트는 데이터 바인딩을 지원하지 않습니다</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const widget = selectedComponent as WidgetComponent;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg bg-blue-50 p-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Database className="h-4 w-4 text-blue-600" />
|
||||
<span className="text-sm font-medium text-blue-900">데이터 바인딩</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 테이블 컬럼 */}
|
||||
<div>
|
||||
<Label>테이블 컬럼</Label>
|
||||
<Input
|
||||
value={widget.columnName || ""}
|
||||
onChange={(e) => handleUpdate("columnName", e.target.value)}
|
||||
placeholder="컬럼명 입력"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 기본값 */}
|
||||
<div>
|
||||
<Label>기본값</Label>
|
||||
<Input
|
||||
value={widget.defaultValue || ""}
|
||||
onChange={(e) => handleUpdate("defaultValue", e.target.value)}
|
||||
placeholder="기본값 입력"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-white">
|
||||
{/* 헤더 */}
|
||||
<div className="border-b border-gray-200 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Settings className="h-5 w-5 text-blue-600" />
|
||||
<h3 className="font-semibold text-gray-900">속성 편집</h3>
|
||||
</div>
|
||||
<Badge variant="outline">{selectedComponent.type}</Badge>
|
||||
</div>
|
||||
{selectedComponent.type === "widget" && (
|
||||
<div className="mt-2 text-xs text-gray-600">
|
||||
{(selectedComponent as WidgetComponent).label || selectedComponent.id}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 탭 컨텐츠 */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex h-full flex-col">
|
||||
<TabsList className="w-full justify-start rounded-none border-b px-4">
|
||||
<TabsTrigger value="basic">기본</TabsTrigger>
|
||||
<TabsTrigger value="detail">상세</TabsTrigger>
|
||||
<TabsTrigger value="data">데이터</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<TabsContent value="basic" className="m-0 p-4">
|
||||
{renderBasicTab()}
|
||||
</TabsContent>
|
||||
<TabsContent value="detail" className="m-0 p-4">
|
||||
{renderDetailTab()}
|
||||
</TabsContent>
|
||||
<TabsContent value="data" className="m-0 p-4">
|
||||
{renderDataTab()}
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnifiedPropertiesPanel;
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Database, Layout, Cog, Settings, Palette, Monitor, Square } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface ToolbarButton {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
shortcut: string;
|
||||
group: "source" | "editor";
|
||||
panelWidth: number;
|
||||
}
|
||||
|
||||
interface LeftUnifiedToolbarProps {
|
||||
buttons: ToolbarButton[];
|
||||
panelStates: Record<string, { isOpen: boolean }>;
|
||||
onTogglePanel: (panelId: string) => void;
|
||||
}
|
||||
|
||||
export const LeftUnifiedToolbar: React.FC<LeftUnifiedToolbarProps> = ({ buttons, panelStates, onTogglePanel }) => {
|
||||
// 그룹별로 버튼 분류
|
||||
const sourceButtons = buttons.filter((btn) => btn.group === "source");
|
||||
const editorButtons = buttons.filter((btn) => btn.group === "editor");
|
||||
|
||||
const renderButton = (button: ToolbarButton) => {
|
||||
const isActive = panelStates[button.id]?.isOpen || false;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={button.id}
|
||||
variant="ghost"
|
||||
onClick={() => onTogglePanel(button.id)}
|
||||
title={`${button.label} (${button.shortcut})`}
|
||||
className={cn(
|
||||
"flex h-14 w-14 flex-col items-center justify-center gap-1 rounded-lg transition-all duration-200",
|
||||
isActive
|
||||
? "bg-gradient-to-br from-blue-500 to-blue-600 text-white shadow-lg hover:from-blue-600 hover:to-blue-700"
|
||||
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900",
|
||||
)}
|
||||
>
|
||||
<div className="relative">
|
||||
{button.icon}
|
||||
{isActive && <div className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-white" />}
|
||||
</div>
|
||||
<span className="text-[10px] font-medium">{button.label}</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-[60px] flex-col border-r border-slate-200 bg-white">
|
||||
{/* 입력/소스 그룹 */}
|
||||
<div className="flex flex-col gap-1 border-b border-slate-200 p-1">{sourceButtons.map(renderButton)}</div>
|
||||
|
||||
{/* 편집/설정 그룹 */}
|
||||
<div className="flex flex-col gap-1 p-1">{editorButtons.map(renderButton)}</div>
|
||||
|
||||
{/* 하단 여백 */}
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 기본 버튼 설정
|
||||
export const defaultToolbarButtons: ToolbarButton[] = [
|
||||
// 입력/소스 그룹
|
||||
{
|
||||
id: "tables",
|
||||
label: "테이블",
|
||||
icon: <Database className="h-5 w-5" />,
|
||||
shortcut: "T",
|
||||
group: "source",
|
||||
panelWidth: 380,
|
||||
},
|
||||
{
|
||||
id: "components",
|
||||
label: "컴포넌트",
|
||||
icon: <Cog className="h-5 w-5" />,
|
||||
shortcut: "C",
|
||||
group: "source",
|
||||
panelWidth: 350,
|
||||
},
|
||||
|
||||
// 편집/설정 그룹
|
||||
{
|
||||
id: "properties",
|
||||
label: "속성",
|
||||
icon: <Settings className="h-5 w-5" />,
|
||||
shortcut: "P",
|
||||
group: "editor",
|
||||
panelWidth: 400,
|
||||
},
|
||||
{
|
||||
id: "styles",
|
||||
label: "스타일",
|
||||
icon: <Palette className="h-5 w-5" />,
|
||||
shortcut: "S",
|
||||
group: "editor",
|
||||
panelWidth: 360,
|
||||
},
|
||||
{
|
||||
id: "resolution",
|
||||
label: "해상도",
|
||||
icon: <Monitor className="h-5 w-5" />,
|
||||
shortcut: "E",
|
||||
group: "editor",
|
||||
panelWidth: 300,
|
||||
},
|
||||
{
|
||||
id: "zone",
|
||||
label: "구역",
|
||||
icon: <Square className="h-5 w-5" />,
|
||||
shortcut: "Z",
|
||||
group: "editor",
|
||||
panelWidth: 0, // 토글만 (패널 없음)
|
||||
},
|
||||
];
|
||||
|
||||
export default LeftUnifiedToolbar;
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Database, ArrowLeft, Undo, Redo, Play, Save } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SlimToolbarProps {
|
||||
screenName?: string;
|
||||
tableName?: string;
|
||||
onBack: () => void;
|
||||
onSave: () => void;
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
onPreview: () => void;
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
export const SlimToolbar: React.FC<SlimToolbarProps> = ({
|
||||
screenName,
|
||||
tableName,
|
||||
onBack,
|
||||
onSave,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onPreview,
|
||||
canUndo,
|
||||
canRedo,
|
||||
isSaving = false,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex h-14 items-center justify-between border-b border-gray-200 bg-gradient-to-r from-gray-50 to-white px-4 shadow-sm">
|
||||
{/* 좌측: 네비게이션 및 화면 정보 */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button variant="ghost" size="sm" onClick={onBack} className="flex items-center space-x-2">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
<span>목록으로</span>
|
||||
</Button>
|
||||
|
||||
<div className="h-6 w-px bg-gray-300" />
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold text-gray-900">{screenName || "화면 설계"}</h1>
|
||||
{tableName && (
|
||||
<div className="mt-0.5 flex items-center space-x-1">
|
||||
<Database className="h-3 w-3 text-gray-500" />
|
||||
<span className="font-mono text-xs text-gray-500">{tableName}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 우측: 액션 버튼들 */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onUndo}
|
||||
disabled={!canUndo}
|
||||
className="flex items-center space-x-1"
|
||||
>
|
||||
<Undo className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">실행취소</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onRedo}
|
||||
disabled={!canRedo}
|
||||
className="flex items-center space-x-1"
|
||||
>
|
||||
<Redo className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">다시실행</span>
|
||||
</Button>
|
||||
|
||||
<div className="h-6 w-px bg-gray-300" />
|
||||
|
||||
<Button variant="outline" size="sm" onClick={onPreview} className="flex items-center space-x-2">
|
||||
<Play className="h-4 w-4" />
|
||||
<span>미리보기</span>
|
||||
</Button>
|
||||
|
||||
<Button onClick={onSave} disabled={isSaving} className="flex items-center space-x-2">
|
||||
<Save className="h-4 w-4" />
|
||||
<span>{isSaving ? "저장 중..." : "저장"}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SlimToolbar;
|
||||
Loading…
Reference in New Issue