Shadcn 사용 수정

This commit is contained in:
dohyeons
2025-10-30 10:06:45 +09:00
parent 2959f66e0c
commit 8f38b176ab

View File

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