ERP-node/frontend/components/admin/dashboard/ElementConfigModal.tsx

403 lines
16 KiB
TypeScript
Raw Normal View History

2025-10-14 10:12:40 +09:00
"use client";
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";
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";
interface ElementConfigModalProps {
element: DashboardElement;
isOpen: boolean;
onClose: () => void;
onSave: (element: DashboardElement) => void;
}
/**
2025-10-14 13:59:54 +09:00
* ()
* - 2 플로우: 데이터
2025-10-14 13:59:54 +09:00
* -
*/
export function ElementConfigModal({ element, isOpen, onClose, onSave }: ElementConfigModalProps) {
const [dataSource, setDataSource] = useState<ChartDataSource>(
element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 },
);
2025-10-14 10:12:40 +09:00
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);
2025-10-15 15:05:20 +09:00
// 차트 설정이 필요 없는 위젯 (쿼리/API만 필요)
2025-10-15 15:05:20 +09:00
const isSimpleWidget =
2025-10-20 11:52:23 +09:00
element.subtype === "todo" || // To-Do 위젯
element.subtype === "booking-alert" || // 예약 알림 위젯
element.subtype === "maintenance" || // 정비 일정 위젯
element.subtype === "document" || // 문서 위젯
element.subtype === "risk-alert" || // 리스크 알림 위젯
2025-10-15 15:05:20 +09:00
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"; // 커스텀 통계 카드 위젯 (쿼리 필요)
2025-10-20 11:52:23 +09:00
// 자체 기능 위젯 (DB 연결 불필요, 헤더 설정만 가능)
const isSelfContainedWidget =
2025-10-20 11:52:23 +09:00
element.subtype === "weather" || // 날씨 위젯 (외부 API)
element.subtype === "exchange" || // 환율 위젯 (외부 API)
element.subtype === "calculator"; // 계산기 위젯 (자체 기능)
2025-10-15 15:05:20 +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-23 13:17:21 +09:00
const dataSourceToSet = element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 };
setDataSource(dataSourceToSet);
2025-10-14 13:59:54 +09:00
setChartConfig(element.chartConfig || {});
setQueryResult(null);
setCurrentStep(1);
setCustomTitle(element.customTitle || "");
2025-10-20 11:52:23 +09:00
setShowHeader(element.showHeader !== false); // showHeader 초기화
2025-10-23 13:17:21 +09:00
// 쿼리가 이미 있으면 자동 실행
if (dataSourceToSet.type === "database" && dataSourceToSet.query) {
console.log("🔄 기존 쿼리 자동 실행:", dataSourceToSet.query);
executeQueryAutomatically(dataSourceToSet);
}
2025-10-14 13:59:54 +09:00
}
}, [isOpen, element]);
2025-10-23 13:17:21 +09:00
// 쿼리 자동 실행 함수
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);
// 실패해도 모달은 열리도록 (사용자가 다시 실행 가능)
}
};
2025-10-14 13:59:54 +09:00
// 데이터 소스 타입 변경
const handleDataSourceTypeChange = useCallback((type: "database" | "api") => {
if (type === "database") {
setDataSource({
type: "database",
connectionType: "current",
refreshInterval: 0,
2025-10-14 13:59:54 +09:00
});
} else {
setDataSource({
type: "api",
method: "GET",
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 }));
}, []);
// 차트 설정 변경 처리
const handleChartConfigChange = useCallback((newConfig: ChartConfig) => {
setChartConfig(newConfig);
}, []);
// 쿼리 테스트 결과 처리
const handleQueryTest = useCallback((result: QueryResult) => {
setQueryResult(result);
// 쿼리가 변경되었으므로 차트 설정 초기화 (X/Y축 리셋)
// console.log("🔄 쿼리 변경 감지 - 차트 설정 초기화");
setChartConfig({});
}, []);
// 다음 단계로 이동
2025-10-14 13:59:54 +09:00
const handleNext = useCallback(() => {
if (currentStep === 1) {
setCurrentStep(2);
}
}, [currentStep]);
2025-10-14 13:59:54 +09:00
// 이전 단계로 이동
const handlePrev = useCallback(() => {
if (currentStep > 1) {
setCurrentStep((prev) => (prev - 1) as 1 | 2);
2025-10-14 13:59:54 +09:00
}
}, [currentStep]);
// 저장 처리
const handleSave = useCallback(() => {
const updatedElement: DashboardElement = {
...element,
dataSource,
chartConfig,
customTitle: customTitle.trim() || undefined, // 빈 문자열이면 undefined
showHeader, // 헤더 표시 여부
};
2025-10-15 15:05:20 +09:00
// console.log(" 저장할 element:", updatedElement);
2025-10-15 15:05:20 +09:00
onSave(updatedElement);
onClose();
}, [element, dataSource, chartConfig, customTitle, showHeader, onSave, onClose]);
// 모달이 열려있지 않으면 렌더링하지 않음
if (!isOpen) return null;
2025-10-20 11:52:23 +09:00
// 시계, 달력, 날씨, 환율, 계산기 위젯은 헤더 설정만 가능
const isHeaderOnlyWidget =
element.type === "widget" &&
(element.subtype === "clock" || element.subtype === "calendar" || isSelfContainedWidget);
// 기사관리 위젯은 자체 설정 UI를 가지고 있으므로 모달 표시하지 않음
if (element.type === "widget" && element.subtype === "driver-management") {
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));
// customTitle이 변경되었는지 확인
const isTitleChanged = customTitle.trim() !== (element.customTitle || "");
2025-10-20 11:52:23 +09:00
// showHeader가 변경되었는지 확인
const isHeaderChanged = showHeader !== (element.showHeader !== false);
2025-10-16 16:43:04 +09:00
const canSave =
isTitleChanged || // 제목만 변경해도 저장 가능
2025-10-20 11:52:23 +09:00
isHeaderChanged || // 헤더 표시 여부만 변경해도 저장 가능
(isSimpleWidget
2025-10-20 11:52:23 +09:00
? // 간단한 위젯: 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));
2025-10-14 13:59:54 +09:00
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 ${
2025-10-20 11:52:23 +09:00
currentStep === 1 && !isSimpleWidget ? "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>
</div>
<Button variant="ghost" size="icon" onClick={onClose} className="h-8 w-8">
<X className="h-5 w-5" />
</Button>
</div>
2025-10-16 16:43:04 +09:00
{/* 커스텀 제목 입력 */}
<div className="mt-4">
2025-10-16 16:43:04 +09:00
<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="예: 정비 일정 목록, 창고 위치 현황 등 (비워두면 자동 생성)"
2025-10-16 16:43:04 +09:00
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="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>
</div>
{/* 진행 상황 표시 - 간단한 위젯과 헤더 전용 위젯은 표시 안 함 */}
{!isSimpleWidget && !isHeaderOnlyWidget && (
<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">
<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-14 13:59:54 +09:00
{/* 단계별 내용 */}
{!isHeaderOnlyWidget && (
<div className="flex-1 overflow-auto p-6">
{currentStep === 1 && (
<DataSourceSelector dataSource={dataSource} onTypeChange={handleDataSourceTypeChange} />
)}
2025-10-14 13:59:54 +09:00
{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>
)}
{/* 모달 푸터 */}
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
<div className="flex gap-3">
{!isSimpleWidget && !isHeaderOnlyWidget && 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-10-14 13:59:54 +09:00
</Button>
{isHeaderOnlyWidget ? (
// 헤더 전용 위젯: 바로 저장
<Button onClick={handleSave}>
<Save className="mr-2 h-4 w-4" />
</Button>
) : currentStep === 1 ? (
2025-10-20 11:52:23 +09:00
// 1단계: 다음 버튼 (차트 위젯, 간단한 위젯 모두)
2025-10-14 13:59:54 +09:00
<Button onClick={handleNext}>
<ChevronRight className="ml-2 h-4 w-4" />
</Button>
) : (
// 2단계: 저장 버튼
2025-10-14 13:59:54 +09:00
<Button onClick={handleSave} disabled={!canSave}>
<Save className="mr-2 h-4 w-4" />
</Button>
)}
</div>
</div>
</div>
</div>
);
}