diff --git a/frontend/app/(main)/admin/screenMng/page.tsx b/frontend/app/(main)/admin/screenMng/page.tsx index 54da701b..94cb2dfa 100644 --- a/frontend/app/(main)/admin/screenMng/page.tsx +++ b/frontend/app/(main)/admin/screenMng/page.tsx @@ -17,6 +17,9 @@ export default function ScreenManagementPage() { const [selectedScreen, setSelectedScreen] = useState(null); const [stepHistory, setStepHistory] = useState(["list"]); + // 화면 설계 모드일 때는 전체 화면 사용 + const isDesignMode = currentStep === "design"; + // 단계별 제목과 설명 const stepConfig = { list: { @@ -65,11 +68,16 @@ export default function ScreenManagementPage() { // 현재 단계가 마지막 단계인지 확인 const isLastStep = currentStep === "template"; + // 화면 설계 모드일 때는 레이아웃 없이 전체 화면 사용 + if (isDesignMode) { + return goToStep("list")} />; + } + return (
-
+
{/* 페이지 제목 */} -
+

화면 관리

화면을 설계하고 템플릿을 관리합니다

@@ -81,40 +89,27 @@ export default function ScreenManagementPage() { {/* 화면 목록 단계 */} {currentStep === "list" && (
-
+

{stepConfig.list.title}

- { - setSelectedScreen(screen); - goToNextStep("design"); - }} - /> -
- )} - - {/* 화면 설계 단계 */} - {currentStep === "design" && ( -
-
-

{stepConfig.design.title}

- -
- goToStep("list")} /> + { + setSelectedScreen(screen); + goToNextStep("design"); + }} + />
)} {/* 템플릿 관리 단계 */} {currentStep === "template" && (
-
+

{stepConfig.template.title}

+
+
+ { + const dragData = { + type: column ? "column" : "table", + table, + column, + }; + e.dataTransfer.setData("application/json", JSON.stringify(dragData)); + }} + selectedTableName={selectedScreen.tableName} + /> +
-
+ )} - {/* 실제 작업 캔버스 (해상도 크기) - 반응형 개선 */} -
+ {panelStates.components?.isOpen && ( +
+
+

컴포넌트

+ +
+
+ +
+
+ )} + + {panelStates.properties?.isOpen && ( +
+
+

속성

+ +
+
+ 0 ? tables[0] : undefined} + currentTableName={selectedScreen?.tableName} + dragState={dragState} + /> +
+
+ )} + + {panelStates.styles?.isOpen && ( +
+
+

스타일

+ +
+
+ {selectedComponent ? ( + { + if (selectedComponent) { + updateComponentProperty(selectedComponent.id, "style", style); + } + }} + /> + ) : ( +
+ 컴포넌트를 선택하여 스타일을 편집하세요 +
+ )} +
+
+ )} + + {panelStates.resolution?.isOpen && ( +
+
+

해상도

+ +
+
+ +
+
+ )} + + {/* 메인 캔버스 영역 (스크롤 가능한 컨테이너) - 좌우 최소화, 위아래 넉넉한 여유 */} +
+ {/* 해상도 정보 표시 - 적당한 여백 */} +
+
+ + {screenResolution.name} ({screenResolution.width} × {screenResolution.height}) + +
+
+ + {/* 실제 작업 캔버스 (해상도 크기) - 반응형 개선 */}
{ - if (e.target === e.currentTarget && !selectionDrag.wasSelecting) { - setSelectedComponent(null); - setGroupState((prev) => ({ ...prev, selectedComponents: [] })); - } - }} - onMouseDown={(e) => { - if (e.target === e.currentTarget) { - startSelectionDrag(e); - } - }} - onDragOver={(e) => { - e.preventDefault(); - e.dataTransfer.dropEffect = "copy"; - }} - onDrop={(e) => { - e.preventDefault(); - // console.log("🎯 캔버스 드롭 이벤트 발생"); - handleDrop(e); + className="mx-auto bg-white shadow-lg" + style={{ + width: screenResolution.width, + height: Math.max(screenResolution.height, 800), // 최소 높이 보장 + minHeight: screenResolution.height, }} > - {/* 격자 라인 */} - {gridLines.map((line, index) => ( -
- ))} +
{ + if (e.target === e.currentTarget && !selectionDrag.wasSelecting) { + setSelectedComponent(null); + setGroupState((prev) => ({ ...prev, selectedComponents: [] })); + } + }} + onMouseDown={(e) => { + if (e.target === e.currentTarget) { + startSelectionDrag(e); + } + }} + onDragOver={(e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + }} + onDrop={(e) => { + e.preventDefault(); + // console.log("🎯 캔버스 드롭 이벤트 발생"); + handleDrop(e); + }} + > + {/* 격자 라인 */} + {gridLines.map((line, index) => ( +
+ ))} - {/* 컴포넌트들 */} - {layout.components - .filter((component) => !component.parentId) // 최상위 컴포넌트만 렌더링 - .map((component) => { - const children = - component.type === "group" - ? layout.components.filter((child) => child.parentId === component.id) - : []; + {/* 컴포넌트들 */} + {layout.components + .filter((component) => !component.parentId) // 최상위 컴포넌트만 렌더링 + .map((component) => { + const children = + component.type === "group" + ? layout.components.filter((child) => child.parentId === component.id) + : []; - // 드래그 중 시각적 피드백 (다중 선택 지원) - const isDraggingThis = dragState.isDragging && dragState.draggedComponent?.id === component.id; - const isBeingDragged = - dragState.isDragging && dragState.draggedComponents.some((dragComp) => dragComp.id === component.id); + // 드래그 중 시각적 피드백 (다중 선택 지원) + const isDraggingThis = dragState.isDragging && dragState.draggedComponent?.id === component.id; + const isBeingDragged = + dragState.isDragging && + dragState.draggedComponents.some((dragComp) => dragComp.id === component.id); - let displayComponent = component; - - if (isBeingDragged) { - if (isDraggingThis) { - // 주 드래그 컴포넌트: 마우스 위치 기반으로 실시간 위치 업데이트 - displayComponent = { - ...component, - position: dragState.currentPosition, - style: { - ...component.style, - opacity: 0.8, - transform: "scale(1.02)", - transition: "none", - zIndex: 50, - }, - }; - } else { - // 다른 선택된 컴포넌트들: 상대적 위치로 실시간 업데이트 - const originalComponent = dragState.draggedComponents.find( - (dragComp) => dragComp.id === component.id, - ); - if (originalComponent) { - const deltaX = dragState.currentPosition.x - dragState.originalPosition.x; - const deltaY = dragState.currentPosition.y - dragState.originalPosition.y; + let displayComponent = component; + if (isBeingDragged) { + if (isDraggingThis) { + // 주 드래그 컴포넌트: 마우스 위치 기반으로 실시간 위치 업데이트 displayComponent = { ...component, - position: { - x: originalComponent.position.x + deltaX, - y: originalComponent.position.y + deltaY, - z: originalComponent.position.z || 1, - } as Position, + position: dragState.currentPosition, style: { ...component.style, opacity: 0.8, + transform: "scale(1.02)", transition: "none", - zIndex: 40, // 주 컴포넌트보다 약간 낮게 + zIndex: 50, }, }; + } else { + // 다른 선택된 컴포넌트들: 상대적 위치로 실시간 업데이트 + const originalComponent = dragState.draggedComponents.find( + (dragComp) => dragComp.id === component.id, + ); + if (originalComponent) { + const deltaX = dragState.currentPosition.x - dragState.originalPosition.x; + const deltaY = dragState.currentPosition.y - dragState.originalPosition.y; + + displayComponent = { + ...component, + position: { + x: originalComponent.position.x + deltaX, + y: originalComponent.position.y + deltaY, + z: originalComponent.position.z || 1, + } as Position, + style: { + ...component.style, + opacity: 0.8, + transition: "none", + zIndex: 40, // 주 컴포넌트보다 약간 낮게 + }, + }; + } } } - } - // 전역 파일 상태도 key에 포함하여 실시간 리렌더링 - const globalFileState = typeof window !== "undefined" ? (window as any).globalFileState || {} : {}; - const globalFiles = globalFileState[component.id] || []; - const componentFiles = (component as any).uploadedFiles || []; - const fileStateKey = `${globalFiles.length}-${JSON.stringify(globalFiles.map((f: any) => f.objid) || [])}-${componentFiles.length}`; + // 전역 파일 상태도 key에 포함하여 실시간 리렌더링 + const globalFileState = typeof window !== "undefined" ? (window as any).globalFileState || {} : {}; + const globalFiles = globalFileState[component.id] || []; + const componentFiles = (component as any).uploadedFiles || []; + const fileStateKey = `${globalFiles.length}-${JSON.stringify(globalFiles.map((f: any) => f.objid) || [])}-${componentFiles.length}`; - return ( - handleComponentClick(component, e)} - onDoubleClick={(e) => handleComponentDoubleClick(component, e)} - onDragStart={(e) => startComponentDrag(component, e)} - onDragEnd={endDrag} - selectedScreen={selectedScreen} - // onZoneComponentDrop 제거 - onZoneClick={handleZoneClick} - // 설정 변경 핸들러 (테이블 페이지 크기 등 설정을 상세설정에 반영) - onConfigChange={(config) => { - // console.log("📤 테이블 설정 변경을 상세설정에 반영:", config); + return ( + handleComponentClick(component, e)} + onDoubleClick={(e) => handleComponentDoubleClick(component, e)} + onDragStart={(e) => startComponentDrag(component, e)} + onDragEnd={endDrag} + selectedScreen={selectedScreen} + // onZoneComponentDrop 제거 + onZoneClick={handleZoneClick} + // 설정 변경 핸들러 (테이블 페이지 크기 등 설정을 상세설정에 반영) + onConfigChange={(config) => { + // console.log("📤 테이블 설정 변경을 상세설정에 반영:", config); - // 컴포넌트의 componentConfig 업데이트 - const updatedComponents = layout.components.map((comp) => { - if (comp.id === component.id) { - return { - ...comp, - componentConfig: { - ...comp.componentConfig, - ...config, - }, - }; - } - return comp; - }); + // 컴포넌트의 componentConfig 업데이트 + const updatedComponents = layout.components.map((comp) => { + if (comp.id === component.id) { + return { + ...comp, + componentConfig: { + ...comp.componentConfig, + ...config, + }, + }; + } + return comp; + }); - const newLayout = { - ...layout, - components: updatedComponents, - }; + const newLayout = { + ...layout, + components: updatedComponents, + }; - setLayout(newLayout); - saveToHistory(newLayout); + setLayout(newLayout); + saveToHistory(newLayout); - console.log("✅ 컴포넌트 설정 업데이트 완료:", { - componentId: component.id, - updatedConfig: config, - }); - }} - > - {/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */} - {(component.type === "group" || component.type === "container" || component.type === "area") && - layout.components - .filter((child) => child.parentId === component.id) - .map((child) => { - // 자식 컴포넌트에도 드래그 피드백 적용 - const isChildDraggingThis = - dragState.isDragging && dragState.draggedComponent?.id === child.id; - const isChildBeingDragged = - dragState.isDragging && - dragState.draggedComponents.some((dragComp) => dragComp.id === child.id); + console.log("✅ 컴포넌트 설정 업데이트 완료:", { + componentId: component.id, + updatedConfig: config, + }); + }} + > + {/* 컨테이너, 그룹, 영역의 자식 컴포넌트들 렌더링 (레이아웃은 독립적으로 렌더링) */} + {(component.type === "group" || component.type === "container" || component.type === "area") && + layout.components + .filter((child) => child.parentId === component.id) + .map((child) => { + // 자식 컴포넌트에도 드래그 피드백 적용 + const isChildDraggingThis = + dragState.isDragging && dragState.draggedComponent?.id === child.id; + const isChildBeingDragged = + dragState.isDragging && + dragState.draggedComponents.some((dragComp) => dragComp.id === child.id); - let displayChild = child; - - if (isChildBeingDragged) { - if (isChildDraggingThis) { - // 주 드래그 자식 컴포넌트 - displayChild = { - ...child, - position: dragState.currentPosition, - style: { - ...child.style, - opacity: 0.8, - transform: "scale(1.02)", - transition: "none", - zIndex: 50, - }, - }; - } else { - // 다른 선택된 자식 컴포넌트들 - const originalChildComponent = dragState.draggedComponents.find( - (dragComp) => dragComp.id === child.id, - ); - if (originalChildComponent) { - const deltaX = dragState.currentPosition.x - dragState.originalPosition.x; - const deltaY = dragState.currentPosition.y - dragState.originalPosition.y; + let displayChild = child; + if (isChildBeingDragged) { + if (isChildDraggingThis) { + // 주 드래그 자식 컴포넌트 displayChild = { ...child, - position: { - x: originalChildComponent.position.x + deltaX, - y: originalChildComponent.position.y + deltaY, - z: originalChildComponent.position.z || 1, - } as Position, + position: dragState.currentPosition, style: { ...child.style, opacity: 0.8, + transform: "scale(1.02)", transition: "none", - zIndex: 8888, + zIndex: 50, }, }; + } else { + // 다른 선택된 자식 컴포넌트들 + const originalChildComponent = dragState.draggedComponents.find( + (dragComp) => dragComp.id === child.id, + ); + if (originalChildComponent) { + const deltaX = dragState.currentPosition.x - dragState.originalPosition.x; + const deltaY = dragState.currentPosition.y - dragState.originalPosition.y; + + displayChild = { + ...child, + position: { + x: originalChildComponent.position.x + deltaX, + y: originalChildComponent.position.y + deltaY, + z: originalChildComponent.position.z || 1, + } as Position, + style: { + ...child.style, + opacity: 0.8, + transition: "none", + zIndex: 8888, + }, + }; + } } } - } - // 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정 - const relativeChildComponent = { - ...displayChild, - position: { - x: displayChild.position.x - component.position.x, - y: displayChild.position.y - component.position.y, - z: displayChild.position.z || 1, - }, - }; + // 자식 컴포넌트의 위치를 부모 기준 상대 좌표로 조정 + const relativeChildComponent = { + ...displayChild, + position: { + x: displayChild.position.x - component.position.x, + y: displayChild.position.y - component.position.y, + z: displayChild.position.z || 1, + }, + }; - return ( - f.objid) || [])}`} - component={relativeChildComponent} - isSelected={ - selectedComponent?.id === child.id || groupState.selectedComponents.includes(child.id) - } - isDesignMode={true} // 편집 모드로 설정 - onClick={(e) => handleComponentClick(child, e)} - onDoubleClick={(e) => handleComponentDoubleClick(child, e)} - onDragStart={(e) => startComponentDrag(child, e)} - onDragEnd={endDrag} - selectedScreen={selectedScreen} - // onZoneComponentDrop 제거 - onZoneClick={handleZoneClick} - // 설정 변경 핸들러 (자식 컴포넌트용) - onConfigChange={(config) => { - // console.log("📤 자식 컴포넌트 설정 변경을 상세설정에 알림:", config); - // TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요 - }} - /> - ); - })} - - ); - })} + return ( + f.objid) || [])}`} + component={relativeChildComponent} + isSelected={ + selectedComponent?.id === child.id || groupState.selectedComponents.includes(child.id) + } + isDesignMode={true} // 편집 모드로 설정 + onClick={(e) => handleComponentClick(child, e)} + onDoubleClick={(e) => handleComponentDoubleClick(child, e)} + onDragStart={(e) => startComponentDrag(child, e)} + onDragEnd={endDrag} + selectedScreen={selectedScreen} + // onZoneComponentDrop 제거 + onZoneClick={handleZoneClick} + // 설정 변경 핸들러 (자식 컴포넌트용) + onConfigChange={(config) => { + // console.log("📤 자식 컴포넌트 설정 변경을 상세설정에 알림:", config); + // TODO: 실제 구현은 DetailSettingsPanel과의 연동 필요 + }} + /> + ); + })} + + ); + })} - {/* 드래그 선택 영역 */} - {selectionDrag.isSelecting && ( -
- )} + {/* 드래그 선택 영역 */} + {selectionDrag.isSelecting && ( +
+ )} - {/* 빈 캔버스 안내 */} - {layout.components.length === 0 && ( -
-
- -

캔버스가 비어있습니다

-

좌측 패널에서 테이블/컬럼이나 템플릿을 드래그하여 화면을 설계하세요

-

- 단축키: T(테이블), M(템플릿), P(속성), S(스타일), R(격자), D(상세설정), E(해상도) -

-

- 편집: Ctrl+C(복사), Ctrl+V(붙여넣기), Ctrl+S(저장), Ctrl+Z(실행취소), Delete(삭제) -

-

- ⚠️ 브라우저 기본 단축키가 차단되어 애플리케이션 기능만 작동합니다 -

+ {/* 빈 캔버스 안내 */} + {layout.components.length === 0 && ( +
+
+ +

캔버스가 비어있습니다

+

좌측 패널에서 테이블/컬럼이나 템플릿을 드래그하여 화면을 설계하세요

+

+ 단축키: T(테이블), M(템플릿), P(속성), S(스타일), R(격자), D(상세설정), E(해상도) +

+

+ 편집: Ctrl+C(복사), Ctrl+V(붙여넣기), Ctrl+S(저장), Ctrl+Z(실행취소), Delete(삭제) +

+

+ ⚠️ 브라우저 기본 단축키가 차단되어 애플리케이션 기능만 작동합니다 +

+
-
- )} + )} +
-
- - {/* 플로팅 패널들 */} - closePanel("tables")} - position="left" - width={380} - height={700} - autoHeight={false} - > - { - // console.log("🚀 드래그 시작:", { table: table.tableName, column: column?.columnName }); - const dragData = { - type: column ? "column" : "table", - table, - column, - }; - // console.log("📦 드래그 데이터:", dragData); - e.dataTransfer.setData("application/json", JSON.stringify(dragData)); - }} - selectedTableName={selectedScreen.tableName} - /> - - - closePanel("templates")} - position="left" - width={380} - height={700} - autoHeight={false} - > - { - // React 컴포넌트(icon)를 제외하고 JSON으로 직렬화 가능한 데이터만 전송 - const serializableTemplate = { - id: template.id, - name: template.name, - description: template.description, - category: template.category, - defaultSize: template.defaultSize, - components: template.components, - }; - - const dragData = { - type: "template", - template: serializableTemplate, - }; - e.dataTransfer.setData("application/json", JSON.stringify(dragData)); - }} - /> - - - closePanel("layouts")} - position="left" - width={380} - height={700} - autoHeight={false} - > - { - const dragData = { - type: "layout", - layout: layoutData, - }; - e.dataTransfer.setData("application/json", JSON.stringify(dragData)); - }} - gridSettings={layout.gridSettings || { columns: 12, gap: 16, padding: 0, snapToGrid: true }} - screenResolution={screenResolution} - /> - - - closePanel("components")} - position="left" - width={380} - height={700} - autoHeight={false} - > - - - - closePanel("properties")} - position="right" - width={360} - height={400} - autoHeight={true} - > - { - console.log("🔧 속성 업데이트 요청:", { - componentId: selectedComponent?.id, - componentType: selectedComponent?.type, - path, - value: typeof value === "object" ? JSON.stringify(value).substring(0, 100) + "..." : value, - }); - if (selectedComponent) { - updateComponentProperty(selectedComponent.id, path, value); - } - }} - onDeleteComponent={deleteComponent} - onCopyComponent={copyComponent} - /> - - - closePanel("styles")} - position="right" - width={360} - height={400} - autoHeight={true} - > - {selectedComponent ? ( -
- { - console.log("🔧 StyleEditor 스타일 변경:", { - componentId: selectedComponent.id, - newStyle, - hasHeight: !!newStyle.height, - }); - - // 스타일 업데이트 - updateComponentProperty(selectedComponent.id, "style", newStyle); - - // ✅ 높이만 업데이트 (너비는 gridColumnSpan으로 제어) - if (newStyle.height) { - const height = parseInt(newStyle.height.replace("px", "")); - - console.log("📏 높이 업데이트:", { - originalHeight: selectedComponent.size.height, - newHeight: height, - styleHeight: newStyle.height, - }); - - updateComponentProperty(selectedComponent.id, "size.height", height); - } - }} - /> -
- ) : ( -
- 컴포넌트를 선택하여 스타일을 편집하세요 -
- )} -
- - closePanel("grid")} - position="right" - width={320} - height={400} - autoHeight={true} - > - { - const defaultSettings = { columns: 12, gap: 16, padding: 0, snapToGrid: true, showGrid: true }; - updateGridSettings(defaultSettings); - }} - onForceGridUpdate={handleForceGridUpdate} - screenResolution={screenResolution} - /> - - - closePanel("detailSettings")} - position="right" - width={400} - height={400} - autoHeight={true} - > - { - updateComponentProperty(componentId, path, value); - }} - currentTable={tables.length > 0 ? tables[0] : undefined} - currentTableName={selectedScreen?.tableName} - /> - - - closePanel("resolution")} - position="right" - width={320} - height={400} - autoHeight={true} - > -
- -
-
- - {/* 그룹 생성 툴바 (필요시) */} - {false && groupState.selectedComponents.length > 1 && ( -
- groupState.selectedComponents.includes(comp.id))} - allComponents={layout.components} - groupState={groupState} - onGroupStateChange={setGroupState} - onGroupCreate={(componentIds: string[], title: string, style?: any) => { - handleGroupCreate(componentIds, title, style); - }} - onGroupUngroup={() => { - // TODO: 그룹 해제 구현 - }} - showCreateDialog={showGroupCreateDialog} - onShowCreateDialogChange={setShowGroupCreateDialog} - /> -
- )} - +
{" "} + {/* 메인 컨테이너 닫기 */} + {/* 모달들 */} {/* 메뉴 할당 모달 */} - setShowMenuAssignmentModal(false)} - screenInfo={selectedScreen} - onAssignmentComplete={() => { - // console.log("메뉴 할당 완료"); - // 필요시 추가 작업 수행 - }} - onBackToList={onBackToList} - /> - + {showMenuAssignmentModal && selectedScreen && ( + setShowMenuAssignmentModal(false)} + /> + )} {/* 파일첨부 상세 모달 */} - + {showFileAttachmentModal && selectedFileComponent && ( + { + setShowFileAttachmentModal(false); + setSelectedFileComponent(null); + }} + component={selectedFileComponent} + screenId={selectedScreen.screenId} + /> + )}
); } diff --git a/frontend/components/screen/StyleEditor.tsx b/frontend/components/screen/StyleEditor.tsx index 8d45013e..4f399798 100644 --- a/frontend/components/screen/StyleEditor.tsx +++ b/frontend/components/screen/StyleEditor.tsx @@ -18,10 +18,10 @@ interface StyleEditorProps { } export default function StyleEditor({ style, onStyleChange, className }: StyleEditorProps) { - const [localStyle, setLocalStyle] = useState(style); + const [localStyle, setLocalStyle] = useState(style || {}); useEffect(() => { - setLocalStyle(style); + setLocalStyle(style || {}); }, [style]); const handleStyleChange = (property: keyof ComponentStyle, value: any) => { diff --git a/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx new file mode 100644 index 00000000..e486a4bc --- /dev/null +++ b/frontend/components/screen/panels/UnifiedPropertiesPanel.tsx @@ -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 = ({ + selectedComponent, + tables, + onUpdateProperty, + onDeleteComponent, + onCopyComponent, + currentTable, + currentTableName, + dragState, +}) => { + const { webTypes } = useWebTypes({ active: "Y" }); + const [activeTab, setActiveTab] = useState("basic"); + const [localComponentDetailType, setLocalComponentDetailType] = useState(""); + + // 새로운 컴포넌트 시스템의 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 ( +
+ +

컴포넌트를 선택하여

+

속성을 편집하세요

+
+ ); + } + + 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 ; + + case "card": + return ; + + case "dashboard": + return ; + + case "stats": + case "stats-card": + return ; + + case "progress": + case "progress-bar": + return ; + + case "chart": + case "chart-basic": + return ; + + case "alert": + case "alert-info": + return ; + + case "badge": + case "badge-status": + return ; + + default: + return null; + } + }; + + // 기본 정보 탭 + const renderBasicTab = () => { + const widget = selectedComponent as WidgetComponent; + const group = selectedComponent as GroupComponent; + const area = selectedComponent as AreaComponent; + + return ( +
+ {/* 컴포넌트 정보 */} +
+
+
+ + 컴포넌트 정보 +
+ + {selectedComponent.type} + +
+
+
ID: {selectedComponent.id}
+ {widget.widgetType &&
위젯: {widget.widgetType}
} +
+
+ + {/* 라벨 */} +
+ + handleUpdate("label", e.target.value)} + placeholder="컴포넌트 라벨" + /> +
+ + {/* Placeholder (widget만) */} + {selectedComponent.type === "widget" && ( +
+ + handleUpdate("placeholder", e.target.value)} + placeholder="입력 안내 텍스트" + /> +
+ )} + + {/* Title (group/area) */} + {(selectedComponent.type === "group" || selectedComponent.type === "area") && ( +
+ + handleUpdate("title", e.target.value)} + placeholder="제목" + /> +
+ )} + + {/* Description (area만) */} + {selectedComponent.type === "area" && ( +
+ + handleUpdate("description", e.target.value)} + placeholder="설명" + /> +
+ )} + + {/* 크기 */} +
+
+ + handleUpdate("width", parseInt(e.target.value) || 0)} + /> +
+
+ + handleUpdate("height", parseInt(e.target.value) || 0)} + /> +
+
+ + {/* 컬럼 스팬 */} + {widget.columnSpan !== undefined && ( +
+ + +
+ )} + + {/* Grid Columns */} + {(selectedComponent as any).gridColumns !== undefined && ( +
+ + +
+ )} + + {/* 위치 */} +
+
+ + +
+
+ + +
+
+ + handleUpdate("position.z", parseInt(e.target.value) || 1)} + /> +
+
+ + {/* 라벨 스타일 */} + + + 라벨 스타일 + + + +
+ + handleUpdate("style.labelText", e.target.value)} + /> +
+
+
+ + handleUpdate("style.labelFontSize", e.target.value)} + /> +
+
+ + handleUpdate("style.labelColor", e.target.value)} + /> +
+
+
+ + handleUpdate("style.labelMarginBottom", e.target.value)} + /> +
+
+ handleUpdate("style.labelDisplay", checked)} + /> + +
+
+
+ + {/* 옵션 */} +
+
+ handleUpdate("visible", checked)} + /> + +
+
+ handleUpdate("disabled", checked)} + /> + +
+ {widget.required !== undefined && ( +
+ handleUpdate("required", checked)} + /> + +
+ )} + {widget.readonly !== undefined && ( +
+ handleUpdate("readonly", checked)} + /> + +
+ )} +
+ + {/* 액션 버튼 */} + +
+ {onCopyComponent && ( + + )} + {onDeleteComponent && ( + + )} +
+
+ ); + }; + + // 상세 설정 탭 (DetailSettingsPanel의 전체 로직 통합) + const renderDetailTab = () => { + // 1. DataTable 컴포넌트 + if (selectedComponent.type === "datatable") { + return ( + { + Object.entries(updates).forEach(([key, value]) => { + handleUpdate(key, value); + }); + }} + /> + ); + } + + // 3. 파일 컴포넌트 + if (isFileComponent(selectedComponent)) { + return ( + + ); + } + + // 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
{configPanel}
; + } + } + + // 5. 새로운 컴포넌트 시스템 (type: "component") + if (selectedComponent.type === "component") { + const componentId = (selectedComponent as any).componentType || selectedComponent.componentConfig?.type; + const webType = selectedComponent.componentConfig?.webType; + + if (!componentId) { + return ( +
+

컴포넌트 ID가 설정되지 않았습니다

+
+ ); + } + + // 현재 웹타입의 기본 입력 타입 추출 + const currentBaseInputType = webType ? getBaseInputType(webType as any) : null; + + // 선택 가능한 세부 타입 목록 + const availableDetailTypes = currentBaseInputType ? getDetailTypes(currentBaseInputType) : []; + + // 세부 타입 변경 핸들러 + const handleDetailTypeChange = (newDetailType: string) => { + setLocalComponentDetailType(newDetailType); + handleUpdate("componentConfig.webType", newDetailType); + }; + + return ( +
+ {/* 컴포넌트 정보 */} +
+ 컴포넌트: {componentId} + {webType && currentBaseInputType && ( +
입력 타입: {currentBaseInputType}
+ )} +
+ + {/* 세부 타입 선택 */} + {webType && availableDetailTypes.length > 1 && ( +
+ + +

입력 타입 "{currentBaseInputType}"의 세부 형태를 선택하세요

+
+ )} + + {/* DynamicComponentConfigPanel */} + { + Object.entries(newConfig).forEach(([key, value]) => { + handleUpdate(`componentConfig.${key}`, value); + }); + }} + /> + + {/* 웹타입별 특화 설정 */} + {webType && ( +
+

웹타입 설정

+ { + Object.entries(newConfig).forEach(([key, value]) => { + handleUpdate(`componentConfig.${key}`, value); + }); + }} + /> +
+ )} +
+ ); + } + + // 6. Widget 컴포넌트 + if (selectedComponent.type === "widget") { + const widget = selectedComponent as WidgetComponent; + + // Widget에 webType이 있는 경우 + if (widget.webType) { + return ( +
+ {/* WebType 선택 */} +
+ + +
+ + {/* WebType 설정 패널 */} + { + Object.entries(newConfig).forEach(([key, value]) => { + handleUpdate(`webTypeConfig.${key}`, value); + }); + }} + /> +
+ ); + } + + // 새로운 컴포넌트 시스템 (widgetType이 button, card 등) + if ( + widget.widgetType && + ["button", "card", "dashboard", "stats-card", "progress-bar", "chart", "alert", "badge"].includes( + widget.widgetType, + ) + ) { + return ( + { + Object.entries(newConfig).forEach(([key, value]) => { + handleUpdate(`componentConfig.${key}`, value); + }); + }} + /> + ); + } + } + + // 기본 메시지 + return ( +
+

이 컴포넌트는 추가 설정이 없습니다

+
+ ); + }; + + // 데이터 바인딩 탭 + const renderDataTab = () => { + if (selectedComponent.type !== "widget") { + return ( +
+

이 컴포넌트는 데이터 바인딩을 지원하지 않습니다

+
+ ); + } + + const widget = selectedComponent as WidgetComponent; + + return ( +
+
+
+ + 데이터 바인딩 +
+
+ + {/* 테이블 컬럼 */} +
+ + handleUpdate("columnName", e.target.value)} + placeholder="컬럼명 입력" + /> +
+ + {/* 기본값 */} +
+ + handleUpdate("defaultValue", e.target.value)} + placeholder="기본값 입력" + /> +
+
+ ); + }; + + return ( +
+ {/* 헤더 */} +
+
+
+ +

속성 편집

+
+ {selectedComponent.type} +
+ {selectedComponent.type === "widget" && ( +
+ {(selectedComponent as WidgetComponent).label || selectedComponent.id} +
+ )} +
+ + {/* 탭 컨텐츠 */} +
+ + + 기본 + 상세 + 데이터 + + +
+ + {renderBasicTab()} + + + {renderDetailTab()} + + + {renderDataTab()} + +
+
+
+
+ ); +}; + +export default UnifiedPropertiesPanel; diff --git a/frontend/components/screen/toolbar/LeftUnifiedToolbar.tsx b/frontend/components/screen/toolbar/LeftUnifiedToolbar.tsx new file mode 100644 index 00000000..bb6e6a7b --- /dev/null +++ b/frontend/components/screen/toolbar/LeftUnifiedToolbar.tsx @@ -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; + onTogglePanel: (panelId: string) => void; +} + +export const LeftUnifiedToolbar: React.FC = ({ 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 ( + + ); + }; + + return ( +
+ {/* 입력/소스 그룹 */} +
{sourceButtons.map(renderButton)}
+ + {/* 편집/설정 그룹 */} +
{editorButtons.map(renderButton)}
+ + {/* 하단 여백 */} +
+
+ ); +}; + +// 기본 버튼 설정 +export const defaultToolbarButtons: ToolbarButton[] = [ + // 입력/소스 그룹 + { + id: "tables", + label: "테이블", + icon: , + shortcut: "T", + group: "source", + panelWidth: 380, + }, + { + id: "components", + label: "컴포넌트", + icon: , + shortcut: "C", + group: "source", + panelWidth: 350, + }, + + // 편집/설정 그룹 + { + id: "properties", + label: "속성", + icon: , + shortcut: "P", + group: "editor", + panelWidth: 400, + }, + { + id: "styles", + label: "스타일", + icon: , + shortcut: "S", + group: "editor", + panelWidth: 360, + }, + { + id: "resolution", + label: "해상도", + icon: , + shortcut: "E", + group: "editor", + panelWidth: 300, + }, + { + id: "zone", + label: "구역", + icon: , + shortcut: "Z", + group: "editor", + panelWidth: 0, // 토글만 (패널 없음) + }, +]; + +export default LeftUnifiedToolbar; diff --git a/frontend/components/screen/toolbar/SlimToolbar.tsx b/frontend/components/screen/toolbar/SlimToolbar.tsx new file mode 100644 index 00000000..31e0bc08 --- /dev/null +++ b/frontend/components/screen/toolbar/SlimToolbar.tsx @@ -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 = ({ + screenName, + tableName, + onBack, + onSave, + onUndo, + onRedo, + onPreview, + canUndo, + canRedo, + isSaving = false, +}) => { + return ( +
+ {/* 좌측: 네비게이션 및 화면 정보 */} +
+ + +
+ +
+
+

{screenName || "화면 설계"}

+ {tableName && ( +
+ + {tableName} +
+ )} +
+
+
+ + {/* 우측: 액션 버튼들 */} +
+ + + + +
+ + + + +
+
+ ); +}; + +export default SlimToolbar;