357 lines
14 KiB
TypeScript
357 lines
14 KiB
TypeScript
"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, Save } from "lucide-react";
|
|
|
|
interface ElementConfigModalProps {
|
|
element: DashboardElement;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSave: (element: DashboardElement) => void;
|
|
}
|
|
|
|
/**
|
|
* 요소 설정 모달 컴포넌트 (리팩토링)
|
|
* - 2단계 플로우: 데이터 소스 선택 → 데이터 설정 및 차트 설정
|
|
* - 새로운 데이터 소스 컴포넌트 통합
|
|
*/
|
|
export function ElementConfigModal({ element, isOpen, onClose, onSave }: ElementConfigModalProps) {
|
|
const [dataSource, setDataSource] = useState<ChartDataSource>(
|
|
element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 },
|
|
);
|
|
const [chartConfig, setChartConfig] = useState<ChartConfig>(element.chartConfig || {});
|
|
const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
|
|
const [currentStep, setCurrentStep] = useState<1 | 2>(1);
|
|
const [customTitle, setCustomTitle] = useState<string>(element.customTitle || "");
|
|
const [showHeader, setShowHeader] = useState<boolean>(element.showHeader !== false);
|
|
|
|
// 차트 설정이 필요 없는 위젯 (쿼리/API만 필요)
|
|
const isSimpleWidget =
|
|
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";
|
|
|
|
// 지도 위젯 (위도/경도 매핑 필요)
|
|
const isMapWidget = element.subtype === "vehicle-map" || element.subtype === "map-summary";
|
|
|
|
// 주석
|
|
// 모달이 열릴 때 초기화
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setDataSource(element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 });
|
|
setChartConfig(element.chartConfig || {});
|
|
setQueryResult(null);
|
|
setCurrentStep(1);
|
|
setCustomTitle(element.customTitle || "");
|
|
}
|
|
}, [isOpen, element]);
|
|
|
|
// 데이터 소스 타입 변경
|
|
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);
|
|
|
|
// 쿼리가 변경되었으므로 차트 설정 초기화 (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;
|
|
|
|
// 시계, 달력, To-Do 위젯은 헤더 설정만 가능
|
|
const isHeaderOnlyWidget = element.type === "widget" && (element.subtype === "clock" || element.subtype === "calendar" || element.subtype === "todo");
|
|
|
|
// 기사관리 위젯은 자체 설정 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 || "");
|
|
|
|
const canSave =
|
|
isTitleChanged || // 제목만 변경해도 저장 가능
|
|
(isSimpleWidget
|
|
? // 간단한 위젯: 2단계에서 쿼리 테스트 후 저장 가능
|
|
currentStep === 2 && queryResult && queryResult.rows.length > 0
|
|
: isMapWidget
|
|
? // 지도 위젯: 위도/경도 매핑 필요
|
|
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 (
|
|
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
<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"
|
|
}`}
|
|
>
|
|
{/* 모달 헤더 */}
|
|
<div className="border-b p-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex-1">
|
|
<h2 className="text-xl font-semibold text-gray-900">{element.title} 설정</h2>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
{isSimpleWidget
|
|
? "데이터 소스를 설정하세요"
|
|
: currentStep === 1
|
|
? "데이터 소스를 선택하세요"
|
|
: "쿼리를 실행하고 차트를 설정하세요"}
|
|
</p>
|
|
</div>
|
|
<Button variant="ghost" size="icon" onClick={onClose} className="h-8 w-8">
|
|
<X className="h-5 w-5" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 커스텀 제목 입력 */}
|
|
<div className="mt-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) => {
|
|
// 모든 키보드 이벤트를 input 필드 내부에서만 처리
|
|
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"
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500">비워두면 테이블명으로 자동 생성됩니다 (예: "maintenance_schedules 목록")</p>
|
|
</div>
|
|
|
|
{/* 헤더 표시 옵션 */}
|
|
<div className="mt-3 flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
id="showHeader"
|
|
checked={showHeader}
|
|
onChange={(e) => setShowHeader(e.target.checked)}
|
|
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary"
|
|
/>
|
|
<label htmlFor="showHeader" className="text-sm font-medium text-gray-700">
|
|
위젯 헤더 표시 (제목 + 새로고침 버튼)
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 진행 상황 표시 - 간단한 위젯과 헤더 전용 위젯은 표시 안 함 */}
|
|
{!isSimpleWidget && !isHeaderOnlyWidget && (
|
|
<div className="border-b bg-gray-50 px-6 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="text-sm font-medium text-gray-700">
|
|
단계 {currentStep} / 2: {currentStep === 1 ? "데이터 소스 선택" : "데이터 설정 및 차트 설정"}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 단계별 내용 */}
|
|
{!isHeaderOnlyWidget && (
|
|
<div className="flex-1 overflow-auto p-6">
|
|
{currentStep === 1 && (
|
|
<DataSourceSelector dataSource={dataSource} onTypeChange={handleDataSourceTypeChange} />
|
|
)}
|
|
|
|
{currentStep === 2 && (
|
|
<div className={`grid ${isSimpleWidget ? "grid-cols-1" : "grid-cols-2"} gap-6`}>
|
|
{/* 왼쪽: 데이터 설정 */}
|
|
<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>
|
|
|
|
{/* 오른쪽: 설정 패널 */}
|
|
{!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>
|
|
)
|
|
) : // 차트: 차트 설정 패널
|
|
queryResult && queryResult.rows.length > 0 ? (
|
|
<ChartConfigPanel
|
|
config={chartConfig}
|
|
queryResult={queryResult}
|
|
onConfigChange={handleChartConfigChange}
|
|
chartType={element.subtype}
|
|
dataSourceType={dataSource.type}
|
|
query={dataSource.query}
|
|
/>
|
|
) : (
|
|
<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>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 모달 푸터 */}
|
|
<div className="flex items-center justify-between border-t bg-gray-50 p-6">
|
|
<div>{queryResult && <Badge variant="default">{queryResult.rows.length}개 데이터 로드됨</Badge>}</div>
|
|
|
|
<div className="flex gap-3">
|
|
{!isSimpleWidget && !isHeaderOnlyWidget && currentStep > 1 && (
|
|
<Button variant="outline" onClick={handlePrev}>
|
|
<ChevronLeft className="mr-2 h-4 w-4" />
|
|
이전
|
|
</Button>
|
|
)}
|
|
<Button variant="outline" onClick={onClose}>
|
|
취소
|
|
</Button>
|
|
{isHeaderOnlyWidget ? (
|
|
// 헤더 전용 위젯: 바로 저장
|
|
<Button onClick={handleSave}>
|
|
<Save className="mr-2 h-4 w-4" />
|
|
저장
|
|
</Button>
|
|
) : currentStep === 1 ? (
|
|
// 1단계: 다음 버튼
|
|
<Button onClick={handleNext}>
|
|
다음
|
|
<ChevronRight className="ml-2 h-4 w-4" />
|
|
</Button>
|
|
) : (
|
|
// 2단계: 저장 버튼
|
|
<Button onClick={handleSave} disabled={!canSave}>
|
|
<Save className="mr-2 h-4 w-4" />
|
|
저장
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|