Shadcn 사용 수정
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user