데이터 새로고침으로 수정
This commit is contained in:
parent
800bd85811
commit
3b9327f64c
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { ChartDataSource } from "@/components/admin/dashboard/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
|
@ -45,13 +45,15 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult
|
|||
// ExternalDbConnectionAPI 사용 (인증 토큰 자동 포함)
|
||||
const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection");
|
||||
const connections = await ExternalDbConnectionAPI.getConnections({ is_active: "Y" });
|
||||
|
||||
|
||||
console.log("✅ 외부 DB 커넥션 로드 성공:", connections.length, "개");
|
||||
setExternalConnections(connections.map((conn: any) => ({
|
||||
id: String(conn.id),
|
||||
name: conn.connection_name,
|
||||
type: conn.db_type,
|
||||
})));
|
||||
setExternalConnections(
|
||||
connections.map((conn: any) => ({
|
||||
id: String(conn.id),
|
||||
name: conn.connection_name,
|
||||
type: conn.db_type,
|
||||
})),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("❌ 외부 DB 커넥션 로드 실패:", error);
|
||||
setExternalConnections([]);
|
||||
|
|
@ -73,27 +75,27 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult
|
|||
try {
|
||||
// dashboardApi 사용 (인증 토큰 자동 포함)
|
||||
const { dashboardApi } = await import("@/lib/api/dashboard");
|
||||
|
||||
|
||||
if (dataSource.connectionType === "external" && dataSource.externalConnectionId) {
|
||||
// 외부 DB
|
||||
const { ExternalDbConnectionAPI } = await import("@/lib/api/externalDbConnection");
|
||||
const result = await ExternalDbConnectionAPI.executeQuery(
|
||||
parseInt(dataSource.externalConnectionId),
|
||||
dataSource.query
|
||||
dataSource.query,
|
||||
);
|
||||
|
||||
|
||||
if (result.success && result.data) {
|
||||
const rows = Array.isArray(result.data.rows) ? result.data.rows : [];
|
||||
const rowCount = rows.length;
|
||||
|
||||
|
||||
// 컬럼 목록 및 타입 추출
|
||||
if (rows.length > 0) {
|
||||
const columns = Object.keys(rows[0]);
|
||||
setAvailableColumns(columns);
|
||||
|
||||
|
||||
// 컬럼 타입 분석
|
||||
const types: Record<string, string> = {};
|
||||
columns.forEach(col => {
|
||||
columns.forEach((col) => {
|
||||
const value = rows[0][col];
|
||||
if (value === null || value === undefined) {
|
||||
types[col] = "unknown";
|
||||
|
|
@ -113,17 +115,17 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult
|
|||
});
|
||||
setColumnTypes(types);
|
||||
setSampleData(rows.slice(0, 3));
|
||||
|
||||
|
||||
console.log("📊 발견된 컬럼:", columns);
|
||||
console.log("📊 컬럼 타입:", types);
|
||||
}
|
||||
|
||||
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: "쿼리 실행 성공",
|
||||
rowCount,
|
||||
});
|
||||
|
||||
|
||||
// 부모로 테스트 결과 전달 (차트 설정용)
|
||||
if (onTestResult && rows && rows.length > 0) {
|
||||
onTestResult(rows);
|
||||
|
|
@ -134,15 +136,15 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult
|
|||
} else {
|
||||
// 현재 DB
|
||||
const result = await dashboardApi.executeQuery(dataSource.query);
|
||||
|
||||
|
||||
// 컬럼 목록 및 타입 추출
|
||||
if (result.rows && result.rows.length > 0) {
|
||||
const columns = Object.keys(result.rows[0]);
|
||||
setAvailableColumns(columns);
|
||||
|
||||
|
||||
// 컬럼 타입 분석
|
||||
const types: Record<string, string> = {};
|
||||
columns.forEach(col => {
|
||||
columns.forEach((col) => {
|
||||
const value = result.rows[0][col];
|
||||
if (value === null || value === undefined) {
|
||||
types[col] = "unknown";
|
||||
|
|
@ -162,17 +164,17 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult
|
|||
});
|
||||
setColumnTypes(types);
|
||||
setSampleData(result.rows.slice(0, 3));
|
||||
|
||||
|
||||
console.log("📊 발견된 컬럼:", columns);
|
||||
console.log("📊 컬럼 타입:", types);
|
||||
}
|
||||
|
||||
|
||||
setTestResult({
|
||||
success: true,
|
||||
message: "쿼리 실행 성공",
|
||||
rowCount: result.rowCount || 0,
|
||||
});
|
||||
|
||||
|
||||
// 부모로 테스트 결과 전달 (차트 설정용)
|
||||
if (onTestResult && result.rows && result.rows.length > 0) {
|
||||
onTestResult(result.rows);
|
||||
|
|
@ -194,25 +196,17 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult
|
|||
<Label className="text-xs">데이터베이스 연결</Label>
|
||||
<RadioGroup
|
||||
value={dataSource.connectionType || "current"}
|
||||
onValueChange={(value: "current" | "external") =>
|
||||
onChange({ connectionType: value })
|
||||
}
|
||||
onValueChange={(value: "current" | "external") => onChange({ connectionType: value })}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="current" id={`current-\${dataSource.id}`} />
|
||||
<Label
|
||||
htmlFor={`current-\${dataSource.id}`}
|
||||
className="text-xs font-normal"
|
||||
>
|
||||
<RadioGroupItem value="current" id={"current-${dataSource.id}"} />
|
||||
<Label htmlFor={"current-${dataSource.id}"} className="text-xs font-normal">
|
||||
현재 데이터베이스
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="external" id={`external-\${dataSource.id}`} />
|
||||
<Label
|
||||
htmlFor={`external-\${dataSource.id}`}
|
||||
className="text-xs font-normal"
|
||||
>
|
||||
<RadioGroupItem value="external" id={"external-${dataSource.id}"} />
|
||||
<Label htmlFor={"external-${dataSource.id}"} className="text-xs font-normal">
|
||||
외부 데이터베이스
|
||||
</Label>
|
||||
</div>
|
||||
|
|
@ -222,12 +216,12 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult
|
|||
{/* 외부 DB 선택 */}
|
||||
{dataSource.connectionType === "external" && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`external-conn-\${dataSource.id}`} className="text-xs">
|
||||
<Label htmlFor={"external-conn-${dataSource.id}"} className="text-xs">
|
||||
외부 데이터베이스 선택 *
|
||||
</Label>
|
||||
{loadingConnections ? (
|
||||
<div className="flex h-10 items-center justify-center rounded-md border">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
<Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
|
|
@ -252,62 +246,74 @@ export default function MultiDatabaseConfig({ dataSource, onChange, onTestResult
|
|||
{/* SQL 쿼리 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor={`query-\${dataSource.id}`} className="text-xs">
|
||||
<Label htmlFor={"query-${dataSource.id}"} className="text-xs">
|
||||
SQL 쿼리 *
|
||||
</Label>
|
||||
<Select onValueChange={(value) => {
|
||||
const samples = {
|
||||
users: `SELECT
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
const samples = {
|
||||
users: `SELECT
|
||||
dept_name as 부서명,
|
||||
COUNT(*) as 회원수
|
||||
FROM user_info
|
||||
WHERE dept_name IS NOT NULL
|
||||
GROUP BY dept_name
|
||||
ORDER BY 회원수 DESC`,
|
||||
dept: `SELECT
|
||||
dept: `SELECT
|
||||
dept_code as 부서코드,
|
||||
dept_name as 부서명,
|
||||
location_name as 위치,
|
||||
TO_CHAR(regdate, 'YYYY-MM-DD') as 등록일
|
||||
FROM dept_info
|
||||
ORDER BY dept_code`,
|
||||
usersByDate: `SELECT
|
||||
usersByDate: `SELECT
|
||||
DATE_TRUNC('month', regdate)::date as 월,
|
||||
COUNT(*) as 신규사용자수
|
||||
FROM user_info
|
||||
WHERE regdate >= CURRENT_DATE - INTERVAL '12 months'
|
||||
GROUP BY DATE_TRUNC('month', regdate)
|
||||
ORDER BY 월`,
|
||||
usersByPosition: `SELECT
|
||||
usersByPosition: `SELECT
|
||||
position_name as 직급,
|
||||
COUNT(*) as 인원수
|
||||
FROM user_info
|
||||
WHERE position_name IS NOT NULL
|
||||
GROUP BY position_name
|
||||
ORDER BY 인원수 DESC`,
|
||||
deptHierarchy: `SELECT
|
||||
deptHierarchy: `SELECT
|
||||
COALESCE(parent_dept_code, '최상위') as 상위부서코드,
|
||||
COUNT(*) as 하위부서수
|
||||
FROM dept_info
|
||||
GROUP BY parent_dept_code
|
||||
ORDER BY 하위부서수 DESC`,
|
||||
};
|
||||
onChange({ query: samples[value as keyof typeof samples] || "" });
|
||||
}}>
|
||||
};
|
||||
onChange({ query: samples[value as keyof typeof samples] || "" });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-7 w-32 text-xs">
|
||||
<SelectValue placeholder="샘플 쿼리" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="users" className="text-xs">부서별 회원수</SelectItem>
|
||||
<SelectItem value="dept" className="text-xs">부서 목록</SelectItem>
|
||||
<SelectItem value="usersByDate" className="text-xs">월별 신규사용자</SelectItem>
|
||||
<SelectItem value="usersByPosition" className="text-xs">직급별 인원수</SelectItem>
|
||||
<SelectItem value="deptHierarchy" className="text-xs">부서 계층구조</SelectItem>
|
||||
<SelectItem value="users" className="text-xs">
|
||||
부서별 회원수
|
||||
</SelectItem>
|
||||
<SelectItem value="dept" className="text-xs">
|
||||
부서 목록
|
||||
</SelectItem>
|
||||
<SelectItem value="usersByDate" className="text-xs">
|
||||
월별 신규사용자
|
||||
</SelectItem>
|
||||
<SelectItem value="usersByPosition" className="text-xs">
|
||||
직급별 인원수
|
||||
</SelectItem>
|
||||
<SelectItem value="deptHierarchy" className="text-xs">
|
||||
부서 계층구조
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Textarea
|
||||
id={`query-\${dataSource.id}`}
|
||||
id={"query-${dataSource.id}"}
|
||||
value={dataSource.query || ""}
|
||||
onChange={(e) => onChange({ query: e.target.value })}
|
||||
placeholder="SELECT * FROM table_name WHERE ..."
|
||||
|
|
@ -315,35 +321,43 @@ ORDER BY 하위부서수 DESC`,
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* 자동 새로고침 설정 */}
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor={`refresh-${dataSource.id}`} className="text-xs">
|
||||
자동 새로고침 간격
|
||||
{/* 마커 polling 간격 설정 (MapTestWidgetV2 전용) */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="marker-refresh-interval" className="text-xs">
|
||||
데이터 새로고침 간격
|
||||
</Label>
|
||||
<Select
|
||||
value={String(dataSource.refreshInterval || 0)}
|
||||
onValueChange={(value) => onChange({ refreshInterval: Number(value) })}
|
||||
value={(dataSource.refreshInterval ?? 5).toString()}
|
||||
onValueChange={(value) => onChange({ refreshInterval: parseInt(value) })}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="새로고침 안 함" />
|
||||
<SelectTrigger id="marker-refresh-interval" className="h-8 text-xs">
|
||||
<SelectValue placeholder="간격 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">새로고침 안 함</SelectItem>
|
||||
<SelectItem value="10">10초마다</SelectItem>
|
||||
<SelectItem value="30">30초마다</SelectItem>
|
||||
<SelectItem value="60">1분마다</SelectItem>
|
||||
<SelectItem value="300">5분마다</SelectItem>
|
||||
<SelectItem value="600">10분마다</SelectItem>
|
||||
<SelectItem value="1800">30분마다</SelectItem>
|
||||
<SelectItem value="3600">1시간마다</SelectItem>
|
||||
<SelectItem value="0" className="text-xs">
|
||||
없음
|
||||
</SelectItem>
|
||||
<SelectItem value="5" className="text-xs">
|
||||
5초
|
||||
</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>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-muted-foreground text-[10px]">마커 데이터를 자동으로 갱신하는 주기를 설정합니다</p>
|
||||
</div>
|
||||
|
||||
{/* 지도 색상 설정 (MapTestWidgetV2 전용) */}
|
||||
<div className="space-y-2 rounded-lg border bg-muted/30 p-2">
|
||||
<div className="bg-muted/30 space-y-2 rounded-lg border p-2">
|
||||
<h5 className="text-xs font-semibold">🎨 지도 색상</h5>
|
||||
|
||||
|
||||
{/* 색상 팔레트 */}
|
||||
<div className="grid grid-cols-4 gap-1.5">
|
||||
{[
|
||||
|
|
@ -361,14 +375,16 @@ ORDER BY 하위부서수 DESC`,
|
|||
<button
|
||||
key={color.name}
|
||||
type="button"
|
||||
onClick={() => onChange({
|
||||
markerColor: color.marker,
|
||||
polygonColor: color.polygon,
|
||||
polygonOpacity: 0.5,
|
||||
})}
|
||||
onClick={() =>
|
||||
onChange({
|
||||
markerColor: color.marker,
|
||||
polygonColor: color.polygon,
|
||||
polygonOpacity: 0.5,
|
||||
})
|
||||
}
|
||||
className={`flex h-12 flex-col items-center justify-center gap-0.5 rounded-md border-2 transition-all hover:scale-105 ${
|
||||
isSelected
|
||||
? "border-primary bg-primary/10 shadow-md"
|
||||
isSelected
|
||||
? "border-primary bg-primary/10 shadow-md"
|
||||
: "border-border bg-background hover:border-primary/50"
|
||||
}`}
|
||||
>
|
||||
|
|
@ -405,21 +421,13 @@ ORDER BY 하위부서수 DESC`,
|
|||
{testResult && (
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-md p-2 text-xs ${
|
||||
testResult.success
|
||||
? "bg-success/10 text-success"
|
||||
: "bg-destructive/10 text-destructive"
|
||||
testResult.success ? "bg-success/10 text-success" : "bg-destructive/10 text-destructive"
|
||||
}`}
|
||||
>
|
||||
{testResult.success ? (
|
||||
<CheckCircle className="h-3 w-3" />
|
||||
) : (
|
||||
<XCircle className="h-3 w-3" />
|
||||
)}
|
||||
{testResult.success ? <CheckCircle className="h-3 w-3" /> : <XCircle className="h-3 w-3" />}
|
||||
<div>
|
||||
{testResult.message}
|
||||
{testResult.rowCount !== undefined && (
|
||||
<span className="ml-1">({testResult.rowCount}행)</span>
|
||||
)}
|
||||
{testResult.rowCount !== undefined && <span className="ml-1">({testResult.rowCount}행)</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -431,7 +439,7 @@ ORDER BY 하위부서수 DESC`,
|
|||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-xs font-semibold">메트릭 컬럼 선택</Label>
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">
|
||||
<p className="text-muted-foreground mt-0.5 text-[10px]">
|
||||
{dataSource.selectedColumns && dataSource.selectedColumns.length > 0
|
||||
? `${dataSource.selectedColumns.length}개 선택됨`
|
||||
: "모든 컬럼 표시"}
|
||||
|
|
@ -468,18 +476,15 @@ ORDER BY 하위부서수 DESC`,
|
|||
)}
|
||||
|
||||
{/* 컬럼 카드 그리드 */}
|
||||
<div className="grid grid-cols-1 gap-1.5 max-h-60 overflow-y-auto">
|
||||
<div className="grid max-h-60 grid-cols-1 gap-1.5 overflow-y-auto">
|
||||
{availableColumns
|
||||
.filter(col =>
|
||||
!columnSearchTerm ||
|
||||
col.toLowerCase().includes(columnSearchTerm.toLowerCase())
|
||||
)
|
||||
.filter((col) => !columnSearchTerm || col.toLowerCase().includes(columnSearchTerm.toLowerCase()))
|
||||
.map((col) => {
|
||||
const isSelected =
|
||||
!dataSource.selectedColumns ||
|
||||
dataSource.selectedColumns.length === 0 ||
|
||||
const isSelected =
|
||||
!dataSource.selectedColumns ||
|
||||
dataSource.selectedColumns.length === 0 ||
|
||||
dataSource.selectedColumns.includes(col);
|
||||
|
||||
|
||||
const type = columnTypes[col] || "unknown";
|
||||
const typeIcon = {
|
||||
number: "🔢",
|
||||
|
|
@ -487,51 +492,53 @@ ORDER BY 하위부서수 DESC`,
|
|||
date: "📅",
|
||||
boolean: "✓",
|
||||
object: "📦",
|
||||
unknown: "❓"
|
||||
unknown: "❓",
|
||||
}[type];
|
||||
|
||||
|
||||
const typeColor = {
|
||||
number: "text-primary bg-primary/10",
|
||||
string: "text-foreground bg-muted",
|
||||
date: "text-primary bg-primary/10",
|
||||
boolean: "text-success bg-success/10",
|
||||
object: "text-warning bg-warning/10",
|
||||
unknown: "text-muted-foreground bg-muted"
|
||||
unknown: "text-muted-foreground bg-muted",
|
||||
}[type];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={col}
|
||||
onClick={() => {
|
||||
const currentSelected = dataSource.selectedColumns && dataSource.selectedColumns.length > 0
|
||||
? dataSource.selectedColumns
|
||||
: availableColumns;
|
||||
|
||||
const currentSelected =
|
||||
dataSource.selectedColumns && dataSource.selectedColumns.length > 0
|
||||
? dataSource.selectedColumns
|
||||
: availableColumns;
|
||||
|
||||
const newSelected = isSelected
|
||||
? currentSelected.filter(c => c !== col)
|
||||
? currentSelected.filter((c) => c !== col)
|
||||
: [...currentSelected, col];
|
||||
|
||||
|
||||
onChange({ selectedColumns: newSelected });
|
||||
}}
|
||||
className={`
|
||||
relative flex items-start gap-2 rounded-lg border p-2 cursor-pointer transition-all
|
||||
${isSelected
|
||||
? "border-primary bg-primary/5 shadow-sm"
|
||||
className={`relative flex cursor-pointer items-start gap-2 rounded-lg border p-2 transition-all ${
|
||||
isSelected
|
||||
? "border-primary bg-primary/5 shadow-sm"
|
||||
: "border-border bg-card hover:border-primary/50 hover:bg-muted/50"
|
||||
}
|
||||
`}
|
||||
} `}
|
||||
>
|
||||
{/* 체크박스 */}
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<div className={`
|
||||
h-4 w-4 rounded border-2 flex items-center justify-center transition-colors
|
||||
${isSelected
|
||||
? "border-primary bg-primary"
|
||||
: "border-border bg-background"
|
||||
}
|
||||
`}>
|
||||
<div className="mt-0.5 flex-shrink-0">
|
||||
<div
|
||||
className={`flex h-4 w-4 items-center justify-center rounded border-2 transition-colors ${
|
||||
isSelected ? "border-primary bg-primary" : "border-border bg-background"
|
||||
} `}
|
||||
>
|
||||
{isSelected && (
|
||||
<svg className="h-3 w-3 text-primary-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg
|
||||
className="text-primary-foreground h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
|
|
@ -539,17 +546,17 @@ ORDER BY 하위부서수 DESC`,
|
|||
</div>
|
||||
|
||||
{/* 컬럼 정보 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium truncate">{col}</span>
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded ${typeColor}`}>
|
||||
<span className="truncate text-sm font-medium">{col}</span>
|
||||
<span className={`rounded px-1.5 py-0.5 text-xs ${typeColor}`}>
|
||||
{typeIcon} {type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 샘플 데이터 */}
|
||||
{sampleData.length > 0 && (
|
||||
<div className="mt-1.5 text-xs text-muted-foreground">
|
||||
<div className="text-muted-foreground mt-1.5 text-xs">
|
||||
<span className="font-medium">예시:</span>{" "}
|
||||
{sampleData.slice(0, 2).map((row, i) => (
|
||||
<span key={i}>
|
||||
|
|
@ -567,33 +574,28 @@ ORDER BY 하위부서수 DESC`,
|
|||
</div>
|
||||
|
||||
{/* 검색 결과 없음 */}
|
||||
{columnSearchTerm && availableColumns.filter(col =>
|
||||
col.toLowerCase().includes(columnSearchTerm.toLowerCase())
|
||||
).length === 0 && (
|
||||
<div className="text-center py-8 text-sm text-muted-foreground">
|
||||
"{columnSearchTerm}"에 대한 컬럼을 찾을 수 없습니다
|
||||
</div>
|
||||
)}
|
||||
{columnSearchTerm &&
|
||||
availableColumns.filter((col) => col.toLowerCase().includes(columnSearchTerm.toLowerCase())).length ===
|
||||
0 && (
|
||||
<div className="text-muted-foreground py-8 text-center text-sm">
|
||||
"{columnSearchTerm}"에 대한 컬럼을 찾을 수 없습니다
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 컬럼 매핑 (쿼리 테스트 성공 후에만 표시) */}
|
||||
{testResult?.success && availableColumns.length > 0 && (
|
||||
<div className="space-y-3 rounded-lg border bg-muted/30 p-3">
|
||||
<div className="bg-muted/30 space-y-3 rounded-lg border p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h5 className="text-xs font-semibold">🔄 컬럼 매핑 (선택사항)</h5>
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">
|
||||
<p className="text-muted-foreground mt-0.5 text-[10px]">
|
||||
다른 데이터 소스와 통합할 때 컬럼명을 통일할 수 있습니다
|
||||
</p>
|
||||
</div>
|
||||
{dataSource.columnMapping && Object.keys(dataSource.columnMapping).length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onChange({ columnMapping: {} })}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
<Button variant="ghost" size="sm" onClick={() => onChange({ columnMapping: {} })} className="h-7 text-xs">
|
||||
초기화
|
||||
</Button>
|
||||
)}
|
||||
|
|
@ -605,11 +607,7 @@ ORDER BY 하위부서수 DESC`,
|
|||
{Object.entries(dataSource.columnMapping).map(([original, mapped]) => (
|
||||
<div key={original} className="flex items-center gap-2">
|
||||
{/* 원본 컬럼 (읽기 전용) */}
|
||||
<Input
|
||||
value={original}
|
||||
disabled
|
||||
className="h-8 flex-1 text-xs bg-muted"
|
||||
/>
|
||||
<Input value={original} disabled className="bg-muted h-8 flex-1 text-xs" />
|
||||
|
||||
{/* 화살표 */}
|
||||
<span className="text-muted-foreground text-xs">→</span>
|
||||
|
|
@ -658,19 +656,16 @@ ORDER BY 하위부서수 DESC`,
|
|||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableColumns
|
||||
.filter(col => !dataSource.columnMapping || !dataSource.columnMapping[col])
|
||||
.map(col => (
|
||||
.filter((col) => !dataSource.columnMapping || !dataSource.columnMapping[col])
|
||||
.map((col) => (
|
||||
<SelectItem key={col} value={col} className="text-xs">
|
||||
{col}
|
||||
</SelectItem>
|
||||
))
|
||||
}
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
💡 매핑하지 않은 컬럼은 원본 이름 그대로 사용됩니다
|
||||
</p>
|
||||
<p className="text-muted-foreground text-[10px]">💡 매핑하지 않은 컬럼은 원본 이름 그대로 사용됩니다</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -685,7 +680,7 @@ ORDER BY 하위부서수 DESC`,
|
|||
{dataSource.popupFields && dataSource.popupFields.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{dataSource.popupFields.map((field, index) => (
|
||||
<div key={index} className="space-y-2 rounded-lg border bg-muted/30 p-3">
|
||||
<div key={index} className="bg-muted/30 space-y-2 rounded-lg border p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium">필드 {index + 1}</span>
|
||||
<Button
|
||||
|
|
@ -757,11 +752,21 @@ ORDER BY 하위부서수 DESC`,
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="text" className="text-xs">텍스트</SelectItem>
|
||||
<SelectItem value="number" className="text-xs">숫자</SelectItem>
|
||||
<SelectItem value="date" className="text-xs">날짜</SelectItem>
|
||||
<SelectItem value="datetime" className="text-xs">날짜시간</SelectItem>
|
||||
<SelectItem value="url" className="text-xs">URL</SelectItem>
|
||||
<SelectItem value="text" className="text-xs">
|
||||
텍스트
|
||||
</SelectItem>
|
||||
<SelectItem value="number" className="text-xs">
|
||||
숫자
|
||||
</SelectItem>
|
||||
<SelectItem value="date" className="text-xs">
|
||||
날짜
|
||||
</SelectItem>
|
||||
<SelectItem value="datetime" className="text-xs">
|
||||
날짜시간
|
||||
</SelectItem>
|
||||
<SelectItem value="url" className="text-xs">
|
||||
URL
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
@ -790,7 +795,7 @@ ORDER BY 하위부서수 DESC`,
|
|||
필드 추가
|
||||
</Button>
|
||||
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
<p className="text-muted-foreground text-[10px]">
|
||||
마커 클릭 시 팝업에 표시할 필드를 선택하고 한글 라벨을 지정하세요
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue