모달 -> 사이드바로 변경
This commit is contained in:
parent
1470bb2e73
commit
bdf9bd0075
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import React, { useState, useCallback, useRef, useEffect } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { DashboardElement, QueryResult } from "./types";
|
||||
import { DashboardElement, QueryResult, Position } from "./types";
|
||||
import { ChartRenderer } from "./charts/ChartRenderer";
|
||||
import { GRID_CONFIG } from "./gridUtils";
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ import { CalendarWidget } from "./widgets/CalendarWidget";
|
|||
// 기사 관리 위젯 임포트
|
||||
import { DriverManagementWidget } from "./widgets/DriverManagementWidget";
|
||||
import { ListWidget } from "./widgets/ListWidget";
|
||||
import { MoreHorizontal, X } from "lucide-react";
|
||||
import { X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
// 야드 관리 3D 위젯
|
||||
|
|
@ -137,12 +137,11 @@ interface CanvasElementProps {
|
|||
canvasWidth?: number;
|
||||
onUpdate: (id: string, updates: Partial<DashboardElement>) => void;
|
||||
onUpdateMultiple?: (updates: { id: string; updates: Partial<DashboardElement> }[]) => void; // 🔥 다중 업데이트
|
||||
onMultiDragStart?: (draggedId: string, otherOffsets: Record<string, { x: number; y: number }>) => void; // 🔥 다중 드래그 시작
|
||||
onMultiDragMove?: (draggedElement: DashboardElement, tempPosition: { x: number; y: number }) => void; // 🔥 다중 드래그 중
|
||||
onMultiDragEnd?: () => void; // 🔥 다중 드래그 종료
|
||||
onMultiDragStart?: (draggedId: string, otherOffsets: Record<string, { x: number; y: number }>) => void;
|
||||
onMultiDragMove?: (draggedElement: DashboardElement, tempPosition: { x: number; y: number }) => void;
|
||||
onMultiDragEnd?: () => void;
|
||||
onRemove: (id: string) => void;
|
||||
onSelect: (id: string | null) => void;
|
||||
onConfigure?: (element: DashboardElement) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -167,7 +166,6 @@ export function CanvasElement({
|
|||
onMultiDragEnd,
|
||||
onRemove,
|
||||
onSelect,
|
||||
onConfigure,
|
||||
}: CanvasElementProps) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
|
|
@ -219,7 +217,7 @@ export function CanvasElement({
|
|||
elementX: element.position.x,
|
||||
elementY: element.position.y,
|
||||
});
|
||||
|
||||
|
||||
// 🔥 다중 선택된 경우, 다른 위젯들의 오프셋 계산
|
||||
if (selectedElements.length > 1 && selectedElements.includes(element.id) && onMultiDragStart) {
|
||||
const offsets: Record<string, { x: number; y: number }> = {};
|
||||
|
|
@ -236,10 +234,19 @@ export function CanvasElement({
|
|||
});
|
||||
onMultiDragStart(element.id, offsets);
|
||||
}
|
||||
|
||||
|
||||
e.preventDefault();
|
||||
},
|
||||
[element.id, element.position.x, element.position.y, onSelect, isSelected, selectedElements, allElements, onMultiDragStart],
|
||||
[
|
||||
element.id,
|
||||
element.position.x,
|
||||
element.position.y,
|
||||
onSelect,
|
||||
isSelected,
|
||||
selectedElements,
|
||||
allElements,
|
||||
onMultiDragStart,
|
||||
],
|
||||
);
|
||||
|
||||
// 리사이즈 핸들 마우스다운
|
||||
|
|
@ -285,7 +292,7 @@ export function CanvasElement({
|
|||
const snappedY = Math.round(rawY / subGridSize) * subGridSize;
|
||||
|
||||
setTempPosition({ x: snappedX, y: snappedY });
|
||||
|
||||
|
||||
// 🔥 다중 드래그 중 - 다른 위젯들의 위치 업데이트
|
||||
if (selectedElements.length > 1 && selectedElements.includes(element.id) && onMultiDragMove) {
|
||||
onMultiDragMove(element, { x: snappedX, y: snappedY });
|
||||
|
|
@ -382,32 +389,37 @@ export function CanvasElement({
|
|||
.map((id) => {
|
||||
const targetElement = allElements.find((el) => el.id === id);
|
||||
if (!targetElement) return null;
|
||||
|
||||
|
||||
// 현재 요소와의 상대적 위치 유지
|
||||
const relativeX = targetElement.position.x - dragStart.elementX;
|
||||
const relativeY = targetElement.position.y - dragStart.elementY;
|
||||
|
||||
|
||||
const newPosition: Position = {
|
||||
x: Math.max(0, Math.min(canvasWidth - targetElement.size.width, finalX + relativeX)),
|
||||
y: Math.max(0, finalY + relativeY),
|
||||
};
|
||||
|
||||
if (targetElement.position.z !== undefined) {
|
||||
newPosition.z = targetElement.position.z;
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
updates: {
|
||||
position: {
|
||||
x: Math.max(0, Math.min(canvasWidth - targetElement.size.width, finalX + relativeX)),
|
||||
y: Math.max(0, finalY + relativeY),
|
||||
z: targetElement.position.z,
|
||||
},
|
||||
position: newPosition,
|
||||
},
|
||||
};
|
||||
})
|
||||
.filter((update): update is { id: string; updates: Partial<DashboardElement> } => update !== null);
|
||||
|
||||
.filter((update): update is { id: string; updates: { position: Position } } => update !== null);
|
||||
|
||||
if (updates.length > 0) {
|
||||
console.log("🔥 다중 선택 요소 함께 이동:", updates);
|
||||
onUpdateMultiple(updates);
|
||||
onUpdateMultiple(updates as { id: string; updates: Partial<DashboardElement> }[]);
|
||||
}
|
||||
}
|
||||
|
||||
setTempPosition(null);
|
||||
|
||||
|
||||
// 🔥 다중 드래그 종료
|
||||
if (onMultiDragEnd) {
|
||||
onMultiDragEnd();
|
||||
|
|
@ -583,11 +595,15 @@ export function CanvasElement({
|
|||
|
||||
// 드래그/리사이즈 중일 때는 임시 위치/크기 사용, 아니면 실제 값 사용
|
||||
// 🔥 다중 드래그 중이면 multiDragOffset 적용 (단, 드래그 중인 위젯은 tempPosition 우선)
|
||||
const displayPosition = tempPosition || (multiDragOffset && !isDragging ? {
|
||||
x: element.position.x + multiDragOffset.x,
|
||||
y: element.position.y + multiDragOffset.y,
|
||||
z: element.position.z,
|
||||
} : element.position);
|
||||
const displayPosition: Position =
|
||||
tempPosition ||
|
||||
(multiDragOffset && !isDragging
|
||||
? {
|
||||
x: element.position.x + multiDragOffset.x,
|
||||
y: element.position.y + multiDragOffset.y,
|
||||
...(element.position.z !== undefined && { z: element.position.z }),
|
||||
}
|
||||
: element.position);
|
||||
const displaySize = tempSize || element.size;
|
||||
|
||||
return (
|
||||
|
|
@ -609,18 +625,6 @@ export function CanvasElement({
|
|||
<div className="flex cursor-move items-center justify-between p-3">
|
||||
<span className="text-sm font-bold text-gray-800">{element.customTitle || element.title}</span>
|
||||
<div className="flex gap-1">
|
||||
{/* 설정 버튼 (기사관리 위젯만 자체 설정 UI 사용) */}
|
||||
{onConfigure && !(element.type === "widget" && element.subtype === "driver-management") && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-gray-400"
|
||||
onClick={() => onConfigure(element)}
|
||||
title="설정"
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
{/* 삭제 버튼 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import React, { useState, useRef, useCallback } from "react";
|
|||
import { useRouter } from "next/navigation";
|
||||
import { DashboardCanvas } from "./DashboardCanvas";
|
||||
import { DashboardTopMenu } from "./DashboardTopMenu";
|
||||
import { ElementConfigModal } from "./ElementConfigModal";
|
||||
import { ElementConfigSidebar } from "./ElementConfigSidebar";
|
||||
import { ListWidgetConfigModal } from "./widgets/ListWidgetConfigModal";
|
||||
import { YardWidgetConfigModal } from "./widgets/YardWidgetConfigModal";
|
||||
import { DashboardSaveModal } from "./DashboardSaveModal";
|
||||
|
|
@ -44,9 +44,8 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
const { refreshMenus } = useMenu();
|
||||
const [elements, setElements] = useState<DashboardElement[]>([]);
|
||||
const [selectedElement, setSelectedElement] = useState<string | null>(null);
|
||||
const [selectedElements, setSelectedElements] = useState<string[]>([]); // 🔥 다중 선택
|
||||
const [selectedElements, setSelectedElements] = useState<string[]>([]); // 다중 선택
|
||||
const [elementCounter, setElementCounter] = useState(0);
|
||||
const [configModalElement, setConfigModalElement] = useState<DashboardElement | null>(null);
|
||||
const [dashboardId, setDashboardId] = useState<string | null>(initialDashboardId || null);
|
||||
const [dashboardTitle, setDashboardTitle] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
|
@ -59,6 +58,10 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
const [successModalOpen, setSuccessModalOpen] = useState(false);
|
||||
const [clearConfirmOpen, setClearConfirmOpen] = useState(false);
|
||||
|
||||
// 사이드바 상태
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const [sidebarElement, setSidebarElement] = useState<DashboardElement | null>(null);
|
||||
|
||||
// 클립보드 (복사/붙여넣기용)
|
||||
const [clipboard, setClipboard] = useState<DashboardElement | null>(null);
|
||||
|
||||
|
|
@ -336,7 +339,7 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
onDelete: handleDeleteSelected,
|
||||
onCopy: handleCopyElement,
|
||||
onPaste: handlePasteElement,
|
||||
enabled: !saveModalOpen && !successModalOpen && !clearConfirmOpen,
|
||||
enabled: !saveModalOpen && !successModalOpen && !clearConfirmOpen && !sidebarOpen,
|
||||
});
|
||||
|
||||
// 전체 삭제 확인 모달 열기
|
||||
|
|
@ -352,32 +355,32 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
setClearConfirmOpen(false);
|
||||
}, []);
|
||||
|
||||
// 요소 설정 모달 열기
|
||||
const openConfigModal = useCallback((element: DashboardElement) => {
|
||||
setConfigModalElement(element);
|
||||
}, []);
|
||||
|
||||
// 요소 설정 모달 닫기
|
||||
const closeConfigModal = useCallback(() => {
|
||||
setConfigModalElement(null);
|
||||
}, []);
|
||||
|
||||
// 요소 설정 저장
|
||||
const saveElementConfig = useCallback(
|
||||
(updatedElement: DashboardElement) => {
|
||||
updateElement(updatedElement.id, updatedElement);
|
||||
},
|
||||
[updateElement],
|
||||
);
|
||||
|
||||
// 리스트 위젯 설정 저장 (Partial 업데이트)
|
||||
const saveListWidgetConfig = useCallback(
|
||||
// 리스트/야드 위젯 설정 저장 (Partial 업데이트)
|
||||
const saveWidgetConfig = useCallback(
|
||||
(updates: Partial<DashboardElement>) => {
|
||||
if (configModalElement) {
|
||||
updateElement(configModalElement.id, updates);
|
||||
if (sidebarElement) {
|
||||
updateElement(sidebarElement.id, updates);
|
||||
}
|
||||
},
|
||||
[configModalElement, updateElement],
|
||||
[sidebarElement, updateElement],
|
||||
);
|
||||
|
||||
// 사이드바 닫기
|
||||
const handleCloseSidebar = useCallback(() => {
|
||||
setSidebarOpen(false);
|
||||
setSidebarElement(null);
|
||||
setSelectedElement(null);
|
||||
}, []);
|
||||
|
||||
// 사이드바 적용
|
||||
const handleApplySidebar = useCallback(
|
||||
(updatedElement: DashboardElement) => {
|
||||
updateElement(updatedElement.id, updatedElement);
|
||||
// 사이드바는 열린 채로 유지하여 연속 수정 가능
|
||||
// 단, sidebarElement도 업데이트해서 최신 상태 반영
|
||||
setSidebarElement(updatedElement);
|
||||
},
|
||||
[updateElement],
|
||||
);
|
||||
|
||||
// 레이아웃 저장
|
||||
|
|
@ -560,14 +563,28 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
onRemoveElement={removeElement}
|
||||
onSelectElement={(id) => {
|
||||
setSelectedElement(id);
|
||||
setSelectedElements([]); // 단일 선택 시 다중 선택 해제
|
||||
setSelectedElements([]);
|
||||
|
||||
// 선택된 요소 찾아서 사이드바 열기
|
||||
const element = elements.find((el) => el.id === id);
|
||||
if (element) {
|
||||
// 리스트/야드 위젯은 별도 모달 사용
|
||||
if (element.subtype === "list" || element.subtype === "yard-management-3d") {
|
||||
setSidebarElement(element);
|
||||
setSidebarOpen(false);
|
||||
} else {
|
||||
setSidebarElement(element);
|
||||
setSidebarOpen(true);
|
||||
}
|
||||
}
|
||||
}}
|
||||
onSelectMultiple={(ids) => {
|
||||
console.log("🎯 DashboardDesigner - onSelectMultiple 호출:", ids);
|
||||
setSelectedElements(ids);
|
||||
setSelectedElement(null); // 다중 선택 시 단일 선택 해제
|
||||
setSelectedElement(null);
|
||||
setSidebarOpen(false);
|
||||
setSidebarElement(null);
|
||||
}}
|
||||
onConfigureElement={openConfigModal}
|
||||
onConfigureElement={() => {}}
|
||||
backgroundColor={canvasBackgroundColor}
|
||||
canvasWidth={canvasConfig.width}
|
||||
canvasHeight={dynamicCanvasHeight}
|
||||
|
|
@ -575,32 +592,38 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* 요소 설정 모달 */}
|
||||
{configModalElement && (
|
||||
<>
|
||||
{configModalElement.type === "widget" && configModalElement.subtype === "list" ? (
|
||||
<ListWidgetConfigModal
|
||||
element={configModalElement}
|
||||
isOpen={true}
|
||||
onClose={closeConfigModal}
|
||||
onSave={saveListWidgetConfig}
|
||||
/>
|
||||
) : configModalElement.type === "widget" && configModalElement.subtype === "yard-management-3d" ? (
|
||||
<YardWidgetConfigModal
|
||||
element={configModalElement}
|
||||
isOpen={true}
|
||||
onClose={closeConfigModal}
|
||||
onSave={saveListWidgetConfig}
|
||||
/>
|
||||
) : (
|
||||
<ElementConfigModal
|
||||
element={configModalElement}
|
||||
isOpen={true}
|
||||
onClose={closeConfigModal}
|
||||
onSave={saveElementConfig}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{/* 요소 설정 사이드바 */}
|
||||
<ElementConfigSidebar
|
||||
element={sidebarElement}
|
||||
isOpen={sidebarOpen}
|
||||
onClose={handleCloseSidebar}
|
||||
onApply={handleApplySidebar}
|
||||
/>
|
||||
|
||||
{/* 리스트 위젯 전용 모달 */}
|
||||
{sidebarElement && sidebarElement.subtype === "list" && (
|
||||
<ListWidgetConfigModal
|
||||
element={sidebarElement}
|
||||
isOpen={true}
|
||||
onClose={() => {
|
||||
setSidebarElement(null);
|
||||
setSelectedElement(null);
|
||||
}}
|
||||
onSave={saveWidgetConfig}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 야드 위젯 전용 모달 */}
|
||||
{sidebarElement && sidebarElement.subtype === "yard-management-3d" && (
|
||||
<YardWidgetConfigModal
|
||||
element={sidebarElement}
|
||||
isOpen={true}
|
||||
onClose={() => {
|
||||
setSidebarElement(null);
|
||||
setSelectedElement(null);
|
||||
}}
|
||||
onSave={saveWidgetConfig}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 저장 모달 */}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,339 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { DashboardElement, ChartDataSource, ChartConfig, QueryResult } from "./types";
|
||||
import { QueryEditor } from "./QueryEditor";
|
||||
import { ChartConfigPanel } from "./ChartConfigPanel";
|
||||
import { VehicleMapConfigPanel } from "./VehicleMapConfigPanel";
|
||||
import { DataSourceSelector } from "./data-sources/DataSourceSelector";
|
||||
import { DatabaseConfig } from "./data-sources/DatabaseConfig";
|
||||
import { ApiConfig } from "./data-sources/ApiConfig";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { X, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ElementConfigSidebarProps {
|
||||
element: DashboardElement | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onApply: (element: DashboardElement) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 요소 설정 사이드바 컴포넌트
|
||||
* - 왼쪽에서 슬라이드 인/아웃
|
||||
* - 캔버스 위에 오버레이
|
||||
* - "적용" 버튼으로 명시적 저장
|
||||
*/
|
||||
export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: ElementConfigSidebarProps) {
|
||||
const [dataSource, setDataSource] = useState<ChartDataSource>({
|
||||
type: "database",
|
||||
connectionType: "current",
|
||||
refreshInterval: 0,
|
||||
});
|
||||
const [chartConfig, setChartConfig] = useState<ChartConfig>({});
|
||||
const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
|
||||
const [currentStep, setCurrentStep] = useState<1 | 2>(1);
|
||||
const [customTitle, setCustomTitle] = useState<string>("");
|
||||
const [showHeader, setShowHeader] = useState<boolean>(true);
|
||||
|
||||
// 사이드바가 열릴 때 초기화
|
||||
useEffect(() => {
|
||||
if (isOpen && element) {
|
||||
setDataSource(element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 });
|
||||
setChartConfig(element.chartConfig || {});
|
||||
setQueryResult(null);
|
||||
setCurrentStep(1);
|
||||
setCustomTitle(element.customTitle || "");
|
||||
setShowHeader(element.showHeader !== false);
|
||||
}
|
||||
}, [isOpen, element]);
|
||||
|
||||
// Esc 키로 사이드바 닫기
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleEsc = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleEsc);
|
||||
return () => window.removeEventListener("keydown", handleEsc);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// 데이터 소스 타입 변경
|
||||
const handleDataSourceTypeChange = useCallback((type: "database" | "api") => {
|
||||
if (type === "database") {
|
||||
setDataSource({
|
||||
type: "database",
|
||||
connectionType: "current",
|
||||
refreshInterval: 0,
|
||||
});
|
||||
} else {
|
||||
setDataSource({
|
||||
type: "api",
|
||||
method: "GET",
|
||||
refreshInterval: 0,
|
||||
});
|
||||
}
|
||||
|
||||
setQueryResult(null);
|
||||
setChartConfig({});
|
||||
}, []);
|
||||
|
||||
// 데이터 소스 업데이트
|
||||
const handleDataSourceUpdate = useCallback((updates: Partial<ChartDataSource>) => {
|
||||
setDataSource((prev) => ({ ...prev, ...updates }));
|
||||
}, []);
|
||||
|
||||
// 차트 설정 변경 처리
|
||||
const handleChartConfigChange = useCallback((newConfig: ChartConfig) => {
|
||||
setChartConfig(newConfig);
|
||||
}, []);
|
||||
|
||||
// 쿼리 테스트 결과 처리
|
||||
const handleQueryTest = useCallback((result: QueryResult) => {
|
||||
setQueryResult(result);
|
||||
setChartConfig({});
|
||||
}, []);
|
||||
|
||||
// 다음 단계로 이동
|
||||
const handleNext = useCallback(() => {
|
||||
if (currentStep === 1) {
|
||||
setCurrentStep(2);
|
||||
}
|
||||
}, [currentStep]);
|
||||
|
||||
// 이전 단계로 이동
|
||||
const handlePrev = useCallback(() => {
|
||||
if (currentStep > 1) {
|
||||
setCurrentStep((prev) => (prev - 1) as 1 | 2);
|
||||
}
|
||||
}, [currentStep]);
|
||||
|
||||
// 취소 처리
|
||||
const handleCancel = useCallback(() => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
// 적용 처리
|
||||
const handleApply = useCallback(() => {
|
||||
if (!element) return;
|
||||
|
||||
const updatedElement: DashboardElement = {
|
||||
...element,
|
||||
dataSource,
|
||||
chartConfig,
|
||||
customTitle: customTitle.trim() || undefined,
|
||||
showHeader,
|
||||
};
|
||||
|
||||
onApply(updatedElement);
|
||||
// 사이드바는 열린 채로 유지 (연속 수정 가능)
|
||||
}, [element, dataSource, chartConfig, customTitle, showHeader, onApply]);
|
||||
|
||||
// 요소가 없으면 렌더링하지 않음
|
||||
if (!element) return null;
|
||||
|
||||
// 차트 설정이 필요 없는 위젯 (쿼리/API만 필요)
|
||||
const isSimpleWidget =
|
||||
element.subtype === "todo" ||
|
||||
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" ||
|
||||
element.subtype === "delivery-status" ||
|
||||
element.subtype === "delivery-status-summary" ||
|
||||
element.subtype === "delivery-today-stats" ||
|
||||
element.subtype === "cargo-list" ||
|
||||
element.subtype === "customer-issues" ||
|
||||
element.subtype === "driver-management" ||
|
||||
element.subtype === "work-history" ||
|
||||
element.subtype === "transport-stats";
|
||||
|
||||
// 자체 기능 위젯 (DB 연결 불필요, 헤더 설정만 가능)
|
||||
const isSelfContainedWidget =
|
||||
element.subtype === "weather" || element.subtype === "exchange" || element.subtype === "calculator";
|
||||
|
||||
// 지도 위젯 (위도/경도 매핑 필요)
|
||||
const isMapWidget = element.subtype === "vehicle-map" || element.subtype === "map-summary";
|
||||
|
||||
// 헤더 전용 위젯
|
||||
const isHeaderOnlyWidget =
|
||||
element.type === "widget" &&
|
||||
(element.subtype === "clock" || element.subtype === "calendar" || isSelfContainedWidget);
|
||||
|
||||
// 저장 가능 여부 확인
|
||||
const isPieChart = element.subtype === "pie" || element.subtype === "donut";
|
||||
const isApiSource = dataSource.type === "api";
|
||||
|
||||
const hasYAxis =
|
||||
chartConfig.yAxis &&
|
||||
(typeof chartConfig.yAxis === "string" || (Array.isArray(chartConfig.yAxis) && chartConfig.yAxis.length > 0));
|
||||
|
||||
const isTitleChanged = customTitle.trim() !== (element.customTitle || "");
|
||||
const isHeaderChanged = showHeader !== (element.showHeader !== false);
|
||||
|
||||
const canApply =
|
||||
isTitleChanged ||
|
||||
isHeaderChanged ||
|
||||
(isSimpleWidget
|
||||
? currentStep === 2 && queryResult && queryResult.rows.length > 0
|
||||
: isMapWidget
|
||||
? currentStep === 2 &&
|
||||
queryResult &&
|
||||
queryResult.rows.length > 0 &&
|
||||
chartConfig.latitudeColumn &&
|
||||
chartConfig.longitudeColumn
|
||||
: currentStep === 2 &&
|
||||
queryResult &&
|
||||
queryResult.rows.length > 0 &&
|
||||
chartConfig.xAxis &&
|
||||
(isPieChart || isApiSource ? (chartConfig.aggregation === "count" ? true : hasYAxis) : hasYAxis));
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"fixed top-14 left-0 z-[100] flex h-[calc(100vh-3.5rem)] w-96 flex-col bg-white shadow-2xl transition-transform duration-300 ease-in-out",
|
||||
isOpen ? "translate-x-0" : "translate-x-[-100%]",
|
||||
)}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between border-b p-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">{element.title} 설정</h3>
|
||||
<Button variant="ghost" size="icon" onClick={onClose} className="h-8 w-8">
|
||||
<X className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 본문: 스크롤 가능 영역 */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{/* 커스텀 제목 입력 */}
|
||||
<div className="mb-4">
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">위젯 제목</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customTitle}
|
||||
onChange={(e) => setCustomTitle(e.target.value)}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
placeholder="비워두면 자동 생성"
|
||||
className="focus:border-primary focus:ring-primary w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-1 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 헤더 표시 옵션 */}
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="showHeader"
|
||||
checked={showHeader}
|
||||
onChange={(e) => setShowHeader(e.target.checked)}
|
||||
className="text-primary focus:ring-primary h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
<label htmlFor="showHeader" className="text-sm font-medium text-gray-700">
|
||||
위젯 헤더 표시
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 진행 상황 표시 */}
|
||||
{!isSimpleWidget && !isHeaderOnlyWidget && (
|
||||
<div className="mb-4 rounded-md bg-gray-50 p-3">
|
||||
<div className="text-sm font-medium text-gray-700">
|
||||
단계 {currentStep} / 2: {currentStep === 1 ? "데이터 소스 선택" : "데이터 설정"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 단계별 내용 */}
|
||||
{!isHeaderOnlyWidget && (
|
||||
<div className="space-y-4">
|
||||
{currentStep === 1 && (
|
||||
<DataSourceSelector dataSource={dataSource} onTypeChange={handleDataSourceTypeChange} />
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<div className="space-y-4">
|
||||
{/* 데이터 설정 */}
|
||||
{dataSource.type === "database" ? (
|
||||
<>
|
||||
<DatabaseConfig dataSource={dataSource} onChange={handleDataSourceUpdate} />
|
||||
<QueryEditor
|
||||
dataSource={dataSource}
|
||||
onDataSourceChange={handleDataSourceUpdate}
|
||||
onQueryTest={handleQueryTest}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ApiConfig dataSource={dataSource} onChange={handleDataSourceUpdate} onTestResult={handleQueryTest} />
|
||||
)}
|
||||
|
||||
{/* 차트/지도 설정 */}
|
||||
{!isSimpleWidget && queryResult && queryResult.rows.length > 0 && (
|
||||
<div className="mt-4">
|
||||
{isMapWidget ? (
|
||||
<VehicleMapConfigPanel
|
||||
config={chartConfig}
|
||||
queryResult={queryResult}
|
||||
onConfigChange={handleChartConfigChange}
|
||||
/>
|
||||
) : (
|
||||
<ChartConfigPanel
|
||||
config={chartConfig}
|
||||
queryResult={queryResult}
|
||||
onConfigChange={handleChartConfigChange}
|
||||
chartType={element.subtype}
|
||||
dataSourceType={dataSource.type}
|
||||
query={dataSource.query}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 데이터 로드 상태 */}
|
||||
{queryResult && (
|
||||
<div className="mt-4">
|
||||
<Badge variant="default">{queryResult.rows.length}개 데이터 로드됨</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 푸터: 단계 이동 및 적용 버튼 */}
|
||||
<div className="flex items-center justify-between border-t p-4">
|
||||
<div className="flex gap-2">
|
||||
{!isSimpleWidget && !isHeaderOnlyWidget && currentStep > 1 && (
|
||||
<Button variant="outline" size="sm" onClick={handlePrev}>
|
||||
<ChevronLeft className="mr-1 h-4 w-4" />
|
||||
이전
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={handleCancel}>
|
||||
취소
|
||||
</Button>
|
||||
{isHeaderOnlyWidget || currentStep === 1 ? (
|
||||
<Button size="sm" onClick={isHeaderOnlyWidget ? handleApply : handleNext}>
|
||||
{isHeaderOnlyWidget ? "적용" : "다음"}
|
||||
{!isHeaderOnlyWidget && <ChevronRight className="ml-1 h-4 w-4" />}
|
||||
</Button>
|
||||
) : (
|
||||
<Button size="sm" onClick={handleApply} disabled={!canApply}>
|
||||
적용
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ export type ElementSubtype =
|
|||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
z?: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
|
|
|
|||
Loading…
Reference in New Issue