화면관리ui수정
This commit is contained in:
@@ -12,7 +12,6 @@ import {
|
||||
Save,
|
||||
Undo,
|
||||
Redo,
|
||||
Play,
|
||||
ArrowLeft,
|
||||
Cog,
|
||||
Layout,
|
||||
@@ -28,7 +27,6 @@ interface DesignerToolbarProps {
|
||||
onSave: () => void;
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
onPreview: () => void;
|
||||
onTogglePanel: (panelId: string) => void;
|
||||
panelStates: Record<string, { isOpen: boolean }>;
|
||||
canUndo: boolean;
|
||||
@@ -45,7 +43,6 @@ export const DesignerToolbar: React.FC<DesignerToolbarProps> = ({
|
||||
onSave,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onPreview,
|
||||
onTogglePanel,
|
||||
panelStates,
|
||||
canUndo,
|
||||
@@ -229,11 +226,6 @@ export const DesignerToolbar: React.FC<DesignerToolbarProps> = ({
|
||||
|
||||
<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>
|
||||
|
||||
@@ -123,7 +123,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y}px`,
|
||||
width: getWidth(),
|
||||
height: getHeight(),
|
||||
height: getHeight(), // 모든 컴포넌트 고정 높이로 변경
|
||||
zIndex: component.type === "layout" ? 1 : position.z || 2, // 레이아웃은 z-index 1, 다른 컴포넌트는 2 이상
|
||||
...componentStyle,
|
||||
// style.width와 style.height는 이미 getWidth/getHeight에서 처리했으므로 중복 적용됨
|
||||
@@ -162,7 +162,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
|
||||
{/* 동적 컴포넌트 렌더링 */}
|
||||
<div
|
||||
className={`h-full w-full max-w-full ${
|
||||
component.componentConfig?.type === "table-list" ? "overflow-hidden" : "overflow-hidden"
|
||||
component.componentConfig?.type === "table-list" ? "overflow-hidden" : "overflow-visible"
|
||||
}`}
|
||||
>
|
||||
<DynamicComponentRenderer
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { Database } from "lucide-react";
|
||||
import { Database, Cog } from "lucide-react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
ScreenDefinition,
|
||||
ComponentData,
|
||||
@@ -57,7 +58,6 @@ import DetailSettingsPanel from "./panels/DetailSettingsPanel";
|
||||
import GridPanel from "./panels/GridPanel";
|
||||
import ResolutionPanel from "./panels/ResolutionPanel";
|
||||
import { usePanelState, PanelConfig } from "@/hooks/usePanelState";
|
||||
import { ResponsivePreviewModal } from "./ResponsivePreviewModal";
|
||||
|
||||
// 새로운 통합 UI 컴포넌트
|
||||
import { LeftUnifiedToolbar, defaultToolbarButtons } from "./toolbar/LeftUnifiedToolbar";
|
||||
@@ -74,17 +74,9 @@ interface ScreenDesignerProps {
|
||||
onBackToList: () => void;
|
||||
}
|
||||
|
||||
// 패널 설정 (간소화: 템플릿, 격자 제거)
|
||||
// 패널 설정 (컴포넌트와 편집 2개)
|
||||
const panelConfigs: PanelConfig[] = [
|
||||
// 좌측 그룹: 입력/소스
|
||||
{
|
||||
id: "tables",
|
||||
title: "테이블 목록",
|
||||
defaultPosition: "left",
|
||||
defaultWidth: 400,
|
||||
defaultHeight: 700,
|
||||
shortcutKey: "t",
|
||||
},
|
||||
// 컴포넌트 패널 (테이블 + 컴포넌트 탭)
|
||||
{
|
||||
id: "components",
|
||||
title: "컴포넌트",
|
||||
@@ -93,31 +85,15 @@ const panelConfigs: PanelConfig[] = [
|
||||
defaultHeight: 700,
|
||||
shortcutKey: "c",
|
||||
},
|
||||
// 좌측 그룹: 편집/설정
|
||||
// 편집 패널 (속성 + 스타일 & 해상도 탭)
|
||||
{
|
||||
id: "properties",
|
||||
title: "속성",
|
||||
title: "편집",
|
||||
defaultPosition: "left",
|
||||
defaultWidth: 400,
|
||||
defaultHeight: 700,
|
||||
shortcutKey: "p",
|
||||
},
|
||||
{
|
||||
id: "styles",
|
||||
title: "스타일",
|
||||
defaultPosition: "left",
|
||||
defaultWidth: 400,
|
||||
defaultHeight: 700,
|
||||
shortcutKey: "s",
|
||||
},
|
||||
{
|
||||
id: "resolution",
|
||||
title: "해상도",
|
||||
defaultPosition: "left",
|
||||
defaultWidth: 400,
|
||||
defaultHeight: 700,
|
||||
shortcutKey: "e",
|
||||
},
|
||||
];
|
||||
|
||||
export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenDesignerProps) {
|
||||
@@ -145,9 +121,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
const [showFileAttachmentModal, setShowFileAttachmentModal] = useState(false);
|
||||
const [selectedFileComponent, setSelectedFileComponent] = useState<ComponentData | null>(null);
|
||||
|
||||
// 반응형 미리보기 모달 상태
|
||||
const [showResponsivePreview, setShowResponsivePreview] = useState(false);
|
||||
|
||||
// 해상도 설정 상태
|
||||
const [screenResolution, setScreenResolution] = useState<ScreenResolution>(
|
||||
SCREEN_RESOLUTIONS[0], // 기본값: Full HD
|
||||
@@ -198,8 +171,10 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
isPanning: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
scrollLeft: 0,
|
||||
scrollTop: 0,
|
||||
outerScrollLeft: 0,
|
||||
outerScrollTop: 0,
|
||||
innerScrollLeft: 0,
|
||||
innerScrollTop: 0,
|
||||
});
|
||||
const canvasContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -1061,14 +1036,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
if (isPanMode && canvasContainerRef.current) {
|
||||
if (isPanMode) {
|
||||
e.preventDefault();
|
||||
// 외부와 내부 스크롤 컨테이너 모두 저장
|
||||
setPanState({
|
||||
isPanning: true,
|
||||
startX: e.pageX,
|
||||
startY: e.pageY,
|
||||
scrollLeft: canvasContainerRef.current.scrollLeft,
|
||||
scrollTop: canvasContainerRef.current.scrollTop,
|
||||
outerScrollLeft: canvasContainerRef.current?.scrollLeft || 0,
|
||||
outerScrollTop: canvasContainerRef.current?.scrollTop || 0,
|
||||
innerScrollLeft: canvasRef.current?.scrollLeft || 0,
|
||||
innerScrollTop: canvasRef.current?.scrollTop || 0,
|
||||
});
|
||||
// 드래그 중 커서 변경
|
||||
document.body.style.cursor = "grabbing";
|
||||
@@ -1076,12 +1054,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (isPanMode && panState.isPanning && canvasContainerRef.current) {
|
||||
if (isPanMode && panState.isPanning) {
|
||||
e.preventDefault();
|
||||
const dx = e.pageX - panState.startX;
|
||||
const dy = e.pageY - panState.startY;
|
||||
canvasContainerRef.current.scrollLeft = panState.scrollLeft - dx;
|
||||
canvasContainerRef.current.scrollTop = panState.scrollTop - dy;
|
||||
|
||||
// 외부 컨테이너 스크롤
|
||||
if (canvasContainerRef.current) {
|
||||
canvasContainerRef.current.scrollLeft = panState.outerScrollLeft - dx;
|
||||
canvasContainerRef.current.scrollTop = panState.outerScrollTop - dy;
|
||||
}
|
||||
|
||||
// 내부 캔버스 스크롤
|
||||
if (canvasRef.current) {
|
||||
canvasRef.current.scrollLeft = panState.innerScrollLeft - dx;
|
||||
canvasRef.current.scrollTop = panState.innerScrollTop - dy;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1106,7 +1094,16 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
};
|
||||
}, [isPanMode, panState.isPanning, panState.startX, panState.startY, panState.scrollLeft, panState.scrollTop]);
|
||||
}, [
|
||||
isPanMode,
|
||||
panState.isPanning,
|
||||
panState.startX,
|
||||
panState.startY,
|
||||
panState.outerScrollLeft,
|
||||
panState.outerScrollTop,
|
||||
panState.innerScrollLeft,
|
||||
panState.innerScrollTop,
|
||||
]);
|
||||
|
||||
// 마우스 휠로 줌 제어
|
||||
useEffect(() => {
|
||||
@@ -3875,18 +3872,20 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
|
||||
if (!selectedScreen) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Database className="mx-auto mb-4 h-12 w-12 text-gray-400" />
|
||||
<h3 className="text-lg font-medium text-gray-900">화면을 선택하세요</h3>
|
||||
<p className="text-gray-500">설계할 화면을 먼저 선택해주세요.</p>
|
||||
<div className="bg-background flex h-full items-center justify-center">
|
||||
<div className="space-y-4 text-center">
|
||||
<div className="bg-muted mx-auto flex h-16 w-16 items-center justify-center rounded-full">
|
||||
<Database className="text-muted-foreground h-8 w-8" />
|
||||
</div>
|
||||
<h3 className="text-foreground text-lg font-semibold">화면을 선택하세요</h3>
|
||||
<p className="text-muted-foreground max-w-sm text-sm">설계할 화면을 먼저 선택해주세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col bg-gradient-to-br from-gray-50 to-slate-100">
|
||||
<div className="bg-background flex h-full w-full flex-col">
|
||||
{/* 상단 슬림 툴바 */}
|
||||
<SlimToolbar
|
||||
screenName={selectedScreen?.screenName}
|
||||
@@ -3895,7 +3894,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
onBack={onBackToList}
|
||||
onSave={handleSave}
|
||||
isSaving={isSaving}
|
||||
onPreview={() => setShowResponsivePreview(true)}
|
||||
/>
|
||||
{/* 메인 컨테이너 (좌측 툴바 + 패널들 + 캔버스) */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
@@ -3903,20 +3901,23 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
<LeftUnifiedToolbar buttons={defaultToolbarButtons} panelStates={panelStates} onTogglePanel={togglePanel} />
|
||||
|
||||
{/* 열린 패널들 (좌측에서 우측으로 누적) */}
|
||||
{panelStates.tables?.isOpen && (
|
||||
<div className="flex h-full w-[400px] flex-col border-r border-gray-200 bg-white shadow-lg">
|
||||
<div className="flex items-center justify-between border-b border-gray-200 p-3">
|
||||
<h3 className="font-semibold text-gray-900">테이블 목록</h3>
|
||||
<button onClick={() => closePanel("tables")} className="text-gray-400 hover:text-gray-600">
|
||||
{panelStates.components?.isOpen && (
|
||||
<div className="border-border bg-card flex h-full w-[400px] flex-col border-r shadow-sm">
|
||||
<div className="border-border flex items-center justify-between border-b px-6 py-4">
|
||||
<h3 className="text-foreground text-lg font-semibold">컴포넌트</h3>
|
||||
<button
|
||||
onClick={() => closePanel("components")}
|
||||
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring rounded-sm transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<TablesPanel
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ComponentsPanel
|
||||
tables={filteredTables}
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
onDragStart={(e, table, column) => {
|
||||
onTableDragStart={(e, table, column) => {
|
||||
const dragData = {
|
||||
type: column ? "column" : "table",
|
||||
table,
|
||||
@@ -3930,25 +3931,14 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
</div>
|
||||
)}
|
||||
|
||||
{panelStates.components?.isOpen && (
|
||||
<div className="flex h-full w-[400px] flex-col border-r border-gray-200 bg-white shadow-lg">
|
||||
<div className="flex items-center justify-between border-b border-gray-200 p-3">
|
||||
<h3 className="font-semibold text-gray-900">컴포넌트</h3>
|
||||
<button onClick={() => closePanel("components")} className="text-gray-400 hover:text-gray-600">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<ComponentsPanel />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{panelStates.properties?.isOpen && (
|
||||
<div className="flex h-full w-[400px] flex-col border-r border-gray-200 bg-white shadow-lg">
|
||||
<div className="flex items-center justify-between border-b border-gray-200 p-3">
|
||||
<h3 className="font-semibold text-gray-900">속성</h3>
|
||||
<button onClick={() => closePanel("properties")} className="text-gray-400 hover:text-gray-600">
|
||||
<div className="border-border bg-card flex h-full w-[400px] flex-col border-r shadow-sm">
|
||||
<div className="border-border flex items-center justify-between border-b px-6 py-4">
|
||||
<h3 className="text-foreground text-lg font-semibold">속성</h3>
|
||||
<button
|
||||
onClick={() => closePanel("properties")}
|
||||
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring rounded-sm transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
@@ -3962,85 +3952,49 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
currentTable={tables.length > 0 ? tables[0] : undefined}
|
||||
currentTableName={selectedScreen?.tableName}
|
||||
dragState={dragState}
|
||||
onStyleChange={(style) => {
|
||||
if (selectedComponent) {
|
||||
updateComponentProperty(selectedComponent.id, "style", style);
|
||||
}
|
||||
}}
|
||||
currentResolution={screenResolution}
|
||||
onResolutionChange={handleResolutionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{panelStates.styles?.isOpen && (
|
||||
<div className="flex h-full w-[400px] flex-col border-r border-gray-200 bg-white shadow-lg">
|
||||
<div className="flex items-center justify-between border-b border-gray-200 p-3">
|
||||
<h3 className="font-semibold text-gray-900">스타일</h3>
|
||||
<button onClick={() => closePanel("styles")} className="text-gray-400 hover:text-gray-600">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{selectedComponent ? (
|
||||
<StyleEditor
|
||||
style={selectedComponent.style || {}}
|
||||
onStyleChange={(style) => {
|
||||
if (selectedComponent) {
|
||||
updateComponentProperty(selectedComponent.id, "style", style);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-gray-500">
|
||||
컴포넌트를 선택하여 스타일을 편집하세요
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{panelStates.resolution?.isOpen && (
|
||||
<div className="flex h-full w-[400px] flex-col border-r border-gray-200 bg-white shadow-lg">
|
||||
<div className="flex items-center justify-between border-b border-gray-200 p-3">
|
||||
<h3 className="font-semibold text-gray-900">해상도</h3>
|
||||
<button onClick={() => closePanel("resolution")} className="text-gray-400 hover:text-gray-600">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<ResolutionPanel currentResolution={screenResolution} onResolutionChange={handleResolutionChange} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 스타일과 해상도 패널은 속성 패널의 탭으로 통합됨 */}
|
||||
|
||||
{/* 메인 캔버스 영역 (스크롤 가능한 컨테이너) - 좌우 최소화, 위아래 넉넉한 여유 */}
|
||||
<div
|
||||
ref={canvasContainerRef}
|
||||
className="relative flex-1 overflow-auto bg-gradient-to-br from-gray-50 to-slate-100 px-2 py-6"
|
||||
>
|
||||
<div ref={canvasContainerRef} className="bg-muted relative flex-1 overflow-auto px-16 py-6">
|
||||
{/* Pan 모드 안내 - 제거됨 */}
|
||||
{/* 줌 레벨 표시 */}
|
||||
<div className="pointer-events-none fixed right-6 bottom-6 z-50 rounded-lg bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-lg ring-1 ring-gray-200">
|
||||
<div className="bg-card text-foreground border-border pointer-events-none fixed right-6 bottom-6 z-50 rounded-lg border px-4 py-2 text-sm font-medium shadow-md">
|
||||
🔍 {Math.round(zoomLevel * 100)}%
|
||||
</div>
|
||||
{/* 🔥 줌 적용 시 스크롤 영역 확보를 위한 래퍼 */}
|
||||
<div
|
||||
className="mx-auto"
|
||||
className="flex justify-center"
|
||||
style={{
|
||||
width: screenResolution.width * zoomLevel,
|
||||
height: Math.max(screenResolution.height, 800) * zoomLevel,
|
||||
width: "100%",
|
||||
minHeight: Math.max(screenResolution.height, 800) * zoomLevel,
|
||||
}}
|
||||
>
|
||||
{/* 실제 작업 캔버스 (해상도 크기) - 반응형 개선 + 줌 적용 */}
|
||||
<div
|
||||
className="bg-white shadow-lg"
|
||||
className="bg-background border-border border shadow-lg"
|
||||
style={{
|
||||
width: screenResolution.width,
|
||||
height: Math.max(screenResolution.height, 800), // 최소 높이 보장
|
||||
minHeight: screenResolution.height,
|
||||
transform: `scale(${zoomLevel})`, // 줌 레벨에 따라 시각적으로 확대/축소
|
||||
transformOrigin: "top center",
|
||||
transition: "transform 0.1s ease-out",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={canvasRef}
|
||||
className="relative h-full w-full overflow-auto bg-gradient-to-br from-slate-50/30 to-gray-100/20"
|
||||
className="bg-background relative h-full w-full overflow-auto"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget && !selectionDrag.wasSelecting && !isPanMode) {
|
||||
setSelectedComponent(null);
|
||||
@@ -4067,14 +4021,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
{gridLines.map((line, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="pointer-events-none absolute"
|
||||
className="bg-border pointer-events-none absolute"
|
||||
style={{
|
||||
left: line.type === "vertical" ? `${line.position}px` : 0,
|
||||
top: line.type === "horizontal" ? `${line.position}px` : 0,
|
||||
width: line.type === "vertical" ? "1px" : "100%",
|
||||
height: line.type === "horizontal" ? "1px" : "100%",
|
||||
backgroundColor: layout.gridSettings?.gridColor || "#d1d5db",
|
||||
opacity: layout.gridSettings?.gridOpacity || 0.5,
|
||||
opacity: layout.gridSettings?.gridOpacity || 0.3,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -4286,15 +4239,12 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
{/* 드래그 선택 영역 */}
|
||||
{selectionDrag.isSelecting && (
|
||||
<div
|
||||
className="pointer-events-none absolute"
|
||||
className="border-primary bg-primary/5 pointer-events-none absolute rounded-md border-2 border-dashed"
|
||||
style={{
|
||||
left: `${Math.min(selectionDrag.startPoint.x, selectionDrag.currentPoint.x)}px`,
|
||||
top: `${Math.min(selectionDrag.startPoint.y, selectionDrag.currentPoint.y)}px`,
|
||||
width: `${Math.abs(selectionDrag.currentPoint.x - selectionDrag.startPoint.x)}px`,
|
||||
height: `${Math.abs(selectionDrag.currentPoint.y - selectionDrag.startPoint.y)}px`,
|
||||
border: "2px dashed #3b82f6",
|
||||
backgroundColor: "rgba(59, 130, 246, 0.05)", // 매우 투명한 배경 (5%)
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -4302,19 +4252,28 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
{/* 빈 캔버스 안내 */}
|
||||
{layout.components.length === 0 && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-center text-gray-400">
|
||||
<Database className="mx-auto mb-4 h-16 w-16" />
|
||||
<h3 className="mb-2 text-xl font-medium">캔버스가 비어있습니다</h3>
|
||||
<p className="text-sm">좌측 패널에서 테이블/컬럼이나 템플릿을 드래그하여 화면을 설계하세요</p>
|
||||
<p className="mt-2 text-xs">
|
||||
단축키: T(테이블), M(템플릿), P(속성), S(스타일), R(격자), D(상세설정), E(해상도)
|
||||
</p>
|
||||
<p className="mt-1 text-xs">
|
||||
편집: Ctrl+C(복사), Ctrl+V(붙여넣기), Ctrl+S(저장), Ctrl+Z(실행취소), Delete(삭제)
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-amber-600">
|
||||
⚠️ 브라우저 기본 단축키가 차단되어 애플리케이션 기능만 작동합니다
|
||||
<div className="max-w-2xl space-y-4 px-6 text-center">
|
||||
<div className="bg-muted mx-auto flex h-16 w-16 items-center justify-center rounded-full">
|
||||
<Database className="text-muted-foreground h-8 w-8" />
|
||||
</div>
|
||||
<h3 className="text-foreground text-xl font-semibold">캔버스가 비어있습니다</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
좌측 패널에서 테이블/컬럼이나 템플릿을 드래그하여 화면을 설계하세요
|
||||
</p>
|
||||
<div className="text-muted-foreground space-y-2 text-xs">
|
||||
<p>
|
||||
<span className="font-medium">단축키:</span> T(테이블), M(템플릿), P(속성), S(스타일),
|
||||
R(격자), D(상세설정), E(해상도)
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">편집:</span> Ctrl+C(복사), Ctrl+V(붙여넣기), Ctrl+S(저장),
|
||||
Ctrl+Z(실행취소), Delete(삭제)
|
||||
</p>
|
||||
<p className="text-warning flex items-center justify-center gap-2">
|
||||
<span>⚠️</span>
|
||||
<span>브라우저 기본 단축키가 차단되어 애플리케이션 기능만 작동합니다</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -4352,13 +4311,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||
screenId={selectedScreen.screenId}
|
||||
/>
|
||||
)}
|
||||
{/* 반응형 미리보기 모달 */}
|
||||
<ResponsivePreviewModal
|
||||
isOpen={showResponsivePreview}
|
||||
onClose={() => setShowResponsivePreview(false)}
|
||||
components={layout.components}
|
||||
screenWidth={screenResolution.width}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -255,45 +255,45 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="fontWeight" className="text-xs font-medium">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="max-w-[140px] space-y-0.5">
|
||||
<Label htmlFor="fontWeight" className="text-[10px] font-medium">
|
||||
굵기
|
||||
</Label>
|
||||
<Select
|
||||
value={localStyle.fontWeight || "normal"}
|
||||
onValueChange={(value) => handleStyleChange("fontWeight", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectTrigger className="h-6 w-full text-[10px] px-2">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="normal">보통</SelectItem>
|
||||
<SelectItem value="bold">굵게</SelectItem>
|
||||
<SelectItem value="100">100</SelectItem>
|
||||
<SelectItem value="400">400</SelectItem>
|
||||
<SelectItem value="500">500</SelectItem>
|
||||
<SelectItem value="600">600</SelectItem>
|
||||
<SelectItem value="700">700</SelectItem>
|
||||
<SelectItem value="normal" className="text-[10px]">보통</SelectItem>
|
||||
<SelectItem value="bold" className="text-[10px]">굵게</SelectItem>
|
||||
<SelectItem value="100" className="text-[10px]">100</SelectItem>
|
||||
<SelectItem value="400" className="text-[10px]">400</SelectItem>
|
||||
<SelectItem value="500" className="text-[10px]">500</SelectItem>
|
||||
<SelectItem value="600" className="text-[10px]">600</SelectItem>
|
||||
<SelectItem value="700" className="text-[10px]">700</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="textAlign" className="text-xs font-medium">
|
||||
<div className="max-w-[140px] space-y-0.5">
|
||||
<Label htmlFor="textAlign" className="text-[10px] font-medium">
|
||||
정렬
|
||||
</Label>
|
||||
<Select
|
||||
value={localStyle.textAlign || "left"}
|
||||
onValueChange={(value) => handleStyleChange("textAlign", value)}
|
||||
>
|
||||
<SelectTrigger className="h-8">
|
||||
<SelectTrigger className="h-6 w-full text-[10px] px-2">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="left">왼쪽</SelectItem>
|
||||
<SelectItem value="center">가운데</SelectItem>
|
||||
<SelectItem value="right">오른쪽</SelectItem>
|
||||
<SelectItem value="justify">양쪽</SelectItem>
|
||||
<SelectItem value="left" className="text-[10px]">왼쪽</SelectItem>
|
||||
<SelectItem value="center" className="text-[10px]">가운데</SelectItem>
|
||||
<SelectItem value="right" className="text-[10px]">오른쪽</SelectItem>
|
||||
<SelectItem value="justify" className="text-[10px]">양쪽</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -6,13 +6,28 @@ import { Badge } from "@/components/ui/badge";
|
||||
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, MousePointer, Edit3, BarChart3 } from "lucide-react";
|
||||
import { Search, Package, Grid, Layers, Palette, Zap, MousePointer, Edit3, BarChart3, Database } from "lucide-react";
|
||||
import { TableInfo, ColumnInfo } from "@/types/screen";
|
||||
import TablesPanel from "./TablesPanel";
|
||||
|
||||
interface ComponentsPanelProps {
|
||||
className?: string;
|
||||
// 테이블 관련 props
|
||||
tables?: TableInfo[];
|
||||
searchTerm?: string;
|
||||
onSearchChange?: (value: string) => void;
|
||||
onTableDragStart?: (e: React.DragEvent, table: TableInfo, column?: ColumnInfo) => void;
|
||||
selectedTableName?: string;
|
||||
}
|
||||
|
||||
export function ComponentsPanel({ className }: ComponentsPanelProps) {
|
||||
export function ComponentsPanel({
|
||||
className,
|
||||
tables = [],
|
||||
searchTerm = "",
|
||||
onSearchChange,
|
||||
onTableDragStart,
|
||||
selectedTableName
|
||||
}: ComponentsPanelProps) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
// 레지스트리에서 모든 컴포넌트 조회
|
||||
@@ -160,7 +175,11 @@ export function ComponentsPanel({ className }: ComponentsPanelProps) {
|
||||
|
||||
{/* 카테고리 탭 */}
|
||||
<Tabs defaultValue="input" className="flex flex-1 flex-col">
|
||||
<TabsList className="mb-3 grid h-8 w-full grid-cols-4">
|
||||
<TabsList className="mb-3 grid h-8 w-full grid-cols-5">
|
||||
<TabsTrigger value="tables" className="flex items-center gap-1 px-1 text-xs">
|
||||
<Database className="h-3 w-3" />
|
||||
<span className="hidden sm:inline">테이블</span>
|
||||
</TabsTrigger>
|
||||
<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>
|
||||
@@ -179,6 +198,26 @@ export function ComponentsPanel({ className }: ComponentsPanelProps) {
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 테이블 탭 */}
|
||||
<TabsContent value="tables" className="mt-0 flex-1 overflow-y-auto">
|
||||
{tables.length > 0 && onTableDragStart ? (
|
||||
<TablesPanel
|
||||
tables={tables}
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={onSearchChange || (() => {})}
|
||||
onDragStart={onTableDragStart}
|
||||
selectedTableName={selectedTableName}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-32 items-center justify-center text-center">
|
||||
<div className="p-6">
|
||||
<Database className="text-muted-foreground/40 mx-auto mb-2 h-10 w-10" />
|
||||
<p className="text-muted-foreground text-xs font-medium">테이블이 없습니다</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* 입력 컴포넌트 */}
|
||||
<TabsContent value="input" className="mt-0 flex-1 space-y-2 overflow-y-auto">
|
||||
{getFilteredComponents("input").length > 0
|
||||
|
||||
@@ -926,7 +926,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||
|
||||
<div className="col-span-2">
|
||||
<Label htmlFor="height" className="text-sm font-medium">
|
||||
높이 (40px 단위)
|
||||
최소 높이 (40px 단위)
|
||||
</Label>
|
||||
<div className="mt-1 flex items-center space-x-2">
|
||||
<Input
|
||||
@@ -946,7 +946,7 @@ const PropertiesPanelComponent: React.FC<PropertiesPanelProps> = ({
|
||||
<span className="text-sm text-gray-500">행 = {localInputs.height || 40}px</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
1행 = 40px (현재 {Math.round((localInputs.height || 40) / 40)}행)
|
||||
1행 = 40px (현재 {Math.round((localInputs.height || 40) / 40)}행) - 내부 콘텐츠에 맞춰 늘어남
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -80,17 +80,6 @@ const ResolutionPanel: React.FC<ResolutionPanelProps> = ({ currentResolution, on
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 현재 해상도 표시 */}
|
||||
<div className="rounded-lg border bg-gray-50 p-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
{getCategoryIcon(currentResolution.category)}
|
||||
<span className="text-sm font-medium">{currentResolution.name}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-500">
|
||||
{currentResolution.width} × {currentResolution.height} 픽셀
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 프리셋 선택 */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">해상도 프리셋</Label>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"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";
|
||||
@@ -10,7 +9,8 @@ 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ChevronDown, Settings, Info, Database, Trash2, Copy, Palette, Monitor } from "lucide-react";
|
||||
import {
|
||||
ComponentData,
|
||||
WebType,
|
||||
@@ -48,10 +48,11 @@ 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 { ResponsiveConfigPanel } from "./ResponsiveConfigPanel";
|
||||
import { AlertConfigPanel } from "../config-panels/AlertConfigPanel";
|
||||
import { BadgeConfigPanel } from "../config-panels/BadgeConfigPanel";
|
||||
import { DynamicComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel";
|
||||
import StyleEditor from "../StyleEditor";
|
||||
import ResolutionPanel from "./ResolutionPanel";
|
||||
|
||||
interface UnifiedPropertiesPanelProps {
|
||||
selectedComponent?: ComponentData;
|
||||
@@ -62,6 +63,11 @@ interface UnifiedPropertiesPanelProps {
|
||||
currentTable?: TableInfo;
|
||||
currentTableName?: string;
|
||||
dragState?: any;
|
||||
// 스타일 관련
|
||||
onStyleChange?: (style: any) => void;
|
||||
// 해상도 관련
|
||||
currentResolution?: { name: string; width: number; height: number };
|
||||
onResolutionChange?: (resolution: { name: string; width: number; height: number }) => void;
|
||||
}
|
||||
|
||||
export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
@@ -73,9 +79,11 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
currentTable,
|
||||
currentTableName,
|
||||
dragState,
|
||||
onStyleChange,
|
||||
currentResolution,
|
||||
onResolutionChange,
|
||||
}) => {
|
||||
const { webTypes } = useWebTypes({ active: "Y" });
|
||||
const [activeTab, setActiveTab] = useState("basic");
|
||||
const [localComponentDetailType, setLocalComponentDetailType] = useState<string>("");
|
||||
|
||||
// 새로운 컴포넌트 시스템의 webType 동기화
|
||||
@@ -91,10 +99,10 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
// 컴포넌트가 선택되지 않았을 때
|
||||
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 className="flex h-full flex-col items-center justify-center p-4 text-center">
|
||||
<Settings className="mb-2 h-8 w-8 text-gray-300" />
|
||||
<p className="text-[10px] text-gray-500">컴포넌트를 선택하여</p>
|
||||
<p className="text-[10px] text-gray-500">속성을 편집하세요</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -164,119 +172,92 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
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 className="space-y-1.5">
|
||||
{/* 컴포넌트 정보 - 간소화 */}
|
||||
<div className="flex items-center justify-between rounded bg-muted px-2 py-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<Info className="h-2.5 w-2.5 text-muted-foreground" />
|
||||
<span className="text-[10px] font-medium text-foreground">{selectedComponent.type}</span>
|
||||
</div>
|
||||
<span className="text-[9px] text-muted-foreground">{selectedComponent.id.slice(0, 8)}</span>
|
||||
</div>
|
||||
|
||||
{/* 라벨 */}
|
||||
<div>
|
||||
<Label>라벨</Label>
|
||||
<Input
|
||||
value={widget.label || ""}
|
||||
onChange={(e) => handleUpdate("label", e.target.value)}
|
||||
placeholder="컴포넌트 라벨"
|
||||
/>
|
||||
{/* 라벨 + 최소 높이 (같은 행) */}
|
||||
<div className="grid grid-cols-2 gap-1.5">
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-[10px]">라벨</Label>
|
||||
<Input
|
||||
value={widget.label || ""}
|
||||
onChange={(e) => handleUpdate("label", e.target.value)}
|
||||
placeholder="라벨"
|
||||
className="h-6 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-[10px]">높이</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={selectedComponent.size?.height || 0}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value) || 0;
|
||||
const roundedValue = Math.max(40, Math.round(value / 40) * 40);
|
||||
handleUpdate("size.height", roundedValue);
|
||||
}}
|
||||
step={40}
|
||||
placeholder="40"
|
||||
className="h-6 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Placeholder (widget만) */}
|
||||
{selectedComponent.type === "widget" && (
|
||||
<div>
|
||||
<Label>Placeholder</Label>
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-[10px]">Placeholder</Label>
|
||||
<Input
|
||||
value={widget.placeholder || ""}
|
||||
onChange={(e) => handleUpdate("placeholder", e.target.value)}
|
||||
placeholder="입력 안내 텍스트"
|
||||
className="h-6 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Title (group/area) */}
|
||||
{(selectedComponent.type === "group" || selectedComponent.type === "area") && (
|
||||
<div>
|
||||
<Label>제목</Label>
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-[10px]">제목</Label>
|
||||
<Input
|
||||
value={group.title || area.title || ""}
|
||||
onChange={(e) => handleUpdate("title", e.target.value)}
|
||||
placeholder="제목"
|
||||
className="h-6 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description (area만) */}
|
||||
{selectedComponent.type === "area" && (
|
||||
<div>
|
||||
<Label>설명</Label>
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-[10px]">설명</Label>
|
||||
<Input
|
||||
value={area.description || ""}
|
||||
onChange={(e) => handleUpdate("description", e.target.value)}
|
||||
placeholder="설명"
|
||||
className="h-6 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 크기 */}
|
||||
<div>
|
||||
<Label>높이 (px)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={selectedComponent.size?.height || 0}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value) || 0;
|
||||
// 40 단위로 반올림
|
||||
const roundedValue = Math.max(40, Math.round(value / 40) * 40);
|
||||
handleUpdate("size.height", roundedValue);
|
||||
}}
|
||||
step={40}
|
||||
placeholder="40 단위로 입력"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">40 단위로 자동 조정됩니다</p>
|
||||
</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>
|
||||
<div className="space-y-0.5">
|
||||
<Label className="text-[10px]">Grid Columns</Label>
|
||||
<Select
|
||||
value={((selectedComponent as any).gridColumns || 12).toString()}
|
||||
onValueChange={(value) => handleUpdate("gridColumns", parseInt(value))}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="h-6 text-[10px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -432,8 +413,6 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
<DataTableConfigPanel
|
||||
component={selectedComponent as DataTableComponent}
|
||||
tables={tables}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
onUpdateComponent={(updates) => {
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
handleUpdate(key, value);
|
||||
@@ -613,99 +592,80 @@ export const UnifiedPropertiesPanel: React.FC<UnifiedPropertiesPanelProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
// 데이터 바인딩 탭
|
||||
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>
|
||||
{/* 헤더 - 간소화 */}
|
||||
<div className="border-b border-gray-200 px-3 py-2">
|
||||
{selectedComponent.type === "widget" && (
|
||||
<div className="mt-2 text-xs text-gray-600">
|
||||
<div className="text-[10px] text-gray-600 truncate">
|
||||
{(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>
|
||||
<TabsTrigger value="responsive">반응형</TabsTrigger>
|
||||
</TabsList>
|
||||
<Tabs defaultValue="properties" className="flex flex-1 flex-col overflow-hidden">
|
||||
<TabsList className="grid h-7 w-full flex-shrink-0 grid-cols-2">
|
||||
<TabsTrigger value="properties" className="text-[10px]">
|
||||
편집
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="styles" className="text-[10px]">
|
||||
<Palette className="mr-0.5 h-2.5 w-2.5" />
|
||||
스타일 & 해상도
|
||||
</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>
|
||||
<TabsContent value="responsive" className="m-0 p-4">
|
||||
<ResponsiveConfigPanel
|
||||
component={selectedComponent}
|
||||
onUpdate={(config) => {
|
||||
onUpdateProperty(selectedComponent.id, "responsiveConfig", config);
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
{/* 속성 탭 */}
|
||||
<TabsContent value="properties" className="mt-0 flex-1 overflow-y-auto p-2">
|
||||
<div className="space-y-2 text-xs">
|
||||
{/* 기본 설정 */}
|
||||
{renderBasicTab()}
|
||||
|
||||
{/* 상세 설정 통합 */}
|
||||
<Separator className="my-2" />
|
||||
{renderDetailTab()}
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 스타일 & 해상도 탭 */}
|
||||
<TabsContent value="styles" className="mt-0 flex-1 overflow-y-auto">
|
||||
<div className="space-y-2">
|
||||
{/* 해상도 설정 */}
|
||||
{currentResolution && onResolutionChange && (
|
||||
<div className="border-b pb-2 px-2">
|
||||
<ResolutionPanel
|
||||
currentResolution={currentResolution}
|
||||
onResolutionChange={onResolutionChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 스타일 설정 */}
|
||||
{selectedComponent ? (
|
||||
<div>
|
||||
<div className="mb-1.5 flex items-center gap-1.5 px-2">
|
||||
<Palette className="h-3 w-3 text-primary" />
|
||||
<h4 className="text-xs font-semibold">컴포넌트 스타일</h4>
|
||||
</div>
|
||||
<StyleEditor
|
||||
style={selectedComponent.style || {}}
|
||||
onStyleChange={(style) => {
|
||||
if (onStyleChange) {
|
||||
onStyleChange(style);
|
||||
} else {
|
||||
handleUpdate("style", style);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground text-xs">
|
||||
컴포넌트를 선택하여 스타일을 편집하세요
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -65,51 +65,27 @@ export const LeftUnifiedToolbar: React.FC<LeftUnifiedToolbarProps> = ({ buttons,
|
||||
);
|
||||
};
|
||||
|
||||
// 기본 버튼 설정
|
||||
// 기본 버튼 설정 (컴포넌트와 편집 2개)
|
||||
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" />,
|
||||
icon: <Layout className="h-5 w-5" />,
|
||||
shortcut: "C",
|
||||
group: "source",
|
||||
panelWidth: 350,
|
||||
panelWidth: 400,
|
||||
},
|
||||
|
||||
// 편집/설정 그룹
|
||||
// 편집 그룹 (속성 + 스타일 & 해상도 탭)
|
||||
{
|
||||
id: "properties",
|
||||
label: "속성",
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
export default LeftUnifiedToolbar;
|
||||
|
||||
Reference in New Issue
Block a user