From 6c45686157d54b9fd469bf959c339d8855f9fbce Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 1 Sep 2025 14:26:39 +0900 Subject: [PATCH] =?UTF-8?q?=ED=99=94=EB=A9=B4=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EA=B7=B8=EC=95=A4=20=EB=93=9C=EB=A1=AD=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/screen/ScreenDesigner.tsx | 387 ++++++++++++------ 1 file changed, 272 insertions(+), 115 deletions(-) diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 339d84c0..82ac8100 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, useEffect } from "react"; +import { useState, useCallback, useEffect, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; @@ -26,6 +26,8 @@ import { Settings, ChevronDown, ChevronRight, + List, + AlignLeft, } from "lucide-react"; import { ScreenDefinition, @@ -70,13 +72,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD gridSettings: { columns: 12, gap: 16, padding: 16 }, }); const [selectedComponent, setSelectedComponent] = useState(null); - const [dragState, setDragState] = useState({ + const [dragState, setDragState] = useState({ isDragging: false, - draggedItem: null, - draggedComponent: null, - dragSource: "toolbox", - dropTarget: null, - dragOffset: { x: 0, y: 0 }, + draggedComponent: null as ComponentData | null, + originalPosition: { x: 0, y: 0 }, + currentPosition: { x: 0, y: 0 }, }); const [groupState, setGroupState] = useState({ isGrouping: false, @@ -94,6 +94,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD const [tables, setTables] = useState([]); const [expandedTables, setExpandedTables] = useState>(new Set()); + // 테이블 검색 및 페이징 상태 추가 + const [searchTerm, setSearchTerm] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage] = useState(10); + // 테이블 데이터 로드 (실제로는 API에서 가져와야 함) useEffect(() => { const fetchTables = async () => { @@ -128,6 +133,45 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD fetchTables(); }, []); + // 검색된 테이블 필터링 + const filteredTables = useMemo(() => { + if (!searchTerm.trim()) return tables; + + return tables.filter( + (table) => + table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) || + table.tableLabel.toLowerCase().includes(searchTerm.toLowerCase()) || + table.columns.some( + (column) => + column.columnName.toLowerCase().includes(searchTerm.toLowerCase()) || + (column.columnLabel || column.columnName).toLowerCase().includes(searchTerm.toLowerCase()), + ), + ); + }, [tables, searchTerm]); + + // 페이징된 테이블 + const paginatedTables = useMemo(() => { + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + return filteredTables.slice(startIndex, endIndex); + }, [filteredTables, currentPage, itemsPerPage]); + + // 총 페이지 수 계산 + const totalPages = Math.ceil(filteredTables.length / itemsPerPage); + + // 페이지 변경 핸들러 + const handlePageChange = (page: number) => { + setCurrentPage(page); + setExpandedTables(new Set()); // 페이지 변경 시 확장 상태 초기화 + }; + + // 검색어 변경 핸들러 + const handleSearchChange = (value: string) => { + setSearchTerm(value); + setCurrentPage(1); // 검색 시 첫 페이지로 이동 + setExpandedTables(new Set()); // 검색 시 확장 상태 초기화 + }; + // 임시 테이블 데이터 (API 실패 시 사용) const getMockTables = (): TableInfo[] => [ { @@ -405,47 +449,121 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD } }, [layout, selectedScreen]); - // 드래그 시작 - const startDrag = useCallback((componentData: Partial, e: React.DragEvent) => { - e.dataTransfer.setData("application/json", JSON.stringify(componentData)); - setDragState((prev) => ({ - ...prev, + // 드래그 시작 (새 컴포넌트 추가) + const startDrag = useCallback((component: Partial, e: React.DragEvent) => { + setDragState({ isDragging: true, - draggedComponent: componentData as ComponentData, - })); + draggedComponent: component as ComponentData, + originalPosition: { x: 0, y: 0 }, + currentPosition: { x: 0, y: 0 }, + }); + e.dataTransfer.setData("application/json", JSON.stringify(component)); + }, []); + + // 기존 컴포넌트 드래그 시작 (재배치) + const startComponentDrag = useCallback((component: ComponentData, e: React.DragEvent) => { + setDragState({ + isDragging: true, + draggedComponent: component, + originalPosition: component.position, + currentPosition: component.position, + }); + e.dataTransfer.setData("application/json", JSON.stringify({ ...component, isMoving: true })); + }, []); + + // 드래그 중 + const onDragOver = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + if (dragState.isDragging) { + const rect = e.currentTarget.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / 80) * 80; + const y = Math.floor((e.clientY - rect.top) / 60) * 60; + + setDragState((prev) => ({ + ...prev, + currentPosition: { x, y }, + })); + } + }, + [dragState.isDragging], + ); + + // 드롭 처리 + const onDrop = useCallback((e: React.DragEvent) => { + e.preventDefault(); + + try { + const data = JSON.parse(e.dataTransfer.getData("application/json")); + + if (data.isMoving) { + // 기존 컴포넌트 재배치 + const rect = e.currentTarget.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / 80) * 80; + const y = Math.floor((e.clientY - rect.top) / 60) * 60; + + setLayout((prev) => ({ + ...prev, + components: prev.components.map((comp) => (comp.id === data.id ? { ...comp, position: { x, y } } : comp)), + })); + } else { + // 새 컴포넌트 추가 + const rect = e.currentTarget.getBoundingClientRect(); + const x = Math.floor((e.clientX - rect.left) / 80) * 80; + const y = Math.floor((e.clientY - rect.top) / 60) * 60; + + const newComponent: ComponentData = { + ...data, + id: generateComponentId(), + position: { x, y }, + } as ComponentData; + + setLayout((prev) => ({ + ...prev, + components: [...prev.components, newComponent], + })); + } + } catch (error) { + console.error("드롭 처리 중 오류:", error); + } + + setDragState({ + isDragging: false, + draggedComponent: null, + originalPosition: { x: 0, y: 0 }, + currentPosition: { x: 0, y: 0 }, + }); }, []); // 드래그 종료 const endDrag = useCallback(() => { - setDragState((prev) => ({ - ...prev, + setDragState({ isDragging: false, draggedComponent: null, - })); + originalPosition: { x: 0, y: 0 }, + currentPosition: { x: 0, y: 0 }, + }); }, []); - // 드롭 처리 - const handleDrop = useCallback( - (e: React.DragEvent) => { - e.preventDefault(); - const componentData = JSON.parse(e.dataTransfer.getData("application/json")); + // 컴포넌트 클릭 (선택) + const handleComponentClick = useCallback((component: ComponentData) => { + setSelectedComponent(component); + }, []); - // 드롭 위치 계산 (그리드 기반) - 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(); + // 컴포넌트 삭제 + const deleteComponent = useCallback( + (componentId: string) => { + setLayout((prev) => ({ + ...prev, + components: prev.components.filter((comp) => comp.id !== componentId), + })); + if (selectedComponent?.id === componentId) { + setSelectedComponent(null); + } }, - [addComponent, endDrag], + [selectedComponent], ); - // 드래그 오버 처리 - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault(); - }, []); - // 화면이 선택되지 않았을 때 처리 if (!selectedScreen) { return ( @@ -490,21 +608,57 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD {/* 메인 컨텐츠 영역 */}
- {/* 좌측: 테이블 타입 관리 */} -
-
+ {/* 좌측 사이드바 - 테이블 타입 */} +
+

테이블 타입

+ + {/* 검색 입력창 */} +
+ handleSearchChange(e.target.value)} + className="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-blue-500 focus:outline-none" + /> +
+ + {/* 검색 결과 정보 */} +
+ 총 {filteredTables.length}개 테이블 중 {(currentPage - 1) * itemsPerPage + 1}- + {Math.min(currentPage * itemsPerPage, filteredTables.length)}번째 +
+

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

-
- {tables.map((table) => ( -
+ {/* 테이블 목록 */} +
+ {paginatedTables.map((table) => ( +
{/* 테이블 헤더 */} -
+
+ startDrag( + { + type: "container", + tableName: table.tableName, + label: table.tableLabel, + size: { width: 12, height: 120 }, + }, + e, + ) + } + >
- {table.tableLabel} +
+
{table.tableLabel}
+
{table.tableName}
+
- {/* 테이블 드래그 가능 */} -
- 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( @@ -567,28 +695,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD 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} - +
+ {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.columnName}
))} @@ -597,6 +719,35 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
))}
+ + {/* 페이징 컨트롤 */} + {totalPages > 1 && ( +
+
+ + +
+ {currentPage} / {totalPages} +
+ + +
+
+ )} {/* 중앙: 캔버스 영역 */} @@ -604,8 +755,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
{layout.components.length === 0 ? (
@@ -630,43 +781,49 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD {layout.components.map((component) => (
setSelectedComponent(component)} + onClick={() => handleComponentClick(component)} + draggable + onDragStart={(e) => startComponentDrag(component, e)} + onDragEnd={endDrag} > -
+
{component.type === "container" && ( - <> - +
+
-
{component.label}
+
{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.widgetType === "text" && } + {component.widgetType === "number" && } + {component.widgetType === "date" && } + {component.widgetType === "select" && } + {component.widgetType === "textarea" && } + {component.widgetType === "checkbox" && } + {component.widgetType === "radio" && } + {!["text", "number", "date", "select", "textarea", "checkbox", "radio"].includes( + component.widgetType || "text", + ) && }
-
{component.label}
+
{component.label}
{component.columnName}
- +
)}