From ff58c84ac0294c505da0ca951192aba45575e638 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Mon, 20 Oct 2025 11:52:23 +0900 Subject: [PATCH] =?UTF-8?q?=EC=98=A4=EB=A5=98=EB=82=9C=EA=B2=83=EB=93=A4?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/CanvasElement.tsx | 8 ++-- .../admin/dashboard/DashboardCanvas.tsx | 20 ++++++---- .../admin/dashboard/DashboardDesigner.tsx | 40 ++++++++++++------- .../admin/dashboard/ElementConfigModal.tsx | 31 +++++++++++--- .../widgets/YardManagement3DWidget.tsx | 15 +++++++ .../components/dashboard/DashboardViewer.tsx | 8 ++++ 6 files changed, 91 insertions(+), 31 deletions(-) diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index f725497c..070116f0 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -4,7 +4,7 @@ import React, { useState, useCallback, useRef, useEffect } from "react"; import dynamic from "next/dynamic"; import { DashboardElement, QueryResult } from "./types"; import { ChartRenderer } from "./charts/ChartRenderer"; -import { snapToGrid, snapSizeToGrid, GRID_CONFIG } from "./gridUtils"; +import { GRID_CONFIG } from "./gridUtils"; // 위젯 동적 임포트 const WeatherWidget = dynamic(() => import("@/components/dashboard/widgets/WeatherWidget"), { @@ -116,6 +116,7 @@ interface CanvasElementProps { element: DashboardElement; isSelected: boolean; cellSize: number; + subGridSize: number; canvasWidth?: number; onUpdate: (id: string, updates: Partial) => void; onRemove: (id: string) => void; @@ -133,6 +134,7 @@ export function CanvasElement({ element, isSelected, cellSize, + subGridSize, canvasWidth = 1560, onUpdate, onRemove, @@ -233,7 +235,6 @@ export function CanvasElement({ rawX = Math.min(rawX, maxX); // 드래그 중 실시간 스냅 (마그네틱 스냅) - const subGridSize = Math.floor(cellSize / 3); const gridSize = cellSize + 5; // GAP 포함한 실제 그리드 크기 const magneticThreshold = 15; // 큰 그리드에 끌리는 거리 (px) @@ -291,7 +292,6 @@ export function CanvasElement({ newWidth = Math.min(newWidth, maxWidth); // 리사이즈 중 실시간 스냅 (마그네틱 스냅) - const subGridSize = Math.floor(cellSize / 3); const gridSize = cellSize + 5; // GAP 포함한 실제 그리드 크기 const magneticThreshold = 15; @@ -336,6 +336,7 @@ export function CanvasElement({ element.subtype, canvasWidth, cellSize, + subGridSize, ], ); @@ -726,6 +727,7 @@ export function CanvasElement({ isEditMode={true} config={element.yardConfig} onConfigChange={(newConfig) => { + console.log("🏗️ 야드 설정 업데이트:", { elementId: element.id, newConfig }); onUpdate(element.id, { yardConfig: newConfig }); }} /> diff --git a/frontend/components/admin/dashboard/DashboardCanvas.tsx b/frontend/components/admin/dashboard/DashboardCanvas.tsx index 45d2cf3c..3170880a 100644 --- a/frontend/components/admin/dashboard/DashboardCanvas.tsx +++ b/frontend/components/admin/dashboard/DashboardCanvas.tsx @@ -156,8 +156,7 @@ export const DashboardCanvas = forwardRef( const rawY = e.clientY - rect.top + (ref.current?.scrollTop || 0); // 마그네틱 스냅 (큰 그리드 우선, 없으면 서브그리드) - const subGridSize = Math.floor(cellSize / 3); - const gridSize = cellSize + 5; // GAP 포함한 실제 그리드 크기 + const gridSize = cellSize + GRID_CONFIG.GAP; // GAP 포함한 실제 그리드 크기 const magneticThreshold = 15; // X 좌표 스냅 @@ -196,6 +195,9 @@ export const DashboardCanvas = forwardRef( // 동적 그리드 크기 계산 const cellWithGap = cellSize + GRID_CONFIG.GAP; const gridSize = `${cellWithGap}px ${cellWithGap}px`; + + // 서브그리드 크기 계산 (gridConfig에서 정확하게 계산된 값 사용) + const subGridSize = gridConfig.SUB_GRID_SIZE; // 12개 컬럼 구분선 위치 계산 const columnLines = Array.from({ length: GRID_CONFIG.COLUMNS + 1 }, (_, i) => i * cellWithGap); @@ -208,12 +210,12 @@ export const DashboardCanvas = forwardRef( backgroundColor, height: `${canvasHeight}px`, minHeight: `${canvasHeight}px`, - // 세밀한 그리드 배경 + // 서브그리드 배경 (세밀한 점선) backgroundImage: ` - linear-gradient(rgba(59, 130, 246, 0.08) 1px, transparent 1px), - linear-gradient(90deg, rgba(59, 130, 246, 0.08) 1px, transparent 1px) + linear-gradient(rgba(59, 130, 246, 0.05) 1px, transparent 1px), + linear-gradient(90deg, rgba(59, 130, 246, 0.05) 1px, transparent 1px) `, - backgroundSize: gridSize, + backgroundSize: `${subGridSize}px ${subGridSize}px`, backgroundPosition: "0 0", backgroundRepeat: "repeat", }} @@ -229,8 +231,9 @@ export const DashboardCanvas = forwardRef( className="pointer-events-none absolute top-0 h-full" style={{ left: `${x}px`, - width: "2px", - zIndex: 1, + width: "1px", + backgroundColor: i === 0 || i === GRID_CONFIG.COLUMNS ? "rgba(59, 130, 246, 0.3)" : "rgba(59, 130, 246, 0.15)", + zIndex: 0, }} /> ))} @@ -248,6 +251,7 @@ export const DashboardCanvas = forwardRef( element={element} isSelected={selectedElement === element.id} cellSize={cellSize} + subGridSize={subGridSize} canvasWidth={canvasWidth} onUpdate={handleUpdateWithCollisionDetection} onRemove={onRemoveElement} diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx index 27115ee1..9a776de8 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx @@ -332,21 +332,31 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D try { const { dashboardApi } = await import("@/lib/api/dashboard"); - const elementsData = elements.map((el) => ({ - id: el.id, - type: el.type, - subtype: el.subtype, - position: el.position, - size: el.size, - title: el.title, - customTitle: el.customTitle, - showHeader: el.showHeader, - content: el.content, - dataSource: el.dataSource, - chartConfig: el.chartConfig, - listConfig: el.listConfig, - yardConfig: el.yardConfig, - })); + const elementsData = elements.map((el) => { + // 야드 위젯인 경우 설정 로그 출력 + if (el.subtype === "yard-management-3d") { + console.log("💾 야드 위젯 저장:", { + id: el.id, + yardConfig: el.yardConfig, + hasLayoutId: !!el.yardConfig?.layoutId, + }); + } + return { + id: el.id, + type: el.type, + subtype: el.subtype, + position: el.position, + size: el.size, + title: el.title, + customTitle: el.customTitle, + showHeader: el.showHeader, + content: el.content, + dataSource: el.dataSource, + chartConfig: el.chartConfig, + listConfig: el.listConfig, + yardConfig: el.yardConfig, + }; + }); let savedDashboard; diff --git a/frontend/components/admin/dashboard/ElementConfigModal.tsx b/frontend/components/admin/dashboard/ElementConfigModal.tsx index ad4de687..fdfcc2c2 100644 --- a/frontend/components/admin/dashboard/ElementConfigModal.tsx +++ b/frontend/components/admin/dashboard/ElementConfigModal.tsx @@ -36,6 +36,11 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element // 차트 설정이 필요 없는 위젯 (쿼리/API만 필요) const isSimpleWidget = + element.subtype === "todo" || // To-Do 위젯 + element.subtype === "booking-alert" || // 예약 알림 위젯 + element.subtype === "maintenance" || // 정비 일정 위젯 + element.subtype === "document" || // 문서 위젯 + element.subtype === "risk-alert" || // 리스크 알림 위젯 element.subtype === "vehicle-status" || element.subtype === "vehicle-list" || element.subtype === "status-summary" || // 커스텀 상태 카드 @@ -46,6 +51,12 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element element.subtype === "cargo-list" || element.subtype === "customer-issues" || element.subtype === "driver-management"; + + // 자체 기능 위젯 (DB 연결 불필요, 헤더 설정만 가능) + const isSelfContainedWidget = + element.subtype === "weather" || // 날씨 위젯 (외부 API) + element.subtype === "exchange" || // 환율 위젯 (외부 API) + element.subtype === "calculator"; // 계산기 위젯 (자체 기능) // 지도 위젯 (위도/경도 매핑 필요) const isMapWidget = element.subtype === "vehicle-map" || element.subtype === "map-summary"; @@ -59,6 +70,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element setQueryResult(null); setCurrentStep(1); setCustomTitle(element.customTitle || ""); + setShowHeader(element.showHeader !== false); // showHeader 초기화 } }, [isOpen, element]); @@ -135,8 +147,12 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element // 모달이 열려있지 않으면 렌더링하지 않음 if (!isOpen) return null; - // 시계, 달력, To-Do 위젯은 헤더 설정만 가능 - const isHeaderOnlyWidget = element.type === "widget" && (element.subtype === "clock" || element.subtype === "calendar" || element.subtype === "todo"); + // 시계, 달력, 날씨, 환율, 계산기 위젯은 헤더 설정만 가능 + const isHeaderOnlyWidget = + element.type === "widget" && + (element.subtype === "clock" || + element.subtype === "calendar" || + isSelfContainedWidget); // 기사관리 위젯은 자체 설정 UI를 가지고 있으므로 모달 표시하지 않음 if (element.type === "widget" && element.subtype === "driver-management") { @@ -154,11 +170,15 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element // customTitle이 변경되었는지 확인 const isTitleChanged = customTitle.trim() !== (element.customTitle || ""); + + // showHeader가 변경되었는지 확인 + const isHeaderChanged = showHeader !== (element.showHeader !== false); const canSave = isTitleChanged || // 제목만 변경해도 저장 가능 + isHeaderChanged || // 헤더 표시 여부만 변경해도 저장 가능 (isSimpleWidget - ? // 간단한 위젯: 2단계에서 쿼리 테스트 후 저장 가능 + ? // 간단한 위젯: 2단계에서 쿼리 테스트 후 저장 가능 (차트 설정 불필요) currentStep === 2 && queryResult && queryResult.rows.length > 0 : isMapWidget ? // 지도 위젯: 위도/경도 매핑 필요 @@ -184,7 +204,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
{/* 모달 헤더 */} @@ -336,7 +356,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element 저장 ) : currentStep === 1 ? ( - // 1단계: 다음 버튼 + // 1단계: 다음 버튼 (차트 위젯, 간단한 위젯 모두)
); } + diff --git a/frontend/components/admin/dashboard/widgets/YardManagement3DWidget.tsx b/frontend/components/admin/dashboard/widgets/YardManagement3DWidget.tsx index c09e7df6..f3e5a1be 100644 --- a/frontend/components/admin/dashboard/widgets/YardManagement3DWidget.tsx +++ b/frontend/components/admin/dashboard/widgets/YardManagement3DWidget.tsx @@ -57,6 +57,17 @@ export default function YardManagement3DWidget({ } }, [isEditMode]); + // 레이아웃 목록이 로드되었고, 설정이 없으면 첫 번째 레이아웃 자동 선택 + useEffect(() => { + if (isEditMode && layouts.length > 0 && !config?.layoutId && onConfigChange) { + console.log("🔧 첫 번째 야드 레이아웃 자동 선택:", layouts[0]); + onConfigChange({ + layoutId: layouts[0].id, + layoutName: layouts[0].name, + }); + } + }, [isEditMode, layouts, config?.layoutId, onConfigChange]); + // 레이아웃 선택 (편집 모드에서만) const handleSelectLayout = (layout: YardLayout) => { if (onConfigChange) { @@ -243,12 +254,16 @@ export default function YardManagement3DWidget({ // 뷰 모드: 선택된 레이아웃의 3D 뷰어 표시 if (!config?.layoutId) { + console.warn("⚠️ 야드관리 위젯: layoutId가 설정되지 않음", { config, isEditMode }); return (
🏗️
야드 레이아웃이 설정되지 않았습니다
대시보드 편집에서 레이아웃을 선택하세요
+
+ 디버그: config={JSON.stringify(config)} +
); diff --git a/frontend/components/dashboard/DashboardViewer.tsx b/frontend/components/dashboard/DashboardViewer.tsx index 9b6e83f8..cec206ff 100644 --- a/frontend/components/dashboard/DashboardViewer.tsx +++ b/frontend/components/dashboard/DashboardViewer.tsx @@ -82,6 +82,14 @@ function renderWidget(element: DashboardElement) { return ; case "yard-management-3d": + console.log("🏗️ 야드관리 위젯 렌더링:", { + elementId: element.id, + yardConfig: element.yardConfig, + yardConfigType: typeof element.yardConfig, + hasLayoutId: !!element.yardConfig?.layoutId, + layoutId: element.yardConfig?.layoutId, + layoutName: element.yardConfig?.layoutName, + }); return ; // === 차량 관련 (추가 위젯) ===