fix: 테스트 위젯 최종 수정 및 충돌 해결

This commit is contained in:
leeheejin 2025-10-29 13:50:08 +09:00
parent 8edd5e4ca6
commit 398c47618b
7 changed files with 291 additions and 201 deletions

View File

@ -185,7 +185,7 @@ export function DashboardTopMenu({
<SelectGroup>
<SelectLabel> </SelectLabel>
<SelectItem value="map-summary-v2"></SelectItem>
{/* <SelectItem value="chart">차트</SelectItem> */} {/* 주석 처리: 2025-10-29, 시기상조 */}
<SelectItem value="chart"> </SelectItem>
<SelectItem value="list-v2"></SelectItem>
<SelectItem value="custom-metric-v2"> </SelectItem>
<SelectItem value="risk-alert-v2">/</SelectItem>

View File

@ -157,11 +157,14 @@ export function MultiChartConfigPanel({
<SelectValue placeholder="차트 타입 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="line"> </SelectItem>
<SelectItem value="bar"> </SelectItem>
<SelectItem value="area"> </SelectItem>
<SelectItem value="pie"> </SelectItem>
<SelectItem value="donut"> </SelectItem>
<SelectItem value="line">📈 </SelectItem>
<SelectItem value="bar">📊 </SelectItem>
<SelectItem value="horizontal-bar">📊 </SelectItem>
<SelectItem value="stacked-bar">📊 </SelectItem>
<SelectItem value="area">📉 </SelectItem>
<SelectItem value="pie">🥧 </SelectItem>
<SelectItem value="donut">🍩 </SelectItem>
<SelectItem value="combo">🎨 </SelectItem>
</SelectContent>
</Select>
</div>

View File

@ -22,7 +22,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [containerSize, setContainerSize] = useState({ width: 600, height: 400 });
console.log("🧪 ChartTestWidget 렌더링 (D3 기반)!", element);
// console.log("🧪 ChartTestWidget 렌더링 (D3 기반)!", element);
const dataSources = useMemo(() => {
return element?.dataSources || element?.chartConfig?.dataSources;
@ -48,11 +48,11 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
const dataSources = element?.dataSources || element?.chartConfig?.dataSources;
if (!dataSources || dataSources.length === 0) {
console.log("⚠️ 데이터 소스가 없습니다.");
// console.log("⚠️ 데이터 소스가 없습니다.");
return;
}
console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`);
// console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`);
setLoading(true);
setError(null);
@ -60,7 +60,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
const results = await Promise.allSettled(
dataSources.map(async (source) => {
try {
console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`);
// console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`);
if (source.type === "api") {
return await loadRestApiData(source);
@ -87,7 +87,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
}
});
console.log(`✅ 총 ${allData.length}개의 데이터 로딩 완료`);
// console.log(`✅ 총 ${allData.length}개의 데이터 로딩 완료`);
setData(allData);
setLastRefreshTime(new Date());
} catch (err: any) {
@ -100,7 +100,7 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
// 수동 새로고침
const handleManualRefresh = useCallback(() => {
console.log("🔄 수동 새로고침 버튼 클릭");
// console.log("🔄 수동 새로고침 버튼 클릭");
loadMultipleDataSources();
}, [loadMultipleDataSources]);
@ -212,15 +212,15 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
if (intervals.length === 0) return;
const minInterval = Math.min(...intervals);
console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`);
// console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`);
const intervalId = setInterval(() => {
console.log("🔄 자동 새로고침 실행");
// console.log("🔄 자동 새로고침 실행");
loadMultipleDataSources();
}, minInterval * 1000);
return () => {
console.log("⏹️ 자동 새로고침 정리");
// console.log("⏹️ 자동 새로고침 정리");
clearInterval(intervalId);
};
}, [dataSources, loadMultipleDataSources]);
@ -241,14 +241,20 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
// 병합 모드: 여러 데이터 소스를 하나로 합침
if (mergeMode && dataSourceConfigs.length > 1) {
// console.log("🔀 병합 모드 활성화");
// 모든 데이터 소스의 X축 필드 수집 (첫 번째 데이터 소스의 X축 사용)
const baseConfig = dataSourceConfigs[0];
const xAxisField = baseConfig.xAxis;
const yAxisField = baseConfig.yAxis[0];
// console.log("📊 X축 필드:", xAxisField);
// X축 값 수집
dataSourceConfigs.forEach((dsConfig) => {
// X축 값 수집 (모든 데이터 소스에서)
dataSourceConfigs.forEach((dsConfig, idx) => {
const sourceName = dataSources?.find((ds) => ds.id === dsConfig.dataSourceId)?.name;
const sourceData = data.filter((item) => item._source === sourceName);
// console.log(` 소스 ${idx + 1} (${sourceName}): ${sourceData.length}개 행`);
sourceData.forEach((item) => {
if (item[xAxisField] !== undefined) {
labels.add(String(item[xAxisField]));
@ -256,26 +262,36 @@ export default function ChartTestWidget({ element }: ChartTestWidgetProps) {
});
});
// 데이터 병합
const mergedData: number[] = [];
labels.forEach((label) => {
let totalValue = 0;
dataSourceConfigs.forEach((dsConfig) => {
const sourceName = dataSources?.find((ds) => ds.id === dsConfig.dataSourceId)?.name;
const sourceData = data.filter((item) => item._source === sourceName);
const matchingItem = sourceData.find((item) => String(item[xAxisField]) === label);
if (matchingItem && yAxisField) {
totalValue += parseFloat(matchingItem[yAxisField]) || 0;
}
// console.log("📍 수집된 X축 라벨:", Array.from(labels));
// 각 데이터 소스별로 데이터셋 생성 (병합하지 않고 각각 표시)
dataSourceConfigs.forEach((dsConfig, idx) => {
const sourceName = dataSources?.find((ds) => ds.id === dsConfig.dataSourceId)?.name || `소스 ${idx + 1}`;
const sourceData = data.filter((item) => item._source === sourceName);
// 각 Y축 필드마다 데이터셋 생성
(dsConfig.yAxis || []).forEach((yAxisField, yIdx) => {
const datasetData: number[] = [];
labels.forEach((label) => {
const matchingItem = sourceData.find((item) => String(item[xAxisField]) === label);
const value = matchingItem && yAxisField ? parseFloat(matchingItem[yAxisField]) || 0 : 0;
datasetData.push(value);
});
// console.log(` 📈 ${sourceName} - ${yAxisField}: [${datasetData.join(", ")}]`);
datasets.push({
label: `${sourceName} - ${yAxisField}`,
data: datasetData,
backgroundColor: COLORS[(idx * 2 + yIdx) % COLORS.length],
borderColor: COLORS[(idx * 2 + yIdx) % COLORS.length],
type: dsConfig.chartType || chartType, // 데이터 소스별 차트 타입
});
});
mergedData.push(totalValue);
});
datasets.push({
label: yAxisField,
data: mergedData,
color: COLORS[0],
});
// console.log("✅ 병합 모드 데이터셋 생성 완료:", datasets.length, "개");
} else {
// 일반 모드: 각 데이터 소스를 별도로 표시
dataSourceConfigs.forEach((dsConfig, index) => {

View File

@ -64,7 +64,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
const [selectedMetric, setSelectedMetric] = useState<any | null>(null);
const [isDetailOpen, setIsDetailOpen] = useState(false);
console.log("🧪 CustomMetricTestWidget 렌더링!", element);
// console.log("🧪 CustomMetricTestWidget 렌더링!", element);
const dataSources = useMemo(() => {
return element?.dataSources || element?.chartConfig?.dataSources;
@ -101,10 +101,10 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
const { dashboardApi } = await import("@/lib/api/dashboard");
const result = await dashboardApi.executeQuery(groupByDS.query);
if (result.success && result.data?.rows) {
const rows = result.data.rows;
if (result && result.rows) {
const rows = result.rows;
if (rows.length > 0) {
const columns = result.data.columns || Object.keys(rows[0]);
const columns = result.columns || Object.keys(rows[0]);
const labelColumn = columns[0];
const valueColumn = columns[1];
@ -121,12 +121,17 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
else if (dataSourceType === "api") {
if (!groupByDS.endpoint) return;
const { dashboardApi } = await import("@/lib/api/dashboard");
const result = await dashboardApi.fetchExternalApi({
method: "GET",
url: groupByDS.endpoint,
headers: (groupByDS as any).headers || {},
// REST API 호출 (백엔드 프록시 사용)
const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: groupByDS.endpoint,
method: "GET",
headers: (groupByDS as any).headers || {},
}),
});
const result = await response.json();
if (result.success && result.data) {
let rows: any[] = [];
@ -163,11 +168,11 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
const dataSources = element?.dataSources || element?.chartConfig?.dataSources;
if (!dataSources || dataSources.length === 0) {
console.log("⚠️ 데이터 소스가 없습니다.");
// console.log("⚠️ 데이터 소스가 없습니다.");
return;
}
console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`);
// console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`);
setLoading(true);
setError(null);
@ -176,7 +181,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
const results = await Promise.allSettled(
dataSources.map(async (source, sourceIndex) => {
try {
console.log(`📡 데이터 소스 ${sourceIndex + 1} "${source.name || source.id}" 로딩 중...`);
// console.log(`📡 데이터 소스 ${sourceIndex + 1} "${source.name || source.id}" 로딩 중...`);
let rows: any[] = [];
if (source.type === "api") {
@ -185,7 +190,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
rows = await loadDatabaseData(source);
}
console.log(`✅ 데이터 소스 ${sourceIndex + 1}: ${rows.length}개 행`);
// console.log(`✅ 데이터 소스 ${sourceIndex + 1}: ${rows.length}개 행`);
return {
sourceName: source.name || `데이터 소스 ${sourceIndex + 1}`,
@ -203,7 +208,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
}),
);
console.log(`✅ 총 ${results.length}개의 데이터 소스 로딩 완료`);
// console.log(`✅ 총 ${results.length}개의 데이터 소스 로딩 완료`);
// 각 데이터 소스별로 메트릭 생성
const allMetrics: any[] = [];
@ -235,11 +240,11 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
return typeof value === "string" || !numericColumns.includes(col);
});
console.log(`📊 [${sourceName}] 컬럼 분석:`, {
전체: columns,
숫자: numericColumns,
문자열: stringColumns,
});
// console.log(`📊 [${sourceName}] 컬럼 분석:`, {
// 전체: columns,
// 숫자: numericColumns,
// 문자열: stringColumns,
// });
// 🆕 자동 집계 로직: 집계 컬럼 이름으로 판단 (count, 개수, sum, avg 등)
const isAggregated = numericColumns.some((col) =>
@ -248,7 +253,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
if (isAggregated && numericColumns.length > 0) {
// 집계 컬럼이 있으면 이미 집계된 데이터로 판단 (GROUP BY 결과)
console.log(`✅ [${sourceName}] 집계된 데이터로 판단 (집계 컬럼 발견: ${numericColumns.join(", ")})`);
// console.log(`✅ [${sourceName}] 집계된 데이터로 판단 (집계 컬럼 발견: ${numericColumns.join(", ")})`);
rows.forEach((row, index) => {
// 라벨: 첫 번째 문자열 컬럼
@ -259,7 +264,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
const valueField = numericColumns[0];
const value = Number(row[valueField]) || 0;
console.log(` [${sourceName}] 메트릭: ${label} = ${value}`);
// console.log(` [${sourceName}] 메트릭: ${label} = ${value}`);
allMetrics.push({
label: label,
@ -273,11 +278,11 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
});
} else {
// 숫자 컬럼이 없으면 자동 집계 (마지막 컬럼 기준)
console.log(`✅ [${sourceName}] 자동 집계 모드 (숫자 컬럼 없음)`);
// console.log(`✅ [${sourceName}] 자동 집계 모드 (숫자 컬럼 없음)`);
// 마지막 컬럼을 집계 기준으로 사용
const aggregateField = columns[columns.length - 1];
console.log(` [${sourceName}] 집계 기준 컬럼: ${aggregateField}`);
// console.log(` [${sourceName}] 집계 기준 컬럼: ${aggregateField}`);
// 해당 컬럼의 값별로 카운트
const countMap = new Map<string, number>();
@ -288,7 +293,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
// 카운트 결과를 메트릭으로 변환
countMap.forEach((count, label) => {
console.log(` [${sourceName}] 자동 집계: ${label} = ${count}`);
// console.log(` [${sourceName}] 자동 집계: ${label} = ${count}개`);
allMetrics.push({
label: label,
@ -314,20 +319,20 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
}
// 🆕 숫자 컬럼이 없을 때의 기존 로직은 주석 처리
if (false) {
/* if (false && result.status === "fulfilled") {
// 숫자 컬럼이 없으면 각 컬럼별 고유값 개수 표시
console.log(`📊 [${sourceName}] 문자열 데이터, 각 컬럼별 고유값 개수 표시`);
// console.log(`📊 [${sourceName}] 문자열 데이터, 각 컬럼별 고유값 개수 표시`);
// 데이터 소스에서 선택된 컬럼 가져오기
const dataSourceConfig = (element?.dataSources || element?.chartConfig?.dataSources)?.find(
(ds) => ds.name === sourceName || ds.id === result.value.sourceIndex.toString(),
(ds) => ds.name === sourceName || ds.id === result.value?.sourceIndex.toString(),
);
const selectedColumns = dataSourceConfig?.selectedColumns || [];
// 선택된 컬럼이 있으면 해당 컬럼만, 없으면 전체 컬럼 표시
const columnsToShow = selectedColumns.length > 0 ? selectedColumns : columns;
console.log(` [${sourceName}] 표시할 컬럼:`, columnsToShow);
// console.log(` [${sourceName}] 표시할 컬럼:`, columnsToShow);
columnsToShow.forEach((col) => {
// 해당 컬럼이 실제로 존재하는지 확인
@ -340,7 +345,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
const uniqueValues = new Set(rows.map((row) => row[col]));
const uniqueCount = uniqueValues.size;
console.log(` [${sourceName}] ${col}: ${uniqueCount}개 고유값`);
// console.log(` [${sourceName}] ${col}: ${uniqueCount}개 고유값`);
allMetrics.push({
label: `${sourceName} - ${col} (고유값)`,
@ -363,24 +368,24 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
sourceName: sourceName,
rawData: rows, // 원본 데이터 저장
});
}
} */
} else {
// 행이 많으면 각 컬럼별 고유값 개수 + 총 개수 표시
console.log(`📊 [${sourceName}] 일반 데이터 (행 많음), 컬럼별 통계 표시`);
// console.log(`📊 [${sourceName}] 일반 데이터 (행 많음), 컬럼별 통계 표시`);
const firstRow = rows[0];
const columns = Object.keys(firstRow);
// 데이터 소스에서 선택된 컬럼 가져오기
const dataSourceConfig = (element?.dataSources || element?.chartConfig?.dataSources)?.find(
(ds) => ds.name === sourceName || ds.id === result.value.sourceIndex.toString(),
(ds) => ds.name === sourceName || (result.status === "fulfilled" && ds.id === result.value?.sourceIndex.toString()),
);
const selectedColumns = dataSourceConfig?.selectedColumns || [];
// 선택된 컬럼이 있으면 해당 컬럼만, 없으면 전체 컬럼 표시
const columnsToShow = selectedColumns.length > 0 ? selectedColumns : columns;
console.log(` [${sourceName}] 표시할 컬럼:`, columnsToShow);
// console.log(` [${sourceName}] 표시할 컬럼:`, columnsToShow);
// 각 컬럼별 고유값 개수
columnsToShow.forEach((col) => {
@ -393,7 +398,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
const uniqueValues = new Set(rows.map((row) => row[col]));
const uniqueCount = uniqueValues.size;
console.log(` [${sourceName}] ${col}: ${uniqueCount}개 고유값`);
// console.log(` [${sourceName}] ${col}: ${uniqueCount}개 고유값`);
allMetrics.push({
label: `${sourceName} - ${col} (고유값)`,
@ -419,7 +424,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
}
});
console.log(`✅ 총 ${allMetrics.length}개의 메트릭 생성 완료`);
// console.log(`✅ 총 ${allMetrics.length}개의 메트릭 생성 완료`);
setMetrics(allMetrics);
setLastRefreshTime(new Date());
} catch (err) {
@ -460,13 +465,13 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
// 수동 새로고침 핸들러
const handleManualRefresh = useCallback(() => {
console.log("🔄 수동 새로고침 버튼 클릭");
// console.log("🔄 수동 새로고침 버튼 클릭");
loadAllData();
}, [loadAllData]);
// XML 데이터 파싱
const parseXmlData = (xmlText: string): any[] => {
console.log("🔍 XML 파싱 시작");
// console.log("🔍 XML 파싱 시작");
try {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, "text/xml");
@ -486,7 +491,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
result.push(obj);
}
console.log(`✅ XML 파싱 완료: ${result.length}개 레코드`);
// console.log(`✅ XML 파싱 완료: ${result.length}개 레코드`);
return result;
} catch (error) {
console.error("❌ XML 파싱 실패:", error);
@ -496,16 +501,16 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
// 텍스트/CSV 데이터 파싱
const parseTextData = (text: string): any[] => {
console.log("🔍 텍스트 파싱 시작 (처음 500자):", text.substring(0, 500));
// console.log("🔍 텍스트 파싱 시작 (처음 500자):", text.substring(0, 500));
// XML 감지
if (text.trim().startsWith("<?xml") || text.trim().startsWith("<result>")) {
console.log("📄 XML 형식 감지");
// console.log("📄 XML 형식 감지");
return parseXmlData(text);
}
// CSV 파싱
console.log("📄 CSV 형식으로 파싱 시도");
// console.log("📄 CSV 형식으로 파싱 시도");
const lines = text.trim().split("\n");
if (lines.length === 0) return [];
@ -523,7 +528,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
result.push(obj);
}
console.log(`✅ CSV 파싱 완료: ${result.length}개 행`);
// console.log(`✅ CSV 파싱 완료: ${result.length}개 행`);
return result;
};
@ -552,7 +557,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
}
}
console.log("🌐 API 호출:", source.endpoint, "파라미터:", Object.fromEntries(params));
// console.log("🌐 API 호출:", source.endpoint, "파라미터:", Object.fromEntries(params));
const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), {
method: "POST",
@ -578,7 +583,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
}
const result = await response.json();
console.log("✅ API 응답:", result);
// console.log("✅ API 응답:", result);
if (!result.success) {
console.error("❌ API 실패:", result);
@ -589,10 +594,10 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
// 텍스트/XML 데이터 처리
if (typeof processedData === "string") {
console.log("📄 텍스트 형식 데이터 감지");
// console.log("📄 텍스트 형식 데이터 감지");
processedData = parseTextData(processedData);
} else if (processedData && typeof processedData === "object" && processedData.text) {
console.log("📄 래핑된 텍스트 데이터 감지");
// console.log("📄 래핑된 텍스트 데이터 감지");
processedData = parseTextData(processedData.text);
}
@ -608,12 +613,12 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
}
} else if (!Array.isArray(processedData) && typeof processedData === "object") {
// JSON Path 없으면 자동으로 배열 찾기
console.log("🔍 JSON Path 없음, 자동으로 배열 찾기 시도");
// console.log("🔍 JSON Path 없음, 자동으로 배열 찾기 시도");
const arrayKeys = ["data", "items", "result", "records", "rows", "list"];
for (const key of arrayKeys) {
if (Array.isArray(processedData[key])) {
console.log(`✅ 배열 발견: ${key}`);
// console.log(`✅ 배열 발견: ${key}`);
processedData = processedData[key];
break;
}
@ -681,15 +686,15 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
if (intervals.length === 0) return;
const minInterval = Math.min(...intervals);
console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`);
// console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`);
const intervalId = setInterval(() => {
console.log("🔄 자동 새로고침 실행");
// console.log("🔄 자동 새로고침 실행");
loadAllData();
}, minInterval * 1000);
return () => {
console.log("⏹️ 자동 새로고침 정리");
// console.log("⏹️ 자동 새로고침 정리");
clearInterval(intervalId);
};
}, [dataSources, loadAllData]);

View File

@ -34,13 +34,13 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
const [currentPage, setCurrentPage] = useState(1);
const [lastRefreshTime, setLastRefreshTime] = useState<Date | null>(null);
// console.log("🧪 ListTestWidget 렌더링!", element);
// // console.log("🧪 ListTestWidget 렌더링!", element);
const dataSources = useMemo(() => {
return element?.dataSources || element?.chartConfig?.dataSources;
}, [element?.dataSources, element?.chartConfig?.dataSources]);
// console.log("📊 dataSources 확인:", {
// // console.log("📊 dataSources 확인:", {
// hasDataSources: !!dataSources,
// dataSourcesLength: dataSources?.length || 0,
// dataSources: dataSources,
@ -61,11 +61,11 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
// 다중 데이터 소스 로딩
const loadMultipleDataSources = useCallback(async () => {
if (!dataSources || dataSources.length === 0) {
console.log("⚠️ 데이터 소스가 없습니다.");
// console.log("⚠️ 데이터 소스가 없습니다.");
return;
}
console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`);
// console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`);
setIsLoading(true);
setError(null);
@ -74,7 +74,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
const results = await Promise.allSettled(
dataSources.map(async (source) => {
try {
console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`);
// console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`);
if (source.type === "api") {
return await loadRestApiData(source);
@ -127,7 +127,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
});
setLastRefreshTime(new Date());
console.log(`✅ 총 ${allRows.length}개의 행 로딩 완료`);
// console.log(`✅ 총 ${allRows.length}개의 행 로딩 완료`);
} catch (err) {
setError(err instanceof Error ? err.message : "데이터 로딩 실패");
} finally {
@ -137,7 +137,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
// 수동 새로고침 핸들러
const handleManualRefresh = useCallback(() => {
console.log("🔄 수동 새로고침 버튼 클릭");
// console.log("🔄 수동 새로고침 버튼 클릭");
loadMultipleDataSources();
}, [loadMultipleDataSources]);
@ -161,6 +161,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({
url: source.endpoint,
method: "GET",
@ -180,7 +181,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
}
const result = await response.json();
console.log("✅ API 응답:", result);
// console.log("✅ API 응답:", result);
if (!result.success) {
console.error("❌ API 실패:", result);
@ -247,7 +248,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
const { dashboardApi } = await import("@/lib/api/dashboard");
const result = await dashboardApi.executeQuery(source.query);
// console.log("💾 내부 DB 쿼리 결과:", {
// // console.log("💾 내부 DB 쿼리 결과:", {
// hasRows: !!result.rows,
// rowCount: result.rows?.length || 0,
// hasColumns: !!result.columns,
@ -260,7 +261,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
const mappedRows = applyColumnMapping(result.rows, source.columnMapping);
const columns = mappedRows.length > 0 ? Object.keys(mappedRows[0]) : result.columns;
// console.log("✅ 매핑 후:", {
// // console.log("✅ 매핑 후:", {
// columns,
// rowCount: mappedRows.length,
// firstMappedRow: mappedRows[0],
@ -291,15 +292,15 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
if (intervals.length === 0) return;
const minInterval = Math.min(...intervals);
console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`);
// console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`);
const intervalId = setInterval(() => {
console.log("🔄 자동 새로고침 실행");
// console.log("🔄 자동 새로고침 실행");
loadMultipleDataSources();
}, minInterval * 1000);
return () => {
console.log("⏹️ 자동 새로고침 정리");
// console.log("⏹️ 자동 새로고침 정리");
clearInterval(intervalId);
};
}, [dataSources, loadMultipleDataSources]);

View File

@ -67,8 +67,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
const [geoJsonData, setGeoJsonData] = useState<any>(null);
const [lastRefreshTime, setLastRefreshTime] = useState<Date | null>(null);
console.log("🧪 MapTestWidgetV2 렌더링!", element);
console.log("📍 마커:", markers.length, "🔷 폴리곤:", polygons.length);
// // console.log("🧪 MapTestWidgetV2 렌더링!", element);
// // console.log("📍 마커:", markers.length, "🔷 폴리곤:", polygons.length);
// dataSources를 useMemo로 추출 (circular reference 방지)
const dataSources = useMemo(() => {
@ -80,11 +80,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
const dataSourcesList = dataSources;
if (!dataSources || dataSources.length === 0) {
console.log("⚠️ 데이터 소스가 없습니다.");
// // console.log("⚠️ 데이터 소스가 없습니다.");
return;
}
console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`);
// // console.log(`🔄 ${dataSources.length}개의 데이터 소스 로딩 시작...`);
setLoading(true);
setError(null);
@ -93,7 +93,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
const results = await Promise.allSettled(
dataSources.map(async (source) => {
try {
console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`);
// // console.log(`📡 데이터 소스 "${source.name || source.id}" 로딩 중...`);
if (source.type === "api") {
return await loadRestApiData(source);
@ -114,21 +114,21 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
const allPolygons: PolygonData[] = [];
results.forEach((result, index) => {
console.log(`🔍 결과 ${index}:`, result);
// // console.log(`🔍 결과 ${index}:`, result);
if (result.status === "fulfilled" && result.value) {
const value = result.value as { markers: MarkerData[]; polygons: PolygonData[] };
console.log(`✅ 데이터 소스 ${index} 성공:`, value);
// // console.log(`✅ 데이터 소스 ${index} 성공:`, value);
// 마커 병합
if (value.markers && Array.isArray(value.markers)) {
console.log(` → 마커 ${value.markers.length}개 추가`);
// // console.log(` → 마커 ${value.markers.length}개 추가`);
allMarkers.push(...value.markers);
}
// 폴리곤 병합
if (value.polygons && Array.isArray(value.polygons)) {
console.log(` → 폴리곤 ${value.polygons.length}개 추가`);
// // console.log(` → 폴리곤 ${value.polygons.length}개 추가`);
allPolygons.push(...value.polygons);
}
} else if (result.status === "rejected") {
@ -136,9 +136,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
}
});
console.log(`✅ 총 ${allMarkers.length}개의 마커, ${allPolygons.length}개의 폴리곤 로딩 완료`);
console.log("📍 최종 마커 데이터:", allMarkers);
console.log("🔷 최종 폴리곤 데이터:", allPolygons);
// // console.log(`✅ 총 ${allMarkers.length}개의 마커, ${allPolygons.length}개의 폴리곤 로딩 완료`);
// // console.log("📍 최종 마커 데이터:", allMarkers);
// // console.log("🔷 최종 폴리곤 데이터:", allPolygons);
setMarkers(allMarkers);
setPolygons(allPolygons);
@ -153,13 +153,13 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// 수동 새로고침 핸들러
const handleManualRefresh = useCallback(() => {
console.log("🔄 수동 새로고침 버튼 클릭");
// // console.log("🔄 수동 새로고침 버튼 클릭");
loadMultipleDataSources();
}, [loadMultipleDataSources]);
// REST API 데이터 로딩
const loadRestApiData = async (source: ChartDataSource): Promise<{ markers: MarkerData[]; polygons: PolygonData[] }> => {
console.log(`🌐 REST API 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType);
// // console.log(`🌐 REST API 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType);
if (!source.endpoint) {
throw new Error("API endpoint가 없습니다.");
@ -215,10 +215,10 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// 텍스트 형식 데이터 체크 (기상청 API 등)
if (data && typeof data === 'object' && data.text && typeof data.text === 'string') {
console.log("📄 텍스트 형식 데이터 감지, CSV 파싱 시도");
// // console.log("📄 텍스트 형식 데이터 감지, CSV 파싱 시도");
const parsedData = parseTextData(data.text);
if (parsedData.length > 0) {
console.log(`✅ CSV 파싱 성공: ${parsedData.length}개 행`);
// // console.log(`✅ CSV 파싱 성공: ${parsedData.length}개 행`);
// 컬럼 매핑 적용
const mappedData = applyColumnMapping(parsedData, source.columnMapping);
return convertToMapData(mappedData, source.name || source.id || "API", source.mapDisplayType, source);
@ -244,7 +244,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// Database 데이터 로딩
const loadDatabaseData = async (source: ChartDataSource): Promise<{ markers: MarkerData[]; polygons: PolygonData[] }> => {
console.log(`💾 Database 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType);
// // console.log(`💾 Database 데이터 로딩 시작:`, source.name, `mapDisplayType:`, source.mapDisplayType);
if (!source.query) {
throw new Error("SQL 쿼리가 없습니다.");
@ -287,7 +287,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// XML 데이터 파싱 (UTIC API 등)
const parseXmlData = (xmlText: string): any[] => {
try {
console.log(" 📄 XML 파싱 시작");
// // console.log(" 📄 XML 파싱 시작");
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, "text/xml");
@ -306,7 +306,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
results.push(obj);
}
console.log(` ✅ XML 파싱 완료: ${results.length}개 레코드`);
// // console.log(` ✅ XML 파싱 완료: ${results.length}개 레코드`);
return results;
} catch (error) {
console.error(" ❌ XML 파싱 실패:", error);
@ -317,11 +317,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// 텍스트 데이터 파싱 (CSV, 기상청 형식 등)
const parseTextData = (text: string): any[] => {
try {
console.log(" 🔍 원본 텍스트 (처음 500자):", text.substring(0, 500));
// // console.log(" 🔍 원본 텍스트 (처음 500자):", text.substring(0, 500));
// XML 형식 감지
if (text.trim().startsWith("<?xml") || text.trim().startsWith("<result>")) {
console.log(" 📄 XML 형식 데이터 감지");
// // console.log(" 📄 XML 형식 데이터 감지");
return parseXmlData(text);
}
@ -333,7 +333,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
!trimmed.startsWith('---');
});
console.log(` 📝 유효한 라인: ${lines.length}`);
// // console.log(` 📝 유효한 라인: ${lines.length}개`);
if (lines.length === 0) return [];
@ -344,7 +344,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
const line = lines[i];
const values = line.split(',').map(v => v.trim().replace(/,=$/g, ''));
console.log(` 라인 ${i}:`, values);
// // console.log(` 라인 ${i}:`, values);
// 기상특보 형식: 지역코드, 지역명, 하위코드, 하위지역명, 발표시각, 특보종류, 등급, 발표상태, 설명
if (values.length >= 4) {
@ -364,11 +364,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
obj.name = obj.subRegion || obj.region || obj.code;
result.push(obj);
console.log(` ✅ 파싱 성공:`, obj);
// console.log(` ✅ 파싱 성공:`, obj);
}
}
console.log(" 📊 최종 파싱 결과:", result.length, "개");
// // console.log(" 📊 최종 파싱 결과:", result.length, "개");
return result;
} catch (error) {
console.error(" ❌ 텍스트 파싱 오류:", error);
@ -383,9 +383,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
mapDisplayType?: "auto" | "marker" | "polygon",
dataSource?: ChartDataSource
): { markers: MarkerData[]; polygons: PolygonData[] } => {
console.log(`🔄 ${sourceName} 데이터 변환 시작:`, rows.length, "개 행");
console.log(` 📌 mapDisplayType:`, mapDisplayType, `(타입: ${typeof mapDisplayType})`);
console.log(` 🎨 마커 색상:`, dataSource?.markerColor, `폴리곤 색상:`, dataSource?.polygonColor);
// // console.log(`🔄 ${sourceName} 데이터 변환 시작:`, rows.length, "개 행");
// // console.log(` 📌 mapDisplayType:`, mapDisplayType, `(타입: ${typeof mapDisplayType})`);
// // console.log(` 🎨 마커 색상:`, dataSource?.markerColor, `폴리곤 색상:`, dataSource?.polygonColor);
if (rows.length === 0) return { markers: [], polygons: [] };
@ -393,13 +393,13 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
const polygons: PolygonData[] = [];
rows.forEach((row, index) => {
console.log(`${index}:`, row);
// // console.log(` 행 ${index}:`, row);
// 텍스트 데이터 체크 (기상청 API 등)
if (row && typeof row === 'object' && row.text && typeof row.text === 'string') {
console.log(" 📄 텍스트 형식 데이터 감지, CSV 파싱 시도");
// // console.log(" 📄 텍스트 형식 데이터 감지, CSV 파싱 시도");
const parsedData = parseTextData(row.text);
console.log(` ✅ CSV 파싱 결과: ${parsedData.length}개 행`);
// // console.log(` ✅ CSV 파싱 결과: ${parsedData.length}개 행`);
// 파싱된 데이터를 재귀적으로 변환 (색상 정보 전달)
const result = convertToMapData(parsedData, sourceName, mapDisplayType, dataSource);
@ -410,11 +410,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// 폴리곤 데이터 체크 (coordinates 필드가 배열인 경우 또는 강제 polygon 모드)
if (row.coordinates && Array.isArray(row.coordinates) && row.coordinates.length > 0) {
console.log(` → coordinates 발견:`, row.coordinates.length, "개");
// // console.log(` → coordinates 발견:`, row.coordinates.length, "개");
// coordinates가 [lat, lng] 배열의 배열인지 확인
const firstCoord = row.coordinates[0];
if (Array.isArray(firstCoord) && firstCoord.length === 2) {
console.log(` → 폴리곤으로 처리:`, row.name);
// console.log(` → 폴리곤으로 처리:`, row.name);
polygons.push({
id: row.id || row.code || `polygon-${index}`,
name: row.name || row.title || `영역 ${index + 1}`,
@ -431,7 +431,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// 지역명으로 해상 구역 확인 (auto 또는 polygon 모드일 때만)
const regionName = row.name || row.area || row.region || row.location || row.subRegion;
if (regionName && MARITIME_ZONES[regionName] && mapDisplayType !== "marker") {
console.log(` → 해상 구역 발견: ${regionName}, 폴리곤으로 처리`);
// // console.log(` → 해상 구역 발견: ${regionName}, 폴리곤으로 처리`);
polygons.push({
id: `${sourceName}-polygon-${index}-${row.code || row.id || Date.now()}`, // 고유 ID 생성
name: regionName,
@ -451,24 +451,24 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// 위도/경도가 없으면 지역 코드/지역명으로 변환 시도
if ((lat === undefined || lng === undefined) && (row.code || row.areaCode || row.regionCode || row.tmFc || row.stnId)) {
const regionCode = row.code || row.areaCode || row.regionCode || row.tmFc || row.stnId;
console.log(` → 지역 코드 발견: ${regionCode}, 위도/경도 변환 시도`);
// // console.log(` → 지역 코드 발견: ${regionCode}, 위도/경도 변환 시도`);
const coords = getCoordinatesByRegionCode(regionCode);
if (coords) {
lat = coords.lat;
lng = coords.lng;
console.log(` → 변환 성공: (${lat}, ${lng})`);
// console.log(` → 변환 성공: (${lat}, ${lng})`);
}
}
// 지역명으로도 시도
if ((lat === undefined || lng === undefined) && (row.name || row.area || row.region || row.location)) {
const regionName = row.name || row.area || row.region || row.location;
console.log(` → 지역명 발견: ${regionName}, 위도/경도 변환 시도`);
// // console.log(` → 지역명 발견: ${regionName}, 위도/경도 변환 시도`);
const coords = getCoordinatesByRegionName(regionName);
if (coords) {
lat = coords.lat;
lng = coords.lng;
console.log(` → 변환 성공: (${lat}, ${lng})`);
// console.log(` → 변환 성공: (${lat}, ${lng})`);
}
}
@ -476,7 +476,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
if (mapDisplayType === "polygon") {
const regionName = row.name || row.subRegion || row.region || row.area;
if (regionName) {
console.log(` 🔷 강제 폴리곤 모드: ${regionName} → 폴리곤으로 추가 (GeoJSON 매칭)`);
// console.log(` 🔷 강제 폴리곤 모드: ${regionName} → 폴리곤으로 추가 (GeoJSON 매칭)`);
polygons.push({
id: `${sourceName}-polygon-${index}-${row.code || row.id || Date.now()}`,
name: regionName,
@ -487,14 +487,14 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
color: dataSource?.polygonColor || getColorByStatus(row.status || row.level),
});
} else {
console.log(` ⚠️ 강제 폴리곤 모드지만 지역명 없음 - 스킵`);
// console.log(` ⚠️ 강제 폴리곤 모드지만 지역명 없음 - 스킵`);
}
return; // 폴리곤으로 처리했으므로 마커로는 추가하지 않음
}
// 위도/경도가 있고 marker 모드가 아니면 마커로 처리
if (lat !== undefined && lng !== undefined && mapDisplayType !== "polygon") {
console.log(` → 마커로 처리: (${lat}, ${lng})`);
// // console.log(` → 마커로 처리: (${lat}, ${lng})`);
markers.push({
id: `${sourceName}-marker-${index}-${row.code || row.id || Date.now()}`, // 고유 ID 생성
lat: Number(lat),
@ -511,7 +511,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// 위도/경도가 없는 육지 지역 → 폴리곤으로 추가 (GeoJSON 매칭용)
const regionName = row.name || row.subRegion || row.region || row.area;
if (regionName) {
console.log(` 📍 위도/경도 없지만 지역명 있음: ${regionName} → 폴리곤으로 추가 (GeoJSON 매칭)`);
// console.log(` 📍 위도/경도 없지만 지역명 있음: ${regionName} → 폴리곤으로 추가 (GeoJSON 매칭)`);
polygons.push({
id: `${sourceName}-polygon-${index}-${row.code || row.id || Date.now()}`,
name: regionName,
@ -522,13 +522,13 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
color: dataSource?.polygonColor || getColorByStatus(row.status || row.level),
});
} else {
console.log(` ⚠️ 위도/경도 없고 지역명도 없음 - 스킵`);
console.log(` 데이터:`, row);
// console.log(` ⚠️ 위도/경도 없고 지역명도 없음 - 스킵`);
// console.log(` 데이터:`, row);
}
}
});
console.log(`${sourceName}: 마커 ${markers.length}개, 폴리곤 ${polygons.length}개 변환 완료`);
// // console.log(`✅ ${sourceName}: 마커 ${markers.length}개, 폴리곤 ${polygons.length}개 변환 완료`);
return { markers, polygons };
};
@ -757,7 +757,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
try {
const response = await fetch("/geojson/korea-municipalities.json");
const data = await response.json();
console.log("🗺️ GeoJSON 로드 완료:", data.features?.length, "개 시/군/구");
// // console.log("🗺️ GeoJSON 로드 완료:", data.features?.length, "개 시/군/구");
setGeoJsonData(data);
} catch (err) {
console.error("❌ GeoJSON 로드 실패:", err);
@ -769,11 +769,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// 초기 로드
useEffect(() => {
const dataSources = element?.dataSources || element?.chartConfig?.dataSources;
console.log("🔄 useEffect 트리거! dataSources:", dataSources);
// // console.log("🔄 useEffect 트리거! dataSources:", dataSources);
if (dataSources && dataSources.length > 0) {
loadMultipleDataSources();
} else {
console.log("⚠️ dataSources가 없거나 비어있음");
// // console.log("⚠️ dataSources가 없거나 비어있음");
setMarkers([]);
setPolygons([]);
}
@ -791,15 +791,15 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
if (intervals.length === 0) return;
const minInterval = Math.min(...intervals);
console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`);
// // console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`);
const intervalId = setInterval(() => {
console.log("🔄 자동 새로고침 실행");
// // console.log("🔄 자동 새로고침 실행");
loadMultipleDataSources();
}, minInterval * 1000);
return () => {
console.log("⏹️ 자동 새로고침 정리");
// // console.log("⏹️ 자동 새로고침 정리");
clearInterval(intervalId);
};
}, [dataSources, loadMultipleDataSources]);
@ -876,11 +876,11 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
{/* 폴리곤 렌더링 */}
{/* GeoJSON 렌더링 (육지 지역 경계선) */}
{(() => {
console.log(`🗺️ GeoJSON 렌더링 조건 체크:`, {
geoJsonData: !!geoJsonData,
polygonsLength: polygons.length,
polygonNames: polygons.map(p => p.name),
});
// console.log(`🗺️ GeoJSON 렌더링 조건 체크:`, {
// geoJsonData: !!geoJsonData,
// polygonsLength: polygons.length,
// polygonNames: polygons.map(p => p.name),
// });
return null;
})()}
{geoJsonData && polygons.length > 0 ? (
@ -897,31 +897,31 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// 정확한 매칭
if (p.name === sigName) {
console.log(`✅ 정확 매칭: ${p.name} === ${sigName}`);
// console.log(`✅ 정확 매칭: ${p.name} === ${sigName}`);
return true;
}
if (p.name === ctpName) {
console.log(`✅ 정확 매칭: ${p.name} === ${ctpName}`);
// console.log(`✅ 정확 매칭: ${p.name} === ${ctpName}`);
return true;
}
// 부분 매칭 (GeoJSON 지역명에 폴리곤 이름이 포함되는지)
if (sigName && sigName.includes(p.name)) {
console.log(`✅ 부분 매칭: ${sigName} includes ${p.name}`);
// console.log(`✅ 부분 매칭: ${sigName} includes ${p.name}`);
return true;
}
if (ctpName && ctpName.includes(p.name)) {
console.log(`✅ 부분 매칭: ${ctpName} includes ${p.name}`);
// console.log(`✅ 부분 매칭: ${ctpName} includes ${p.name}`);
return true;
}
// 역방향 매칭 (폴리곤 이름에 GeoJSON 지역명이 포함되는지)
if (sigName && p.name.includes(sigName)) {
console.log(`✅ 역방향 매칭: ${p.name} includes ${sigName}`);
// console.log(`✅ 역방향 매칭: ${p.name} includes ${sigName}`);
return true;
}
if (ctpName && p.name.includes(ctpName)) {
console.log(`✅ 역방향 매칭: ${p.name} includes ${ctpName}`);
// console.log(`✅ 역방향 매칭: ${p.name} includes ${ctpName}`);
return true;
}
@ -969,7 +969,9 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
}}
/>
) : (
<>{console.log(`⚠️ GeoJSON 렌더링 안 됨: geoJsonData=${!!geoJsonData}, polygons=${polygons.length}`)}</>
<>
{/* console.log(`⚠️ GeoJSON 렌더링 안 됨: geoJsonData=${!!geoJsonData}, polygons=${polygons.length}`) */}
</>
)}
{/* 폴리곤 렌더링 (해상 구역만) */}

View File

@ -39,12 +39,12 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
const parseTextData = (text: string): any[] => {
// XML 형식 감지
if (text.trim().startsWith("<?xml") || text.trim().startsWith("<result>")) {
console.log("📄 XML 형식 데이터 감지");
// console.log("📄 XML 형식 데이터 감지");
return parseXmlData(text);
}
// CSV 형식 (기상청 특보)
console.log("📄 CSV 형식 데이터 감지");
// console.log("📄 CSV 형식 데이터 감지");
const lines = text.split("\n").filter((line) => {
const trimmed = line.trim();
return trimmed && !trimmed.startsWith("#") && trimmed !== "=";
@ -98,7 +98,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
results.push(obj);
}
console.log(`✅ XML 파싱 완료: ${results.length}개 레코드`);
// console.log(`✅ XML 파싱 완료: ${results.length}개 레코드`);
return results;
} catch (error) {
console.error("❌ XML 파싱 실패:", error);
@ -107,14 +107,78 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
};
const loadRestApiData = useCallback(async (source: ChartDataSource) => {
if (!source.endpoint) {
// 🆕 외부 연결 ID가 있으면 먼저 외부 연결 정보를 가져옴
let actualEndpoint = source.endpoint;
let actualQueryParams = source.queryParams;
let actualHeaders = source.headers;
if (source.externalConnectionId) {
// console.log("🔗 외부 연결 ID 감지:", source.externalConnectionId);
try {
const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection");
const connection = await ExternalDbConnectionAPI.getApiConnectionById(Number(source.externalConnectionId));
if (connection) {
// console.log("✅ 외부 연결 정보 가져옴:", connection);
// 전체 엔드포인트 URL 생성
actualEndpoint = connection.endpoint_path
? `${connection.base_url}${connection.endpoint_path}`
: connection.base_url;
// console.log("📍 실제 엔드포인트:", actualEndpoint);
// 기본 헤더 적용
const headers: any[] = [];
if (connection.default_headers && Object.keys(connection.default_headers).length > 0) {
Object.entries(connection.default_headers).forEach(([key, value]) => {
headers.push({ key, value });
});
}
// 인증 정보 적용
const queryParams: any[] = [];
if (connection.auth_type && connection.auth_type !== "none" && connection.auth_config) {
const authConfig = connection.auth_config;
if (connection.auth_type === "api-key") {
if (authConfig.keyLocation === "header" && authConfig.keyName && authConfig.keyValue) {
headers.push({ key: authConfig.keyName, value: authConfig.keyValue });
// console.log("🔑 API Key 헤더 추가:", authConfig.keyName);
} else if (authConfig.keyLocation === "query" && authConfig.keyName && authConfig.keyValue) {
const actualKeyName = authConfig.keyName === "apiKey" ? "key" : authConfig.keyName;
queryParams.push({ key: actualKeyName, value: authConfig.keyValue });
// console.log("🔑 API Key 쿼리 파라미터 추가:", actualKeyName);
}
} else if (connection.auth_type === "bearer" && authConfig.token) {
headers.push({ key: "Authorization", value: `Bearer ${authConfig.token}` });
// console.log("🔑 Bearer Token 헤더 추가");
} else if (connection.auth_type === "basic" && authConfig.username && authConfig.password) {
const credentials = btoa(`${authConfig.username}:${authConfig.password}`);
headers.push({ key: "Authorization", value: `Basic ${credentials}` });
// console.log("🔑 Basic Auth 헤더 추가");
}
}
actualHeaders = headers;
actualQueryParams = queryParams;
// console.log("✅ 최종 헤더:", actualHeaders);
// console.log("✅ 최종 쿼리 파라미터:", actualQueryParams);
}
} catch (err) {
console.error("❌ 외부 연결 정보 가져오기 실패:", err);
}
}
if (!actualEndpoint) {
throw new Error("API endpoint가 없습니다.");
}
// 쿼리 파라미터 처리
const queryParamsObj: Record<string, string> = {};
if (source.queryParams && Array.isArray(source.queryParams)) {
source.queryParams.forEach((param) => {
if (actualQueryParams && Array.isArray(actualQueryParams)) {
actualQueryParams.forEach((param) => {
if (param.key && param.value) {
queryParamsObj[param.key] = param.value;
}
@ -123,34 +187,33 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
// 헤더 처리
const headersObj: Record<string, string> = {};
if (source.headers && Array.isArray(source.headers)) {
source.headers.forEach((header) => {
if (actualHeaders && Array.isArray(actualHeaders)) {
actualHeaders.forEach((header) => {
if (header.key && header.value) {
headersObj[header.key] = header.value;
}
});
}
console.log("🌐 API 호출 준비:", {
endpoint: source.endpoint,
queryParams: queryParamsObj,
headers: headersObj,
});
console.log("🔍 원본 source.queryParams:", source.queryParams);
console.log("🔍 원본 source.headers:", source.headers);
// console.log("🌐 API 호출 준비:", {
// endpoint: actualEndpoint,
// queryParams: queryParamsObj,
// headers: headersObj,
// });
const response = await fetch(getApiUrl("/api/dashboards/fetch-external-api"), {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
url: source.endpoint,
url: actualEndpoint,
method: "GET",
headers: headersObj,
queryParams: queryParamsObj,
}),
});
console.log("🌐 API 응답 상태:", response.status);
// console.log("🌐 API 응답 상태:", response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
@ -163,22 +226,22 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
let apiData = result.data;
console.log("🔍 API 응답 데이터 타입:", typeof apiData);
console.log("🔍 API 응답 데이터 (처음 500자):", typeof apiData === "string" ? apiData.substring(0, 500) : JSON.stringify(apiData).substring(0, 500));
// console.log("🔍 API 응답 데이터 타입:", typeof apiData);
// console.log("🔍 API 응답 데이터 (처음 500자):", typeof apiData === "string" ? apiData.substring(0, 500) : JSON.stringify(apiData).substring(0, 500));
// 백엔드가 {text: "XML..."} 형태로 감싼 경우 처리
if (apiData && typeof apiData === "object" && apiData.text && typeof apiData.text === "string") {
console.log("📦 백엔드가 text 필드로 감싼 데이터 감지");
// console.log("📦 백엔드가 text 필드로 감싼 데이터 감지");
apiData = parseTextData(apiData.text);
console.log("✅ 파싱 성공:", apiData.length, "개 행");
// console.log("✅ 파싱 성공:", apiData.length, "개 행");
} else if (typeof apiData === "string") {
console.log("📄 텍스트 형식 데이터 감지, 파싱 시도");
// console.log("📄 텍스트 형식 데이터 감지, 파싱 시도");
apiData = parseTextData(apiData);
console.log("✅ 파싱 성공:", apiData.length, "개 행");
// console.log("✅ 파싱 성공:", apiData.length, "개 행");
} else if (Array.isArray(apiData)) {
console.log("✅ 이미 배열 형태의 데이터입니다.");
// console.log("✅ 이미 배열 형태의 데이터입니다.");
} else {
console.log("⚠️ 예상치 못한 데이터 형식입니다. 배열로 변환 시도.");
// console.log("⚠️ 예상치 못한 데이터 형식입니다. 배열로 변환 시도.");
apiData = [apiData];
}
@ -220,7 +283,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
}, []);
const convertToAlerts = useCallback((rows: any[], sourceName: string): Alert[] => {
console.log("🔄 convertToAlerts 호출:", rows.length, "개 행");
// console.log("🔄 convertToAlerts 호출:", rows.length, "개 행");
return rows.map((row: any, index: number) => {
// 타입 결정 (UTIC XML 기준)
@ -325,7 +388,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
source: sourceName,
};
console.log(` ✅ Alert ${index}:`, alert);
// console.log(` ✅ Alert ${index}:`, alert);
return alert;
});
}, []);
@ -338,19 +401,19 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
setLoading(true);
setError(null);
console.log("🔄 RiskAlertTestWidget 데이터 로딩 시작:", dataSources.length, "개 소스");
// console.log("🔄 RiskAlertTestWidget 데이터 로딩 시작:", dataSources.length, "개 소스");
try {
const results = await Promise.allSettled(
dataSources.map(async (source, index) => {
console.log(`📡 데이터 소스 ${index + 1} 로딩 중:`, source.name, source.type);
// console.log(`📡 데이터 소스 ${index + 1} 로딩 중:`, source.name, source.type);
if (source.type === "api") {
const alerts = await loadRestApiData(source);
console.log(`✅ 데이터 소스 ${index + 1} 완료:`, alerts.length, "개 알림");
// console.log(`✅ 데이터 소스 ${index + 1} 완료:`, alerts.length, "개 알림");
return alerts;
} else {
const alerts = await loadDatabaseData(source);
console.log(`✅ 데이터 소스 ${index + 1} 완료:`, alerts.length, "개 알림");
// console.log(`✅ 데이터 소스 ${index + 1} 완료:`, alerts.length, "개 알림");
return alerts;
}
})
@ -359,14 +422,14 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
const allAlerts: Alert[] = [];
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`✅ 결과 ${index + 1} 병합:`, result.value.length, "개 알림");
// console.log(`✅ 결과 ${index + 1} 병합:`, result.value.length, "개 알림");
allAlerts.push(...result.value);
} else {
console.error(`❌ 결과 ${index + 1} 실패:`, result.reason);
}
});
console.log("✅ 총", allAlerts.length, "개 알림 로딩 완료");
// console.log("✅ 총", allAlerts.length, "개 알림 로딩 완료");
allAlerts.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
setAlerts(allAlerts);
setLastRefreshTime(new Date());
@ -380,7 +443,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
// 수동 새로고침 핸들러
const handleManualRefresh = useCallback(() => {
console.log("🔄 수동 새로고침 버튼 클릭");
// console.log("🔄 수동 새로고침 버튼 클릭");
loadMultipleDataSources();
}, [loadMultipleDataSources]);
@ -403,15 +466,15 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
if (intervals.length === 0) return;
const minInterval = Math.min(...intervals);
console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`);
// console.log(`⏱️ 자동 새로고침 설정: ${minInterval}초마다`);
const intervalId = setInterval(() => {
console.log("🔄 자동 새로고침 실행");
// console.log("🔄 자동 새로고침 실행");
loadMultipleDataSources();
}, minInterval * 1000);
return () => {
console.log("⏹️ 자동 새로고침 정리");
// console.log("⏹️ 자동 새로고침 정리");
clearInterval(intervalId);
};
}, [dataSources, loadMultipleDataSources]);
@ -516,7 +579,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
</div>
{/* 컨텐츠 */}
<div className="flex-1 overflow-hidden p-2">
<div className="flex flex-1 flex-col overflow-hidden p-2">
<div className="mb-2 flex gap-1 overflow-x-auto">
<Button
variant={filter === "all" ? "default" : "outline"}
@ -545,7 +608,7 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
})}
</div>
<div className="flex-1 space-y-1.5 overflow-y-auto">
<div className="flex-1 space-y-1.5 overflow-y-auto pr-1">
{filteredAlerts.length === 0 ? (
<div className="flex h-full items-center justify-center text-gray-500">
<p className="text-sm"> </p>