Merge pull request 'fix(배치관리): DB 커넥션 변경 시 테이블 목록이 안 바뀌는 버그' (#30) from hjjeong into main
Build & Deploy to K8s / build-and-deploy (push) Successful in 8m51s

Reviewed-on: #30
This commit was merged in pull request #30.
This commit is contained in:
2026-05-22 05:57:24 +00:00
8 changed files with 469 additions and 33 deletions
@@ -128,9 +128,11 @@ function Sparkline({ data }: { data: SparklineData[] }) {
return (
<div className="flex h-8 items-end gap-[2px]">
{data.map((slot, i) => {
const hasFail = slot.failed > 0;
const hasSuccess = slot.success > 0;
const height = hasFail ? "40%" : hasSuccess ? `${Math.max(30, Math.min(95, 50 + slot.success * 10))}%` : "8%";
const failed = Number(slot.failed_count) || 0;
const success = Number(slot.success_count) || 0;
const hasFail = failed > 0;
const hasSuccess = success > 0;
const height = hasFail ? "40%" : hasSuccess ? `${Math.max(30, Math.min(95, 50 + success * 10))}%` : "8%";
const colorClass = hasFail
? "bg-destructive/70 hover:bg-destructive"
: hasSuccess
@@ -141,7 +143,7 @@ function Sparkline({ data }: { data: SparklineData[] }) {
key={i}
className={`min-w-[4px] flex-1 rounded-t-sm transition-colors ${colorClass}`}
style={{ height }}
title={`${slot.hour?.slice(11, 16) || i}시 | 성공: ${slot.success} 실패: ${slot.failed}`}
title={`${slot.hour_slot?.slice(11, 16) || i}시 | 성공: ${success} 실패: ${failed}`}
/>
);
})}
@@ -278,8 +280,10 @@ function BatchDetailPanel({ batch, sparkline, recentLogs }: { batch: BatchConfig
);
}
function GlobalSparkline({ stats }: { stats: BatchStats | null }) {
if (!stats) return null;
function GlobalSparkline({ data }: { data: SparklineData[] }) {
if (!data || data.length === 0) return null;
// 24개 슬롯 중 가장 큰 success_count 를 100% 로 맞춰 비율 스케일링
const maxSuccess = data.reduce((m, s) => Math.max(m, Number(s.success_count) || 0), 0);
return (
<div className="rounded-lg border bg-card p-4">
<div className="mb-3 flex items-center justify-between">
@@ -294,22 +298,31 @@ function GlobalSparkline({ stats }: { stats: BatchStats | null }) {
</div>
</div>
<div className="flex h-10 items-end gap-[3px]">
{Array.from({ length: 24 }).map((_, i) => {
const hasExec = Math.random() > 0.3;
const hasFail = hasExec && Math.random() < 0.08;
const h = hasFail ? 35 : hasExec ? 25 + Math.random() * 70 : 6;
{data.map((slot, i) => {
const success = Number(slot.success_count) || 0;
const failed = Number(slot.failed_count) || 0;
const hasFail = failed > 0;
const hasExec = success > 0 || hasFail;
// 실패가 하나라도 있으면 실패 색으로 강조, 아니면 success 비율
const h = hasFail
? Math.max(35, Math.min(95, 35 + (failed / Math.max(maxSuccess, 1)) * 60))
: hasExec
? Math.max(20, Math.min(95, (success / Math.max(maxSuccess, 1)) * 90))
: 6;
const hour = slot.hour_slot?.slice(11, 16) || "";
return (
<div
key={i}
className={`flex-1 rounded-t-sm transition-colors ${hasFail ? "bg-destructive/60 hover:bg-destructive" : hasExec ? "bg-emerald-500/40 hover:bg-emerald-500/70" : "bg-muted-foreground/8"}`}
className={`flex-1 rounded-t-sm transition-colors ${hasFail ? "bg-destructive/60 hover:bg-destructive" : hasExec ? "bg-emerald-500/40 hover:bg-emerald-500/70" : "bg-muted-foreground/10"}`}
style={{ height: `${h}%` }}
title={`${hour} | 성공 ${success} 실패 ${failed}`}
/>
);
})}
</div>
<div className="mt-1 flex justify-between text-[10px] text-muted-foreground">
<span>24 </span>
<span>12 </span>
<span>6 </span>
<span></span>
</div>
</div>
@@ -327,6 +340,7 @@ export default function BatchManagementPage() {
const [executingBatch, setExecutingBatch] = useState<number | null>(null);
const [expandedBatch, setExpandedBatch] = useState<number | null>(null);
const [stats, setStats] = useState<BatchStats | null>(null);
const [globalSparkline, setGlobalSparkline] = useState<SparklineData[]>([]);
const [sparklineCache, setSparklineCache] = useState<Record<number, SparklineData[]>>({});
const [recentLogsCache, setRecentLogsCache] = useState<Record<number, RecentLog[]>>({});
const [isBatchTypeModalOpen, setIsBatchTypeModalOpen] = useState(false);
@@ -339,10 +353,12 @@ export default function BatchManagementPage() {
const loadBatchConfigs = useCallback(async () => {
setLoading(true);
try {
const [configsResponse, statsData] = await Promise.all([
const [configsResponse, statsData, globalSpark] = await Promise.all([
BatchAPI.getBatchConfigs({ page: 1, limit: 200 }),
BatchAPI.getBatchStats(),
BatchAPI.getGlobalSparkline(),
]);
setGlobalSparkline(globalSpark);
// cross-tenant 메타 (단일 모드면 undefined → null)
setCrossTenantMeta((configsResponse as any)?.cross_tenant_meta ?? null);
if (configsResponse.success && configsResponse.data) {
@@ -461,8 +477,12 @@ export default function BatchManagementPage() {
const activeBatches = batchConfigs.filter(b => b.is_active === "Y").length;
const inactiveBatches = batchConfigs.length - activeBatches;
const execDiff = stats ? stats.todayExecutions - stats.prevDayExecutions : 0;
const failDiff = stats ? stats.todayFailures - stats.prevDayFailures : 0;
const todayExec = Number(stats?.today_count) || 0;
const todayFail = Number(stats?.today_failed_count) || 0;
const yestExec = Number(stats?.yesterday_count) || 0;
const yestFail = Number(stats?.yesterday_failed_count) || 0;
const execDiff = todayExec - yestExec;
const failDiff = todayFail - yestFail;
return (
<div className="bg-background flex h-full min-h-0 w-full flex-col overflow-hidden">
@@ -502,7 +522,7 @@ export default function BatchManagementPage() {
<div className="h-8 w-px bg-border" />
<div className="flex flex-1 flex-col px-4 py-3">
<span className="text-[11px] text-muted-foreground"> </span>
<span className="text-lg font-bold text-emerald-600">{stats.todayExecutions}</span>
<span className="text-lg font-bold text-emerald-600">{todayExec}</span>
{execDiff !== 0 && (
<span className={`text-[10px] ${execDiff > 0 ? "text-emerald-500" : "text-muted-foreground"}`}>
{execDiff > 0 ? "+" : ""}{execDiff}
@@ -512,8 +532,8 @@ export default function BatchManagementPage() {
<div className="h-8 w-px bg-border" />
<div className="flex flex-1 flex-col px-4 py-3">
<span className="text-[11px] text-muted-foreground"></span>
<span className={`text-lg font-bold ${stats.todayFailures > 0 ? "text-destructive" : "text-muted-foreground"}`}>
{stats.todayFailures}
<span className={`text-lg font-bold ${todayFail > 0 ? "text-destructive" : "text-muted-foreground"}`}>
{todayFail}
</span>
{failDiff !== 0 && (
<span className={`text-[10px] ${failDiff > 0 ? "text-destructive" : "text-emerald-500"}`}>
@@ -525,7 +545,7 @@ export default function BatchManagementPage() {
)}
{/* 24시간 차트 */}
<GlobalSparkline stats={stats} />
<GlobalSparkline data={globalSparkline} />
{/* 검색 + 필터 */}
<div className="flex flex-wrap items-center gap-3">
@@ -331,14 +331,14 @@ export default function BatchManagementNewPage() {
// 내부 데이터베이스 선택
connection = connections.find((conn) => conn.type === "internal") || null;
} else {
// 외부 데이터베이스 선택
const connectionId = parseInt(connectionValue);
connection = connections.find((conn) => conn.id === connectionId) || null;
// 외부 데이터베이스 선택 — id 가 number/string 어느 쪽이든 안전하게 비교
connection = connections.find((conn) => conn.id?.toString() === connectionValue) || null;
}
setToConnection(connection);
setToTable("");
setToColumns([]);
setToTables([]);
if (connection) {
try {
@@ -383,12 +383,12 @@ export default function BatchManagementNewPage() {
if (connectionValue === "internal") {
connection = connections.find((conn) => conn.type === "internal") || null;
} else {
const connectionId = parseInt(connectionValue);
connection = connections.find((conn) => conn.id === connectionId) || null;
connection = connections.find((conn) => conn.id?.toString() === connectionValue) || null;
}
setFromConnection(connection);
setFromTable("");
setFromColumns([]);
setFromTables([]);
if (connection) {
try {
+30 -9
View File
@@ -37,19 +37,22 @@ export interface NodeFlowInfo {
node_count: number;
}
// 백엔드 mapper (batchManagement.xml) 가 snake_case 로 응답하므로 그대로 사용한다.
// 프로젝트 컨벤션: Map 키 = snake_case (CLAUDE.md 백엔드 규칙 참조)
export interface BatchStats {
totalBatches: number;
activeBatches: number;
todayExecutions: number;
todayFailures: number;
prevDayExecutions: number;
prevDayFailures: number;
total_count: number;
active_count: number;
today_count: number;
today_failed_count: number;
yesterday_count: number;
yesterday_failed_count: number;
}
export interface SparklineData {
hour: string;
success: number;
failed: number;
hour_slot: string;
total_count: number;
success_count: number;
failed_count: number;
}
export interface RecentLog {
@@ -621,6 +624,24 @@ export class BatchAPI {
}
}
/**
* 회사 전체 배치의 최근 24시간 스파크라인 데이터 (24개 슬롯 고정)
*/
static async getGlobalSparkline(): Promise<SparklineData[]> {
try {
const response = await apiClient.get<ApiResponse<SparklineData[]>>(
`/batch-management/sparkline`
);
if (response.data.success) {
return response.data.data || [];
}
return [];
} catch (error) {
console.error("글로벌 스파크라인 조회 오류:", error);
return [];
}
}
/**
* 배치별 최근 24시간 스파크라인 데이터
*/