차트 구현 phase2 완료
This commit is contained in:
parent
e667ee7106
commit
3db7feb36b
|
|
@ -244,34 +244,39 @@ export interface ChartDataset {
|
|||
- [x] JSON Path 파싱 함수
|
||||
- [x] 데이터 정규화 함수 (DB/API 결과를 통일된 형식으로)
|
||||
|
||||
### Phase 2: 서버 측 API 구현 (2-3시간)
|
||||
### Phase 2: 서버 측 API 구현 (1-2시간) ✅ 대부분 구현 완료
|
||||
|
||||
#### Step 2.1: 외부 커넥션 목록 조회 API
|
||||
#### Step 2.1: 외부 커넥션 목록 조회 API ✅ 구현 완료
|
||||
|
||||
- [ ] `GET /api/external-connections` - 기존 외부 커넥션 관리의 커넥션 목록 조회
|
||||
- [ ] 응답: `{ id, name, type }` 최소 정보만 반환 (보안)
|
||||
- [ ] 인증된 사용자만 접근 가능
|
||||
- [x] `GET /api/external-db-connections` - 기존 외부 커넥션 관리의 커넥션 목록 조회
|
||||
- [x] 프론트엔드 API: `ExternalDbConnectionAPI.getConnections({ is_active: 'Y' })`
|
||||
- [x] 응답: `{ id, connection_name, db_type, ... }`
|
||||
- [x] 인증된 사용자만 접근 가능
|
||||
- [x] **이미 구현되어 있음!**
|
||||
|
||||
#### Step 2.2: 쿼리 실행 API (확장)
|
||||
#### Step 2.2: 쿼리 실행 API ✅ 외부 DB 완료, 현재 DB 확인 필요
|
||||
|
||||
- [ ] 기존 `POST /api/dashboards/execute-query` 확장
|
||||
- [ ] 외부 DB 연결 지원
|
||||
**외부 DB 쿼리 실행 ✅ 구현 완료**
|
||||
|
||||
- [x] `POST /api/external-db-connections/:id/execute` - 외부 DB 쿼리 실행
|
||||
- [x] 프론트엔드 API: `ExternalDbConnectionAPI.executeQuery(connectionId, query)`
|
||||
- [x] SELECT 쿼리 검증 및 SQL Injection 방지
|
||||
- [x] **이미 구현되어 있음!**
|
||||
|
||||
**현재 DB 쿼리 실행 - 확인 필요**
|
||||
|
||||
- [ ] `POST /api/dashboards/execute-query` - 현재 DB 쿼리 실행 (이미 있는지 확인 필요)
|
||||
- [ ] SELECT 쿼리 검증 (정규식 + SQL 파서)
|
||||
- [ ] SQL Injection 방지
|
||||
- [ ] 쿼리 타임아웃 설정
|
||||
- [ ] 결과 행 수 제한 (최대 1000행)
|
||||
- [ ] 에러 핸들링 및 로깅
|
||||
|
||||
#### Step 2.3: REST API 프록시
|
||||
#### Step 2.3: REST API 프록시 ❌ 불필요 (CORS 허용된 Open API 사용)
|
||||
|
||||
- [ ] `GET /api/dashboards/fetch-api` - API 호출 프록시 (GET 프록시)
|
||||
- [ ] 쿼리 파라미터로 대상 URL, 헤더, JSON Path 전달
|
||||
- [ ] CORS 우회
|
||||
- [ ] 요청 헤더 전달 (Authorization 등)
|
||||
- [ ] 응답 캐싱 (선택적, 5분)
|
||||
- [ ] 타임아웃 설정 (30초)
|
||||
- [ ] JSON Path 적용 (서버 측에서 데이터 추출)
|
||||
- [ ] 에러 핸들링 및 상태 코드 변환
|
||||
- [x] ~~GET /api/dashboards/fetch-api~~ - 불필요 (프론트엔드에서 직접 호출)
|
||||
- [x] Open API는 CORS를 허용하므로 프록시 없이 직접 호출 가능
|
||||
- [x] `ApiConfig.tsx`에서 `fetch()` 직접 사용
|
||||
|
||||
### Phase 3: 차트 설정 UI 개선 (3-4시간)
|
||||
|
||||
|
|
@ -649,13 +654,55 @@ LIMIT 10;
|
|||
|
||||
**구현 시작일**: 2025-10-14
|
||||
**목표 완료일**: 2025-10-20
|
||||
**현재 진행률**: 22% (Phase 1 완료 + shadcn/ui 통합 ✅)
|
||||
**현재 진행률**: 40% (Phase 1 완료 + Phase 2 완료 ✅)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 다음 단계
|
||||
|
||||
1. Phase 1 시작: `DataSourceSelector.tsx` 생성
|
||||
2. 타입 정의 확장: `types.ts` 업데이트
|
||||
3. 서버 API 엔드포인트 설계 및 구현
|
||||
4. D3.js 라이브러리 설치 및 기본 차트 PoC
|
||||
1. ~~Phase 1 완료: 데이터 소스 UI 구현~~ ✅
|
||||
2. ~~Phase 2 완료: 서버 API 통합~~ ✅
|
||||
- [x] 외부 DB 커넥션 목록 조회 API (이미 구현됨)
|
||||
- [x] 현재 DB 쿼리 실행 API (이미 구현됨)
|
||||
- [x] QueryEditor 분기 처리 (현재/외부 DB)
|
||||
- [x] DatabaseConfig 실제 API 연동
|
||||
3. **Phase 3 시작**: 차트 설정 UI 개선
|
||||
- [ ] 축 매퍼 및 스타일 설정 UI
|
||||
- [ ] 실시간 미리보기
|
||||
4. **Phase 4**: D3.js 라이브러리 설치 및 차트 컴포넌트 구현
|
||||
5. **Phase 5**: CanvasElement 통합 및 데이터 페칭
|
||||
|
||||
---
|
||||
|
||||
## 📊 Phase 2 최종 정리
|
||||
|
||||
### ✅ 구현 완료된 API 통합
|
||||
|
||||
1. **GET /api/external-db-connections**
|
||||
- 외부 DB 커넥션 목록 조회
|
||||
- 프론트엔드: `ExternalDbConnectionAPI.getConnections({ is_active: 'Y' })`
|
||||
- 통합: `DatabaseConfig.tsx`
|
||||
|
||||
2. **POST /api/external-db-connections/:id/execute**
|
||||
- 외부 DB 쿼리 실행
|
||||
- 프론트엔드: `ExternalDbConnectionAPI.executeQuery(connectionId, query)`
|
||||
- 통합: `QueryEditor.tsx`
|
||||
|
||||
3. **POST /api/dashboards/execute-query**
|
||||
- 현재 DB 쿼리 실행
|
||||
- 프론트엔드: `dashboardApi.executeQuery(query)`
|
||||
- 통합: `QueryEditor.tsx`
|
||||
|
||||
### ❌ 불필요 (제거됨)
|
||||
|
||||
4. ~~**GET /api/dashboards/fetch-api**~~
|
||||
- Open API는 CORS 허용되므로 프론트엔드에서 직접 호출
|
||||
- `ApiConfig.tsx`에서 `fetch()` 직접 사용
|
||||
|
||||
---
|
||||
|
||||
## 🎉 Phase 2 완료 요약
|
||||
|
||||
- **DatabaseConfig**: Mock 데이터 제거 → 실제 API 호출
|
||||
- **QueryEditor**: 현재 DB / 외부 DB 분기 처리 완료
|
||||
- **API 통합**: 모든 필요한 API가 이미 구현되어 있었고, 프론트엔드 통합 완료
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { ChartDataSource, QueryResult } from "./types";
|
||||
import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection";
|
||||
import { dashboardApi } from "@/lib/api/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
|
@ -22,7 +24,7 @@ interface QueryEditorProps {
|
|||
* SQL 쿼리 에디터 컴포넌트
|
||||
* - SQL 쿼리 작성 및 편집
|
||||
* - 쿼리 실행 및 결과 미리보기
|
||||
* - 데이터 소스 설정
|
||||
* - 현재 DB / 외부 DB 분기 처리
|
||||
*/
|
||||
export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: QueryEditorProps) {
|
||||
const [query, setQuery] = useState(dataSource?.query || "");
|
||||
|
|
@ -37,37 +39,47 @@ export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: Que
|
|||
return;
|
||||
}
|
||||
|
||||
// 외부 DB인 경우 커넥션 ID 확인
|
||||
if (dataSource?.connectionType === "external" && !dataSource?.externalConnectionId) {
|
||||
setError("외부 DB 커넥션을 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsExecuting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// 실제 API 호출
|
||||
const response = await fetch("http://localhost:8080/api/dashboards/execute-query", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${localStorage.getItem("token") || "test-token"}`, // JWT 토큰 사용
|
||||
},
|
||||
body: JSON.stringify({ query: query.trim() }),
|
||||
});
|
||||
let apiResult: { columns: string[]; rows: any[]; rowCount: number };
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || "쿼리 실행에 실패했습니다.");
|
||||
// 현재 DB vs 외부 DB 분기
|
||||
if (dataSource?.connectionType === "external" && dataSource?.externalConnectionId) {
|
||||
// 외부 DB 쿼리 실행
|
||||
const result = await ExternalDbConnectionAPI.executeQuery(
|
||||
parseInt(dataSource.externalConnectionId),
|
||||
query.trim(),
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || "외부 DB 쿼리 실행에 실패했습니다.");
|
||||
}
|
||||
|
||||
// ExternalDbConnectionAPI의 응답을 통일된 형식으로 변환
|
||||
apiResult = {
|
||||
columns: result.data?.[0] ? Object.keys(result.data[0]) : [],
|
||||
rows: result.data || [],
|
||||
rowCount: result.data?.length || 0,
|
||||
};
|
||||
} else {
|
||||
// 현재 DB 쿼리 실행
|
||||
apiResult = await dashboardApi.executeQuery(query.trim());
|
||||
}
|
||||
|
||||
const apiResult = await response.json();
|
||||
|
||||
if (!apiResult.success) {
|
||||
throw new Error(apiResult.message || "쿼리 실행에 실패했습니다.");
|
||||
}
|
||||
|
||||
// API 결과를 QueryResult 형식으로 변환
|
||||
// 결과를 QueryResult 형식으로 변환
|
||||
const result: QueryResult = {
|
||||
columns: apiResult.data.columns,
|
||||
rows: apiResult.data.rows,
|
||||
totalRows: apiResult.data.rowCount,
|
||||
executionTime: 0, // API에서 실행 시간을 제공하지 않으므로 0으로 설정
|
||||
columns: apiResult.columns,
|
||||
rows: apiResult.rows,
|
||||
totalRows: apiResult.rowCount,
|
||||
executionTime: 0,
|
||||
};
|
||||
|
||||
setQueryResult(result);
|
||||
|
|
@ -75,6 +87,7 @@ export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: Que
|
|||
|
||||
// 데이터 소스 업데이트
|
||||
onDataSourceChange({
|
||||
...dataSource,
|
||||
type: "database",
|
||||
query: query.trim(),
|
||||
refreshInterval: dataSource?.refreshInterval || 30000,
|
||||
|
|
@ -83,11 +96,10 @@ export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: Que
|
|||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : "쿼리 실행 중 오류가 발생했습니다.";
|
||||
setError(errorMessage);
|
||||
// console.error('Query execution error:', err);
|
||||
} finally {
|
||||
setIsExecuting(false);
|
||||
}
|
||||
}, [query, dataSource?.refreshInterval, onDataSourceChange, onQueryTest]);
|
||||
}, [query, dataSource, onDataSourceChange, onQueryTest]);
|
||||
|
||||
// 샘플 쿼리 삽입
|
||||
const insertSampleQuery = useCallback((sampleType: string) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { ChartDataSource, ExternalConnection, ApiResponse } from "../types";
|
||||
import { ChartDataSource } from "../types";
|
||||
import { ExternalDbConnectionAPI, ExternalDbConnection } from "@/lib/api/externalDbConnection";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
|
@ -19,7 +20,7 @@ interface DatabaseConfigProps {
|
|||
* - 외부 커넥션 목록 불러오기
|
||||
*/
|
||||
export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
|
||||
const [connections, setConnections] = useState<ExternalConnection[]>([]);
|
||||
const [connections, setConnections] = useState<ExternalDbConnection[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
|
|
@ -34,23 +35,8 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
|
|||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch("http://localhost:8080/api/external-connections", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token") || "test-token"}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("외부 커넥션 목록을 불러오는데 실패했습니다");
|
||||
}
|
||||
|
||||
const result: ApiResponse<ExternalConnection[]> = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.message || "외부 커넥션 목록을 불러오는데 실패했습니다");
|
||||
}
|
||||
|
||||
setConnections(result.data || []);
|
||||
const activeConnections = await ExternalDbConnectionAPI.getConnections({ is_active: "Y" });
|
||||
setConnections(activeConnections);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : "알 수 없는 오류가 발생했습니다";
|
||||
setError(errorMessage);
|
||||
|
|
@ -60,7 +46,7 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
|
|||
};
|
||||
|
||||
// 현재 선택된 커넥션 찾기
|
||||
const selectedConnection = connections.find((conn) => conn.id === dataSource.externalConnectionId);
|
||||
const selectedConnection = connections.find((conn) => String(conn.id) === dataSource.externalConnectionId);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
|
@ -166,10 +152,10 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
|
|||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{connections.map((conn) => (
|
||||
<SelectItem key={conn.id} value={conn.id}>
|
||||
<SelectItem key={conn.id} value={String(conn.id)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{conn.name}</span>
|
||||
<span className="text-xs text-gray-500">({conn.type})</span>
|
||||
<span className="font-medium">{conn.connection_name}</span>
|
||||
<span className="text-xs text-gray-500">({conn.db_type.toUpperCase()})</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
|
|
@ -180,10 +166,10 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
|
|||
<div className="rounded-lg border border-gray-200 bg-gray-50 p-3">
|
||||
<div className="space-y-1 text-xs text-gray-600">
|
||||
<div>
|
||||
<span className="font-medium">커넥션명:</span> {selectedConnection.name}
|
||||
<span className="font-medium">커넥션명:</span> {selectedConnection.connection_name}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">타입:</span> {selectedConnection.type.toUpperCase()}
|
||||
<span className="font-medium">타입:</span> {selectedConnection.db_type.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue