2025-10-22 13:40:15 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import React, { useState, useCallback, useEffect } from "react";
|
|
|
|
|
import { DashboardElement, ChartDataSource, QueryResult, ListWidgetConfig } from "../types";
|
|
|
|
|
import { X } from "lucide-react";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
|
|
|
import { DatabaseConfig } from "../data-sources/DatabaseConfig";
|
|
|
|
|
import { ApiConfig } from "../data-sources/ApiConfig";
|
|
|
|
|
import { QueryEditor } from "../QueryEditor";
|
2025-10-22 15:29:57 +09:00
|
|
|
import { UnifiedColumnEditor } from "./list-widget/UnifiedColumnEditor";
|
2025-10-22 13:40:15 +09:00
|
|
|
import { ListTableOptions } from "./list-widget/ListTableOptions";
|
|
|
|
|
|
|
|
|
|
interface ListWidgetConfigSidebarProps {
|
|
|
|
|
element: DashboardElement;
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
onApply: (element: DashboardElement) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 리스트 위젯 설정 사이드바
|
|
|
|
|
*/
|
|
|
|
|
export function ListWidgetConfigSidebar({ element, isOpen, onClose, onApply }: ListWidgetConfigSidebarProps) {
|
|
|
|
|
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 || {
|
|
|
|
|
viewMode: "table",
|
|
|
|
|
columns: [],
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
enablePagination: true,
|
|
|
|
|
showHeader: true,
|
|
|
|
|
stripedRows: true,
|
|
|
|
|
compactMode: false,
|
|
|
|
|
cardColumns: 3,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 사이드바 열릴 때 초기화
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isOpen) {
|
|
|
|
|
setTitle(element.title || "📋 리스트");
|
|
|
|
|
if (element.dataSource) {
|
|
|
|
|
setDataSource(element.dataSource);
|
|
|
|
|
}
|
|
|
|
|
if (element.listConfig) {
|
|
|
|
|
setListConfig(element.listConfig);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [isOpen, element]);
|
|
|
|
|
|
|
|
|
|
// Esc 키로 닫기
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isOpen) return;
|
|
|
|
|
|
|
|
|
|
const handleEsc = (e: KeyboardEvent) => {
|
|
|
|
|
if (e.key === "Escape") {
|
|
|
|
|
onClose();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.addEventListener("keydown", handleEsc);
|
|
|
|
|
return () => window.removeEventListener("keydown", handleEsc);
|
|
|
|
|
}, [isOpen, onClose]);
|
|
|
|
|
|
|
|
|
|
// 데이터 소스 타입 변경
|
|
|
|
|
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);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 데이터 소스 업데이트
|
|
|
|
|
const handleDataSourceUpdate = useCallback((updates: Partial<ChartDataSource>) => {
|
|
|
|
|
setDataSource((prev) => ({ ...prev, ...updates }));
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 쿼리 실행 결과 처리
|
|
|
|
|
const handleQueryTest = useCallback((result: QueryResult) => {
|
|
|
|
|
setQueryResult(result);
|
|
|
|
|
|
2025-10-28 15:42:53 +09:00
|
|
|
// 쿼리 실행 시마다 컬럼 설정 초기화 (새로운 쿼리 결과로 덮어쓰기)
|
|
|
|
|
const newColumns = result.columns.map((col, idx) => ({
|
|
|
|
|
id: `col_${Date.now()}_${idx}`,
|
|
|
|
|
field: col,
|
|
|
|
|
label: col,
|
|
|
|
|
visible: true,
|
|
|
|
|
align: "left" as const,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
setListConfig((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
columns: newColumns,
|
|
|
|
|
}));
|
2025-10-22 13:40:15 +09:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 컬럼 설정 변경
|
|
|
|
|
const handleListConfigChange = useCallback((updates: Partial<ListWidgetConfig>) => {
|
|
|
|
|
setListConfig((prev) => ({ ...prev, ...updates }));
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// 적용
|
|
|
|
|
const handleApply = useCallback(() => {
|
|
|
|
|
const updatedElement: DashboardElement = {
|
|
|
|
|
...element,
|
|
|
|
|
title,
|
|
|
|
|
dataSource,
|
|
|
|
|
listConfig,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onApply(updatedElement);
|
|
|
|
|
}, [element, title, dataSource, listConfig, onApply]);
|
|
|
|
|
|
|
|
|
|
// 저장 가능 여부
|
2025-10-22 15:29:57 +09:00
|
|
|
const canApply = listConfig.columns.length > 0 && listConfig.columns.some((col) => col.visible && col.field);
|
2025-10-22 13:40:15 +09:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
2025-10-29 17:53:03 +09:00
|
|
|
"fixed top-14 left-0 z-[100] flex h-[calc(100vh-3.5rem)] w-80 flex-col bg-muted transition-transform duration-300 ease-in-out",
|
2025-10-22 13:40:15 +09:00
|
|
|
isOpen ? "translate-x-0" : "translate-x-[-100%]",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{/* 헤더 */}
|
2025-10-29 17:53:03 +09:00
|
|
|
<div className="flex items-center justify-between bg-background px-3 py-2 shadow-sm">
|
2025-10-22 13:40:15 +09:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<div className="bg-primary/10 flex h-6 w-6 items-center justify-center rounded">
|
|
|
|
|
<span className="text-primary text-xs font-bold">📋</span>
|
|
|
|
|
</div>
|
2025-10-29 17:53:03 +09:00
|
|
|
<span className="text-xs font-semibold text-foreground">리스트 위젯 설정</span>
|
2025-10-22 13:40:15 +09:00
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={onClose}
|
2025-10-29 17:53:03 +09:00
|
|
|
className="flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-muted"
|
2025-10-22 13:40:15 +09:00
|
|
|
>
|
2025-10-29 17:53:03 +09:00
|
|
|
<X className="h-3.5 w-3.5 text-muted-foreground" />
|
2025-10-22 13:40:15 +09:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 본문: 스크롤 가능 영역 */}
|
|
|
|
|
<div className="flex-1 overflow-y-auto p-3">
|
|
|
|
|
{/* 기본 설정 */}
|
2025-10-29 17:53:03 +09:00
|
|
|
<div className="mb-3 rounded-lg bg-background p-3 shadow-sm">
|
|
|
|
|
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">기본 설정</div>
|
2025-10-22 13:40:15 +09:00
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={title}
|
|
|
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
|
|
|
onKeyDown={(e) => e.stopPropagation()}
|
|
|
|
|
placeholder="리스트 이름"
|
2025-10-29 17:53:03 +09:00
|
|
|
className="focus:border-primary focus:ring-primary/20 h-8 w-full rounded border border-border bg-muted px-2 text-xs placeholder:text-muted-foreground focus:bg-background focus:ring-1 focus:outline-none"
|
2025-10-22 13:40:15 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 데이터 소스 */}
|
2025-10-29 17:53:03 +09:00
|
|
|
<div className="rounded-lg bg-background p-3 shadow-sm">
|
|
|
|
|
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">데이터 소스</div>
|
2025-10-22 13:40:15 +09:00
|
|
|
|
|
|
|
|
<Tabs
|
|
|
|
|
defaultValue={dataSource.type}
|
|
|
|
|
onValueChange={(value) => handleDataSourceTypeChange(value as "database" | "api")}
|
|
|
|
|
className="w-full"
|
|
|
|
|
>
|
2025-10-29 17:53:03 +09:00
|
|
|
<TabsList className="grid h-7 w-full grid-cols-2 bg-muted p-0.5">
|
2025-10-22 13:40:15 +09:00
|
|
|
<TabsTrigger
|
|
|
|
|
value="database"
|
2025-10-29 17:53:03 +09:00
|
|
|
className="h-6 rounded text-[11px] data-[state=active]:bg-background data-[state=active]:shadow-sm"
|
2025-10-22 13:40:15 +09:00
|
|
|
>
|
|
|
|
|
데이터베이스
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
<TabsTrigger
|
|
|
|
|
value="api"
|
2025-10-29 17:53:03 +09:00
|
|
|
className="h-6 rounded text-[11px] data-[state=active]:bg-background data-[state=active]:shadow-sm"
|
2025-10-22 13:40:15 +09:00
|
|
|
>
|
|
|
|
|
REST API
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
</TabsList>
|
|
|
|
|
|
|
|
|
|
<TabsContent value="database" className="mt-2 space-y-2">
|
|
|
|
|
<DatabaseConfig dataSource={dataSource} onChange={handleDataSourceUpdate} />
|
|
|
|
|
<QueryEditor
|
|
|
|
|
dataSource={dataSource}
|
|
|
|
|
onDataSourceChange={handleDataSourceUpdate}
|
|
|
|
|
onQueryTest={handleQueryTest}
|
|
|
|
|
/>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
<TabsContent value="api" className="mt-2 space-y-2">
|
|
|
|
|
<ApiConfig dataSource={dataSource} onChange={handleDataSourceUpdate} onTestResult={handleQueryTest} />
|
|
|
|
|
</TabsContent>
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
|
|
|
|
{/* 데이터 로드 상태 */}
|
|
|
|
|
{queryResult && (
|
2025-10-29 17:53:03 +09:00
|
|
|
<div className="mt-2 flex items-center gap-1.5 rounded bg-success/10 px-2 py-1">
|
|
|
|
|
<div className="h-1.5 w-1.5 rounded-full bg-success" />
|
|
|
|
|
<span className="text-[10px] font-medium text-success">{queryResult.rows.length}개 데이터 로드됨</span>
|
2025-10-22 13:40:15 +09:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-10-22 15:29:57 +09:00
|
|
|
|
|
|
|
|
{/* 컬럼 설정 - 쿼리 실행 후에만 표시 */}
|
|
|
|
|
{queryResult && (
|
2025-10-29 17:53:03 +09:00
|
|
|
<div className="mt-3 rounded-lg bg-background p-3 shadow-sm">
|
|
|
|
|
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">컬럼 설정</div>
|
2025-10-22 15:29:57 +09:00
|
|
|
<UnifiedColumnEditor
|
|
|
|
|
queryResult={queryResult}
|
|
|
|
|
config={listConfig}
|
|
|
|
|
onConfigChange={handleListConfigChange}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 테이블 옵션 - 컬럼이 있을 때만 표시 */}
|
|
|
|
|
{listConfig.columns.length > 0 && (
|
2025-10-29 17:53:03 +09:00
|
|
|
<div className="mt-3 rounded-lg bg-background p-3 shadow-sm">
|
|
|
|
|
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">테이블 옵션</div>
|
2025-10-22 15:29:57 +09:00
|
|
|
<ListTableOptions config={listConfig} onConfigChange={handleListConfigChange} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-10-22 13:40:15 +09:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 푸터: 적용 버튼 */}
|
2025-10-29 17:53:03 +09:00
|
|
|
<div className="flex gap-2 bg-background p-2 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
|
2025-10-22 13:40:15 +09:00
|
|
|
<button
|
|
|
|
|
onClick={onClose}
|
2025-10-29 17:53:03 +09:00
|
|
|
className="flex-1 rounded bg-muted py-2 text-xs font-medium text-foreground transition-colors hover:bg-muted"
|
2025-10-22 13:40:15 +09:00
|
|
|
>
|
|
|
|
|
취소
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleApply}
|
|
|
|
|
disabled={!canApply}
|
2025-10-30 15:39:39 +09:00
|
|
|
className="bg-primary hover:bg-primary/90 flex-1 rounded py-2 text-xs font-medium text-primary-foreground transition-colors disabled:cursor-not-allowed disabled:opacity-50"
|
2025-10-22 13:40:15 +09:00
|
|
|
>
|
|
|
|
|
적용
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|