안쓰는 코드 삭제

This commit is contained in:
dohyeons 2025-10-29 18:30:50 +09:00
parent c6a51279d6
commit 2959f66e0c
6 changed files with 7 additions and 1615 deletions

View File

@ -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 (
<div className="absolute top-5 left-5 bg-background p-3 rounded-lg shadow-lg z-50 flex gap-3">
<button
onClick={onClearCanvas}
className="
px-4 py-2 border border-border bg-background rounded-md
text-sm font-medium text-foreground
hover:bg-muted hover:border-border/80
transition-colors duration-200
"
>
🗑
</button>
<button
onClick={onSaveLayout}
className="
px-4 py-2 border border-border bg-background rounded-md
text-sm font-medium text-foreground
hover:bg-muted hover:border-border/80
transition-colors duration-200
"
>
💾
</button>
{/* 캔버스 배경색 변경 버튼 */}
<div className="relative">
<button
onClick={() => setShowColorPicker(!showColorPicker)}
className="
px-4 py-2 border border-border bg-background rounded-md
text-sm font-medium text-foreground
hover:bg-muted hover:border-border/80
transition-colors duration-200
flex items-center gap-2
"
>
🎨
<div
className="w-4 h-4 rounded border border-border"
style={{ backgroundColor: canvasBackgroundColor }}
/>
</button>
{/* 색상 선택 패널 */}
{showColorPicker && (
<div className="absolute top-full left-0 mt-2 bg-background p-4 rounded-lg shadow-xl z-50 border border-border w-[280px]">
<div className="flex items-center gap-3 mb-3">
<input
type="color"
value={canvasBackgroundColor}
onChange={(e) => onCanvasBackgroundColorChange(e.target.value)}
className="h-10 w-16 border border-border rounded cursor-pointer"
/>
<input
type="text"
value={canvasBackgroundColor}
onChange={(e) => onCanvasBackgroundColorChange(e.target.value)}
placeholder="#ffffff"
className="flex-1 px-2 py-1 text-sm border border-border rounded"
/>
</div>
{/* 프리셋 색상 */}
<div className="grid grid-cols-6 gap-2 mb-3">
{[
'#ffffff', '#f9fafb', '#f3f4f6', '#e5e7eb',
'#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b',
'#10b981', '#06b6d4', '#6366f1', '#84cc16',
].map((color) => (
<button
key={color}
onClick={() => onCanvasBackgroundColorChange(color)}
className={`h-8 rounded border-2 ${canvasBackgroundColor === color ? 'border-primary ring-2 ring-primary/20' : 'border-border'}`}
style={{ backgroundColor: color }}
title={color}
/>
))}
</div>
<button
onClick={() => setShowColorPicker(false)}
className="w-full px-3 py-1.5 text-sm text-foreground border border-border rounded hover:bg-muted"
>
</button>
</div>
)}
</div>
</div>
);
}

View File

@ -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<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 === "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<ChartDataSource>) => {
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 (
<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-background shadow-2xl ${
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-foreground">{element.title} </h2>
</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-foreground"> ()</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-border px-3 py-2 text-sm focus:ring-1 focus:outline-none"
/>
<p className="mt-1 text-xs text-muted-foreground">
(: "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-border"
/>
<label htmlFor="showHeader" className="text-sm font-medium text-foreground">
( + )
</label>
</div>
</div>
{/* 진행 상황 표시 - 간단한 위젯과 헤더 전용 위젯은 표시 안 함 */}
{!isSimpleWidget && !isHeaderOnlyWidget && (
<div className="border-b bg-muted px-6 py-4">
<div className="flex items-center justify-between">
<div className="text-sm font-medium text-foreground">
{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 ? (
// 지도 위젯: 위도/경도 매핑 패널
element.subtype === "map-test" ? (
// 🧪 지도 테스트 위젯: 타일맵 URL 필수, 마커 데이터 선택사항
<MapTestConfigPanel
config={chartConfig}
queryResult={queryResult || undefined}
onConfigChange={handleChartConfigChange}
/>
) : 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-border bg-muted p-8 text-center">
<div>
<div className="mt-1 text-xs text-muted-foreground"> </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-border bg-muted p-8 text-center">
<div>
<div className="mt-1 text-xs text-muted-foreground"> </div>
</div>
</div>
)}
</div>
)}
</div>
)}
</div>
)}
{/* 모달 푸터 */}
<div className="flex items-center justify-between border-t bg-muted 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>
);
}

View File

@ -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";

View File

@ -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<DashboardElement>) => 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<ChartDataSource>(
element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 },
);
const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
const [listConfig, setListConfig] = useState<ListWidgetConfig>(
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<ChartDataSource>) => {
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 (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="flex max-h-[90vh] w-[90vw] max-w-6xl flex-col rounded-xl border bg-background shadow-2xl">
{/* 헤더 */}
<div className="space-y-4 border-b px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold">📋 </h2>
<p className="mt-1 text-sm text-foreground"> </p>
</div>
<button onClick={onClose} className="rounded-lg p-2 transition-colors hover:bg-muted">
<X className="h-5 w-5" />
</button>
</div>
{/* 제목 입력 */}
<div>
<Label htmlFor="list-title" className="text-sm font-medium">
</Label>
<Input
id="list-title"
value={title}
onChange={(e) => setTitle(e.target.value)}
onKeyDown={(e) => {
// 모든 키보드 이벤트를 input 필드 내부에서만 처리
e.stopPropagation();
}}
placeholder="예: 사용자 목록"
className="mt-1"
/>
</div>
{/* 참고: 리스트 위젯은 제목이 항상 표시됩니다 */}
<div className="rounded bg-primary/10 p-2 text-xs text-primary">💡 </div>
</div>
{/* 진행 상태 표시 */}
<div className="border-b bg-muted px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className={`flex items-center gap-2 ${currentStep >= 1 ? "text-primary" : "text-muted-foreground"}`}>
<div
className={`flex h-8 w-8 items-center justify-center rounded-full ${currentStep >= 1 ? "bg-primary text-white" : "bg-muted"}`}
>
1
</div>
<span className="text-sm font-medium"> </span>
</div>
<div className="h-0.5 w-12 bg-muted" />
<div className={`flex items-center gap-2 ${currentStep >= 2 ? "text-primary" : "text-muted-foreground"}`}>
<div
className={`flex h-8 w-8 items-center justify-center rounded-full ${currentStep >= 2 ? "bg-primary text-white" : "bg-muted"}`}
>
2
</div>
<span className="text-sm font-medium"> </span>
</div>
<div className="h-0.5 w-12 bg-muted" />
<div className={`flex items-center gap-2 ${currentStep >= 3 ? "text-primary" : "text-muted-foreground"}`}>
<div
className={`flex h-8 w-8 items-center justify-center rounded-full ${currentStep >= 3 ? "bg-primary text-white" : "bg-muted"}`}
>
3
</div>
<span className="text-sm font-medium"> </span>
</div>
</div>
</div>
</div>
{/* 컨텐츠 */}
<div className="flex-1 overflow-y-auto p-6">
{currentStep === 1 && (
<DataSourceSelector dataSource={dataSource} onTypeChange={handleDataSourceTypeChange} />
)}
{currentStep === 2 && (
<div className="grid grid-cols-2 gap-6">
{/* 왼쪽: 데이터 소스 설정 */}
<div>
{dataSource.type === "database" ? (
<DatabaseConfig dataSource={dataSource} onChange={handleDataSourceUpdate} />
) : (
<ApiConfig dataSource={dataSource} onChange={handleDataSourceUpdate} onTestResult={handleQueryTest} />
)}
{dataSource.type === "database" && (
<div className="mt-4">
<QueryEditor
dataSource={dataSource}
onDataSourceChange={handleDataSourceUpdate}
onQueryTest={handleQueryTest}
/>
</div>
)}
</div>
{/* 오른쪽: 데이터 미리보기 */}
<div>
{queryResult && queryResult.rows.length > 0 ? (
<div className="rounded-lg border bg-muted p-4">
<h3 className="mb-3 font-semibold text-foreground">📋 </h3>
<div className="overflow-x-auto rounded bg-background p-3">
<Badge variant="secondary" className="mb-2">
{queryResult.totalRows}
</Badge>
<pre className="text-xs text-foreground">
{JSON.stringify(queryResult.rows.slice(0, 3), null, 2)}
</pre>
</div>
</div>
) : (
<div className="flex h-full items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted p-8 text-center">
<div>
<div className="mt-1 text-xs text-muted-foreground"> </div>
</div>
</div>
)}
</div>
</div>
)}
{currentStep === 3 && queryResult && (
<div className="space-y-6">
{listConfig.columnMode === "auto" ? (
<ColumnSelector
availableColumns={queryResult.columns}
selectedColumns={listConfig.columns}
sampleData={queryResult.rows[0]}
onChange={(columns) => setListConfig((prev) => ({ ...prev, columns }))}
/>
) : (
<ManualColumnEditor
availableFields={queryResult.columns}
columns={listConfig.columns}
onChange={(columns) => setListConfig((prev) => ({ ...prev, columns }))}
/>
)}
<ListTableOptions
config={listConfig}
onChange={(updates) => setListConfig((prev) => ({ ...prev, ...updates }))}
/>
</div>
)}
</div>
{/* 푸터 */}
<div className="flex items-center justify-between border-t bg-muted p-6">
<div>
{queryResult && (
<Badge variant="default" className="bg-success">
📊 {queryResult.rows.length}
</Badge>
)}
</div>
<div className="flex gap-3">
{currentStep > 1 && (
<Button variant="outline" onClick={handlePrev}>
<ChevronLeft className="mr-2 h-4 w-4" />
</Button>
)}
<Button variant="outline" onClick={onClose}>
</Button>
{currentStep < 3 ? (
<Button onClick={handleNext} disabled={currentStep === 2 && !queryResult}>
<ChevronRight className="ml-2 h-4 w-4" />
</Button>
) : (
<Button onClick={handleSave} disabled={!canSave}>
<Save className="mr-2 h-4 w-4" />
</Button>
)}
</div>
</div>
</div>
</div>
);
}

View File

@ -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<DashboardElement>) => 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<ChartDataSource>(
element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 },
);
const [queryResult, setQueryResult] = useState<QueryResult | null>(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<ChartDataSource>) => {
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 (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50">
<div className="relative flex h-[90vh] w-[90vw] max-w-6xl flex-col rounded-lg bg-background shadow-xl">
{/* 헤더 */}
<div className="flex items-center justify-between border-b border-border px-6 py-4">
<div>
<h2 className="text-xl font-bold text-foreground"> </h2>
<p className="mt-1 text-sm text-muted-foreground">
</p>
</div>
<button
onClick={onClose}
className="rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
<X className="h-5 w-5" />
</button>
</div>
{/* 진행 상태 */}
<div className="border-b border-border bg-muted px-6 py-3">
<div className="flex items-center gap-4">
<div className={`flex items-center gap-2 ${currentStep === 1 ? "text-primary" : "text-muted-foreground"}`}>
<div
className={`flex h-8 w-8 items-center justify-center rounded-full font-semibold ${
currentStep === 1 ? "bg-primary text-white" : "bg-muted"
}`}
>
1
</div>
<span className="font-medium"> </span>
</div>
<ChevronRight className="h-4 w-4 text-muted-foreground" />
<div className={`flex items-center gap-2 ${currentStep === 2 ? "text-primary" : "text-muted-foreground"}`}>
<div
className={`flex h-8 w-8 items-center justify-center rounded-full font-semibold ${
currentStep === 2 ? "bg-primary text-white" : "bg-muted"
}`}
>
2
</div>
<span className="font-medium"> </span>
</div>
</div>
</div>
{/* 본문 */}
<div className="flex-1 overflow-y-auto p-6">
{/* Step 1: 데이터 소스 선택 */}
{currentStep === 1 && (
<div className="space-y-6">
<div>
<Label className="text-base font-semibold"></Label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="예: 오늘의 일정"
className="mt-2"
/>
</div>
<div>
<Label className="text-base font-semibold"> </Label>
<DataSourceSelector
dataSource={dataSource}
onTypeChange={handleDataSourceTypeChange}
/>
</div>
{dataSource.type === "database" && (
<DatabaseConfig dataSource={dataSource} onUpdate={handleDataSourceUpdate} />
)}
{dataSource.type === "api" && <ApiConfig dataSource={dataSource} onUpdate={handleDataSourceUpdate} />}
</div>
)}
{/* Step 2: 쿼리 입력 및 테스트 */}
{currentStep === 2 && (
<div className="space-y-6">
<div>
<div className="mb-4 rounded-lg bg-primary/10 p-4">
<h3 className="mb-2 font-semibold text-primary">💡 </h3>
<p className="mb-2 text-sm text-primary">
:
</p>
<ul className="space-y-1 text-sm text-primary">
<li>
<code className="rounded bg-primary/10 px-1 py-0.5">id</code> - ID ( )
</li>
<li>
<code className="rounded bg-primary/10 px-1 py-0.5">title</code>,{" "}
<code className="rounded bg-primary/10 px-1 py-0.5">task</code>,{" "}
<code className="rounded bg-primary/10 px-1 py-0.5">name</code> - ()
</li>
<li>
<code className="rounded bg-primary/10 px-1 py-0.5">description</code>,{" "}
<code className="rounded bg-primary/10 px-1 py-0.5">desc</code>,{" "}
<code className="rounded bg-primary/10 px-1 py-0.5">content</code> -
</li>
<li>
<code className="rounded bg-primary/10 px-1 py-0.5">priority</code> - (urgent, high,
normal, low)
</li>
<li>
<code className="rounded bg-primary/10 px-1 py-0.5">status</code> - (pending, in_progress,
completed)
</li>
<li>
<code className="rounded bg-primary/10 px-1 py-0.5">assigned_to</code>,{" "}
<code className="rounded bg-primary/10 px-1 py-0.5">assignedTo</code>,{" "}
<code className="rounded bg-primary/10 px-1 py-0.5">user</code> -
</li>
<li>
<code className="rounded bg-primary/10 px-1 py-0.5">due_date</code>,{" "}
<code className="rounded bg-primary/10 px-1 py-0.5">dueDate</code>,{" "}
<code className="rounded bg-primary/10 px-1 py-0.5">deadline</code> -
</li>
<li>
<code className="rounded bg-primary/10 px-1 py-0.5">is_urgent</code>,{" "}
<code className="rounded bg-primary/10 px-1 py-0.5">isUrgent</code>,{" "}
<code className="rounded bg-primary/10 px-1 py-0.5">urgent</code> -
</li>
</ul>
</div>
<QueryEditor
dataSource={dataSource}
onDataSourceChange={handleDataSourceUpdate}
onQueryTest={handleQueryTest}
/>
</div>
{/* 디버그: 항상 표시되는 테스트 메시지 */}
<div className="mt-4 rounded-lg bg-warning/10 border-2 border-warning p-4">
<p className="text-sm font-bold text-warning">
🔍 디버그: queryResult = {queryResult ? "있음" : "없음"}
</p>
{queryResult && (
<p className="text-xs text-warning mt-1">
rows: {queryResult.rows?.length}, error: {queryResult.error || "없음"}
</p>
)}
</div>
{queryResult && !queryResult.error && queryResult.rows && queryResult.rows.length > 0 && (
<div className="mt-4 rounded-lg bg-success/10 border-2 border-success p-4">
<h3 className="mb-2 font-semibold text-success"> !</h3>
<p className="text-sm text-success">
<strong>{queryResult.rows.length}</strong> .
</p>
<div className="mt-3 rounded bg-background p-3">
<p className="mb-2 text-xs font-semibold text-foreground"> :</p>
<pre className="overflow-x-auto text-xs text-foreground">
{JSON.stringify(queryResult.rows[0], null, 2)}
</pre>
</div>
</div>
)}
{/* 데이터베이스 연동 쿼리 (선택사항) */}
<div className="mt-6 space-y-4 rounded-lg border-2 border-purple-500 bg-purple-500/10 p-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-purple-700">🔗 ()</h3>
<p className="text-sm text-purple-700">
//
</p>
</div>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={enableDbSync}
onChange={(e) => setEnableDbSync(e.target.checked)}
className="h-4 w-4 rounded border-purple-500/50"
/>
<span className="text-sm font-medium text-purple-700"></span>
</label>
</div>
{enableDbSync && (
<>
{/* 모드 선택 */}
<div className="flex gap-2">
<button
onClick={() => setDbSyncMode("simple")}
className={`flex-1 rounded px-4 py-2 text-sm font-medium transition-colors ${
dbSyncMode === "simple"
? "bg-purple-500 text-white"
: "bg-background text-purple-500 hover:bg-purple-500/10"
}`}
>
</button>
<button
onClick={() => setDbSyncMode("advanced")}
className={`flex-1 rounded px-4 py-2 text-sm font-medium transition-colors ${
dbSyncMode === "advanced"
? "bg-purple-500 text-white"
: "bg-background text-purple-500 hover:bg-purple-500/10"
}`}
>
</button>
</div>
{/* 간편 모드 */}
{dbSyncMode === "simple" && (
<div className="space-y-4 rounded-lg border border-purple-500/50 bg-background p-4">
<p className="text-sm text-purple-700">
INSERT/UPDATE/DELETE .
</p>
{/* 테이블명 */}
<div>
<Label className="text-sm font-semibold text-purple-700"> *</Label>
<Input
value={tableName}
onChange={(e) => setTableName(e.target.value)}
placeholder="예: tasks"
className="mt-2"
/>
</div>
{/* 컬럼 매핑 */}
<div>
<Label className="text-sm font-semibold text-purple-700"> </Label>
<div className="mt-2 grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-foreground">ID </label>
<Input
value={columnMapping.id}
onChange={(e) => setColumnMapping({ ...columnMapping, id: e.target.value })}
placeholder="id"
className="mt-1 h-8 text-sm"
/>
</div>
<div>
<label className="text-xs text-foreground"> </label>
<Input
value={columnMapping.title}
onChange={(e) => setColumnMapping({ ...columnMapping, title: e.target.value })}
placeholder="title"
className="mt-1 h-8 text-sm"
/>
</div>
<div>
<label className="text-xs text-foreground"> </label>
<Input
value={columnMapping.description}
onChange={(e) => setColumnMapping({ ...columnMapping, description: e.target.value })}
placeholder="description"
className="mt-1 h-8 text-sm"
/>
</div>
<div>
<label className="text-xs text-foreground"> </label>
<Input
value={columnMapping.priority}
onChange={(e) => setColumnMapping({ ...columnMapping, priority: e.target.value })}
placeholder="priority"
className="mt-1 h-8 text-sm"
/>
</div>
<div>
<label className="text-xs text-foreground"> </label>
<Input
value={columnMapping.status}
onChange={(e) => setColumnMapping({ ...columnMapping, status: e.target.value })}
placeholder="status"
className="mt-1 h-8 text-sm"
/>
</div>
<div>
<label className="text-xs text-foreground"> </label>
<Input
value={columnMapping.assignedTo}
onChange={(e) => setColumnMapping({ ...columnMapping, assignedTo: e.target.value })}
placeholder="assigned_to"
className="mt-1 h-8 text-sm"
/>
</div>
<div>
<label className="text-xs text-foreground"> </label>
<Input
value={columnMapping.dueDate}
onChange={(e) => setColumnMapping({ ...columnMapping, dueDate: e.target.value })}
placeholder="due_date"
className="mt-1 h-8 text-sm"
/>
</div>
<div>
<label className="text-xs text-foreground"> </label>
<Input
value={columnMapping.isUrgent}
onChange={(e) => setColumnMapping({ ...columnMapping, isUrgent: e.target.value })}
placeholder="is_urgent"
className="mt-1 h-8 text-sm"
/>
</div>
</div>
</div>
</div>
)}
{/* 고급 모드 */}
{dbSyncMode === "advanced" && (
<div className="space-y-4">
<p className="text-sm text-purple-700">
.
</p>
{/* INSERT 쿼리 */}
<div>
<Label className="text-sm font-semibold text-purple-700">INSERT ()</Label>
<p className="mb-2 text-xs text-purple-500">
변수: ${"{title}"}, ${"{description}"}, ${"{priority}"}, ${"{status}"}, ${"{assignedTo}"}, ${"{dueDate}"}, ${"{isUrgent}"}
</p>
<textarea
value={element.chartConfig?.insertQuery || ""}
onChange={(e) => {
const updates = {
...element,
chartConfig: {
...element.chartConfig,
insertQuery: e.target.value,
},
};
Object.assign(element, updates);
}}
placeholder="예: INSERT INTO tasks (title, description, status) VALUES ('${title}', '${description}', '${status}')"
className="h-20 w-full rounded border border-purple-500/50 bg-background px-3 py-2 text-sm font-mono focus:border-purple-500 focus:outline-none"
/>
</div>
{/* UPDATE 쿼리 */}
<div>
<Label className="text-sm font-semibold text-purple-700">UPDATE ( )</Label>
<p className="mb-2 text-xs text-purple-500">
변수: ${"{id}"}, ${"{status}"}
</p>
<textarea
value={element.chartConfig?.updateQuery || ""}
onChange={(e) => {
const updates = {
...element,
chartConfig: {
...element.chartConfig,
updateQuery: e.target.value,
},
};
Object.assign(element, updates);
}}
placeholder="예: UPDATE tasks SET status = '${status}' WHERE id = ${id}"
className="h-20 w-full rounded border border-purple-500/50 bg-background px-3 py-2 text-sm font-mono focus:border-purple-500 focus:outline-none"
/>
</div>
{/* DELETE 쿼리 */}
<div>
<Label className="text-sm font-semibold text-purple-700">DELETE ()</Label>
<p className="mb-2 text-xs text-purple-500">
변수: ${"{id}"}
</p>
<textarea
value={element.chartConfig?.deleteQuery || ""}
onChange={(e) => {
const updates = {
...element,
chartConfig: {
...element.chartConfig,
deleteQuery: e.target.value,
},
};
Object.assign(element, updates);
}}
placeholder="예: DELETE FROM tasks WHERE id = ${id}"
className="h-20 w-full rounded border border-purple-500/50 bg-background px-3 py-2 text-sm font-mono focus:border-purple-500 focus:outline-none"
/>
</div>
</div>
)}
</>
)}
</div>
</div>
)}
</div>
{/* 하단 버튼 */}
<div className="flex items-center justify-between border-t border-border px-6 py-4">
<div>
{currentStep > 1 && (
<Button onClick={handlePrev} variant="outline">
<ChevronLeft className="mr-1 h-4 w-4" />
</Button>
)}
</div>
<div className="flex gap-2">
<Button onClick={onClose} variant="outline">
</Button>
{currentStep < 2 ? (
<Button onClick={handleNext}>
<ChevronRight className="ml-1 h-4 w-4" />
</Button>
) : (
<Button
onClick={handleSave}
disabled={(() => {
const isDisabled = !queryResult || queryResult.error || !queryResult.rows || queryResult.rows.length === 0;
// console.log("💾 저장 버튼 disabled:", isDisabled);
// console.log("💾 queryResult:", queryResult);
return isDisabled;
})()}
>
<Save className="mr-1 h-4 w-4" />
</Button>
)}
</div>
</div>
</div>
</div>
);
}

View File

@ -1,79 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { DashboardElement } from "../types";
interface YardWidgetConfigModalProps {
element: DashboardElement;
isOpen: boolean;
onClose: () => void;
onSave: (updates: Partial<DashboardElement>) => void;
}
export function YardWidgetConfigModal({ element, isOpen, onClose, onSave }: YardWidgetConfigModalProps) {
const [customTitle, setCustomTitle] = useState(element.customTitle || "");
const [showHeader, setShowHeader] = useState(element.showHeader !== false);
useEffect(() => {
if (isOpen) {
setCustomTitle(element.customTitle || "");
setShowHeader(element.showHeader !== false);
}
}, [isOpen, element]);
const handleSave = () => {
onSave({
customTitle,
showHeader,
});
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent onPointerDown={(e) => e.stopPropagation()} className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle> </DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
{/* 위젯 제목 */}
<div className="space-y-2">
<Label htmlFor="customTitle"> </Label>
<Input
id="customTitle"
value={customTitle}
onChange={(e) => setCustomTitle(e.target.value)}
placeholder="제목을 입력하세요 (비워두면 기본 제목 사용)"
/>
<p className="text-xs text-muted-foreground"> 제목: 야드 3D</p>
</div>
{/* 헤더 표시 여부 */}
<div className="flex items-center space-x-2">
<Checkbox
id="showHeader"
checked={showHeader}
onCheckedChange={(checked) => setShowHeader(checked === true)}
/>
<Label htmlFor="showHeader" className="cursor-pointer text-sm font-normal">
</Label>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={onClose}>
</Button>
<Button onClick={handleSave}></Button>
</div>
</DialogContent>
</Dialog>
);
}