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

261 lines
9.2 KiB
TypeScript
Raw Normal View History

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";
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);
// 쿼리 실행 시마다 컬럼 설정 초기화 (새로운 쿼리 결과로 덮어쓰기)
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]);
// 저장 가능 여부
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>
{/* 컬럼 설정 - 쿼리 실행 후에만 표시 */}
{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>
<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>
<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}
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>
);
}