"use client"; import { useState, useCallback, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Palette, Grid3X3, Type, Calendar, Hash, CheckSquare, Radio, FileText, Save, Undo, Redo, Eye, Group, Ungroup, Database, Trash2, } from "lucide-react"; import { ScreenDefinition, ComponentData, LayoutData, DragState, GroupState, ComponentType, WebType, WidgetComponent, ColumnInfo, } from "@/types/screen"; import { generateComponentId } from "@/lib/utils/generateId"; import ContainerComponent from "./layout/ContainerComponent"; import RowComponent from "./layout/RowComponent"; import ColumnComponent from "./layout/ColumnComponent"; import WidgetFactory from "./WidgetFactory"; import TableTypeSelector from "./TableTypeSelector"; import ScreenPreview from "./ScreenPreview"; import TemplateManager from "./TemplateManager"; import StyleEditor from "./StyleEditor"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; interface ScreenDesignerProps { screen: ScreenDefinition; } interface ComponentMoveState { isMoving: boolean; movingComponent: ComponentData | null; originalPosition: { x: number; y: number }; currentPosition: { x: number; y: number }; } export default function ScreenDesigner({ screen }: ScreenDesignerProps) { const [layout, setLayout] = useState({ components: [], gridSettings: { columns: 12, gap: 16, padding: 16 }, }); const [selectedComponent, setSelectedComponent] = useState(null); const [dragState, setDragState] = useState({ isDragging: false, draggedItem: null, draggedComponent: null, dragSource: "toolbox", dropTarget: null, dragOffset: { x: 0, y: 0 }, }); const [groupState, setGroupState] = useState({ isGrouping: false, selectedComponents: [], groupTarget: null, groupMode: "create", }); const [moveState, setMoveState] = useState({ isMoving: false, movingComponent: null, originalPosition: { x: 0, y: 0 }, currentPosition: { x: 0, y: 0 }, }); // 기본 컴포넌트 정의 const basicComponents = [ { type: "text", label: "텍스트 입력", color: "bg-blue-500", icon: "Type" }, { type: "number", label: "숫자 입력", color: "bg-green-500", icon: "Hash" }, { type: "date", label: "날짜 선택", color: "bg-purple-500", icon: "Calendar" }, { type: "select", label: "선택 박스", color: "bg-orange-500", icon: "CheckSquare" }, { type: "textarea", label: "텍스트 영역", color: "bg-indigo-500", icon: "FileText" }, { type: "checkbox", label: "체크박스", color: "bg-pink-500", icon: "CheckSquare" }, { type: "radio", label: "라디오 버튼", color: "bg-yellow-500", icon: "Radio" }, { type: "file", label: "파일 업로드", color: "bg-red-500", icon: "FileText" }, { type: "code", label: "코드 입력", color: "bg-gray-500", icon: "Hash" }, { type: "entity", label: "엔티티 선택", color: "bg-teal-500", icon: "Database" }, ]; const layoutComponents = [ { type: "container", label: "컨테이너", color: "bg-gray-500" }, { type: "row", label: "행", color: "bg-yellow-500" }, { type: "column", label: "열", color: "bg-red-500" }, { type: "group", label: "그룹", color: "bg-teal-500" }, ]; // 드래그 시작 const startDrag = useCallback((componentType: ComponentType, source: "toolbox" | "canvas") => { let componentData: ComponentData; if (componentType === "widget") { // 위젯 컴포넌트 생성 componentData = { id: generateComponentId(componentType), type: "widget", position: { x: 0, y: 0 }, size: { width: 6, height: 50 }, label: "새 위젯", tableName: "", columnName: "", widgetType: "text", required: false, readonly: false, }; } else if (componentType === "container") { // 컨테이너 컴포넌트 생성 componentData = { id: generateComponentId(componentType), type: "container", position: { x: 0, y: 0 }, size: { width: 12, height: 100 }, title: "새 컨테이너", children: [], }; } else if (componentType === "row") { // 행 컴포넌트 생성 componentData = { id: generateComponentId(componentType), type: "row", position: { x: 0, y: 0 }, size: { width: 12, height: 100 }, children: [], }; } else if (componentType === "column") { // 열 컴포넌트 생성 componentData = { id: generateComponentId(componentType), type: "column", position: { x: 0, y: 0 }, size: { width: 6, height: 100 }, children: [], }; } else if (componentType === "group") { // 그룹 컴포넌트 생성 componentData = { id: generateComponentId(componentType), type: "group", position: { x: 0, y: 0 }, size: { width: 12, height: 200 }, title: "새 그룹", children: [], }; } else { throw new Error(`지원하지 않는 컴포넌트 타입: ${componentType}`); } setDragState((prev) => ({ ...prev, isDragging: true, draggedComponent: componentData, dragOffset: { x: 0, y: 0 }, })); }, []); // 드래그 종료 const endDrag = useCallback(() => { setDragState({ isDragging: false, draggedComponent: null, dragOffset: { x: 0, y: 0 }, }); }, []); // 컴포넌트 추가 const addComponent = useCallback((component: ComponentData, position: { x: number; y: number }) => { const newComponent = { ...component, id: generateComponentId(component.type), position, }; setLayout((prev) => ({ ...prev, components: [...prev.components, newComponent], })); }, []); // 컴포넌트 선택 const selectComponent = useCallback((component: ComponentData) => { setSelectedComponent(component); }, []); // 컴포넌트 삭제 const removeComponent = useCallback((componentId: string) => { setLayout((prev) => ({ ...prev, components: prev.components.filter((c) => c.id !== componentId), })); setSelectedComponent(null); }, []); // 컴포넌트 속성 업데이트 const updateComponentProperty = useCallback((componentId: string, path: string, value: any) => { setLayout((prev) => ({ ...prev, components: prev.components.map((c) => { if (c.id === componentId) { const newComponent = { ...c } as any; const keys = path.split("."); let current = newComponent; for (let i = 0; i < keys.length - 1; i++) { current = current[keys[i]]; } current[keys[keys.length - 1]] = value; return newComponent; } return c; }), })); }, []); // 레이아웃 저장 const saveLayout = useCallback(() => { console.log("레이아웃 저장:", layout); // TODO: API 호출로 레이아웃 저장 }, [layout]); // 컴포넌트 재배치 시작 const startComponentMove = useCallback((component: ComponentData, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setMoveState({ isMoving: true, movingComponent: component, originalPosition: { ...component.position }, currentPosition: { ...component.position }, }); setDragState((prev) => ({ ...prev, isDragging: true, draggedComponent: component, dragOffset: { x: 0, y: 0 }, })); }, []); // 컴포넌트 재배치 중 const handleComponentMove = useCallback( (e: MouseEvent) => { if (!moveState.isMoving || !moveState.movingComponent) return; const canvas = document.getElementById("design-canvas"); if (!canvas) return; const rect = canvas.getBoundingClientRect(); const x = Math.floor((e.clientX - rect.left) / 50); const y = Math.floor((e.clientY - rect.top) / 50); setMoveState((prev) => ({ ...prev, currentPosition: { x, y }, })); }, [moveState.isMoving, moveState.movingComponent], ); // 컴포넌트 재배치 완료 const endComponentMove = useCallback(() => { if (!moveState.isMoving || !moveState.movingComponent) return; const { movingComponent, currentPosition } = moveState; // 위치 업데이트 setLayout((prev) => ({ ...prev, components: prev.components.map((c) => (c.id === movingComponent.id ? { ...c, position: currentPosition } : c)), })); // 상태 초기화 setMoveState({ isMoving: false, movingComponent: null, originalPosition: { x: 0, y: 0 }, currentPosition: { x: 0, y: 0 }, }); setDragState((prev) => ({ ...prev, isDragging: false, draggedComponent: null, dragOffset: { x: 0, y: 0 }, })); }, [moveState]); // 마우스 이벤트 리스너 등록/해제 useEffect(() => { if (moveState.isMoving) { document.addEventListener("mousemove", handleComponentMove); document.addEventListener("mouseup", endComponentMove); return () => { document.removeEventListener("mousemove", handleComponentMove); document.removeEventListener("mouseup", endComponentMove); }; } }, [moveState.isMoving, handleComponentMove, endComponentMove]); // 컴포넌트 렌더링 const renderComponent = useCallback( (component: ComponentData) => { const isSelected = selectedComponent === component; const isMoving = moveState.isMoving && moveState.movingComponent?.id === component.id; const currentPosition = isMoving ? moveState.currentPosition : component.position; switch (component.type) { case "container": return ( selectComponent(component)} onMouseDown={(e) => startComponentMove(component, e)} isMoving={isMoving} > {/* 컨테이너 내부의 자식 컴포넌트들 */} {layout.components.filter((c) => c.parentId === component.id).map(renderComponent)} ); case "row": return ( selectComponent(component)} onMouseDown={(e) => startComponentMove(component, e)} isMoving={isMoving} > {/* 행 내부의 자식 컴포넌트들 */} {layout.components.filter((c) => c.parentId === component.id).map(renderComponent)} ); case "column": return ( selectComponent(component)} onMouseDown={(e) => startComponentMove(component, e)} isMoving={isMoving} > {/* 열 내부의 자식 컴포넌트들 */} {layout.components.filter((c) => c.parentId === component.id).map(renderComponent)} ); case "widget": return (
selectComponent(component)} onMouseDown={(e) => startComponentMove(component, e)} >
); default: return null; } }, [selectedComponent, moveState, selectComponent, startComponentMove, layout.components], ); // 테이블 타입에서 컬럼 선택 시 위젯 생성 const handleColumnSelect = useCallback( (column: ColumnInfo) => { const widgetComponent: WidgetComponent = { id: generateComponentId("widget"), type: "widget", position: { x: 0, y: 0 }, size: { width: 6, height: 50 }, parentId: undefined, tableName: column.tableName, columnName: column.columnName, widgetType: column.webType || "text", label: column.columnLabel || column.columnName, placeholder: `${column.columnLabel || column.columnName}을(를) 입력하세요`, required: column.isNullable === "NO", readonly: false, validationRules: [], displayProperties: {}, style: { // 웹 타입별 기본 스타일 ...(column.webType === "date" && { backgroundColor: "#fef3c7", border: "1px solid #f59e0b", }), ...(column.webType === "number" && { backgroundColor: "#dbeafe", border: "1px solid #3b82f6", }), ...(column.webType === "select" && { backgroundColor: "#f3e8ff", border: "1px solid #8b5cf6", }), ...(column.webType === "checkbox" && { backgroundColor: "#dcfce7", border: "1px solid #22c55e", }), ...(column.webType === "radio" && { backgroundColor: "#fef3c7", border: "1px solid #f59e0b", }), ...(column.webType === "textarea" && { backgroundColor: "#f1f5f9", border: "1px solid #64748b", }), ...(column.webType === "file" && { backgroundColor: "#fef2f2", border: "1px solid #ef4444", }), ...(column.webType === "code" && { backgroundColor: "#fef2f2", border: "1px solid #ef4444", fontFamily: "monospace", }), ...(column.webType === "entity" && { backgroundColor: "#f0f9ff", border: "1px solid #0ea5e9", }), }, }; // 현재 캔버스의 빈 위치 찾기 const occupiedPositions = new Set(); layout.components.forEach((comp) => { for (let x = comp.position.x; x < comp.position.x + comp.size.width; x++) { for (let y = comp.position.y; y < comp.position.y + comp.size.height; y++) { occupiedPositions.add(`${x},${y}`); } } }); // 빈 위치 찾기 let newX = 0, newY = 0; for (let y = 0; y < 20; y++) { for (let x = 0; x < 12; x++) { let canPlace = true; for (let dx = 0; dx < widgetComponent.size.width; dx++) { for (let dy = 0; dy < Math.ceil(widgetComponent.size.height / 50); dy++) { if (occupiedPositions.has(`${x + dx},${y + dy}`)) { canPlace = false; break; } } if (!canPlace) break; } if (canPlace) { newX = x; newY = y; break; } } if (newX !== 0 || newY !== 0) break; } widgetComponent.position = { x: newX, y: newY }; addComponent(widgetComponent, { x: newX, y: newY }); }, [layout.components, addComponent], ); return (
{/* 왼쪽 툴바 */}
{/* 기본 컴포넌트 */} 기본 컴포넌트 {basicComponents.map((component) => (
startDrag(component.type as ComponentType, "toolbox")} onDragEnd={endDrag} >
{component.label}
))} {/* 레이아웃 컴포넌트 */} 레이아웃 컴포넌트 {layoutComponents.map((component) => (
startDrag(component.type as ComponentType, "toolbox")} onDragEnd={endDrag} >
{component.label}
))}
{/* 중앙 메인 영역 */}
화면 설계 테이블 타입 미리보기 {/* 화면 설계 탭 */}
{screen.screenName} - 캔버스
{ e.preventDefault(); if (dragState.draggedComponent && dragState.draggedComponent.type === "widget") { const rect = e.currentTarget.getBoundingClientRect(); const x = Math.floor((e.clientX - rect.left) / 50); const y = Math.floor((e.clientY - rect.top) / 50); // 위젯 컴포넌트의 경우 기본 컴포넌트에서 타입 정보를 가져옴 const basicComponent = basicComponents.find( (c) => c.type === (dragState.draggedComponent as any).widgetType, ); if (basicComponent) { const widgetComponent: ComponentData = { ...dragState.draggedComponent, position: { x, y }, label: basicComponent.label, widgetType: basicComponent.type as WebType, } as WidgetComponent; addComponent(widgetComponent, { x, y }); } } else if (dragState.draggedComponent) { const rect = e.currentTarget.getBoundingClientRect(); const x = Math.floor((e.clientX - rect.left) / 50); const y = Math.floor((e.clientY - rect.top) / 50); addComponent(dragState.draggedComponent, { x, y }); } }} onDragOver={(e) => e.preventDefault()} > {/* 그리드 가이드 */}
{Array.from({ length: 12 }).map((_, i) => (
))}
{/* 컴포넌트들 렌더링 */} {layout.components.length > 0 ? ( layout.components .filter((c) => !c.parentId) // 최상위 컴포넌트만 렌더링 .map(renderComponent) ) : (

왼쪽 툴바에서 컴포넌트를 드래그하여 배치하세요

)}
{/* 테이블 타입 탭 */} console.log("테이블 선택:", tableName)} onColumnSelect={handleColumnSelect} className="h-full" /> {/* 미리보기 탭 */}
{/* 오른쪽 속성 패널 */}
속성 {selectedComponent ? ( 일반 스타일 고급 {/* 일반 속성 탭 */}
updateComponentProperty(selectedComponent.id, "position.x", parseInt(e.target.value)) } />
updateComponentProperty(selectedComponent.id, "position.y", parseInt(e.target.value)) } />
updateComponentProperty(selectedComponent.id, "size.width", parseInt(e.target.value)) } />
updateComponentProperty(selectedComponent.id, "size.height", parseInt(e.target.value)) } />
{/* 위젯 전용 속성 */} {selectedComponent.type === "widget" && ( <>
updateComponentProperty(selectedComponent.id, "label", e.target.value)} />
updateComponentProperty(selectedComponent.id, "placeholder", e.target.value)} />
updateComponentProperty(selectedComponent.id, "required", e.target.checked) } />
updateComponentProperty(selectedComponent.id, "readonly", e.target.checked) } />
)}
{/* 스타일 속성 탭 */} updateComponentProperty(selectedComponent.id, "style", newStyle)} /> {/* 고급 속성 탭 */}
) : (

컴포넌트를 선택하여 속성을 편집하세요

)}
); }