337 lines
13 KiB
TypeScript
337 lines
13 KiB
TypeScript
"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 { 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;
|
|
}
|
|
|
|
/**
|
|
* To-Do 위젯 설정 모달
|
|
* - 2단계 설정: 데이터 소스 → 쿼리 입력/테스트
|
|
*/
|
|
export function TodoWidgetConfigModal({ isOpen, element, onClose, onSave }: TodoWidgetConfigModalProps) {
|
|
const [currentStep, setCurrentStep] = useState<1 | 2>(1);
|
|
const [title, setTitle] = useState(element.title || "✅ To-Do / 긴급 지시");
|
|
const [dataSource, setDataSource] = useState<ChartDataSource>(
|
|
element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 },
|
|
);
|
|
const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
|
|
|
|
// 모달 열릴 때 element에서 설정 로드
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setTitle(element.title || "✅ To-Do / 긴급 지시");
|
|
if (element.dataSource) {
|
|
setDataSource(element.dataSource);
|
|
}
|
|
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;
|
|
}
|
|
|
|
onSave({
|
|
title,
|
|
dataSource,
|
|
});
|
|
|
|
onClose();
|
|
}, [title, dataSource, queryResult, 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-white shadow-xl">
|
|
{/* 헤더 */}
|
|
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
|
<div>
|
|
<h2 className="text-xl font-bold text-gray-800">To-Do 위젯 설정</h2>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
데이터 소스와 쿼리를 설정하면 자동으로 To-Do 목록이 표시됩니다
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700"
|
|
>
|
|
<X className="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* 진행 상태 */}
|
|
<div className="border-b border-gray-200 bg-gray-50 px-6 py-3">
|
|
<div className="flex items-center gap-4">
|
|
<div className={`flex items-center gap-2 ${currentStep === 1 ? "text-primary" : "text-gray-400"}`}>
|
|
<div
|
|
className={`flex h-8 w-8 items-center justify-center rounded-full font-semibold ${
|
|
currentStep === 1 ? "bg-primary text-white" : "bg-gray-200"
|
|
}`}
|
|
>
|
|
1
|
|
</div>
|
|
<span className="font-medium">데이터 소스 선택</span>
|
|
</div>
|
|
<ChevronRight className="h-4 w-4 text-gray-400" />
|
|
<div className={`flex items-center gap-2 ${currentStep === 2 ? "text-primary" : "text-gray-400"}`}>
|
|
<div
|
|
className={`flex h-8 w-8 items-center justify-center rounded-full font-semibold ${
|
|
currentStep === 2 ? "bg-primary text-white" : "bg-gray-200"
|
|
}`}
|
|
>
|
|
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-blue-50 p-4">
|
|
<h3 className="mb-2 font-semibold text-blue-900">💡 컬럼명 가이드</h3>
|
|
<p className="mb-2 text-sm text-blue-700">
|
|
쿼리 결과에 다음 컬럼명이 있으면 자동으로 To-Do 항목으로 변환됩니다:
|
|
</p>
|
|
<ul className="space-y-1 text-sm text-blue-600">
|
|
<li>
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">id</code> - 고유 ID (없으면 자동 생성)
|
|
</li>
|
|
<li>
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">title</code>,{" "}
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">task</code>,{" "}
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">name</code> - 제목 (필수)
|
|
</li>
|
|
<li>
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">description</code>,{" "}
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">desc</code>,{" "}
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">content</code> - 상세 설명
|
|
</li>
|
|
<li>
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">priority</code> - 우선순위 (urgent, high,
|
|
normal, low)
|
|
</li>
|
|
<li>
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">status</code> - 상태 (pending, in_progress,
|
|
completed)
|
|
</li>
|
|
<li>
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">assigned_to</code>,{" "}
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">assignedTo</code>,{" "}
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">user</code> - 담당자
|
|
</li>
|
|
<li>
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">due_date</code>,{" "}
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">dueDate</code>,{" "}
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">deadline</code> - 마감일
|
|
</li>
|
|
<li>
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">is_urgent</code>,{" "}
|
|
<code className="rounded bg-blue-100 px-1 py-0.5">isUrgent</code>,{" "}
|
|
<code className="rounded bg-blue-100 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-yellow-50 border-2 border-yellow-500 p-4">
|
|
<p className="text-sm font-bold text-yellow-900">
|
|
🔍 디버그: queryResult 상태 = {queryResult ? "있음" : "없음"}
|
|
</p>
|
|
{queryResult && (
|
|
<p className="text-xs text-yellow-700 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-green-50 border-2 border-green-500 p-4">
|
|
<h3 className="mb-2 font-semibold text-green-900">✅ 쿼리 테스트 성공!</h3>
|
|
<p className="text-sm text-green-700">
|
|
총 <strong>{queryResult.rows.length}개</strong>의 To-Do 항목을 찾았습니다.
|
|
</p>
|
|
<div className="mt-3 rounded bg-white p-3">
|
|
<p className="mb-2 text-xs font-semibold text-gray-600">첫 번째 데이터 미리보기:</p>
|
|
<pre className="overflow-x-auto text-xs text-gray-700">
|
|
{JSON.stringify(queryResult.rows[0], null, 2)}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 하단 버튼 */}
|
|
<div className="flex items-center justify-between border-t border-gray-200 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>
|
|
);
|
|
}
|
|
|