308 lines
12 KiB
TypeScript
308 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { CustomMetricConfig, QueryResult } from "../types";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
import { AlertCircle, Plus, X } from "lucide-react";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
interface CustomMetricSectionProps {
|
|
queryResult: QueryResult | null;
|
|
config: CustomMetricConfig;
|
|
onConfigChange: (updates: Partial<CustomMetricConfig>) => void;
|
|
}
|
|
|
|
/**
|
|
* 통계 카드 설정 섹션
|
|
* - 쿼리 결과를 받아서 어떻게 통계를 낼지 설정
|
|
* - 컬럼 선택, 계산 방식(합계/평균/개수 등), 표시 방식
|
|
* - 필터 조건 추가 가능
|
|
*/
|
|
export function CustomMetricSection({ queryResult, config, onConfigChange }: CustomMetricSectionProps) {
|
|
console.log("⚙️ [CustomMetricSection] 렌더링:", { config, queryResult });
|
|
|
|
// 초기값 설정 (aggregation이 없으면 기본값 "sum" 설정)
|
|
React.useEffect(() => {
|
|
if (queryResult && queryResult.columns && queryResult.columns.length > 0 && !config.aggregation) {
|
|
console.log("🔧 기본 aggregation 설정: sum");
|
|
onConfigChange({ aggregation: "sum" });
|
|
}
|
|
}, [queryResult, config.aggregation, onConfigChange]);
|
|
|
|
// 쿼리 결과가 없으면 안내 메시지
|
|
if (!queryResult || !queryResult.columns || queryResult.columns.length === 0) {
|
|
return (
|
|
<div className="bg-background rounded-lg p-3 shadow-sm">
|
|
<Label className="mb-2 block text-xs font-semibold">통계 카드 설정</Label>
|
|
<Alert>
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription className="text-xs">
|
|
먼저 데이터 소스 탭에서 쿼리를 실행하고 결과를 확인해주세요.
|
|
</AlertDescription>
|
|
</Alert>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 필터 추가
|
|
const addFilter = () => {
|
|
const newFilters = [
|
|
...(config.filters || []),
|
|
{ column: queryResult.columns[0] || "", operator: "=" as const, value: "" },
|
|
];
|
|
onConfigChange({ filters: newFilters });
|
|
};
|
|
|
|
// 필터 제거
|
|
const removeFilter = (index: number) => {
|
|
const newFilters = [...(config.filters || [])];
|
|
newFilters.splice(index, 1);
|
|
onConfigChange({ filters: newFilters });
|
|
};
|
|
|
|
// 필터 업데이트
|
|
const updateFilter = (index: number, field: string, value: string) => {
|
|
const newFilters = [...(config.filters || [])];
|
|
newFilters[index] = { ...newFilters[index], [field]: value };
|
|
onConfigChange({ filters: newFilters });
|
|
};
|
|
|
|
// 통계 설정
|
|
return (
|
|
<div className="bg-background space-y-4 rounded-lg p-3 shadow-sm">
|
|
<div>
|
|
<Label className="mb-2 block text-xs font-semibold">통계 카드 설정</Label>
|
|
<p className="text-muted-foreground text-xs">쿼리 결과를 바탕으로 통계를 계산하고 표시합니다</p>
|
|
</div>
|
|
|
|
{/* 1. 필터 조건 (선택사항) */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label className="text-xs font-medium">필터 조건 (선택사항)</Label>
|
|
<Button onClick={addFilter} variant="outline" size="sm" className="h-7 gap-1 text-xs">
|
|
<Plus className="h-3 w-3" />
|
|
필터 추가
|
|
</Button>
|
|
</div>
|
|
|
|
{config.filters && config.filters.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{config.filters.map((filter, index) => (
|
|
<div key={index} className="bg-muted/50 space-y-2 rounded-md border p-3">
|
|
{/* 첫 번째 줄: 컬럼 선택 */}
|
|
<div className="flex items-center gap-2">
|
|
<Select value={filter.column} onValueChange={(value) => updateFilter(index, "column", value)}>
|
|
<SelectTrigger className="h-9 flex-1 text-sm">
|
|
<SelectValue placeholder="컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{queryResult.columns.map((col) => (
|
|
<SelectItem key={col} value={col} className="text-sm">
|
|
{col}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* 삭제 버튼 */}
|
|
<Button onClick={() => removeFilter(index)} variant="ghost" size="icon" className="h-9 w-9 shrink-0">
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 두 번째 줄: 연산자 선택 */}
|
|
<Select value={filter.operator} onValueChange={(value) => updateFilter(index, "operator", value)}>
|
|
<SelectTrigger className="h-9 w-full text-sm">
|
|
<SelectValue placeholder="연산자 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="=" className="text-sm">
|
|
같음 (=)
|
|
</SelectItem>
|
|
<SelectItem value="!=" className="text-sm">
|
|
다름 (≠)
|
|
</SelectItem>
|
|
<SelectItem value=">" className="text-sm">
|
|
큼 (>)
|
|
</SelectItem>
|
|
<SelectItem value="<" className="text-sm">
|
|
작음 (<)
|
|
</SelectItem>
|
|
<SelectItem value=">=" className="text-sm">
|
|
크거나 같음 (≥)
|
|
</SelectItem>
|
|
<SelectItem value="<=" className="text-sm">
|
|
작거나 같음 (≤)
|
|
</SelectItem>
|
|
<SelectItem value="contains" className="text-sm">
|
|
포함
|
|
</SelectItem>
|
|
<SelectItem value="not_contains" className="text-sm">
|
|
미포함
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
{/* 세 번째 줄: 값 입력 */}
|
|
<Input
|
|
value={filter.value}
|
|
onChange={(e) => updateFilter(index, "value", e.target.value)}
|
|
placeholder="값을 입력하세요"
|
|
className="h-9 w-full text-sm"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-muted-foreground text-xs">필터 없음 (전체 데이터 사용)</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* 2. 계산할 컬럼 선택 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">계산 컬럼</Label>
|
|
<Select value={config.valueColumn || ""} onValueChange={(value) => onConfigChange({ valueColumn: value })}>
|
|
<SelectTrigger className="h-9 text-xs">
|
|
<SelectValue placeholder="컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{queryResult.columns.map((col) => (
|
|
<SelectItem key={col} value={col} className="text-xs">
|
|
{col}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 3. 계산 방식 선택 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">계산 방식</Label>
|
|
<Select
|
|
value={config.aggregation || "sum"}
|
|
onValueChange={(value) => {
|
|
console.log("📐 계산 방식 변경:", value);
|
|
onConfigChange({ aggregation: value as "sum" | "avg" | "count" | "min" | "max" });
|
|
}}
|
|
>
|
|
<SelectTrigger className="h-9 text-xs">
|
|
<SelectValue placeholder="계산 방식" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="sum" className="text-xs">
|
|
합계 (SUM)
|
|
</SelectItem>
|
|
<SelectItem value="avg" className="text-xs">
|
|
평균 (AVG)
|
|
</SelectItem>
|
|
<SelectItem value="count" className="text-xs">
|
|
개수 (COUNT)
|
|
</SelectItem>
|
|
<SelectItem value="min" className="text-xs">
|
|
최소값 (MIN)
|
|
</SelectItem>
|
|
<SelectItem value="max" className="text-xs">
|
|
최대값 (MAX)
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{/* 4. 카드 제목 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">카드 제목</Label>
|
|
<Input
|
|
value={config.title || ""}
|
|
onChange={(e) => onConfigChange({ title: e.target.value })}
|
|
placeholder="예: 총 매출액"
|
|
className="h-9 text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 5. 표시 단위 (선택사항) */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">표시 단위 (선택사항)</Label>
|
|
<Input
|
|
value={config.unit || ""}
|
|
onChange={(e) => onConfigChange({ unit: e.target.value })}
|
|
placeholder="예: 원, 건, %"
|
|
className="h-9 text-xs"
|
|
/>
|
|
</div>
|
|
|
|
{/* 6. 소수점 자릿수 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">소수점 자릿수</Label>
|
|
<Select
|
|
value={(config.decimals ?? 0).toString()}
|
|
onValueChange={(value) => onConfigChange({ decimals: parseInt(value) })}
|
|
>
|
|
<SelectTrigger className="h-9 text-xs">
|
|
<SelectValue placeholder="자릿수 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="0" className="text-xs">정수 (0자리)</SelectItem>
|
|
<SelectItem value="1" className="text-xs">소수점 1자리</SelectItem>
|
|
<SelectItem value="2" className="text-xs">소수점 2자리</SelectItem>
|
|
<SelectItem value="3" className="text-xs">소수점 3자리</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
표시할 소수점 자릿수 (평균, 비율 등에 유용)
|
|
</p>
|
|
</div>
|
|
|
|
{/* 7. 자동 새로고침 간격 */}
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-medium">자동 새로고침</Label>
|
|
<Select
|
|
value={(config.refreshInterval ?? 30).toString()}
|
|
onValueChange={(value) => onConfigChange({ refreshInterval: parseInt(value) })}
|
|
>
|
|
<SelectTrigger className="h-9 text-xs">
|
|
<SelectValue placeholder="간격 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="0" className="text-xs">없음</SelectItem>
|
|
<SelectItem value="10" className="text-xs">10초</SelectItem>
|
|
<SelectItem value="30" className="text-xs">30초</SelectItem>
|
|
<SelectItem value="60" className="text-xs">1분</SelectItem>
|
|
<SelectItem value="300" className="text-xs">5분</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-[10px] text-muted-foreground">
|
|
통계 데이터를 자동으로 갱신하는 주기
|
|
</p>
|
|
</div>
|
|
|
|
{/* 미리보기 */}
|
|
{config.valueColumn && config.aggregation && (
|
|
<div className="bg-muted/50 space-y-1 rounded-md border p-3">
|
|
<p className="text-muted-foreground text-xs font-semibold">설정 미리보기</p>
|
|
|
|
{/* 필터 조건 표시 */}
|
|
{config.filters && config.filters.length > 0 && (
|
|
<div className="space-y-1">
|
|
<p className="text-xs font-medium">필터:</p>
|
|
{config.filters.map((filter, idx) => (
|
|
<p key={idx} className="text-muted-foreground text-xs" dir="ltr">
|
|
· {filter.column} {filter.operator} "{filter.value}"
|
|
</p>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* 계산 표시 */}
|
|
<p className="text-xs font-medium">
|
|
{config.title || "통계 제목"}: {config.aggregation?.toUpperCase()}({config.valueColumn})
|
|
{config.unit ? ` ${config.unit}` : ""}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|