ERP-node/frontend/components/admin/dashboard/widgets/ListWidgetConfigSidebar.tsx

299 lines
11 KiB
TypeScript

"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";
import { ColumnSelector } from "./list-widget/ColumnSelector";
import { ManualColumnEditor } from "./list-widget/ManualColumnEditor";
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 || {
columnMode: "auto",
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);
// 자동 모드인 경우: 쿼리 결과의 컬럼을 자동으로 listConfig.columns에 반영
setListConfig((prev) => {
if (prev.columnMode === "auto") {
return {
...prev,
columns: result.columns.map((col, idx) => ({
id: `col_${idx}`,
key: col,
label: col,
visible: true,
width: "auto",
align: "left",
})),
};
}
return prev;
});
}, []);
// 컬럼 설정 변경
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]);
// 저장 가능 여부
const canApply = queryResult && queryResult.rows.length > 0 && listConfig.columns.length > 0;
return (
<div
className={cn(
"fixed top-14 left-0 z-[100] flex h-[calc(100vh-3.5rem)] w-80 flex-col bg-gray-50 transition-transform duration-300 ease-in-out",
isOpen ? "translate-x-0" : "translate-x-[-100%]",
)}
>
{/* 헤더 */}
<div className="flex items-center justify-between bg-white px-3 py-2 shadow-sm">
<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>
<span className="text-xs font-semibold text-gray-900"> </span>
</div>
<button
onClick={onClose}
className="flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-gray-100"
>
<X className="h-3.5 w-3.5 text-gray-500" />
</button>
</div>
{/* 본문: 스크롤 가능 영역 */}
<div className="flex-1 overflow-y-auto p-3">
{/* 기본 설정 */}
<div className="mb-3 rounded-lg bg-white p-3 shadow-sm">
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase"> </div>
<div className="space-y-2">
<div>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
onKeyDown={(e) => e.stopPropagation()}
placeholder="리스트 이름"
className="focus:border-primary focus:ring-primary/20 h-8 w-full rounded border border-gray-200 bg-gray-50 px-2 text-xs placeholder:text-gray-400 focus:bg-white focus:ring-1 focus:outline-none"
/>
</div>
</div>
</div>
{/* 데이터 소스 */}
<div className="rounded-lg bg-white p-3 shadow-sm">
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase"> </div>
<Tabs
defaultValue={dataSource.type}
onValueChange={(value) => handleDataSourceTypeChange(value as "database" | "api")}
className="w-full"
>
<TabsList className="grid h-7 w-full grid-cols-2 bg-gray-100 p-0.5">
<TabsTrigger
value="database"
className="h-6 rounded text-[11px] data-[state=active]:bg-white data-[state=active]:shadow-sm"
>
</TabsTrigger>
<TabsTrigger
value="api"
className="h-6 rounded text-[11px] data-[state=active]:bg-white data-[state=active]:shadow-sm"
>
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}
/>
{/* 컬럼 설정 */}
{queryResult && queryResult.rows.length > 0 && (
<div className="mt-2">
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase"> </div>
{listConfig.columnMode === "auto" ? (
<ColumnSelector
queryResult={queryResult}
config={listConfig}
onConfigChange={handleListConfigChange}
/>
) : (
<ManualColumnEditor config={listConfig} onConfigChange={handleListConfigChange} />
)}
</div>
)}
{/* 테이블 옵션 */}
{queryResult && queryResult.rows.length > 0 && (
<div className="mt-2">
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase">
</div>
<ListTableOptions config={listConfig} onConfigChange={handleListConfigChange} />
</div>
)}
</TabsContent>
<TabsContent value="api" className="mt-2 space-y-2">
<ApiConfig dataSource={dataSource} onChange={handleDataSourceUpdate} onTestResult={handleQueryTest} />
{/* 컬럼 설정 */}
{queryResult && queryResult.rows.length > 0 && (
<div className="mt-2">
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase"> </div>
{listConfig.columnMode === "auto" ? (
<ColumnSelector
queryResult={queryResult}
config={listConfig}
onConfigChange={handleListConfigChange}
/>
) : (
<ManualColumnEditor config={listConfig} onConfigChange={handleListConfigChange} />
)}
</div>
)}
{/* 테이블 옵션 */}
{queryResult && queryResult.rows.length > 0 && (
<div className="mt-2">
<div className="mb-2 text-[10px] font-semibold tracking-wide text-gray-500 uppercase">
</div>
<ListTableOptions config={listConfig} onConfigChange={handleListConfigChange} />
</div>
)}
</TabsContent>
</Tabs>
{/* 데이터 로드 상태 */}
{queryResult && (
<div className="mt-2 flex items-center gap-1.5 rounded bg-green-50 px-2 py-1">
<div className="h-1.5 w-1.5 rounded-full bg-green-500" />
<span className="text-[10px] font-medium text-green-700">{queryResult.rows.length} </span>
</div>
)}
</div>
</div>
{/* 푸터: 적용 버튼 */}
<div className="flex gap-2 bg-white p-2 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
<button
onClick={onClose}
className="flex-1 rounded bg-gray-100 py-2 text-xs font-medium text-gray-700 transition-colors hover:bg-gray-200"
>
</button>
<button
onClick={handleApply}
disabled={!canApply}
className="bg-primary hover:bg-primary/90 flex-1 rounded py-2 text-xs font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50"
>
</button>
</div>
</div>
);
}