2025-10-14 10:12:40 +09:00
|
|
|
"use client";
|
2025-09-30 13:23:22 +09:00
|
|
|
|
2025-10-14 13:59:54 +09:00
|
|
|
import React, { useState, useCallback, useEffect } from "react";
|
2025-10-14 10:48:17 +09:00
|
|
|
import { DashboardElement, ChartDataSource, ChartConfig, QueryResult } from "./types";
|
2025-10-14 10:12:40 +09:00
|
|
|
import { QueryEditor } from "./QueryEditor";
|
|
|
|
|
import { ChartConfigPanel } from "./ChartConfigPanel";
|
2025-10-15 13:32:20 +09:00
|
|
|
import { VehicleMapConfigPanel } from "./VehicleMapConfigPanel";
|
2025-10-14 13:59:54 +09:00
|
|
|
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";
|
2025-09-30 13:23:22 +09:00
|
|
|
|
|
|
|
|
interface ElementConfigModalProps {
|
|
|
|
|
element: DashboardElement;
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
onSave: (element: DashboardElement) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-14 13:59:54 +09:00
|
|
|
* 요소 설정 모달 컴포넌트 (리팩토링)
|
2025-10-14 15:25:11 +09:00
|
|
|
* - 2단계 플로우: 데이터 소스 선택 → 데이터 설정 및 차트 설정
|
2025-10-14 13:59:54 +09:00
|
|
|
* - 새로운 데이터 소스 컴포넌트 통합
|
2025-09-30 13:23:22 +09:00
|
|
|
*/
|
|
|
|
|
export function ElementConfigModal({ element, isOpen, onClose, onSave }: ElementConfigModalProps) {
|
|
|
|
|
const [dataSource, setDataSource] = useState<ChartDataSource>(
|
2025-10-14 15:25:11 +09:00
|
|
|
element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 },
|
2025-09-30 13:23:22 +09:00
|
|
|
);
|
2025-10-14 10:12:40 +09:00
|
|
|
const [chartConfig, setChartConfig] = useState<ChartConfig>(element.chartConfig || {});
|
2025-09-30 13:23:22 +09:00
|
|
|
const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
|
2025-10-14 15:25:11 +09:00
|
|
|
const [currentStep, setCurrentStep] = useState<1 | 2>(1);
|
2025-10-15 15:05:20 +09:00
|
|
|
|
2025-10-15 13:32:20 +09:00
|
|
|
// 차트 설정이 필요 없는 위젯 (쿼리/API만 필요)
|
2025-10-15 15:05:20 +09:00
|
|
|
const isSimpleWidget =
|
|
|
|
|
element.subtype === "vehicle-status" ||
|
|
|
|
|
element.subtype === "vehicle-list" ||
|
2025-10-15 16:16:27 +09:00
|
|
|
element.subtype === "status-summary" || // 커스텀 상태 카드
|
|
|
|
|
// element.subtype === "list-summary" || // 커스텀 목록 카드 (다른 분 작업 중 - 임시 주석)
|
2025-10-15 13:32:20 +09:00
|
|
|
element.subtype === "delivery-status" ||
|
2025-10-15 16:16:27 +09:00
|
|
|
element.subtype === "delivery-status-summary" ||
|
|
|
|
|
element.subtype === "delivery-today-stats" ||
|
|
|
|
|
element.subtype === "cargo-list" ||
|
|
|
|
|
element.subtype === "customer-issues" ||
|
2025-10-15 13:32:20 +09:00
|
|
|
element.subtype === "driver-management";
|
2025-10-15 15:05:20 +09:00
|
|
|
|
2025-10-15 13:32:20 +09:00
|
|
|
// 지도 위젯 (위도/경도 매핑 필요)
|
2025-10-15 16:16:27 +09:00
|
|
|
const isMapWidget = element.subtype === "vehicle-map" || element.subtype === "map-summary";
|
2025-10-15 15:05:20 +09:00
|
|
|
|
2025-10-15 12:09:30 +09:00
|
|
|
// 주석
|
2025-10-14 13:59:54 +09:00
|
|
|
// 모달이 열릴 때 초기화
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isOpen) {
|
2025-10-14 15:25:11 +09:00
|
|
|
setDataSource(element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 });
|
2025-10-14 13:59:54 +09:00
|
|
|
setChartConfig(element.chartConfig || {});
|
|
|
|
|
setQueryResult(null);
|
|
|
|
|
setCurrentStep(1);
|
|
|
|
|
}
|
|
|
|
|
}, [isOpen, element]);
|
|
|
|
|
|
|
|
|
|
// 데이터 소스 타입 변경
|
|
|
|
|
const handleDataSourceTypeChange = useCallback((type: "database" | "api") => {
|
|
|
|
|
if (type === "database") {
|
|
|
|
|
setDataSource({
|
|
|
|
|
type: "database",
|
|
|
|
|
connectionType: "current",
|
2025-10-14 15:25:11 +09:00
|
|
|
refreshInterval: 0,
|
2025-10-14 13:59:54 +09:00
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
setDataSource({
|
|
|
|
|
type: "api",
|
|
|
|
|
method: "GET",
|
2025-10-14 15:25:11 +09:00
|
|
|
refreshInterval: 0,
|
2025-10-14 13:59:54 +09:00
|
|
|
});
|
|
|
|
|
}
|
2025-10-15 10:14:10 +09:00
|
|
|
|
|
|
|
|
// 데이터 소스 변경 시 쿼리 결과와 차트 설정 초기화
|
|
|
|
|
setQueryResult(null);
|
|
|
|
|
setChartConfig({});
|
2025-10-14 13:59:54 +09:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 데이터 소스 업데이트
|
|
|
|
|
const handleDataSourceUpdate = useCallback((updates: Partial<ChartDataSource>) => {
|
|
|
|
|
setDataSource((prev) => ({ ...prev, ...updates }));
|
2025-09-30 13:23:22 +09:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 차트 설정 변경 처리
|
|
|
|
|
const handleChartConfigChange = useCallback((newConfig: ChartConfig) => {
|
|
|
|
|
setChartConfig(newConfig);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 쿼리 테스트 결과 처리
|
|
|
|
|
const handleQueryTest = useCallback((result: QueryResult) => {
|
|
|
|
|
setQueryResult(result);
|
2025-10-15 15:17:05 +09:00
|
|
|
|
|
|
|
|
// 쿼리가 변경되었으므로 차트 설정 초기화 (X/Y축 리셋)
|
|
|
|
|
console.log("🔄 쿼리 변경 감지 - 차트 설정 초기화");
|
|
|
|
|
setChartConfig({});
|
2025-09-30 13:23:22 +09:00
|
|
|
}, []);
|
|
|
|
|
|
2025-10-14 15:25:11 +09:00
|
|
|
// 다음 단계로 이동
|
2025-10-14 13:59:54 +09:00
|
|
|
const handleNext = useCallback(() => {
|
|
|
|
|
if (currentStep === 1) {
|
|
|
|
|
setCurrentStep(2);
|
|
|
|
|
}
|
2025-10-14 15:25:11 +09:00
|
|
|
}, [currentStep]);
|
2025-10-14 13:59:54 +09:00
|
|
|
|
|
|
|
|
// 이전 단계로 이동
|
|
|
|
|
const handlePrev = useCallback(() => {
|
|
|
|
|
if (currentStep > 1) {
|
2025-10-14 15:25:11 +09:00
|
|
|
setCurrentStep((prev) => (prev - 1) as 1 | 2);
|
2025-10-14 13:59:54 +09:00
|
|
|
}
|
|
|
|
|
}, [currentStep]);
|
|
|
|
|
|
2025-09-30 13:23:22 +09:00
|
|
|
// 저장 처리
|
|
|
|
|
const handleSave = useCallback(() => {
|
|
|
|
|
const updatedElement: DashboardElement = {
|
|
|
|
|
...element,
|
|
|
|
|
dataSource,
|
|
|
|
|
chartConfig,
|
|
|
|
|
};
|
2025-10-15 15:05:20 +09:00
|
|
|
|
|
|
|
|
console.log(" 저장할 element:", updatedElement);
|
|
|
|
|
|
2025-09-30 13:23:22 +09:00
|
|
|
onSave(updatedElement);
|
|
|
|
|
onClose();
|
|
|
|
|
}, [element, dataSource, chartConfig, onSave, onClose]);
|
|
|
|
|
|
|
|
|
|
// 모달이 열려있지 않으면 렌더링하지 않음
|
|
|
|
|
if (!isOpen) return null;
|
|
|
|
|
|
2025-10-14 11:26:53 +09:00
|
|
|
// 시계, 달력, 기사관리 위젯은 자체 설정 UI를 가지고 있으므로 모달 표시하지 않음
|
|
|
|
|
if (
|
|
|
|
|
element.type === "widget" &&
|
|
|
|
|
(element.subtype === "clock" || element.subtype === "calendar" || element.subtype === "driver-management")
|
|
|
|
|
) {
|
2025-10-14 10:23:20 +09:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-14 13:59:54 +09:00
|
|
|
// 저장 가능 여부 확인
|
2025-10-15 10:02:32 +09:00
|
|
|
const isPieChart = element.subtype === "pie" || element.subtype === "donut";
|
|
|
|
|
const isApiSource = dataSource.type === "api";
|
|
|
|
|
|
2025-10-15 15:45:58 +09:00
|
|
|
// Y축 검증 헬퍼
|
|
|
|
|
const hasYAxis =
|
|
|
|
|
chartConfig.yAxis &&
|
|
|
|
|
(typeof chartConfig.yAxis === "string" || (Array.isArray(chartConfig.yAxis) && chartConfig.yAxis.length > 0));
|
|
|
|
|
|
2025-10-15 13:32:20 +09:00
|
|
|
const canSave = isSimpleWidget
|
|
|
|
|
? // 간단한 위젯: 2단계에서 쿼리 테스트 후 저장 가능
|
2025-10-15 15:05:20 +09:00
|
|
|
currentStep === 2 && queryResult && queryResult.rows.length > 0
|
2025-10-15 13:32:20 +09:00
|
|
|
: isMapWidget
|
|
|
|
|
? // 지도 위젯: 위도/경도 매핑 필요
|
|
|
|
|
currentStep === 2 &&
|
|
|
|
|
queryResult &&
|
|
|
|
|
queryResult.rows.length > 0 &&
|
|
|
|
|
chartConfig.latitudeColumn &&
|
|
|
|
|
chartConfig.longitudeColumn
|
|
|
|
|
: // 차트: 기존 로직 (2단계에서 차트 설정 필요)
|
2025-10-15 15:05:20 +09:00
|
|
|
currentStep === 2 &&
|
|
|
|
|
queryResult &&
|
|
|
|
|
queryResult.rows.length > 0 &&
|
|
|
|
|
chartConfig.xAxis &&
|
|
|
|
|
(isPieChart || isApiSource
|
2025-10-15 15:45:58 +09:00
|
|
|
? // 파이/도넛 차트 또는 REST API
|
2025-10-15 15:05:20 +09:00
|
|
|
chartConfig.aggregation === "count"
|
2025-10-15 15:45:58 +09:00
|
|
|
? true // count는 Y축 없어도 됨
|
|
|
|
|
: hasYAxis // 다른 집계(sum, avg, max, min) 또는 집계 없음 → Y축 필수
|
2025-10-15 15:05:20 +09:00
|
|
|
: // 일반 차트 (DB): Y축 필수
|
2025-10-15 15:45:58 +09:00
|
|
|
hasYAxis);
|
2025-10-14 13:59:54 +09:00
|
|
|
|
2025-09-30 13:23:22 +09:00
|
|
|
return (
|
2025-10-15 13:32:20 +09:00
|
|
|
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
2025-10-14 15:25:11 +09:00
|
|
|
<div
|
|
|
|
|
className={`flex flex-col rounded-xl border bg-white shadow-2xl ${
|
|
|
|
|
currentStep === 1 ? "h-auto max-h-[70vh] w-full max-w-3xl" : "h-[85vh] w-full max-w-5xl"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
2025-09-30 13:23:22 +09:00
|
|
|
{/* 모달 헤더 */}
|
2025-10-14 13:59:54 +09:00
|
|
|
<div className="flex items-center justify-between border-b p-6">
|
2025-09-30 13:23:22 +09:00
|
|
|
<div>
|
2025-10-14 13:59:54 +09:00
|
|
|
<h2 className="text-xl font-semibold text-gray-900">{element.title} 설정</h2>
|
2025-10-14 15:25:11 +09:00
|
|
|
<p className="mt-1 text-sm text-gray-500">
|
2025-10-15 13:32:20 +09:00
|
|
|
{isSimpleWidget
|
|
|
|
|
? "데이터 소스를 설정하세요"
|
|
|
|
|
: currentStep === 1
|
|
|
|
|
? "데이터 소스를 선택하세요"
|
|
|
|
|
: "쿼리를 실행하고 차트를 설정하세요"}
|
2025-10-14 15:25:11 +09:00
|
|
|
</p>
|
2025-09-30 13:23:22 +09:00
|
|
|
</div>
|
2025-10-14 13:59:54 +09:00
|
|
|
<Button variant="ghost" size="icon" onClick={onClose} className="h-8 w-8">
|
|
|
|
|
<X className="h-5 w-5" />
|
|
|
|
|
</Button>
|
2025-09-30 13:23:22 +09:00
|
|
|
</div>
|
|
|
|
|
|
2025-10-15 13:32:20 +09:00
|
|
|
{/* 진행 상황 표시 - 간단한 위젯은 표시 안 함 */}
|
|
|
|
|
{!isSimpleWidget && (
|
|
|
|
|
<div className="border-b bg-gray-50 px-6 py-4">
|
2025-10-15 15:45:58 +09:00
|
|
|
<div className="flex items-center justify-between">
|
2025-10-15 13:32:20 +09:00
|
|
|
<div className="text-sm font-medium text-gray-700">
|
|
|
|
|
단계 {currentStep} / 2: {currentStep === 1 ? "데이터 소스 선택" : "데이터 설정 및 차트 설정"}
|
|
|
|
|
</div>
|
2025-10-14 13:59:54 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-10-15 13:32:20 +09:00
|
|
|
)}
|
2025-09-30 13:23:22 +09:00
|
|
|
|
2025-10-14 13:59:54 +09:00
|
|
|
{/* 단계별 내용 */}
|
2025-09-30 13:23:22 +09:00
|
|
|
<div className="flex-1 overflow-auto p-6">
|
2025-10-14 13:59:54 +09:00
|
|
|
{currentStep === 1 && (
|
|
|
|
|
<DataSourceSelector dataSource={dataSource} onTypeChange={handleDataSourceTypeChange} />
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{currentStep === 2 && (
|
2025-10-15 15:05:20 +09:00
|
|
|
<div className={`grid ${isSimpleWidget ? "grid-cols-1" : "grid-cols-2"} gap-6`}>
|
2025-10-14 15:25:11 +09:00
|
|
|
{/* 왼쪽: 데이터 설정 */}
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
{dataSource.type === "database" ? (
|
|
|
|
|
<>
|
|
|
|
|
<DatabaseConfig dataSource={dataSource} onChange={handleDataSourceUpdate} />
|
|
|
|
|
<QueryEditor
|
|
|
|
|
dataSource={dataSource}
|
|
|
|
|
onDataSourceChange={handleDataSourceUpdate}
|
|
|
|
|
onQueryTest={handleQueryTest}
|
|
|
|
|
/>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<ApiConfig dataSource={dataSource} onChange={handleDataSourceUpdate} onTestResult={handleQueryTest} />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-10-15 13:32:20 +09:00
|
|
|
{/* 오른쪽: 설정 패널 */}
|
|
|
|
|
{!isSimpleWidget && (
|
|
|
|
|
<div>
|
|
|
|
|
{isMapWidget ? (
|
|
|
|
|
// 지도 위젯: 위도/경도 매핑 패널
|
|
|
|
|
queryResult && queryResult.rows.length > 0 ? (
|
|
|
|
|
<VehicleMapConfigPanel
|
|
|
|
|
config={chartConfig}
|
|
|
|
|
queryResult={queryResult}
|
|
|
|
|
onConfigChange={handleChartConfigChange}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-8 text-center">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="mt-1 text-xs text-gray-500">데이터를 가져온 후 지도 설정이 표시됩니다</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
2025-10-15 15:05:20 +09:00
|
|
|
) : // 차트: 차트 설정 패널
|
|
|
|
|
queryResult && queryResult.rows.length > 0 ? (
|
|
|
|
|
<ChartConfigPanel
|
|
|
|
|
config={chartConfig}
|
|
|
|
|
queryResult={queryResult}
|
|
|
|
|
onConfigChange={handleChartConfigChange}
|
|
|
|
|
chartType={element.subtype}
|
|
|
|
|
dataSourceType={dataSource.type}
|
|
|
|
|
query={dataSource.query}
|
|
|
|
|
/>
|
2025-10-15 13:32:20 +09:00
|
|
|
) : (
|
2025-10-15 15:05:20 +09:00
|
|
|
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-8 text-center">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="mt-1 text-xs text-gray-500">데이터를 가져온 후 차트 설정이 표시됩니다</div>
|
2025-10-15 13:32:20 +09:00
|
|
|
</div>
|
2025-10-15 15:05:20 +09:00
|
|
|
</div>
|
2025-10-15 13:32:20 +09:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-10-14 13:59:54 +09:00
|
|
|
</div>
|
2025-09-30 13:23:22 +09:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 모달 푸터 */}
|
2025-10-14 13:59:54 +09:00
|
|
|
<div className="flex items-center justify-between border-t bg-gray-50 p-6">
|
2025-10-15 15:45:58 +09:00
|
|
|
<div>{queryResult && <Badge variant="default">{queryResult.rows.length}개 데이터 로드됨</Badge>}</div>
|
2025-10-14 10:12:40 +09:00
|
|
|
|
2025-09-30 13:23:22 +09:00
|
|
|
<div className="flex gap-3">
|
2025-10-15 13:32:20 +09:00
|
|
|
{!isSimpleWidget && currentStep > 1 && (
|
2025-10-14 13:59:54 +09:00
|
|
|
<Button variant="outline" onClick={handlePrev}>
|
|
|
|
|
<ChevronLeft className="mr-2 h-4 w-4" />
|
|
|
|
|
이전
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
<Button variant="outline" onClick={onClose}>
|
2025-09-30 13:23:22 +09:00
|
|
|
취소
|
2025-10-14 13:59:54 +09:00
|
|
|
</Button>
|
2025-10-14 15:25:11 +09:00
|
|
|
{currentStep === 1 ? (
|
2025-10-15 13:32:20 +09:00
|
|
|
// 1단계: 다음 버튼 (모든 타입 공통)
|
2025-10-14 13:59:54 +09:00
|
|
|
<Button onClick={handleNext}>
|
|
|
|
|
다음
|
|
|
|
|
<ChevronRight className="ml-2 h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
) : (
|
2025-10-15 13:32:20 +09:00
|
|
|
// 2단계: 저장 버튼 (모든 타입 공통)
|
2025-10-14 13:59:54 +09:00
|
|
|
<Button onClick={handleSave} disabled={!canSave}>
|
|
|
|
|
<Save className="mr-2 h-4 w-4" />
|
|
|
|
|
저장
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2025-09-30 13:23:22 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|