Shadcn 사용 수정

This commit is contained in:
dohyeons 2025-10-30 10:06:45 +09:00
parent 2959f66e0c
commit 8f38b176ab
1 changed files with 62 additions and 83 deletions

View File

@ -16,6 +16,10 @@ import { X } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import CustomMetricConfigSidebar from "./widgets/custom-metric/CustomMetricConfigSidebar"; import CustomMetricConfigSidebar from "./widgets/custom-metric/CustomMetricConfigSidebar";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
interface ElementConfigSidebarProps { interface ElementConfigSidebarProps {
element: DashboardElement | null; element: DashboardElement | null;
@ -50,16 +54,11 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
// 사이드바가 열릴 때 초기화 // 사이드바가 열릴 때 초기화
useEffect(() => { useEffect(() => {
if (isOpen && element) { if (isOpen && element) {
console.log("🔄 ElementConfigSidebar 초기화 - element.id:", element.id);
console.log("🔄 element.dataSources:", element.dataSources);
console.log("🔄 element.chartConfig?.dataSources:", element.chartConfig?.dataSources);
setDataSource(element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 }); setDataSource(element.dataSource || { type: "database", connectionType: "current", refreshInterval: 0 });
// dataSources는 element.dataSources 또는 chartConfig.dataSources에서 로드 // dataSources는 element.dataSources 또는 chartConfig.dataSources에서 로드
// ⚠️ 중요: 없으면 반드시 빈 배열로 초기화 // ⚠️ 중요: 없으면 반드시 빈 배열로 초기화
const initialDataSources = element.dataSources || element.chartConfig?.dataSources || []; const initialDataSources = element.dataSources || element.chartConfig?.dataSources || [];
console.log("🔄 초기화된 dataSources:", initialDataSources);
setDataSources(initialDataSources); setDataSources(initialDataSources);
setChartConfig(element.chartConfig || {}); setChartConfig(element.chartConfig || {});
@ -69,7 +68,6 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
setShowHeader(element.showHeader !== false); setShowHeader(element.showHeader !== false);
} else if (!isOpen) { } else if (!isOpen) {
// 사이드바가 닫힐 때 모든 상태 초기화 // 사이드바가 닫힐 때 모든 상태 초기화
console.log("🧹 ElementConfigSidebar 닫힘 - 상태 초기화");
setDataSource({ type: "database", connectionType: "current", refreshInterval: 0 }); setDataSource({ type: "database", connectionType: "current", refreshInterval: 0 });
setDataSources([]); setDataSources([]);
setChartConfig({}); setChartConfig({});
@ -124,8 +122,8 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
(newConfig: ChartConfig) => { (newConfig: ChartConfig) => {
setChartConfig(newConfig); setChartConfig(newConfig);
// 🎯 실시간 미리보기: 즉시 부모에게 전달 (map-test 위젯용) // 🎯 실시간 미리보기: 즉시 부모에게 전달 (map-summary-v2 위젯용)
if (element && element.subtype === "map-test" && newConfig.tileMapUrl) { if (element && element.subtype === "map-summary-v2" && newConfig.tileMapUrl) {
onApply({ onApply({
...element, ...element,
chartConfig: newConfig, chartConfig: newConfig,
@ -148,10 +146,6 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
const handleApply = useCallback(() => { const handleApply = useCallback(() => {
if (!element) return; if (!element) return;
console.log("🔧 적용 버튼 클릭 - dataSource:", dataSource);
console.log("🔧 적용 버튼 클릭 - dataSources:", dataSources);
console.log("🔧 적용 버튼 클릭 - chartConfig:", chartConfig);
// 다중 데이터 소스 위젯 체크 // 다중 데이터 소스 위젯 체크
const isMultiDS = const isMultiDS =
element.subtype === "map-summary-v2" || element.subtype === "map-summary-v2" ||
@ -170,7 +164,6 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
showHeader, showHeader,
}; };
console.log("🔧 적용할 요소:", updatedElement);
onApply(updatedElement); onApply(updatedElement);
// 사이드바는 열린 채로 유지 (연속 수정 가능) // 사이드바는 열린 채로 유지 (연속 수정 가능)
}, [element, dataSource, dataSources, chartConfig, customTitle, showHeader, onApply]); }, [element, dataSource, dataSources, chartConfig, customTitle, showHeader, onApply]);
@ -179,7 +172,7 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
if (!element) return null; if (!element) return null;
// 리스트 위젯은 별도 사이드바로 처리 // 리스트 위젯은 별도 사이드바로 처리
if (element.subtype === "list") { if (element.subtype === "list-v2") {
return ( return (
<ListWidgetConfigSidebar <ListWidgetConfigSidebar
element={element} element={element}
@ -207,7 +200,7 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
} }
// 사용자 커스텀 카드 위젯은 사이드바로 처리 // 사용자 커스텀 카드 위젯은 사이드바로 처리
if (element.subtype === "custom-metric") { if (element.subtype === "custom-metric-v2") {
return ( return (
<CustomMetricConfigSidebar <CustomMetricConfigSidebar
element={element} element={element}
@ -226,7 +219,6 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
element.subtype === "booking-alert" || element.subtype === "booking-alert" ||
element.subtype === "maintenance" || element.subtype === "maintenance" ||
element.subtype === "document" || element.subtype === "document" ||
element.subtype === "risk-alert" ||
element.subtype === "vehicle-status" || element.subtype === "vehicle-status" ||
element.subtype === "vehicle-list" || element.subtype === "vehicle-list" ||
element.subtype === "status-summary" || element.subtype === "status-summary" ||
@ -244,8 +236,7 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
element.subtype === "weather" || element.subtype === "exchange" || element.subtype === "calculator"; element.subtype === "weather" || element.subtype === "exchange" || element.subtype === "calculator";
// 지도 위젯 (위도/경도 매핑 필요) // 지도 위젯 (위도/경도 매핑 필요)
const isMapWidget = const isMapWidget = element.subtype === "vehicle-map" || element.subtype === "map-summary-v2";
element.subtype === "vehicle-map" || element.subtype === "map-summary" || element.subtype === "map-test";
// 헤더 전용 위젯 // 헤더 전용 위젯
const isHeaderOnlyWidget = const isHeaderOnlyWidget =
@ -254,12 +245,11 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
// 다중 데이터 소스 위젯 // 다중 데이터 소스 위젯
const isMultiDataSourceWidget = const isMultiDataSourceWidget =
element.subtype === "map-summary-v2" || (element.subtype as string) === "map-summary-v2" ||
element.subtype === "chart" || (element.subtype as string) === "chart" ||
element.subtype === "list-v2" || (element.subtype as string) === "list-v2" ||
element.subtype === "custom-metric-v2" || (element.subtype as string) === "custom-metric-v2" ||
element.subtype === "status-summary-test" || (element.subtype as string) === "risk-alert-v2";
element.subtype === "risk-alert-v2";
// 저장 가능 여부 확인 // 저장 가능 여부 확인
const isPieChart = element.subtype === "pie" || element.subtype === "donut"; const isPieChart = element.subtype === "pie" || element.subtype === "donut";
@ -280,8 +270,8 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
: isSimpleWidget : isSimpleWidget
? queryResult && queryResult.rows.length > 0 ? queryResult && queryResult.rows.length > 0
: isMapWidget : isMapWidget
? element.subtype === "map-test" ? element.subtype === "map-summary-v2"
? chartConfig.tileMapUrl || (queryResult && queryResult.rows.length > 0) // 🧪 지도 테스트 위젯: 타일맵 URL 또는 API 데이터 ? chartConfig.tileMapUrl || (queryResult && queryResult.rows.length > 0) // 지도 위젯: 타일맵 URL 또는 API 데이터
: queryResult && queryResult.rows.length > 0 && chartConfig.latitudeColumn && chartConfig.longitudeColumn : queryResult && queryResult.rows.length > 0 && chartConfig.latitudeColumn && chartConfig.longitudeColumn
: queryResult && : queryResult &&
queryResult.rows.length > 0 && queryResult.rows.length > 0 &&
@ -291,62 +281,58 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
return ( return (
<div <div
className={cn( className={cn(
"fixed top-14 left-0 z-[100] flex h-[calc(100vh-3.5rem)] w-72 flex-col bg-muted transition-transform duration-300 ease-in-out", "bg-muted fixed top-14 left-0 z-[100] flex h-[calc(100vh-3.5rem)] w-72 flex-col transition-transform duration-300 ease-in-out",
isOpen ? "translate-x-0" : "translate-x-[-100%]", isOpen ? "translate-x-0" : "translate-x-[-100%]",
)} )}
> >
{/* 헤더 */} {/* 헤더 */}
<div className="flex items-center justify-between bg-background px-3 py-2 shadow-sm"> <div className="bg-background flex items-center justify-between px-3 py-2 shadow-sm">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="bg-primary/10 flex h-6 w-6 items-center justify-center rounded"> <div className="bg-primary/10 flex h-6 w-6 items-center justify-center rounded">
<span className="text-primary text-xs font-bold"></span> <span className="text-primary text-xs font-bold"></span>
</div> </div>
<span className="text-xs font-semibold text-foreground">{element.title}</span> <span className="text-foreground text-xs font-semibold">{element.title}</span>
</div> </div>
<button <Button onClick={onClose} variant="ghost" size="icon" className="h-6 w-6">
onClick={onClose} <X className="h-3.5 w-3.5" />
className="flex h-6 w-6 items-center justify-center rounded transition-colors hover:bg-muted" </Button>
>
<X className="h-3.5 w-3.5 text-muted-foreground" />
</button>
</div> </div>
{/* 본문: 스크롤 가능 영역 */} {/* 본문: 스크롤 가능 영역 */}
<div className="flex-1 overflow-y-auto p-3"> <div className="flex-1 overflow-y-auto p-3">
{/* 기본 설정 카드 */} {/* 기본 설정 카드 */}
<div className="mb-3 rounded-lg bg-background p-3 shadow-sm"> <div className="bg-background mb-3 rounded-lg p-3 shadow-sm">
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase"> </div> <div className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wide uppercase"> </div>
<div className="space-y-2"> <div className="space-y-2">
{/* 커스텀 제목 입력 */} {/* 커스텀 제목 입력 */}
<div> <div>
<input <Input
type="text"
value={customTitle} value={customTitle}
onChange={(e) => setCustomTitle(e.target.value)} onChange={(e) => setCustomTitle(e.target.value)}
onKeyDown={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}
placeholder="위젯 제목" placeholder="위젯 제목"
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" className="bg-muted focus:bg-background h-8 text-xs"
/> />
</div> </div>
{/* 헤더 표시 옵션 */} {/* 헤더 표시 옵션 */}
<label className="flex cursor-pointer items-center gap-2 rounded border border-border bg-muted px-2 py-1.5 transition-colors hover:border-border"> <div className="border-border bg-muted flex items-center gap-2 rounded border px-2 py-1.5">
<input <Checkbox
type="checkbox"
id="showHeader" id="showHeader"
checked={showHeader} checked={showHeader}
onChange={(e) => setShowHeader(e.target.checked)} onCheckedChange={(checked) => setShowHeader(checked === true)}
className="text-primary focus:ring-primary h-3 w-3 rounded border-border"
/> />
<span className="text-xs text-foreground"> </span> <Label htmlFor="showHeader" className="cursor-pointer text-xs font-normal">
</label>
</Label>
</div>
</div> </div>
</div> </div>
{/* 다중 데이터 소스 위젯 */} {/* 다중 데이터 소스 위젯 */}
{isMultiDataSourceWidget && ( {isMultiDataSourceWidget && (
<> <>
<div className="rounded-lg bg-background p-3 shadow-sm"> <div className="bg-background rounded-lg p-3 shadow-sm">
<MultiDataSourceConfig <MultiDataSourceConfig
dataSources={dataSources} dataSources={dataSources}
onChange={setDataSources} onChange={setDataSources}
@ -357,13 +343,11 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
totalRows: result.rows.length, totalRows: result.rows.length,
executionTime: 0, executionTime: 0,
}); });
console.log("📊 API 테스트 결과 수신:", result, "데이터 소스 ID:", dataSourceId);
// ChartTestWidget용: 각 데이터 소스의 테스트 결과 저장 // 각 데이터 소스의 테스트 결과 저장
setTestResults((prev) => { setTestResults((prev) => {
const updated = new Map(prev); const updated = new Map(prev);
updated.set(dataSourceId, result); updated.set(dataSourceId, result);
console.log("📊 테스트 결과 저장:", dataSourceId, result);
return updated; return updated;
}); });
}} }}
@ -372,11 +356,11 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
{/* 지도 위젯: 타일맵 URL 설정 */} {/* 지도 위젯: 타일맵 URL 설정 */}
{element.subtype === "map-summary-v2" && ( {element.subtype === "map-summary-v2" && (
<div className="rounded-lg bg-background shadow-sm"> <div className="bg-background rounded-lg shadow-sm">
<details className="group"> <details className="group">
<summary className="flex cursor-pointer items-center justify-between p-3 hover:bg-muted"> <summary className="hover:bg-muted flex cursor-pointer items-center justify-between p-3">
<div> <div>
<div className="text-xs font-semibold tracking-wide text-muted-foreground uppercase"> <div className="text-muted-foreground text-xs font-semibold tracking-wide uppercase">
() ()
</div> </div>
<div className="text-muted-foreground mt-0.5 text-[10px]"> VWorld </div> <div className="text-muted-foreground mt-0.5 text-[10px]"> VWorld </div>
@ -403,11 +387,13 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
{/* 차트 위젯: 차트 설정 */} {/* 차트 위젯: 차트 설정 */}
{element.subtype === "chart" && ( {element.subtype === "chart" && (
<div className="rounded-lg bg-background shadow-sm"> <div className="bg-background rounded-lg shadow-sm">
<details className="group" open> <details className="group" open>
<summary className="flex cursor-pointer items-center justify-between p-3 hover:bg-muted"> <summary className="hover:bg-muted flex cursor-pointer items-center justify-between p-3">
<div> <div>
<div className="text-xs font-semibold tracking-wide text-muted-foreground uppercase"> </div> <div className="text-muted-foreground text-xs font-semibold tracking-wide uppercase">
</div>
<div className="text-muted-foreground mt-0.5 text-[10px]"> <div className="text-muted-foreground mt-0.5 text-[10px]">
{testResults.size > 0 {testResults.size > 0
? `${testResults.size}개 데이터 소스 • X축, Y축, 차트 타입 설정` ? `${testResults.size}개 데이터 소스 • X축, Y축, 차트 타입 설정`
@ -439,24 +425,26 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
{/* 헤더 전용 위젯이 아닐 때만 데이터 소스 표시 */} {/* 헤더 전용 위젯이 아닐 때만 데이터 소스 표시 */}
{!isHeaderOnlyWidget && !isMultiDataSourceWidget && ( {!isHeaderOnlyWidget && !isMultiDataSourceWidget && (
<div className="rounded-lg bg-background p-3 shadow-sm"> <div className="bg-background rounded-lg p-3 shadow-sm">
<div className="mb-2 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase"> </div> <div className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wide uppercase">
</div>
<Tabs <Tabs
defaultValue={dataSource.type} defaultValue={dataSource.type}
onValueChange={(value) => handleDataSourceTypeChange(value as "database" | "api")} onValueChange={(value) => handleDataSourceTypeChange(value as "database" | "api")}
className="w-full" className="w-full"
> >
<TabsList className="grid h-7 w-full grid-cols-2 bg-muted p-0.5"> <TabsList className="bg-muted grid h-7 w-full grid-cols-2 p-0.5">
<TabsTrigger <TabsTrigger
value="database" value="database"
className="h-6 rounded text-[11px] data-[state=active]:bg-background data-[state=active]:shadow-sm" className="data-[state=active]:bg-background h-6 rounded text-[11px] data-[state=active]:shadow-sm"
> >
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="api" value="api"
className="h-6 rounded text-[11px] data-[state=active]:bg-background data-[state=active]:shadow-sm" className="data-[state=active]:bg-background h-6 rounded text-[11px] data-[state=active]:shadow-sm"
> >
REST API REST API
</TabsTrigger> </TabsTrigger>
@ -472,10 +460,10 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
{/* 차트/지도 설정 */} {/* 차트/지도 설정 */}
{!isSimpleWidget && {!isSimpleWidget &&
(element.subtype === "map-test" || (queryResult && queryResult.rows.length > 0)) && ( (element.subtype === "map-summary-v2" || (queryResult && queryResult.rows.length > 0)) && (
<div className="mt-2"> <div className="mt-2">
{isMapWidget ? ( {isMapWidget ? (
element.subtype === "map-test" ? ( element.subtype === "map-summary-v2" ? (
<MapTestConfigPanel <MapTestConfigPanel
config={chartConfig} config={chartConfig}
queryResult={queryResult || undefined} queryResult={queryResult || undefined}
@ -513,10 +501,10 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
{/* 차트/지도 설정 */} {/* 차트/지도 설정 */}
{!isSimpleWidget && {!isSimpleWidget &&
(element.subtype === "map-test" || (queryResult && queryResult.rows.length > 0)) && ( (element.subtype === "map-summary-v2" || (queryResult && queryResult.rows.length > 0)) && (
<div className="mt-2"> <div className="mt-2">
{isMapWidget ? ( {isMapWidget ? (
element.subtype === "map-test" ? ( element.subtype === "map-summary-v2" ? (
<MapTestConfigPanel <MapTestConfigPanel
config={chartConfig} config={chartConfig}
queryResult={queryResult || undefined} queryResult={queryResult || undefined}
@ -552,11 +540,9 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
{/* 데이터 로드 상태 */} {/* 데이터 로드 상태 */}
{queryResult && ( {queryResult && (
<div className="mt-2 flex items-center gap-1.5 rounded bg-success/10 px-2 py-1"> <div className="bg-success/10 mt-2 flex items-center gap-1.5 rounded px-2 py-1">
<div className="h-1.5 w-1.5 rounded-full bg-success" /> <div className="bg-success h-1.5 w-1.5 rounded-full" />
<span className="text-[10px] font-medium text-success"> <span className="text-success text-[10px] font-medium">{queryResult.rows.length} </span>
{queryResult.rows.length}
</span>
</div> </div>
)} )}
</div> </div>
@ -564,20 +550,13 @@ export function ElementConfigSidebar({ element, isOpen, onClose, onApply }: Elem
</div> </div>
{/* 푸터: 적용 버튼 */} {/* 푸터: 적용 버튼 */}
<div className="flex gap-2 bg-background p-2 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]"> <div className="bg-background flex gap-2 p-2 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
<button <Button onClick={onClose} variant="outline" className="flex-1 text-xs">
onClick={onClose}
className="flex-1 rounded bg-muted py-2 text-xs font-medium text-foreground transition-colors hover:bg-muted"
>
</button> </Button>
<button <Button onClick={handleApply} disabled={isHeaderOnlyWidget ? false : !canApply} className="flex-1 text-xs">
onClick={handleApply}
disabled={isHeaderOnlyWidget ? false : !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> </Button>
</div> </div>
</div> </div>
); );