이희진 진행사항 중간세이브
This commit is contained in:
@@ -20,6 +20,10 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||
const [apiConnections, setApiConnections] = useState<ExternalApiConnection[]>([]);
|
||||
const [selectedConnectionId, setSelectedConnectionId] = useState<string>("");
|
||||
const [availableColumns, setAvailableColumns] = useState<string[]>([]); // API 테스트 후 발견된 컬럼 목록
|
||||
const [columnTypes, setColumnTypes] = useState<Record<string, string>>({}); // 컬럼 타입 정보
|
||||
const [sampleData, setSampleData] = useState<any[]>([]); // 샘플 데이터 (최대 3개)
|
||||
const [columnSearchTerm, setColumnSearchTerm] = useState(""); // 컬럼 검색어
|
||||
|
||||
console.log("🔧 MultiApiConfig - dataSource:", dataSource);
|
||||
|
||||
@@ -88,12 +92,14 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
});
|
||||
console.log("API Key 헤더 추가:", authConfig.keyName);
|
||||
} else if (authConfig.keyLocation === "query" && authConfig.keyName && authConfig.keyValue) {
|
||||
// UTIC API는 'key'를 사용하므로, 'apiKey'를 'key'로 변환
|
||||
const actualKeyName = authConfig.keyName === "apiKey" ? "key" : authConfig.keyName;
|
||||
queryParams.push({
|
||||
id: `auth_query_${Date.now()}`,
|
||||
key: authConfig.keyName,
|
||||
key: actualKeyName,
|
||||
value: authConfig.keyValue,
|
||||
});
|
||||
console.log("API Key 쿼리 파라미터 추가:", authConfig.keyName);
|
||||
console.log("API Key 쿼리 파라미터 추가:", actualKeyName, "(원본:", authConfig.keyName, ")");
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -299,6 +305,41 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
|
||||
const rows = Array.isArray(data) ? data : [data];
|
||||
|
||||
// 컬럼 목록 및 타입 추출
|
||||
if (rows.length > 0) {
|
||||
const columns = Object.keys(rows[0]);
|
||||
setAvailableColumns(columns);
|
||||
|
||||
// 컬럼 타입 분석 (첫 번째 행 기준)
|
||||
const types: Record<string, string> = {};
|
||||
columns.forEach(col => {
|
||||
const value = rows[0][col];
|
||||
if (value === null || value === undefined) {
|
||||
types[col] = "unknown";
|
||||
} else if (typeof value === "number") {
|
||||
types[col] = "number";
|
||||
} else if (typeof value === "boolean") {
|
||||
types[col] = "boolean";
|
||||
} else if (typeof value === "string") {
|
||||
// 날짜 형식 체크
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
|
||||
types[col] = "date";
|
||||
} else {
|
||||
types[col] = "string";
|
||||
}
|
||||
} else {
|
||||
types[col] = "object";
|
||||
}
|
||||
});
|
||||
setColumnTypes(types);
|
||||
|
||||
// 샘플 데이터 저장 (최대 3개)
|
||||
setSampleData(rows.slice(0, 3));
|
||||
|
||||
console.log("📊 발견된 컬럼:", columns);
|
||||
console.log("📊 컬럼 타입:", types);
|
||||
}
|
||||
|
||||
// 위도/경도 또는 coordinates 필드 또는 지역 코드 체크
|
||||
const hasLocationData = rows.some((row) => {
|
||||
const hasLatLng = (row.lat || row.latitude) && (row.lng || row.longitude);
|
||||
@@ -488,6 +529,34 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 자동 새로고침 설정 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`refresh-${dataSource.id}`} className="text-xs">
|
||||
자동 새로고침 간격
|
||||
</Label>
|
||||
<Select
|
||||
value={String(dataSource.refreshInterval || 0)}
|
||||
onValueChange={(value) => onChange({ refreshInterval: Number(value) })}
|
||||
>
|
||||
<SelectTrigger 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>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
설정한 간격마다 자동으로 데이터를 다시 불러옵니다
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 테스트 버튼 */}
|
||||
<div className="space-y-2 border-t pt-4">
|
||||
<Button
|
||||
@@ -509,7 +578,7 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
|
||||
{testResult && (
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-md p-2 text-xs \${
|
||||
className={`flex items-center gap-2 rounded-md p-2 text-xs ${
|
||||
testResult.success
|
||||
? "bg-green-50 text-green-700"
|
||||
: "bg-red-50 text-red-700"
|
||||
@@ -524,6 +593,158 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 컬럼 선택 (메트릭 위젯용) - 개선된 UI */}
|
||||
{availableColumns.length > 0 && (
|
||||
<div className="space-y-3 border-t pt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-sm font-semibold">메트릭 컬럼 선택</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{dataSource.selectedColumns && dataSource.selectedColumns.length > 0
|
||||
? `${dataSource.selectedColumns.length}개 컬럼 선택됨`
|
||||
: "모든 컬럼 표시"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onChange({ selectedColumns: availableColumns })}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
전체
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onChange({ selectedColumns: [] })}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
해제
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 검색 */}
|
||||
{availableColumns.length > 5 && (
|
||||
<Input
|
||||
placeholder="컬럼 검색..."
|
||||
value={columnSearchTerm}
|
||||
onChange={(e) => setColumnSearchTerm(e.target.value)}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 컬럼 카드 그리드 */}
|
||||
<div className="grid grid-cols-1 gap-2 max-h-80 overflow-y-auto">
|
||||
{availableColumns
|
||||
.filter(col =>
|
||||
!columnSearchTerm ||
|
||||
col.toLowerCase().includes(columnSearchTerm.toLowerCase())
|
||||
)
|
||||
.map((col) => {
|
||||
const isSelected =
|
||||
!dataSource.selectedColumns ||
|
||||
dataSource.selectedColumns.length === 0 ||
|
||||
dataSource.selectedColumns.includes(col);
|
||||
|
||||
const type = columnTypes[col] || "unknown";
|
||||
const typeIcon = {
|
||||
number: "🔢",
|
||||
string: "📝",
|
||||
date: "📅",
|
||||
boolean: "✓",
|
||||
object: "📦",
|
||||
unknown: "❓"
|
||||
}[type];
|
||||
|
||||
const typeColor = {
|
||||
number: "text-blue-600 bg-blue-50",
|
||||
string: "text-gray-600 bg-gray-50",
|
||||
date: "text-purple-600 bg-purple-50",
|
||||
boolean: "text-green-600 bg-green-50",
|
||||
object: "text-orange-600 bg-orange-50",
|
||||
unknown: "text-gray-400 bg-gray-50"
|
||||
}[type];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={col}
|
||||
onClick={() => {
|
||||
const currentSelected = dataSource.selectedColumns && dataSource.selectedColumns.length > 0
|
||||
? dataSource.selectedColumns
|
||||
: availableColumns;
|
||||
|
||||
const newSelected = isSelected
|
||||
? currentSelected.filter(c => c !== col)
|
||||
: [...currentSelected, col];
|
||||
|
||||
onChange({ selectedColumns: newSelected });
|
||||
}}
|
||||
className={`
|
||||
relative flex items-start gap-3 rounded-lg border p-3 cursor-pointer 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-gray-300 bg-background"
|
||||
}
|
||||
`}>
|
||||
{isSelected && (
|
||||
<svg className="h-3 w-3 text-primary-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 컬럼 정보 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<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}`}>
|
||||
{typeIcon} {type}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 샘플 데이터 */}
|
||||
{sampleData.length > 0 && (
|
||||
<div className="mt-1.5 text-xs text-muted-foreground">
|
||||
<span className="font-medium">예시:</span>{" "}
|
||||
{sampleData.slice(0, 2).map((row, i) => (
|
||||
<span key={i}>
|
||||
{String(row[col]).substring(0, 20)}
|
||||
{String(row[col]).length > 20 && "..."}
|
||||
{i < Math.min(sampleData.length - 1, 1) && ", "}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user