Merge pull request 'fix(배치관리): DB 커넥션 변경 시 테이블 목록이 안 바뀌는 버그' (#30) from hjjeong into main
Build & Deploy to K8s / build-and-deploy (push) Successful in 8m51s
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:
@@ -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 {
|
||||
|
||||
@@ -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시간 스파크라인 데이터
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user