From 2959f66e0c6b6aa78191ee85108d763a126f5019 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 29 Oct 2025 18:30:50 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=EC=95=88=EC=93=B0=EB=8A=94=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/DashboardToolbar.tsx | 110 --- .../admin/dashboard/ElementConfigModal.tsx | 427 ----------- frontend/components/admin/dashboard/index.ts | 16 +- .../widgets/ListWidgetConfigModal.tsx | 326 --------- .../widgets/TodoWidgetConfigModal.tsx | 664 ------------------ .../widgets/YardWidgetConfigModal.tsx | 79 --- 6 files changed, 7 insertions(+), 1615 deletions(-) delete mode 100644 frontend/components/admin/dashboard/DashboardToolbar.tsx delete mode 100644 frontend/components/admin/dashboard/ElementConfigModal.tsx delete mode 100644 frontend/components/admin/dashboard/widgets/ListWidgetConfigModal.tsx delete mode 100644 frontend/components/admin/dashboard/widgets/TodoWidgetConfigModal.tsx delete mode 100644 frontend/components/admin/dashboard/widgets/YardWidgetConfigModal.tsx diff --git a/frontend/components/admin/dashboard/DashboardToolbar.tsx b/frontend/components/admin/dashboard/DashboardToolbar.tsx deleted file mode 100644 index c9a9581d..00000000 --- a/frontend/components/admin/dashboard/DashboardToolbar.tsx +++ /dev/null @@ -1,110 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; - -interface DashboardToolbarProps { - onClearCanvas: () => void; - onSaveLayout: () => void; - canvasBackgroundColor: string; - onCanvasBackgroundColorChange: (color: string) => void; -} - -/** - * 대시보드 툴바 컴포넌트 - * - 전체 삭제, 레이아웃 저장 등 주요 액션 버튼 - */ -export function DashboardToolbar({ onClearCanvas, onSaveLayout, canvasBackgroundColor, onCanvasBackgroundColorChange }: DashboardToolbarProps) { - const [showColorPicker, setShowColorPicker] = useState(false); - return ( -
- - - - - {/* 캔버스 배경색 변경 버튼 */} -
- - - {/* 색상 선택 패널 */} - {showColorPicker && ( -
-
- onCanvasBackgroundColorChange(e.target.value)} - className="h-10 w-16 border border-border rounded cursor-pointer" - /> - onCanvasBackgroundColorChange(e.target.value)} - placeholder="#ffffff" - className="flex-1 px-2 py-1 text-sm border border-border rounded" - /> -
- - {/* 프리셋 색상 */} -
- {[ - '#ffffff', '#f9fafb', '#f3f4f6', '#e5e7eb', - '#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', - '#10b981', '#06b6d4', '#6366f1', '#84cc16', - ].map((color) => ( -
- - -
- )} -
-
- ); -} diff --git a/frontend/components/admin/dashboard/ElementConfigModal.tsx b/frontend/components/admin/dashboard/ElementConfigModal.tsx deleted file mode 100644 index 0b59b2ab..00000000 --- a/frontend/components/admin/dashboard/ElementConfigModal.tsx +++ /dev/null @@ -1,427 +0,0 @@ -"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 { MapTestConfigPanel } from "./MapTestConfigPanel"; -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, Save } from "lucide-react"; - -interface ElementConfigModalProps { - element: DashboardElement; - isOpen: boolean; - onClose: () => void; - onSave: (element: DashboardElement) => void; - onPreview?: (element: DashboardElement) => void; // 실시간 미리보기용 (저장 전) -} - -/** - * 요소 설정 모달 컴포넌트 (리팩토링) - * - 2단계 플로우: 데이터 소스 선택 → 데이터 설정 및 차트 설정 - * - 새로운 데이터 소스 컴포넌트 통합 - */ -export function ElementConfigModal({ element, isOpen, onClose, onSave, onPreview }: ElementConfigModalProps) { - const [dataSource, setDataSource] = useState( - element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 }, - ); - const [chartConfig, setChartConfig] = useState(element.chartConfig || {}); - const [queryResult, setQueryResult] = useState(null); - const [currentStep, setCurrentStep] = useState<1 | 2>(1); - const [customTitle, setCustomTitle] = useState(element.customTitle || ""); - const [showHeader, setShowHeader] = useState(element.showHeader !== false); - - // 차트 설정이 필요 없는 위젯 (쿼리/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" || // 커스텀 상태 카드 - // element.subtype === "list-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" || // 날씨 위젯 (외부 API) - element.subtype === "exchange" || // 환율 위젯 (외부 API) - element.subtype === "calculator"; // 계산기 위젯 (자체 기능) - - // 지도 위젯 (위도/경도 매핑 필요) - const isMapWidget = element.subtype === "vehicle-map" || element.subtype === "map-summary" || element.subtype === "map-test"; - - // 주석 - // 모달이 열릴 때 초기화 - useEffect(() => { - if (isOpen) { - const dataSourceToSet = element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 }; - setDataSource(dataSourceToSet); - setChartConfig(element.chartConfig || {}); - setQueryResult(null); - setCurrentStep(1); - setCustomTitle(element.customTitle || ""); - setShowHeader(element.showHeader !== false); // showHeader 초기화 - - // 쿼리가 이미 있으면 자동 실행 - if (dataSourceToSet.type === "database" && dataSourceToSet.query) { - console.log("🔄 기존 쿼리 자동 실행:", dataSourceToSet.query); - executeQueryAutomatically(dataSourceToSet); - } - } - }, [isOpen, element]); - - // 쿼리 자동 실행 함수 - const executeQueryAutomatically = async (dataSourceToExecute: ChartDataSource) => { - if (dataSourceToExecute.type !== "database" || !dataSourceToExecute.query) return; - - try { - const { queryApi } = await import("@/lib/api/query"); - const result = await queryApi.executeQuery({ - query: dataSourceToExecute.query, - connectionType: dataSourceToExecute.connectionType || "current", - externalConnectionId: dataSourceToExecute.externalConnectionId, - }); - - console.log("✅ 쿼리 자동 실행 완료:", result); - setQueryResult(result); - } catch (error) { - console.error("❌ 쿼리 자동 실행 실패:", error); - // 실패해도 모달은 열리도록 (사용자가 다시 실행 가능) - } - }; - - // 데이터 소스 타입 변경 - 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); - - // 🎯 실시간 미리보기: chartConfig 변경 시 즉시 부모에게 전달 - if (onPreview) { - onPreview({ - ...element, - chartConfig: newConfig, - dataSource: dataSource, - customTitle: customTitle, - showHeader: showHeader, - }); - } - }, [element, dataSource, customTitle, showHeader, onPreview]); - - // 쿼리 테스트 결과 처리 - const handleQueryTest = useCallback((result: QueryResult) => { - setQueryResult(result); - - // 쿼리가 변경되었으므로 차트 설정 초기화 (X/Y축 리셋) - // console.log("🔄 쿼리 변경 감지 - 차트 설정 초기화"); - 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 handleSave = useCallback(() => { - const updatedElement: DashboardElement = { - ...element, - dataSource, - chartConfig, - customTitle: customTitle.trim() || undefined, // 빈 문자열이면 undefined - showHeader, // 헤더 표시 여부 - }; - - // console.log(" 저장할 element:", updatedElement); - - onSave(updatedElement); - onClose(); - }, [element, dataSource, chartConfig, customTitle, showHeader, onSave, onClose]); - - // 모달이 열려있지 않으면 렌더링하지 않음 - if (!isOpen) return null; - - // 시계, 달력, 날씨, 환율, 계산기 위젯은 헤더 설정만 가능 - const isHeaderOnlyWidget = - element.type === "widget" && - (element.subtype === "clock" || element.subtype === "calendar" || isSelfContainedWidget); - - // 기사관리 위젯은 자체 설정 UI를 가지고 있으므로 모달 표시하지 않음 - if (element.type === "widget" && element.subtype === "driver-management") { - return null; - } - - // 저장 가능 여부 확인 - const isPieChart = element.subtype === "pie" || element.subtype === "donut"; - const isApiSource = dataSource.type === "api"; - - // Y축 검증 헬퍼 - const hasYAxis = - chartConfig.yAxis && - (typeof chartConfig.yAxis === "string" || (Array.isArray(chartConfig.yAxis) && chartConfig.yAxis.length > 0)); - - // customTitle이 변경되었는지 확인 - const isTitleChanged = customTitle.trim() !== (element.customTitle || ""); - - // showHeader가 변경되었는지 확인 - const isHeaderChanged = showHeader !== (element.showHeader !== false); - - const canSave = - isTitleChanged || // 제목만 변경해도 저장 가능 - isHeaderChanged || // 헤더 표시 여부만 변경해도 저장 가능 - (isSimpleWidget - ? // 간단한 위젯: 2단계에서 쿼리 테스트 후 저장 가능 (차트 설정 불필요) - currentStep === 2 && queryResult && queryResult.rows.length > 0 - : isMapWidget - ? // 지도 위젯: 타일맵 URL 또는 위도/경도 매핑 필요 - element.subtype === "map-test" - ? // 🧪 지도 테스트 위젯: 타일맵 URL만 있으면 저장 가능 - currentStep === 2 && chartConfig.tileMapUrl - : // 기존 지도 위젯: 쿼리 결과 + 위도/경도 필수 - currentStep === 2 && - queryResult && - queryResult.rows.length > 0 && - chartConfig.latitudeColumn && - chartConfig.longitudeColumn - : // 차트: 기존 로직 (2단계에서 차트 설정 필요) - currentStep === 2 && - queryResult && - queryResult.rows.length > 0 && - chartConfig.xAxis && - (isPieChart || isApiSource - ? // 파이/도넛 차트 또는 REST API - chartConfig.aggregation === "count" - ? true // count는 Y축 없어도 됨 - : hasYAxis // 다른 집계(sum, avg, max, min) 또는 집계 없음 → Y축 필수 - : // 일반 차트 (DB): Y축 필수 - hasYAxis)); - - return ( -
-
- {/* 모달 헤더 */} -
-
-
-

{element.title} 설정

-
- -
- - {/* 커스텀 제목 입력 */} -
- - setCustomTitle(e.target.value)} - onKeyDown={(e) => { - // 모든 키보드 이벤트를 input 필드 내부에서만 처리 - e.stopPropagation(); - }} - placeholder="예: 정비 일정 목록, 창고 위치 현황 등 (비워두면 자동 생성)" - className="focus:border-primary focus:ring-primary w-full rounded-md border border-border px-3 py-2 text-sm focus:ring-1 focus:outline-none" - /> -

- 비워두면 테이블명으로 자동 생성됩니다 (예: "maintenance_schedules 목록") -

-
- - {/* 헤더 표시 옵션 */} -
- setShowHeader(e.target.checked)} - className="text-primary focus:ring-primary h-4 w-4 rounded border-border" - /> - -
-
- - {/* 진행 상황 표시 - 간단한 위젯과 헤더 전용 위젯은 표시 안 함 */} - {!isSimpleWidget && !isHeaderOnlyWidget && ( -
-
-
- 단계 {currentStep} / 2: {currentStep === 1 ? "데이터 소스 선택" : "데이터 설정 및 차트 설정"} -
-
-
- )} - - {/* 단계별 내용 */} - {!isHeaderOnlyWidget && ( -
- {currentStep === 1 && ( - - )} - - {currentStep === 2 && ( -
- {/* 왼쪽: 데이터 설정 */} -
- {dataSource.type === "database" ? ( - <> - - - - ) : ( - - )} -
- - {/* 오른쪽: 설정 패널 */} - {!isSimpleWidget && ( -
- {isMapWidget ? ( - // 지도 위젯: 위도/경도 매핑 패널 - element.subtype === "map-test" ? ( - // 🧪 지도 테스트 위젯: 타일맵 URL 필수, 마커 데이터 선택사항 - - ) : queryResult && queryResult.rows.length > 0 ? ( - // 기존 지도 위젯: 쿼리 결과 필수 - - ) : ( -
-
-
데이터를 가져온 후 지도 설정이 표시됩니다
-
-
- ) - ) : // 차트: 차트 설정 패널 - queryResult && queryResult.rows.length > 0 ? ( - - ) : ( -
-
-
데이터를 가져온 후 차트 설정이 표시됩니다
-
-
- )} -
- )} -
- )} -
- )} - - {/* 모달 푸터 */} -
-
{queryResult && {queryResult.rows.length}개 데이터 로드됨}
- -
- {!isSimpleWidget && !isHeaderOnlyWidget && currentStep > 1 && ( - - )} - - {isHeaderOnlyWidget ? ( - // 헤더 전용 위젯: 바로 저장 - - ) : currentStep === 1 ? ( - // 1단계: 다음 버튼 (차트 위젯, 간단한 위젯 모두) - - ) : ( - // 2단계: 저장 버튼 - - )} -
-
-
-
- ); -} diff --git a/frontend/components/admin/dashboard/index.ts b/frontend/components/admin/dashboard/index.ts index 40dc17ab..f8f2e824 100644 --- a/frontend/components/admin/dashboard/index.ts +++ b/frontend/components/admin/dashboard/index.ts @@ -2,12 +2,10 @@ * 대시보드 관리 컴포넌트 인덱스 */ -export { default as DashboardDesigner } from './DashboardDesigner'; -export { DashboardCanvas } from './DashboardCanvas'; -export { DashboardSidebar } from './DashboardSidebar'; -export { DashboardToolbar } from './DashboardToolbar'; -export { CanvasElement } from './CanvasElement'; -export { QueryEditor } from './QueryEditor'; -export { ChartConfigPanel } from './ChartConfigPanel'; -export { ElementConfigModal } from './ElementConfigModal'; -export * from './types'; +export { default as DashboardDesigner } from "./DashboardDesigner"; +export { DashboardCanvas } from "./DashboardCanvas"; +export { DashboardSidebar } from "./DashboardSidebar"; +export { CanvasElement } from "./CanvasElement"; +export { QueryEditor } from "./QueryEditor"; +export { ChartConfigPanel } from "./ChartConfigPanel"; +export * from "./types"; diff --git a/frontend/components/admin/dashboard/widgets/ListWidgetConfigModal.tsx b/frontend/components/admin/dashboard/widgets/ListWidgetConfigModal.tsx deleted file mode 100644 index 77757333..00000000 --- a/frontend/components/admin/dashboard/widgets/ListWidgetConfigModal.tsx +++ /dev/null @@ -1,326 +0,0 @@ -"use client"; - -import React, { useState, useCallback, useEffect } from "react"; -import { DashboardElement, ChartDataSource, QueryResult, ListWidgetConfig, ListColumn } from "../types"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { ChevronLeft, ChevronRight, Save, X } from "lucide-react"; -import { DataSourceSelector } from "../data-sources/DataSourceSelector"; -import { DatabaseConfig } from "../data-sources/DatabaseConfig"; -import { ApiConfig } from "../data-sources/ApiConfig"; -import { QueryEditor } from "../QueryEditor"; -import { ColumnSelector } from "./list-widget/ColumnSelector"; -import { ManualColumnEditor } from "./list-widget/ManualColumnEditor"; -import { ListTableOptions } from "./list-widget/ListTableOptions"; - -interface ListWidgetConfigModalProps { - isOpen: boolean; - element: DashboardElement; - onClose: () => void; - onSave: (updates: Partial) => void; -} - -/** - * 리스트 위젯 설정 모달 - * - 3단계 설정: 데이터 소스 → 데이터 가져오기 → 컬럼 설정 - */ -export function ListWidgetConfigModal({ isOpen, element, onClose, onSave }: ListWidgetConfigModalProps) { - const [currentStep, setCurrentStep] = useState<1 | 2 | 3>(1); - const [title, setTitle] = useState(element.title || "📋 리스트"); - const [dataSource, setDataSource] = useState( - element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 }, - ); - const [queryResult, setQueryResult] = useState(null); - const [listConfig, setListConfig] = useState( - element.listConfig || { - columnMode: "auto", - viewMode: "table", - columns: [], - pageSize: 10, - enablePagination: true, - showHeader: true, - stripedRows: true, - compactMode: false, - cardColumns: 3, - }, - ); - - // 모달 열릴 때 element에서 설정 로드 (한 번만) - useEffect(() => { - if (isOpen) { - // element가 변경되었을 때만 설정을 다시 로드 - setTitle(element.title || "📋 리스트"); - - // 기존 dataSource가 있으면 그대로 사용, 없으면 기본값 - if (element.dataSource) { - setDataSource(element.dataSource); - } - - // 기존 listConfig가 있으면 그대로 사용, 없으면 기본값 - if (element.listConfig) { - setListConfig(element.listConfig); - } - - // 현재 스텝은 1로 초기화 - setCurrentStep(1); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpen, element.id]); // element.id가 변경될 때만 재실행 - - // 데이터 소스 타입 변경 - const handleDataSourceTypeChange = useCallback((type: "database" | "api") => { - if (type === "database") { - setDataSource((prev) => ({ - ...prev, - type: "database", - connectionType: "current", - })); - } else { - setDataSource((prev) => ({ - ...prev, - type: "api", - method: "GET", - })); - } - - // 데이터 소스 타입 변경 시에는 쿼리 결과만 초기화 (컬럼 설정은 유지) - setQueryResult(null); - }, []); - - // 데이터 소스 업데이트 - const handleDataSourceUpdate = useCallback((updates: Partial) => { - setDataSource((prev) => ({ ...prev, ...updates })); - }, []); - - // 쿼리 실행 결과 처리 - const handleQueryTest = useCallback((result: QueryResult) => { - setQueryResult(result); - - // 쿼리 실행할 때마다 컬럼 초기화 후 자동 생성 - if (result.columns.length > 0) { - const autoColumns: ListColumn[] = result.columns.map((col, idx) => ({ - id: `col_${idx}`, - label: col, - field: col, - align: "left", - visible: true, - })); - setListConfig((prev) => ({ ...prev, columns: autoColumns })); - } - }, []); - - // 다음 단계 - const handleNext = () => { - if (currentStep < 3) { - setCurrentStep((prev) => (prev + 1) as 1 | 2 | 3); - } - }; - - // 이전 단계 - const handlePrev = () => { - if (currentStep > 1) { - setCurrentStep((prev) => (prev - 1) as 1 | 2 | 3); - } - }; - - // 저장 - const handleSave = () => { - onSave({ - customTitle: title, - dataSource, - listConfig, - }); - onClose(); - }; - - // 저장 가능 여부 - const canSave = queryResult && queryResult.rows.length > 0 && listConfig.columns.length > 0; - - if (!isOpen) return null; - - return ( -
-
- {/* 헤더 */} -
-
-
-

📋 리스트 위젯 설정

-

데이터 소스와 컬럼을 설정하세요

-
- -
- {/* 제목 입력 */} -
- - setTitle(e.target.value)} - onKeyDown={(e) => { - // 모든 키보드 이벤트를 input 필드 내부에서만 처리 - e.stopPropagation(); - }} - placeholder="예: 사용자 목록" - className="mt-1" - /> -
- - {/* 참고: 리스트 위젯은 제목이 항상 표시됩니다 */} -
💡 리스트 위젯은 제목이 항상 표시됩니다
-
- - {/* 진행 상태 표시 */} -
-
-
-
= 1 ? "text-primary" : "text-muted-foreground"}`}> -
= 1 ? "bg-primary text-white" : "bg-muted"}`} - > - 1 -
- 데이터 소스 -
-
-
= 2 ? "text-primary" : "text-muted-foreground"}`}> -
= 2 ? "bg-primary text-white" : "bg-muted"}`} - > - 2 -
- 데이터 가져오기 -
-
-
= 3 ? "text-primary" : "text-muted-foreground"}`}> -
= 3 ? "bg-primary text-white" : "bg-muted"}`} - > - 3 -
- 컬럼 설정 -
-
-
-
- - {/* 컨텐츠 */} -
- {currentStep === 1 && ( - - )} - - {currentStep === 2 && ( -
- {/* 왼쪽: 데이터 소스 설정 */} -
- {dataSource.type === "database" ? ( - - ) : ( - - )} - - {dataSource.type === "database" && ( -
- -
- )} -
- - {/* 오른쪽: 데이터 미리보기 */} -
- {queryResult && queryResult.rows.length > 0 ? ( -
-

📋 데이터 미리보기

-
- - {queryResult.totalRows}개 데이터 - -
-                        {JSON.stringify(queryResult.rows.slice(0, 3), null, 2)}
-                      
-
-
- ) : ( -
-
-
데이터를 가져온 후 미리보기가 표시됩니다
-
-
- )} -
-
- )} - - {currentStep === 3 && queryResult && ( -
- {listConfig.columnMode === "auto" ? ( - setListConfig((prev) => ({ ...prev, columns }))} - /> - ) : ( - setListConfig((prev) => ({ ...prev, columns }))} - /> - )} - - setListConfig((prev) => ({ ...prev, ...updates }))} - /> -
- )} -
- - {/* 푸터 */} -
-
- {queryResult && ( - - 📊 {queryResult.rows.length}개 데이터 로드됨 - - )} -
- -
- {currentStep > 1 && ( - - )} - - {currentStep < 3 ? ( - - ) : ( - - )} -
-
-
-
- ); -} diff --git a/frontend/components/admin/dashboard/widgets/TodoWidgetConfigModal.tsx b/frontend/components/admin/dashboard/widgets/TodoWidgetConfigModal.tsx deleted file mode 100644 index 6c73f976..00000000 --- a/frontend/components/admin/dashboard/widgets/TodoWidgetConfigModal.tsx +++ /dev/null @@ -1,664 +0,0 @@ -"use client"; - -import React, { useState, useCallback, useEffect } from "react"; -import { DashboardElement, ChartDataSource, QueryResult } from "../types"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { ChevronLeft, ChevronRight, Save, X } from "lucide-react"; -import { DataSourceSelector } from "../data-sources/DataSourceSelector"; -import { DatabaseConfig } from "../data-sources/DatabaseConfig"; -import { ApiConfig } from "../data-sources/ApiConfig"; -import { QueryEditor } from "../QueryEditor"; - -interface TodoWidgetConfigModalProps { - isOpen: boolean; - element: DashboardElement; - onClose: () => void; - onSave: (updates: Partial) => void; -} - -/** - * 일정관리 위젯 설정 모달 (범용) - * - 2단계 설정: 데이터 소스 → 쿼리 입력/테스트 - */ -export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: TodoWidgetConfigModalProps) { - const [currentStep, setCurrentStep] = useState<1 | 2>(1); - const [title, setTitle] = useState(element.title || "일정관리 위젯"); - const [dataSource, setDataSource] = useState( - element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 }, - ); - const [queryResult, setQueryResult] = useState(null); - - // 데이터베이스 연동 설정 - const [enableDbSync, setEnableDbSync] = useState(element.chartConfig?.enableDbSync || false); - const [dbSyncMode, setDbSyncMode] = useState<"simple" | "advanced">(element.chartConfig?.dbSyncMode || "simple"); - const [tableName, setTableName] = useState(element.chartConfig?.tableName || ""); - const [columnMapping, setColumnMapping] = useState(element.chartConfig?.columnMapping || { - id: "id", - title: "title", - description: "description", - priority: "priority", - status: "status", - assignedTo: "assigned_to", - dueDate: "due_date", - isUrgent: "is_urgent", - }); - - // 모달 열릴 때 element에서 설정 로드 - useEffect(() => { - if (isOpen) { - setTitle(element.title || "일정관리 위젯"); - - // 데이터 소스 설정 로드 (저장된 설정 우선, 없으면 기본값) - const loadedDataSource = element.dataSource || { - type: "database", - connectionType: "current", - refreshInterval: 0 - }; - setDataSource(loadedDataSource); - - // 저장된 쿼리가 있으면 자동으로 실행 (실제 결과 가져오기) - if (loadedDataSource.query) { - // 쿼리 자동 실행 - const executeQuery = async () => { - try { - const token = localStorage.getItem("authToken"); - const userLang = localStorage.getItem("userLang") || "KR"; - - const apiUrl = loadedDataSource.connectionType === "external" && loadedDataSource.externalConnectionId - ? `http://localhost:9771/api/external-db/query?userLang=${userLang}` - : `http://localhost:9771/api/dashboards/execute-query?userLang=${userLang}`; - - const requestBody = loadedDataSource.connectionType === "external" && loadedDataSource.externalConnectionId - ? { - connectionId: parseInt(loadedDataSource.externalConnectionId), - query: loadedDataSource.query, - } - : { query: loadedDataSource.query }; - - const response = await fetch(apiUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify(requestBody), - }); - - if (response.ok) { - const result = await response.json(); - const rows = result.data?.rows || result.data || []; - setQueryResult({ - rows: rows, - rowCount: rows.length, - executionTime: 0, - }); - } else { - // 실패해도 더미 결과로 2단계 진입 가능 - setQueryResult({ - rows: [{ _info: "저장된 쿼리가 있습니다. 다시 테스트해주세요." }], - rowCount: 1, - executionTime: 0, - }); - } - } catch (error) { - // 에러 발생해도 2단계 진입 가능 - setQueryResult({ - rows: [{ _info: "저장된 쿼리가 있습니다. 다시 테스트해주세요." }], - rowCount: 1, - executionTime: 0, - }); - } - }; - - executeQuery(); - } - - // DB 동기화 설정 로드 - setEnableDbSync(element.chartConfig?.enableDbSync || false); - setDbSyncMode(element.chartConfig?.dbSyncMode || "simple"); - setTableName(element.chartConfig?.tableName || ""); - if (element.chartConfig?.columnMapping) { - setColumnMapping(element.chartConfig.columnMapping); - } - setCurrentStep(1); - } - }, [isOpen, element.id]); - - // 데이터 소스 타입 변경 - const handleDataSourceTypeChange = useCallback((type: "database" | "api") => { - if (type === "database") { - setDataSource((prev) => ({ - ...prev, - type: "database", - connectionType: "current", - })); - } else { - setDataSource((prev) => ({ - ...prev, - type: "api", - method: "GET", - })); - } - setQueryResult(null); - }, []); - - // 데이터 소스 업데이트 - const handleDataSourceUpdate = useCallback((updates: Partial) => { - setDataSource((prev) => ({ ...prev, ...updates })); - }, []); - - // 쿼리 실행 결과 처리 - const handleQueryTest = useCallback( - (result: QueryResult) => { - // console.log("🎯 TodoWidget - handleQueryTest 호출됨!"); - // console.log("📊 쿼리 결과:", result); - // console.log("📝 rows 개수:", result.rows?.length); - // console.log("❌ error:", result.error); - setQueryResult(result); - // console.log("✅ setQueryResult 호출 완료!"); - - // 강제 리렌더링 확인 - // setTimeout(() => { - // console.log("🔄 1초 후 queryResult 상태:", result); - // }, 1000); - }, - [], - ); - - // 저장 - const handleSave = useCallback(() => { - if (!dataSource.query || !queryResult || queryResult.error) { - alert("쿼리를 입력하고 테스트를 먼저 실행해주세요."); - return; - } - - if (!queryResult.rows || queryResult.rows.length === 0) { - alert("쿼리 결과가 없습니다. 데이터가 반환되는 쿼리를 입력해주세요."); - return; - } - - // 간편 모드에서 테이블명 필수 체크 - if (enableDbSync && dbSyncMode === "simple" && !tableName.trim()) { - alert("데이터베이스 연동을 활성화하려면 테이블명을 입력해주세요."); - return; - } - - onSave({ - title, - dataSource, - chartConfig: { - ...element.chartConfig, - enableDbSync, - dbSyncMode, - tableName, - columnMapping, - insertQuery: element.chartConfig?.insertQuery, - updateQuery: element.chartConfig?.updateQuery, - deleteQuery: element.chartConfig?.deleteQuery, - }, - }); - - onClose(); - }, [title, dataSource, queryResult, enableDbSync, dbSyncMode, tableName, columnMapping, element.chartConfig, onSave, onClose]); - - // 다음 단계로 - const handleNext = useCallback(() => { - if (currentStep === 1) { - if (dataSource.type === "database") { - if (!dataSource.connectionId && dataSource.connectionType === "external") { - alert("외부 데이터베이스를 선택해주세요."); - return; - } - } else if (dataSource.type === "api") { - if (!dataSource.url) { - alert("API URL을 입력해주세요."); - return; - } - } - setCurrentStep(2); - } - }, [currentStep, dataSource]); - - // 이전 단계로 - const handlePrev = useCallback(() => { - if (currentStep === 2) { - setCurrentStep(1); - } - }, [currentStep]); - - if (!isOpen) return null; - - return ( -
-
- {/* 헤더 */} -
-
-

일정관리 위젯 설정

-

- 데이터 소스와 쿼리를 설정하면 자동으로 일정 목록이 표시됩니다 -

-
- -
- - {/* 진행 상태 */} -
-
-
-
- 1 -
- 데이터 소스 선택 -
- -
-
- 2 -
- 쿼리 입력 및 테스트 -
-
-
- - {/* 본문 */} -
- {/* Step 1: 데이터 소스 선택 */} - {currentStep === 1 && ( -
-
- - setTitle(e.target.value)} - placeholder="예: 오늘의 일정" - className="mt-2" - /> -
- -
- - -
- - {dataSource.type === "database" && ( - - )} - - {dataSource.type === "api" && } -
- )} - - {/* Step 2: 쿼리 입력 및 테스트 */} - {currentStep === 2 && ( -
-
-
-

💡 컬럼명 가이드

-

- 쿼리 결과에 다음 컬럼명이 있으면 자동으로 일정 항목으로 변환됩니다: -

-
    -
  • - id - 고유 ID (없으면 자동 생성) -
  • -
  • - title,{" "} - task,{" "} - name - 제목 (필수) -
  • -
  • - description,{" "} - desc,{" "} - content - 상세 설명 -
  • -
  • - priority - 우선순위 (urgent, high, - normal, low) -
  • -
  • - status - 상태 (pending, in_progress, - completed) -
  • -
  • - assigned_to,{" "} - assignedTo,{" "} - user - 담당자 -
  • -
  • - due_date,{" "} - dueDate,{" "} - deadline - 마감일 -
  • -
  • - is_urgent,{" "} - isUrgent,{" "} - urgent - 긴급 여부 -
  • -
-
- - -
- - {/* 디버그: 항상 표시되는 테스트 메시지 */} -
-

- 🔍 디버그: queryResult 상태 = {queryResult ? "있음" : "없음"} -

- {queryResult && ( -

- rows: {queryResult.rows?.length}개, error: {queryResult.error || "없음"} -

- )} -
- - {queryResult && !queryResult.error && queryResult.rows && queryResult.rows.length > 0 && ( -
-

✅ 쿼리 테스트 성공!

-

- 총 {queryResult.rows.length}개의 일정 항목을 찾았습니다. -

-
-

첫 번째 데이터 미리보기:

-
-                      {JSON.stringify(queryResult.rows[0], null, 2)}
-                    
-
-
- )} - - {/* 데이터베이스 연동 쿼리 (선택사항) */} -
-
-
-

🔗 데이터베이스 연동 (선택사항)

-

- 위젯에서 추가/수정/삭제 시 데이터베이스에 직접 반영 -

-
- -
- - {enableDbSync && ( - <> - {/* 모드 선택 */} -
- - -
- - {/* 간편 모드 */} - {dbSyncMode === "simple" && ( -
-

- 테이블명과 컬럼 매핑만 입력하면 자동으로 INSERT/UPDATE/DELETE 쿼리가 생성됩니다. -

- - {/* 테이블명 */} -
- - setTableName(e.target.value)} - placeholder="예: tasks" - className="mt-2" - /> -
- - {/* 컬럼 매핑 */} -
- -
-
- - setColumnMapping({ ...columnMapping, id: e.target.value })} - placeholder="id" - className="mt-1 h-8 text-sm" - /> -
-
- - setColumnMapping({ ...columnMapping, title: e.target.value })} - placeholder="title" - className="mt-1 h-8 text-sm" - /> -
-
- - setColumnMapping({ ...columnMapping, description: e.target.value })} - placeholder="description" - className="mt-1 h-8 text-sm" - /> -
-
- - setColumnMapping({ ...columnMapping, priority: e.target.value })} - placeholder="priority" - className="mt-1 h-8 text-sm" - /> -
-
- - setColumnMapping({ ...columnMapping, status: e.target.value })} - placeholder="status" - className="mt-1 h-8 text-sm" - /> -
-
- - setColumnMapping({ ...columnMapping, assignedTo: e.target.value })} - placeholder="assigned_to" - className="mt-1 h-8 text-sm" - /> -
-
- - setColumnMapping({ ...columnMapping, dueDate: e.target.value })} - placeholder="due_date" - className="mt-1 h-8 text-sm" - /> -
-
- - setColumnMapping({ ...columnMapping, isUrgent: e.target.value })} - placeholder="is_urgent" - className="mt-1 h-8 text-sm" - /> -
-
-
-
- )} - - {/* 고급 모드 */} - {dbSyncMode === "advanced" && ( -
-

- 복잡한 로직이 필요한 경우 직접 쿼리를 작성하세요. -

- - {/* INSERT 쿼리 */} -
- -

- 사용 가능한 변수: ${"{title}"}, ${"{description}"}, ${"{priority}"}, ${"{status}"}, ${"{assignedTo}"}, ${"{dueDate}"}, ${"{isUrgent}"} -

-