"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, Table, Settings, ChevronDown, ChevronRight, } from "lucide-react"; import { ScreenDefinition, ComponentData, LayoutData, DragState, GroupState, ComponentType, WebType, WidgetComponent, ColumnInfo, TableInfo, } 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"; import { Badge } from "@/components/ui/badge"; interface ScreenDesignerProps { selectedScreen: ScreenDefinition | null; onBackToList: () => void; } interface ComponentMoveState { isMoving: boolean; movingComponent: ComponentData | null; originalPosition: { x: number; y: number }; currentPosition: { x: number; y: number }; } export default function ScreenDesigner({ selectedScreen, onBackToList }: 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 [activeTab, setActiveTab] = useState("tables"); const [tables, setTables] = useState([]); const [expandedTables, setExpandedTables] = useState>(new Set()); // 테이블 데이터 로드 (실제로는 API에서 가져와야 함) useEffect(() => { const fetchTables = async () => { try { const response = await fetch("http://localhost:8080/api/screen-management/tables", { headers: { Authorization: `Bearer ${localStorage.getItem("authToken")}`, }, }); if (response.ok) { const data = await response.json(); if (data.success) { setTables(data.data); } else { console.error("테이블 조회 실패:", data.message); // 임시 데이터로 폴백 setTables(getMockTables()); } } else { console.error("테이블 조회 실패:", response.status); // 임시 데이터로 폴백 setTables(getMockTables()); } } catch (error) { console.error("테이블 조회 중 오류:", error); // 임시 데이터로 폴백 setTables(getMockTables()); } }; fetchTables(); }, []); // 임시 테이블 데이터 (API 실패 시 사용) const getMockTables = (): TableInfo[] => [ { tableName: "user_info", tableLabel: "사용자 정보", columns: [ { tableName: "user_info", columnName: "user_id", columnLabel: "사용자 ID", webType: "text", dataType: "VARCHAR", isNullable: "NO", }, { tableName: "user_info", columnName: "user_name", columnLabel: "사용자명", webType: "text", dataType: "VARCHAR", isNullable: "NO", }, { tableName: "user_info", columnName: "email", columnLabel: "이메일", webType: "email", dataType: "VARCHAR", isNullable: "YES", }, { tableName: "user_info", columnName: "phone", columnLabel: "전화번호", webType: "tel", dataType: "VARCHAR", isNullable: "YES", }, { tableName: "user_info", columnName: "birth_date", columnLabel: "생년월일", webType: "date", dataType: "DATE", isNullable: "YES", }, { tableName: "user_info", columnName: "is_active", columnLabel: "활성화", webType: "checkbox", dataType: "BOOLEAN", isNullable: "NO", }, ], }, { tableName: "product_info", tableLabel: "제품 정보", columns: [ { tableName: "product_info", columnName: "product_id", columnLabel: "제품 ID", webType: "text", dataType: "VARCHAR", isNullable: "NO", }, { tableName: "product_info", columnName: "product_name", columnLabel: "제품명", webType: "text", dataType: "VARCHAR", isNullable: "NO", }, { tableName: "product_info", columnName: "category", columnLabel: "카테고리", webType: "select", dataType: "VARCHAR", isNullable: "YES", }, { tableName: "product_info", columnName: "price", columnLabel: "가격", webType: "number", dataType: "DECIMAL", isNullable: "YES", }, { tableName: "product_info", columnName: "description", columnLabel: "설명", webType: "textarea", dataType: "TEXT", isNullable: "YES", }, { tableName: "product_info", columnName: "created_date", columnLabel: "생성일", webType: "date", dataType: "TIMESTAMP", isNullable: "NO", }, ], }, { tableName: "order_info", tableLabel: "주문 정보", columns: [ { tableName: "order_info", columnName: "order_id", columnLabel: "주문 ID", webType: "text", dataType: "VARCHAR", isNullable: "NO", }, { tableName: "order_info", columnName: "customer_name", columnLabel: "고객명", webType: "text", dataType: "VARCHAR", isNullable: "NO", }, { tableName: "order_info", columnName: "order_date", columnLabel: "주문일", webType: "date", dataType: "DATE", isNullable: "NO", }, { tableName: "order_info", columnName: "total_amount", columnLabel: "총 금액", webType: "number", dataType: "DECIMAL", isNullable: "NO", }, { tableName: "order_info", columnName: "status", columnLabel: "상태", webType: "select", dataType: "VARCHAR", isNullable: "NO", }, { tableName: "order_info", columnName: "notes", columnLabel: "비고", webType: "textarea", dataType: "TEXT", isNullable: "YES", }, ], }, ]; // 테이블 확장/축소 토글 const toggleTableExpansion = useCallback((tableName: string) => { setExpandedTables((prev) => { const newSet = new Set(prev); if (newSet.has(tableName)) { newSet.delete(tableName); } else { newSet.add(tableName); } return newSet; }); }, []); // 웹타입에 따른 위젯 타입 매핑 const getWidgetTypeFromWebType = useCallback((webType: string): string => { switch (webType) { case "text": case "email": case "tel": return "text"; case "number": case "decimal": return "number"; case "date": case "datetime": return "date"; case "select": case "dropdown": return "select"; case "textarea": case "text_area": return "textarea"; case "checkbox": case "boolean": return "checkbox"; case "radio": return "radio"; default: return "text"; } }, []); // 컴포넌트 추가 함수 const addComponent = useCallback((componentData: Partial, position: { x: number; y: number }) => { const newComponent: ComponentData = { id: generateComponentId(), type: "widget", position, size: { width: 6, height: 60 }, tableName: "", columnName: "", widgetType: "text", label: "", required: false, readonly: false, ...componentData, } as ComponentData; setLayout((prev) => ({ ...prev, components: [...prev.components, newComponent], })); }, []); // 컴포넌트 제거 함수 const removeComponent = useCallback( (componentId: string) => { setLayout((prev) => ({ ...prev, components: prev.components.filter((comp) => comp.id !== componentId), })); if (selectedComponent?.id === componentId) { setSelectedComponent(null); } }, [selectedComponent], ); // 컴포넌트 속성 업데이트 함수 const updateComponentProperty = useCallback((componentId: string, propertyPath: string, value: any) => { setLayout((prev) => ({ ...prev, components: prev.components.map((comp) => { if (comp.id === componentId) { const newComp = { ...comp }; const pathParts = propertyPath.split("."); let current: any = newComp; for (let i = 0; i < pathParts.length - 1; i++) { current = current[pathParts[i]]; } current[pathParts[pathParts.length - 1]] = value; return newComp; } return comp; }), })); }, []); // 레이아웃 저장 함수 const saveLayout = useCallback(async () => { try { // TODO: 실제 API 호출로 변경 console.log("레이아웃 저장:", layout); // await saveLayoutAPI(selectedScreen.screenId, layout); } catch (error) { console.error("레이아웃 저장 실패:", error); } }, [layout, selectedScreen]); // 드래그 시작 const startDrag = useCallback((componentData: Partial, e: React.DragEvent) => { e.dataTransfer.setData("application/json", JSON.stringify(componentData)); setDragState((prev) => ({ ...prev, isDragging: true, draggedComponent: componentData as ComponentData, })); }, []); // 드래그 종료 const endDrag = useCallback(() => { setDragState((prev) => ({ ...prev, isDragging: false, draggedComponent: null, })); }, []); // 드롭 처리 const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); const componentData = JSON.parse(e.dataTransfer.getData("application/json")); // 드롭 위치 계산 (그리드 기반) const rect = e.currentTarget.getBoundingClientRect(); const x = Math.floor((e.clientX - rect.left) / 80); // 80px = 1 그리드 컬럼 const y = Math.floor((e.clientY - rect.top) / 60); // 60px = 1 그리드 행 addComponent(componentData, { x, y }); endDrag(); }, [addComponent, endDrag], ); // 드래그 오버 처리 const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); }, []); // 화면이 선택되지 않았을 때 처리 if (!selectedScreen) { return (

설계할 화면을 선택해주세요

화면 목록에서 화면을 선택한 후 설계기를 사용하세요

); } return (
{/* 상단 헤더 */}

{selectedScreen.screenName} - 화면 설계

{selectedScreen.tableName}
{/* 메인 컨텐츠 영역 */}
{/* 좌측: 테이블 타입 관리 */}

테이블 타입

테이블과 컬럼을 드래그하여 캔버스에 배치하세요.

{tables.map((table) => (
{/* 테이블 헤더 */}
{table.tableLabel}
{/* 테이블 드래그 가능 */}
startDrag( { type: "container", tableName: table.tableName, label: table.tableLabel, size: { width: 12, height: 80 }, }, e, ) } onDragEnd={endDrag} >
테이블 전체 {table.columns.length} 컬럼 {/* 컬럼 목록 */} {expandedTables.has(table.tableName) && (
{table.columns.map((column) => (
startDrag( { type: "widget", tableName: table.tableName, columnName: column.columnName, widgetType: getWidgetTypeFromWebType(column.webType || "text"), label: column.columnLabel || column.columnName, size: { width: 6, height: 60 }, }, e, ) } onDragEnd={endDrag} >
{column.webType === "text" && } {column.webType === "number" && } {column.webType === "date" && } {column.webType === "select" && } {column.webType === "textarea" && } {column.webType === "checkbox" && } {column.webType === "radio" && } {!["text", "number", "date", "select", "textarea", "checkbox", "radio"].includes( column.webType, ) && }
{column.columnLabel}
{column.columnName}
{column.webType}
))}
)} ))} {/* 중앙: 캔버스 영역 */}
{layout.components.length === 0 ? (

빈 캔버스

좌측에서 테이블이나 컬럼을 드래그하여 배치하세요

) : (
{/* 그리드 가이드 */}
{Array.from({ length: 12 }).map((_, i) => (
))}
{/* 컴포넌트들 */} {layout.components.map((component) => (
setSelectedComponent(component)} >
{component.type === "container" && ( <>
{component.label}
{component.tableName}
)} {component.type === "widget" && ( <>
{component.widgetType === "text" && } {component.widgetType === "number" && } {component.widgetType === "date" && } {component.widgetType === "select" && } {component.widgetType === "textarea" && } {component.widgetType === "checkbox" && } {component.widgetType === "radio" && }
{component.label}
{component.columnName}
)}
))}
)}
{/* 우측: 컴포넌트 스타일 편집 */}

컴포넌트 속성

{selectedComponent ? (
{selectedComponent.type === "container" && "테이블 속성"} {selectedComponent.type === "widget" && "위젯 속성"} {/* 위치 속성 */}
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)} />
{/* 고급 속성 */}
) : (

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

)}
); }