사소한 수정

This commit is contained in:
dohyeons 2025-10-15 15:45:58 +09:00
parent 95c685051d
commit 3613f9eef4
3 changed files with 23 additions and 84 deletions

View File

@ -10,7 +10,7 @@ import { Badge } from "@/components/ui/badge";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { TrendingUp, AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
import { DateFilterPanel } from "./DateFilterPanel"; import { DateFilterPanel } from "./DateFilterPanel";
import { extractTableNameFromQuery } from "./utils/queryHelpers"; import { extractTableNameFromQuery } from "./utils/queryHelpers";
import { dashboardApi } from "@/lib/api/dashboard"; import { dashboardApi } from "@/lib/api/dashboard";
@ -85,34 +85,23 @@ export function ChartConfigPanel({
} }
const tableName = extractTableNameFromQuery(query); const tableName = extractTableNameFromQuery(query);
console.log("📋 쿼리에서 추출한 테이블명:", tableName);
console.log("📋 원본 쿼리:", query);
if (!tableName) { if (!tableName) {
console.log("⚠️ 테이블명을 추출할 수 없습니다.");
setDateColumns([]); setDateColumns([]);
return; return;
} }
console.log("🔍 테이블 스키마 조회 중:", tableName);
dashboardApi dashboardApi
.getTableSchema(tableName) .getTableSchema(tableName)
.then((schema) => { .then((schema) => {
console.log("✅ 스키마 조회 성공:", schema);
console.log("📅 전체 날짜 컬럼:", schema.dateColumns);
console.log("📊 쿼리 결과 컬럼:", availableColumns);
// 원본 테이블의 모든 날짜 컬럼을 표시 // 원본 테이블의 모든 날짜 컬럼을 표시
// (SELECT에 없어도 WHERE 절에 사용 가능) // (SELECT에 없어도 WHERE 절에 사용 가능)
setDateColumns(schema.dateColumns); setDateColumns(schema.dateColumns);
console.log("✨ 사용 가능한 날짜 필터 컬럼:", schema.dateColumns);
}) })
.catch((error) => { .catch((error) => {
console.error("❌ 테이블 스키마 조회 실패:", error); console.error("❌ 테이블 스키마 조회 실패:", error);
// 실패 시 빈 배열 (날짜 필터 비활성화) // 실패 시 빈 배열 (날짜 필터 비활성화)
setDateColumns([]); setDateColumns([]);
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query, queryResult, dataSourceType]); }, [query, queryResult, dataSourceType]);
return ( return (
@ -125,7 +114,7 @@ export function ChartConfigPanel({
<Card className="border-blue-200 bg-blue-50 p-4"> <Card className="border-blue-200 bg-blue-50 p-4">
<div className="mb-2 flex items-center gap-2"> <div className="mb-2 flex items-center gap-2">
<AlertCircle className="h-4 w-4 text-blue-600" /> <AlertCircle className="h-4 w-4 text-blue-600" />
<h4 className="font-semibold text-blue-900">📋 API </h4> <h4 className="font-semibold text-blue-900">API </h4>
</div> </div>
<div className="rounded bg-white p-3 text-xs"> <div className="rounded bg-white p-3 text-xs">
<div className="mb-2 text-gray-600"> {queryResult.totalRows} :</div> <div className="mb-2 text-gray-600"> {queryResult.totalRows} :</div>
@ -139,7 +128,7 @@ export function ChartConfigPanel({
<Alert variant="destructive"> <Alert variant="destructive">
<AlertCircle className="h-4 w-4" /> <AlertCircle className="h-4 w-4" />
<AlertDescription> <AlertDescription>
<div className="font-semibold"> </div> <div className="font-semibold"> </div>
<div className="mt-1 text-sm"> <div className="mt-1 text-sm">
: :
<div className="mt-1 flex flex-wrap gap-1"> <div className="mt-1 flex flex-wrap gap-1">
@ -151,7 +140,7 @@ export function ChartConfigPanel({
</div> </div>
</div> </div>
<div className="mt-2 text-xs text-gray-600"> <div className="mt-2 text-xs text-gray-600">
💡 <strong> :</strong> JSON Path를 . <strong> :</strong> JSON Path를 .
<br /> <br />
: <code className="rounded bg-gray-100 px-1">main</code> {" "} : <code className="rounded bg-gray-100 px-1">main</code> {" "}
<code className="rounded bg-gray-100 px-1">data.items</code> <code className="rounded bg-gray-100 px-1">data.items</code>
@ -203,7 +192,7 @@ export function ChartConfigPanel({
</SelectContent> </SelectContent>
</Select> </Select>
{simpleColumns.length === 0 && ( {simpleColumns.length === 0 && (
<p className="text-xs text-red-500"> . JSON Path를 .</p> <p className="text-xs text-red-500"> . JSON Path를 .</p>
)} )}
</div> </div>
@ -221,17 +210,14 @@ export function ChartConfigPanel({
{/* 숫자 타입 우선 표시 */} {/* 숫자 타입 우선 표시 */}
{numericColumns.length > 0 && ( {numericColumns.length > 0 && (
<> <>
<div className="mb-2 text-xs font-medium text-green-700"> ()</div> <div className="mb-2 text-xs font-medium text-green-700"> ()</div>
{numericColumns.map((col) => { {numericColumns.map((col) => {
const isSelected = Array.isArray(currentConfig.yAxis) const isSelected = Array.isArray(currentConfig.yAxis)
? currentConfig.yAxis.includes(col) ? currentConfig.yAxis.includes(col)
: currentConfig.yAxis === col; : currentConfig.yAxis === col;
return ( return (
<div <div key={col} className="flex items-center gap-2 rounded border-green-500 bg-green-50 p-2">
key={col}
className="flex items-center gap-2 rounded border-l-2 border-green-500 bg-green-50 p-2"
>
<Checkbox <Checkbox
checked={isSelected} checked={isSelected}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
@ -271,7 +257,7 @@ export function ChartConfigPanel({
{simpleColumns.filter((col) => !numericColumns.includes(col)).length > 0 && ( {simpleColumns.filter((col) => !numericColumns.includes(col)).length > 0 && (
<> <>
{numericColumns.length > 0 && <div className="my-2 border-t"></div>} {numericColumns.length > 0 && <div className="my-2 border-t"></div>}
<div className="mb-2 text-xs font-medium text-gray-600">📝 </div> <div className="mb-2 text-xs font-medium text-gray-600"> </div>
{simpleColumns {simpleColumns
.filter((col) => !numericColumns.includes(col)) .filter((col) => !numericColumns.includes(col))
.map((col) => { .map((col) => {
@ -320,7 +306,7 @@ export function ChartConfigPanel({
</div> </div>
</Card> </Card>
{simpleColumns.length === 0 && ( {simpleColumns.length === 0 && (
<p className="text-xs text-red-500"> . JSON Path를 .</p> <p className="text-xs text-red-500"> . JSON Path를 .</p>
)} )}
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
: 여러 (: 갤럭시 vs ) : 여러 (: 갤럭시 vs )
@ -356,7 +342,7 @@ export function ChartConfigPanel({
</SelectContent> </SelectContent>
</Select> </Select>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
💡 . (: 부서별 , ) . (: 부서별 , )
</p> </p>
</div> </div>
@ -432,45 +418,6 @@ export function ChartConfigPanel({
<Separator /> <Separator />
{/* 설정 미리보기 */}
<Card className="bg-gray-50 p-4">
<div className="mb-3 flex items-center gap-2 text-sm font-medium text-gray-700">
<TrendingUp className="h-4 w-4" />
</div>
<div className="space-y-2 text-xs text-gray-600">
<div className="flex gap-2">
<span className="font-medium">X축:</span>
<span>{currentConfig.xAxis || "미설정"}</span>
</div>
<div className="flex gap-2">
<span className="font-medium">Y축:</span>
<span>
{Array.isArray(currentConfig.yAxis) && currentConfig.yAxis.length > 0
? `${currentConfig.yAxis.length}개 (${currentConfig.yAxis.join(", ")})`
: currentConfig.yAxis || "미설정"}
</span>
</div>
<div className="flex gap-2">
<span className="font-medium">:</span>
<span>{currentConfig.aggregation || "없음"}</span>
</div>
{currentConfig.groupBy && (
<div className="flex gap-2">
<span className="font-medium">:</span>
<span>{currentConfig.groupBy}</span>
</div>
)}
<div className="flex gap-2">
<span className="font-medium"> :</span>
<Badge variant="secondary">{queryResult.rows.length}</Badge>
</div>
{Array.isArray(currentConfig.yAxis) && currentConfig.yAxis.length > 1 && (
<div className="mt-2 text-blue-600"> !</div>
)}
</div>
</Card>
{/* 날짜 필터 */} {/* 날짜 필터 */}
{dateColumns.length > 0 && ( {dateColumns.length > 0 && (
<DateFilterPanel config={currentConfig} dateColumns={dateColumns} onChange={updateConfig} /> <DateFilterPanel config={currentConfig} dateColumns={dateColumns} onChange={updateConfig} />

View File

@ -10,7 +10,6 @@ import { DatabaseConfig } from "./data-sources/DatabaseConfig";
import { ApiConfig } from "./data-sources/ApiConfig"; import { ApiConfig } from "./data-sources/ApiConfig";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { X, ChevronLeft, ChevronRight, Save } from "lucide-react"; import { X, ChevronLeft, ChevronRight, Save } from "lucide-react";
interface ElementConfigModalProps { interface ElementConfigModalProps {
@ -110,10 +109,6 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
// 저장 처리 // 저장 처리
const handleSave = useCallback(() => { const handleSave = useCallback(() => {
console.log("💾 저장 버튼 클릭!");
console.log(" 현재 chartConfig:", chartConfig);
console.log(" dateFilter:", chartConfig?.dateFilter);
const updatedElement: DashboardElement = { const updatedElement: DashboardElement = {
...element, ...element,
dataSource, dataSource,
@ -141,6 +136,11 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
const isPieChart = element.subtype === "pie" || element.subtype === "donut"; const isPieChart = element.subtype === "pie" || element.subtype === "donut";
const isApiSource = dataSource.type === "api"; const isApiSource = dataSource.type === "api";
// Y축 검증 헬퍼
const hasYAxis =
chartConfig.yAxis &&
(typeof chartConfig.yAxis === "string" || (Array.isArray(chartConfig.yAxis) && chartConfig.yAxis.length > 0));
const canSave = isSimpleWidget const canSave = isSimpleWidget
? // 간단한 위젯: 2단계에서 쿼리 테스트 후 저장 가능 ? // 간단한 위젯: 2단계에서 쿼리 테스트 후 저장 가능
currentStep === 2 && queryResult && queryResult.rows.length > 0 currentStep === 2 && queryResult && queryResult.rows.length > 0
@ -157,12 +157,12 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
queryResult.rows.length > 0 && queryResult.rows.length > 0 &&
chartConfig.xAxis && chartConfig.xAxis &&
(isPieChart || isApiSource (isPieChart || isApiSource
? // 파이/도넛 차트 또는 REST API: Y축 또는 집계 함수 필요 ? // 파이/도넛 차트 또는 REST API
chartConfig.yAxis ||
(Array.isArray(chartConfig.yAxis) && chartConfig.yAxis.length > 0) ||
chartConfig.aggregation === "count" chartConfig.aggregation === "count"
? true // count는 Y축 없어도 됨
: hasYAxis // 다른 집계(sum, avg, max, min) 또는 집계 없음 → Y축 필수
: // 일반 차트 (DB): Y축 필수 : // 일반 차트 (DB): Y축 필수
chartConfig.yAxis || (Array.isArray(chartConfig.yAxis) && chartConfig.yAxis.length > 0)); hasYAxis);
return ( return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm"> <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50 backdrop-blur-sm">
@ -191,13 +191,11 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
{/* 진행 상황 표시 - 간단한 위젯은 표시 안 함 */} {/* 진행 상황 표시 - 간단한 위젯은 표시 안 함 */}
{!isSimpleWidget && ( {!isSimpleWidget && (
<div className="border-b bg-gray-50 px-6 py-4"> <div className="border-b bg-gray-50 px-6 py-4">
<div className="mb-4 flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-sm font-medium text-gray-700"> <div className="text-sm font-medium text-gray-700">
{currentStep} / 2: {currentStep === 1 ? "데이터 소스 선택" : "데이터 설정 및 차트 설정"} {currentStep} / 2: {currentStep === 1 ? "데이터 소스 선택" : "데이터 설정 및 차트 설정"}
</div> </div>
<Badge variant="secondary">{Math.round((currentStep / 2) * 100)}% </Badge>
</div> </div>
<Progress value={(currentStep / 2) * 100} className="h-2" />
</div> </div>
)} )}
@ -268,13 +266,7 @@ export function ElementConfigModal({ element, isOpen, onClose, onSave }: Element
{/* 모달 푸터 */} {/* 모달 푸터 */}
<div className="flex items-center justify-between border-t bg-gray-50 p-6"> <div className="flex items-center justify-between border-t bg-gray-50 p-6">
<div> <div>{queryResult && <Badge variant="default">{queryResult.rows.length} </Badge>}</div>
{queryResult && (
<Badge variant="default" className="bg-green-600">
📊 {queryResult.rows.length}
</Badge>
)}
</div>
<div className="flex gap-3"> <div className="flex gap-3">
{!isSimpleWidget && currentStep > 1 && ( {!isSimpleWidget && currentStep > 1 && (

View File

@ -299,7 +299,7 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
onClick={() => { onClick={() => {
const headers = normalizeHeaders(); const headers = normalizeHeaders();
onChange({ onChange({
headers: [...headers, { id: `header_${Date.now()}`, key: "Authorization", value: "Bearer YOUR_TOKEN" }], headers: [...headers, { id: `header_${Date.now()}`, key: "Authorization", value: "" }],
}); });
}} }}
> >
@ -398,7 +398,7 @@ export function ApiConfig({ dataSource, onChange, onTestResult }: ApiConfigProps
{/* 테스트 결과 */} {/* 테스트 결과 */}
{testResult && ( {testResult && (
<Card className="border-green-200 bg-green-50 p-4"> <Card className="border-green-200 bg-green-50 p-4">
<div className="mb-2 text-sm font-medium text-green-800"> API </div> <div className="mb-2 text-sm font-medium text-green-800">API </div>
<div className="space-y-1 text-xs text-green-700"> <div className="space-y-1 text-xs text-green-700">
<div> {testResult.rows.length} </div> <div> {testResult.rows.length} </div>
<div>: {testResult.columns.join(", ")}</div> <div>: {testResult.columns.join(", ")}</div>