From bdf9bd0075edd08fc929084a2ffdfb38d49a83d0 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 22 Oct 2025 10:45:10 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20->=20=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=EB=B0=94=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/CanvasElement.tsx | 82 +++-- .../admin/dashboard/DashboardDesigner.tsx | 137 ++++--- .../admin/dashboard/ElementConfigSidebar.tsx | 339 ++++++++++++++++++ frontend/components/admin/dashboard/types.ts | 1 + 4 files changed, 463 insertions(+), 96 deletions(-) create mode 100644 frontend/components/admin/dashboard/ElementConfigSidebar.tsx diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 7bd4165e..6d0cc07f 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -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) => void; onUpdateMultiple?: (updates: { id: string; updates: Partial }[]) => void; // 🔥 다중 업데이트 - onMultiDragStart?: (draggedId: string, otherOffsets: Record) => void; // 🔥 다중 드래그 시작 - onMultiDragMove?: (draggedElement: DashboardElement, tempPosition: { x: number; y: number }) => void; // 🔥 다중 드래그 중 - onMultiDragEnd?: () => void; // 🔥 다중 드래그 종료 + onMultiDragStart?: (draggedId: string, otherOffsets: Record) => 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 = {}; @@ -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 } => 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 }[]); } } 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({
{element.customTitle || element.title}
- {/* 설정 버튼 (기사관리 위젯만 자체 설정 UI 사용) */} - {onConfigure && !(element.type === "widget" && element.subtype === "driver-management") && ( - - )} {/* 삭제 버튼 */}
- {/* 요소 설정 모달 */} - {configModalElement && ( - <> - {configModalElement.type === "widget" && configModalElement.subtype === "list" ? ( - - ) : configModalElement.type === "widget" && configModalElement.subtype === "yard-management-3d" ? ( - - ) : ( - - )} - + {/* 요소 설정 사이드바 */} + + + {/* 리스트 위젯 전용 모달 */} + {sidebarElement && sidebarElement.subtype === "list" && ( + { + setSidebarElement(null); + setSelectedElement(null); + }} + onSave={saveWidgetConfig} + /> + )} + + {/* 야드 위젯 전용 모달 */} + {sidebarElement && sidebarElement.subtype === "yard-management-3d" && ( + { + setSidebarElement(null); + setSelectedElement(null); + }} + onSave={saveWidgetConfig} + /> )} {/* 저장 모달 */} diff --git a/frontend/components/admin/dashboard/ElementConfigSidebar.tsx b/frontend/components/admin/dashboard/ElementConfigSidebar.tsx new file mode 100644 index 00000000..06d2e4e4 --- /dev/null +++ b/frontend/components/admin/dashboard/ElementConfigSidebar.tsx @@ -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({ + type: "database", + connectionType: "current", + refreshInterval: 0, + }); + const [chartConfig, setChartConfig] = useState({}); + const [queryResult, setQueryResult] = useState(null); + const [currentStep, setCurrentStep] = useState<1 | 2>(1); + const [customTitle, setCustomTitle] = useState(""); + const [showHeader, setShowHeader] = useState(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) => { + 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 ( +
+ {/* 헤더 */} +
+

{element.title} 설정

+ +
+ + {/* 본문: 스크롤 가능 영역 */} +
+ {/* 커스텀 제목 입력 */} +
+ + 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" + /> +
+ + {/* 헤더 표시 옵션 */} +
+ setShowHeader(e.target.checked)} + className="text-primary focus:ring-primary h-4 w-4 rounded border-gray-300" + /> + +
+ + {/* 진행 상황 표시 */} + {!isSimpleWidget && !isHeaderOnlyWidget && ( +
+
+ 단계 {currentStep} / 2: {currentStep === 1 ? "데이터 소스 선택" : "데이터 설정"} +
+
+ )} + + {/* 단계별 내용 */} + {!isHeaderOnlyWidget && ( +
+ {currentStep === 1 && ( + + )} + + {currentStep === 2 && ( +
+ {/* 데이터 설정 */} + {dataSource.type === "database" ? ( + <> + + + + ) : ( + + )} + + {/* 차트/지도 설정 */} + {!isSimpleWidget && queryResult && queryResult.rows.length > 0 && ( +
+ {isMapWidget ? ( + + ) : ( + + )} +
+ )} +
+ )} +
+ )} + + {/* 데이터 로드 상태 */} + {queryResult && ( +
+ {queryResult.rows.length}개 데이터 로드됨 +
+ )} +
+ + {/* 푸터: 단계 이동 및 적용 버튼 */} +
+
+ {!isSimpleWidget && !isHeaderOnlyWidget && currentStep > 1 && ( + + )} +
+ +
+ + {isHeaderOnlyWidget || currentStep === 1 ? ( + + ) : ( + + )} +
+
+
+ ); +} diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index 139f42c0..6ce41b6f 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -43,6 +43,7 @@ export type ElementSubtype = export interface Position { x: number; y: number; + z?: number; } export interface Size { From 2433658e01f2326606cdf6634e9d6693e185fe4f Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 22 Oct 2025 10:58:21 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94?= =?UTF-8?q?=EC=97=90=20=ED=83=AD=20=EB=B0=A9=EC=8B=9D=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/ElementConfigSidebar.tsx | 240 ++++++++---------- .../dashboard/data-sources/DatabaseConfig.tsx | 12 +- 2 files changed, 114 insertions(+), 138 deletions(-) diff --git a/frontend/components/admin/dashboard/ElementConfigSidebar.tsx b/frontend/components/admin/dashboard/ElementConfigSidebar.tsx index 06d2e4e4..0523b443 100644 --- a/frontend/components/admin/dashboard/ElementConfigSidebar.tsx +++ b/frontend/components/admin/dashboard/ElementConfigSidebar.tsx @@ -5,13 +5,13 @@ import { DashboardElement, ChartDataSource, ChartConfig, QueryResult } from "./t 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 { X } from "lucide-react"; import { cn } from "@/lib/utils"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; interface ElementConfigSidebarProps { element: DashboardElement | null; @@ -34,7 +34,6 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem }); const [chartConfig, setChartConfig] = useState({}); const [queryResult, setQueryResult] = useState(null); - const [currentStep, setCurrentStep] = useState<1 | 2>(1); const [customTitle, setCustomTitle] = useState(""); const [showHeader, setShowHeader] = useState(true); @@ -44,7 +43,6 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem setDataSource(element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 }); setChartConfig(element.chartConfig || {}); setQueryResult(null); - setCurrentStep(1); setCustomTitle(element.customTitle || ""); setShowHeader(element.showHeader !== false); } @@ -100,25 +98,6 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem 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; @@ -184,15 +163,10 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem isTitleChanged || isHeaderChanged || (isSimpleWidget - ? currentStep === 2 && queryResult && queryResult.rows.length > 0 + ? queryResult && queryResult.rows.length > 0 : isMapWidget - ? currentStep === 2 && - queryResult && - queryResult.rows.length > 0 && - chartConfig.latitudeColumn && - chartConfig.longitudeColumn - : currentStep === 2 && - queryResult && + ? queryResult && queryResult.rows.length > 0 && chartConfig.latitudeColumn && chartConfig.longitudeColumn + : queryResult && queryResult.rows.length > 0 && chartConfig.xAxis && (isPieChart || isApiSource ? (chartConfig.aggregation === "count" ? true : hasYAxis) : hasYAxis)); @@ -214,89 +188,105 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem {/* 본문: 스크롤 가능 영역 */}
- {/* 커스텀 제목 입력 */} -
- - 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" - /> -
- - {/* 헤더 표시 옵션 */} -
- setShowHeader(e.target.checked)} - className="text-primary focus:ring-primary h-4 w-4 rounded border-gray-300" - /> - -
- - {/* 진행 상황 표시 */} - {!isSimpleWidget && !isHeaderOnlyWidget && ( -
-
- 단계 {currentStep} / 2: {currentStep === 1 ? "데이터 소스 선택" : "데이터 설정"} -
+ {/* 기본 설정 */} +
+ {/* 커스텀 제목 입력 */} +
+ + 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" + />
- )} - {/* 단계별 내용 */} + {/* 헤더 표시 옵션 */} +
+ setShowHeader(e.target.checked)} + className="text-primary focus:ring-primary h-4 w-4 rounded border-gray-300" + /> + +
+
+ + {/* 헤더 전용 위젯이 아닐 때만 데이터 소스 탭 표시 */} {!isHeaderOnlyWidget && ( -
- {currentStep === 1 && ( - - )} + handleDataSourceTypeChange(value as "database" | "api")} + className="w-full" + > + + 데이터베이스 + REST API + - {currentStep === 2 && ( -
- {/* 데이터 설정 */} - {dataSource.type === "database" ? ( - <> - - + + + + {/* 차트/지도 설정 */} + {!isSimpleWidget && queryResult && queryResult.rows.length > 0 && ( +
+ {isMapWidget ? ( + - - ) : ( - - )} + ) : ( + + )} +
+ )} + - {/* 차트/지도 설정 */} - {!isSimpleWidget && queryResult && queryResult.rows.length > 0 && ( -
- {isMapWidget ? ( - - ) : ( - - )} -
- )} -
- )} -
+ + + + {/* 차트/지도 설정 */} + {!isSimpleWidget && queryResult && queryResult.rows.length > 0 && ( +
+ {isMapWidget ? ( + + ) : ( + + )} +
+ )} +
+ )} {/* 데이터 로드 상태 */} @@ -307,32 +297,14 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem )}
- {/* 푸터: 단계 이동 및 적용 버튼 */} -
-
- {!isSimpleWidget && !isHeaderOnlyWidget && currentStep > 1 && ( - - )} -
- -
- - {isHeaderOnlyWidget || currentStep === 1 ? ( - - ) : ( - - )} -
+ {/* 푸터: 적용 버튼 */} +
+ +
); diff --git a/frontend/components/admin/dashboard/data-sources/DatabaseConfig.tsx b/frontend/components/admin/dashboard/data-sources/DatabaseConfig.tsx index ff190184..ccd86113 100644 --- a/frontend/components/admin/dashboard/data-sources/DatabaseConfig.tsx +++ b/frontend/components/admin/dashboard/data-sources/DatabaseConfig.tsx @@ -68,8 +68,8 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) { >
-
현재 데이터베이스
-
애플리케이션 기본 DB
+
현재 DB
+
기본 DB
@@ -82,7 +82,7 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) { >
-
외부 데이터베이스
+
외부 DB
등록된 외부 커넥션
@@ -183,7 +183,11 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) { {(dataSource.connectionType === "current" || (dataSource.connectionType === "external" && dataSource.externalConnectionId)) && (
-
✅ 데이터베이스가 선택되었습니다. 아래에서 SQL 쿼리를 작성하세요.
+
+ 데이터베이스가 선택되었습니다. +
+ 아래에서 SQL 쿼리를 작성하세요. +
)}
From 8a421cfced3533de0cf13229053b0ebc38247269 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 22 Oct 2025 11:08:36 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=ED=94=8C=EB=A0=88=EC=9D=B4=EC=8A=A4?= =?UTF-8?q?=ED=99=80=EB=8D=94=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/admin/dashboard/ElementConfigSidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/admin/dashboard/ElementConfigSidebar.tsx b/frontend/components/admin/dashboard/ElementConfigSidebar.tsx index 0523b443..d83749f5 100644 --- a/frontend/components/admin/dashboard/ElementConfigSidebar.tsx +++ b/frontend/components/admin/dashboard/ElementConfigSidebar.tsx @@ -198,7 +198,7 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem value={customTitle} onChange={(e) => setCustomTitle(e.target.value)} onKeyDown={(e) => e.stopPropagation()} - placeholder="비워두면 자동 생성" + 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" /> From 85987af65e0a9636b6365227c54db60da5400745 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 22 Oct 2025 12:48:17 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=8B=A4=EB=93=AC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/ChartConfigPanel.tsx | 123 +++++---- .../admin/dashboard/ElementConfigSidebar.tsx | 246 ++++++++++-------- .../admin/dashboard/QueryEditor.tsx | 156 ++++++----- .../dashboard/data-sources/ApiConfig.tsx | 121 +++++---- .../dashboard/data-sources/DatabaseConfig.tsx | 138 +++++----- 5 files changed, 414 insertions(+), 370 deletions(-) diff --git a/frontend/components/admin/dashboard/ChartConfigPanel.tsx b/frontend/components/admin/dashboard/ChartConfigPanel.tsx index 59f0822a..04dd2d0e 100644 --- a/frontend/components/admin/dashboard/ChartConfigPanel.tsx +++ b/frontend/components/admin/dashboard/ChartConfigPanel.tsx @@ -5,7 +5,6 @@ import { ChartConfig, QueryResult } from "./types"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Label } from "@/components/ui/label"; -import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Checkbox } from "@/components/ui/checkbox"; @@ -97,32 +96,17 @@ export function ChartConfigPanel({ // (SELECT에 없어도 WHERE 절에 사용 가능) setDateColumns(schema.dateColumns); }) - .catch((error) => { - // console.error("❌ 테이블 스키마 조회 실패:", error); + .catch(() => { // 실패 시 빈 배열 (날짜 필터 비활성화) setDateColumns([]); }); }, [query, queryResult, dataSourceType]); return ( -
+
{/* 데이터 필드 매핑 */} {queryResult && ( <> - {/* API 응답 미리보기 */} - {queryResult.rows && queryResult.rows.length > 0 && ( - -
- -

API 응답 데이터 미리보기

-
-
-
총 {queryResult.totalRows}개 데이터 중 첫 번째 행:
-
{JSON.stringify(sampleData, null, 2)}
-
-
- )} - {/* 복잡한 타입 경고 */} {complexColumns.length > 0 && ( @@ -150,26 +134,27 @@ export function ChartConfigPanel({ )} {/* 차트 제목 */} -
- +
+ updateConfig({ title: e.target.value })} placeholder="차트 제목을 입력하세요" + className="h-8 text-xs" />
{/* X축 설정 */} -
-