109 lines
3.9 KiB
TypeScript
109 lines
3.9 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { batchApi, type SyncDataPreviewResponse } from '../api/batchApi';
|
|
import LoadingSpinner from './LoadingSpinner';
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
tableKey: string;
|
|
tableName: string;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export default function SyncDataPreviewModal({ open, tableKey, tableName, onClose }: Props) {
|
|
const [data, setData] = useState<SyncDataPreviewResponse | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open || !tableKey) return;
|
|
setLoading(true);
|
|
setError(null);
|
|
batchApi.getSyncDataPreview(tableKey, 10)
|
|
.then(setData)
|
|
.catch((e) => setError(e.message))
|
|
.finally(() => setLoading(false));
|
|
}, [open, tableKey]);
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-wing-overlay" onClick={onClose}>
|
|
<div
|
|
className="bg-wing-surface rounded-xl shadow-2xl max-w-5xl w-full mx-4 max-h-[80vh] flex flex-col"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-wing-border">
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-wing-text">{tableName}</h3>
|
|
<p className="text-xs text-wing-muted mt-0.5">
|
|
{data ? `${data.targetSchema}.${data.targetTable} | 총 ${data.totalCount.toLocaleString()}건` : ''}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="px-3 py-1.5 text-sm text-wing-muted hover:text-wing-text transition-colors"
|
|
>
|
|
닫기
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="flex-1 overflow-auto p-4">
|
|
{loading && <LoadingSpinner />}
|
|
{error && (
|
|
<div className="text-center py-8 text-wing-muted">
|
|
<p className="text-red-400">조회 실패: {error}</p>
|
|
</div>
|
|
)}
|
|
{!loading && !error && data && data.rows.length === 0 && (
|
|
<div className="text-center py-8 text-wing-muted">데이터가 없습니다</div>
|
|
)}
|
|
{!loading && !error && data && data.rows.length > 0 && (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-xs">
|
|
<thead>
|
|
<tr className="border-b border-wing-border">
|
|
{data.columns.map((col) => (
|
|
<th
|
|
key={col}
|
|
className="px-3 py-2 text-left font-medium text-wing-muted whitespace-nowrap bg-wing-card"
|
|
>
|
|
{col}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.rows.map((row, idx) => (
|
|
<tr key={idx} className="border-b border-wing-border/50 hover:bg-wing-hover">
|
|
{data.columns.map((col) => (
|
|
<td key={col} className="px-3 py-1.5 text-wing-text whitespace-nowrap max-w-[200px] truncate">
|
|
{formatCellValue(row[col])}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
{data && data.rows.length > 0 && (
|
|
<div className="px-6 py-3 border-t border-wing-border text-xs text-wing-muted">
|
|
최근 {data.rows.length}건 표시 (전체 {data.totalCount.toLocaleString()}건)
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function formatCellValue(value: unknown): string {
|
|
if (value === null || value === undefined) return '-';
|
|
if (typeof value === 'object') return JSON.stringify(value);
|
|
return String(value);
|
|
}
|